From b9771fe89fcb5c112e25254cf06e98555d44635f Mon Sep 17 00:00:00 2001 From: Marko Djordjevic Date: Fri, 13 Feb 2026 00:17:09 +0100 Subject: [PATCH] feat: wire frontend state and data flow for multi-chart support - Add activeChartId/charts state to page.tsx with chart fetching on mount - ChartSelector integrated in sidebar between header and file upload - CandleChart and SvgOverlay fetch data scoped by activeChartId - FileUpload returns chart info, auto-selects new chart after upload - Annotation create/delete flows include chart_id - Export scoped by activeChartId - Toolbox receives activeChartId prop --- .../changes/multi-chart-management/tasks.md | 14 +- src/app/page.tsx | 126 ++++++++++++++---- src/components/CandleChart.tsx | 11 +- src/components/FileUpload.tsx | 4 +- src/components/SvgOverlay.tsx | 6 +- src/components/Toolbox.tsx | 2 + 6 files changed, 126 insertions(+), 37 deletions(-) diff --git a/openspec/changes/multi-chart-management/tasks.md b/openspec/changes/multi-chart-management/tasks.md index 1965e03..7fc03f3 100644 --- a/openspec/changes/multi-chart-management/tasks.md +++ b/openspec/changes/multi-chart-management/tasks.md @@ -34,13 +34,13 @@ ## 6. Frontend State & Data Flow -- [ ] 6.1 Add `activeChartId` and `charts` state to `page.tsx` -- [ ] 6.2 Fetch charts list on mount via `GET /api/charts`, auto-select the most recent chart -- [ ] 6.3 Pass `activeChartId` to `CandleChart` component — update it to fetch candles/annotations with `?chartId=` param -- [ ] 6.4 Update `handleUploadSuccess` to receive the new chart from the upload response, add it to `charts` state, and set it as `activeChartId` -- [ ] 6.5 Update annotation create/delete flows to include `chart_id` in requests -- [ ] 6.6 When `activeChartId` changes, refetch candles and annotations for the new chart -- [ ] 6.7 Update export handler to include `?chartId=` in the export URL +- [x] 6.1 Add `activeChartId` and `charts` state to `page.tsx` +- [x] 6.2 Fetch charts list on mount via `GET /api/charts`, auto-select the most recent chart +- [x] 6.3 Pass `activeChartId` to `CandleChart` component — update it to fetch candles/annotations with `?chartId=` param +- [x] 6.4 Update `handleUploadSuccess` to receive the new chart from the upload response, add it to `charts` state, and set it as `activeChartId` +- [x] 6.5 Update annotation create/delete flows to include `chart_id` in requests +- [x] 6.6 When `activeChartId` changes, refetch candles and annotations for the new chart +- [x] 6.7 Update export handler to include `?chartId=` in the export URL ## 7. UI Polish diff --git a/src/app/page.tsx b/src/app/page.tsx index 34638fb..5a4ae33 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,20 @@ 'use client'; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import Toolbox, { Tool } from '@/components/Toolbox'; import FileUpload from '@/components/FileUpload'; import CandleChart, { CandleChartHandle } from '@/components/CandleChart'; +import ChartSelector from '@/components/ChartSelector'; + +interface Chart { + id: number; + name: string; + created_at: number; +} interface Annotation { id: number; + chart_id: number; timestamp: number; label_type: string; geometry: any; @@ -18,28 +26,83 @@ export default function Home() { const [selectedColor, setSelectedColor] = useState('#3b82f6'); const [selectedLabelId, setSelectedLabelId] = useState(null); const [annotations, setAnnotations] = useState([]); + const [charts, setCharts] = useState([]); + const [activeChartId, setActiveChartId] = useState(null); const chartRef = useRef(null); + // Fetch charts list + const fetchCharts = useCallback(async () => { + try { + const response = await fetch('/api/charts'); + const data = await response.json(); + setCharts(data); + return data as Chart[]; + } catch (error) { + console.error('Failed to fetch charts:', error); + return []; + } + }, []); + + // Fetch annotations for active chart + const fetchAnnotations = useCallback(async (chartId: number | null) => { + if (!chartId) { + setAnnotations([]); + return; + } + try { + const response = await fetch(`/api/annotations?chartId=${chartId}`); + const data = await response.json(); + setAnnotations(data); + } catch (error) { + console.error('Failed to fetch annotations:', error); + } + }, []); + + // Fetch charts on mount, auto-select the most recent + useEffect(() => { + const init = async () => { + const chartList = await fetchCharts(); + if (chartList.length > 0) { + setActiveChartId(chartList[0].id); // sorted by created_at desc + } + }; + init(); + }, [fetchCharts]); + + // When activeChartId changes, refetch data + useEffect(() => { + if (activeChartId !== null) { + chartRef.current?.refreshData(); + fetchAnnotations(activeChartId); + setSelectedLabelId(null); + } + }, [activeChartId, fetchAnnotations]); + const handleExport = () => { - window.location.href = '/api/export'; + if (activeChartId) { + window.location.href = `/api/export?chartId=${activeChartId}`; + } else { + window.location.href = '/api/export'; + } }; - const handleUploadSuccess = () => { - // Refresh chart data after successful upload - chartRef.current?.refreshData(); + const handleUploadSuccess = (chart: { id: number; name: string }) => { + // Add new chart to list and select it + const newChart: Chart = { + id: chart.id, + name: chart.name, + created_at: Math.floor(Date.now() / 1000), + }; + setCharts((prev) => [newChart, ...prev]); + setActiveChartId(chart.id); }; const handleAnnotationChange = async () => { - // Refresh chart when annotations change await chartRef.current?.refreshData(); - // Fetch annotations for sidebar - const response = await fetch('/api/annotations'); - const data = await response.json(); - setAnnotations(data); + await fetchAnnotations(activeChartId); }; const handleLabelDelete = async (id: number) => { - // Remove from local state setAnnotations(annotations.filter((a) => a.id !== id)); if (selectedLabelId === id) { setSelectedLabelId(null); @@ -50,19 +113,24 @@ export default function Home() { setSelectedLabelId(id === -1 ? null : id); }; - // Fetch annotations on mount - useEffect(() => { - const fetchAnnotations = async () => { - try { - const response = await fetch('/api/annotations'); - const data = await response.json(); - setAnnotations(data); - } catch (error) { - console.error('Failed to fetch annotations:', error); + const handleSelectChart = (chartId: number) => { + setActiveChartId(chartId); + }; + + const handleDeleteChart = async (chartId: number) => { + try { + const response = await fetch(`/api/charts/${chartId}`, { method: 'DELETE' }); + if (response.ok) { + const remaining = charts.filter((c) => c.id !== chartId); + setCharts(remaining); + if (activeChartId === chartId) { + setActiveChartId(remaining.length > 0 ? remaining[0].id : null); + } } - }; - fetchAnnotations(); - }, []); + } catch (error) { + console.error('Failed to delete chart:', error); + } + }; // Keyboard handler for Delete/Backspace key useEffect(() => { @@ -100,7 +168,15 @@ export default function Home() { Manage Annotation Types -
+
+ +
+
@@ -114,6 +190,7 @@ export default function Home() { selectedLabelId={selectedLabelId} onLabelSelect={handleLabelSelect} onLabelDelete={handleLabelDelete} + activeChartId={activeChartId} />
@@ -127,6 +204,7 @@ export default function Home() { selectedColor={selectedColor} selectedLabelId={selectedLabelId} onLabelSelect={handleLabelSelect} + activeChartId={activeChartId} />
diff --git a/src/components/CandleChart.tsx b/src/components/CandleChart.tsx index 18db121..7e41d6c 100644 --- a/src/components/CandleChart.tsx +++ b/src/components/CandleChart.tsx @@ -42,6 +42,7 @@ interface CandleChartProps { selectedColor: string; selectedLabelId?: number | null; onLabelSelect?: (id: number) => void; + activeChartId?: number | null; } export interface CandleChartHandle { @@ -49,7 +50,7 @@ export interface CandleChartHandle { } const CandleChart = forwardRef( - ({ activeTool, onAnnotationChange, selectedColor, selectedLabelId, onLabelSelect }, ref) => { + ({ activeTool, onAnnotationChange, selectedColor, selectedLabelId, onLabelSelect, activeChartId }, ref) => { const chartContainerRef = useRef(null); const chartRef = useRef(null); const seriesRef = useRef | null>(null); @@ -68,7 +69,8 @@ const CandleChart = forwardRef( // Fetch candles from API const fetchCandles = async () => { try { - const response = await fetch('/api/candles'); + const url = activeChartId ? `/api/candles?chartId=${activeChartId}` : '/api/candles'; + const response = await fetch(url); const data = await response.json(); setCandles(data); setIsEmpty(data.length === 0); @@ -82,7 +84,8 @@ const CandleChart = forwardRef( // Fetch annotations from API const fetchAnnotations = async () => { try { - const response = await fetch('/api/annotations'); + const url = activeChartId ? `/api/annotations?chartId=${activeChartId}` : '/api/annotations'; + const response = await fetch(url); const data = await response.json(); setAnnotations(data); return data; @@ -308,6 +311,7 @@ const CandleChart = forwardRef( body: JSON.stringify({ timestamp: nearestCandle.time, label_type: activeTool, + chart_id: activeChartId, }), }); @@ -410,6 +414,7 @@ const CandleChart = forwardRef( activeTool={activeTool} onAnnotationChange={onAnnotationChange} selectedColor={selectedColor} + activeChartId={activeChartId} /> ); diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 992a4f2..0d97cc1 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'; import { Upload } from 'lucide-react'; interface FileUploadProps { - onUploadSuccess: () => void; + onUploadSuccess: (chart: { id: number; name: string }) => void; } export default function FileUpload({ onUploadSuccess }: FileUploadProps) { @@ -36,7 +36,7 @@ export default function FileUpload({ onUploadSuccess }: FileUploadProps) { type: 'success', text: `Successfully uploaded ${data.count} candle records`, }); - onUploadSuccess(); + onUploadSuccess(data.chart); } else { setMessage({ type: 'error', diff --git a/src/components/SvgOverlay.tsx b/src/components/SvgOverlay.tsx index 2c14698..77100ac 100644 --- a/src/components/SvgOverlay.tsx +++ b/src/components/SvgOverlay.tsx @@ -23,6 +23,7 @@ interface SvgOverlayProps { activeTool: string | null; onAnnotationChange?: () => void; selectedColor: string; + activeChartId?: number | null; } interface Point { @@ -42,6 +43,7 @@ export default function SvgOverlay({ activeTool, onAnnotationChange, selectedColor, + activeChartId, }: SvgOverlayProps) { const svgRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); @@ -54,7 +56,8 @@ export default function SvgOverlay({ // Fetch annotations const fetchAnnotations = async () => { try { - const response = await fetch('/api/annotations'); + const url = activeChartId ? `/api/annotations?chartId=${activeChartId}` : '/api/annotations'; + const response = await fetch(url); const data = await response.json(); setAnnotations(data); } catch (error) { @@ -232,6 +235,7 @@ export default function SvgOverlay({ body: JSON.stringify({ timestamp: drawingLine.start.time, label_type: 'line', + chart_id: activeChartId, color: selectedColor, geometry: { startTime: drawingLine.start.time, diff --git a/src/components/Toolbox.tsx b/src/components/Toolbox.tsx index 65e8d2d..283e90e 100644 --- a/src/components/Toolbox.tsx +++ b/src/components/Toolbox.tsx @@ -36,6 +36,7 @@ interface ToolboxProps { selectedLabelId?: number | null; onLabelSelect?: (id: number) => void; onLabelDelete?: (id: number) => void; + activeChartId?: number | null; } export default function Toolbox({ @@ -48,6 +49,7 @@ export default function Toolbox({ selectedLabelId = null, onLabelSelect, onLabelDelete, + activeChartId, }: ToolboxProps) { const [labelsExpanded, setLabelsExpanded] = useState(true); const [searchText, setSearchText] = useState('');