From 86db8955ef4b9a87e1ff17ce797c0b4738dcefd8 Mon Sep 17 00:00:00 2001 From: Maddie Date: Tue, 10 Mar 2026 18:32:35 -0700 Subject: [PATCH 1/8] new session functionality + fix across session managemnt --- .../nodes/filter-node/filter-node.tsx | 17 ---- .../components/ui-header/settings-bar.tsx | 86 ++++++++++++------- .../ui-react-flow/react-flow-view.tsx | 5 +- .../components/ui-sessions/session-modal.tsx | 86 +++++++------------ 4 files changed, 91 insertions(+), 103 deletions(-) diff --git a/frontend/components/nodes/filter-node/filter-node.tsx b/frontend/components/nodes/filter-node/filter-node.tsx index 2032775..77c2df2 100644 --- a/frontend/components/nodes/filter-node/filter-node.tsx +++ b/frontend/components/nodes/filter-node/filter-node.tsx @@ -196,23 +196,6 @@ export default function FilterNode({ id }: FilterNodeProps) { isDataStreamOn={dataStreaming} /> - {isConnected && ( - <> - setCutoff(Number(e.target.value))} - /> - -

- {selectedFilter === 'lowpass' - ? 'Frequencies below cutoff will pass through' - : 'Frequencies above cutoff will pass through'} -

- - )} ); } \ No newline at end of file diff --git a/frontend/components/ui-header/settings-bar.tsx b/frontend/components/ui-header/settings-bar.tsx index ca8ac76..2ed8f38 100644 --- a/frontend/components/ui-header/settings-bar.tsx +++ b/frontend/components/ui-header/settings-bar.tsx @@ -40,6 +40,7 @@ export default function SettingsBar() { const [leftTimerSeconds, setLeftTimerSeconds] = useState(0); const intervalRef = useRef(null); const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); + const [isNewDialogOpen, setIsNewDialogOpen] = useState(false); const [isSessionModalOpen, setIsSessionModalOpen] = useState(false); const [sessionModalMode, setSessionModalMode] = useState<'save' | 'load'>( 'save' @@ -49,37 +50,19 @@ export default function SettingsBar() { const [fetchingFor, setFetchingFor] = useState<'save' | 'load' | null>(null); const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isDirty, setIsDirty] = useState(false); - const [sessionId, setSessionId] = useState(null); - + // Track unsaved canvas changes useEffect(() => { - async function fetchOrCreateSession() { - try { - const res = await fetch('/api/sessions'); - const sessions = await res.json(); - - if (sessions.length > 0) { - setSessionId(sessions[0].id); - } else { - const created = await fetch('/api/sessions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify('New Session'), - }); - const session = await created.json(); - setSessionId(session.id); - } - } catch (err) { - console.error('Failed to fetch or create session', err); - } - } - - fetchOrCreateSession(); + const handler = () => setIsDirty(true); + window.addEventListener('canvas-changed', handler); + window.addEventListener('reactflow-edges-changed', handler); + return () => { + window.removeEventListener('canvas-changed', handler); + window.removeEventListener('reactflow-edges-changed', handler); + }; }, []); - - // useEffect(() => { - // console.log('dataStreaming:', dataStreaming); - // }); + // Timer effect - starts/stops based on dataStreaming state useEffect(() => { if (dataStreaming) { @@ -171,6 +154,7 @@ export default function SettingsBar() { setIsSaving(true); try { await handleSaveToExistingSession(activeSessionId); + setIsDirty(false); notifications.success({ title: 'Session saved successfully' }); } catch (error) { notifications.error({ @@ -224,6 +208,26 @@ export default function SettingsBar() { } }; + const handleNewClick = () => { + if (isSaving || isLoading || isFetchingSessions) { + return; + } + if (isDirty) { + setIsNewDialogOpen(true); + } else { + handleConfirmNew(); + } + }; + + const handleConfirmNew = () => { + setActiveSessionId(null); + setIsDirty(false); + setDataStreaming(false); + setLeftTimerSeconds(0); + window.dispatchEvent(new Event('pipeline-reset')); + setIsNewDialogOpen(false); + }; + const handleCreateAndSaveSession = async (sessionName: string) => { setIsSaving(true); try { @@ -231,6 +235,7 @@ export default function SettingsBar() { const createdSession = await createSession(sessionName); await saveFrontendState(createdSession.id, state); setActiveSessionId(createdSession.id); + setIsDirty(false); setIsSessionModalOpen(false); notifications.success({ title: 'Session saved successfully' }); } catch (error) { @@ -281,7 +286,7 @@ export default function SettingsBar() { {/* Session ID, Tutorial */} - Session {sessionId ?? 'ID'} + Session {activeSessionId ?? 'ID'} Tutorials @@ -340,6 +345,29 @@ export default function SettingsBar() { ? 'Preparing...' : 'Save'} + + + + + Start a new session? + + Your current session is unsaved. Hitting confirm will clear the current pipeline. Any unsaved changes will be lost. + + +
+ + + + +
+
+
- - - - - - No sessions found. - - {sessions.map((session) => ( - { - setSelectedSessionId(session.id); - setIsSessionPickerOpen(false); - }} - > - - {session.name} - - ID {session.id} - - - ))} - - - - - + + + + No sessions found. + + {sessions.map((session) => ( + setSelectedSessionId(session.id)} + disabled={isSubmitting} + > + + {session.name} + + ID {session.id} + + + ))} + + + {validationError ? (

{validationError}

) : null} From a5c8abdc25f14c6bd868e6f4e8619e4a76c9f2aa Mon Sep 17 00:00:00 2001 From: Maddie Date: Tue, 10 Mar 2026 19:49:08 -0700 Subject: [PATCH 2/8] fix websocket connection --- .../nodes/filter-node/combo-box.tsx | 85 +++++++------------ .../nodes/filter-node/filter-node.tsx | 13 +-- frontend/hooks/useWebsocket.tsx | 26 ++++-- 3 files changed, 59 insertions(+), 65 deletions(-) diff --git a/frontend/components/nodes/filter-node/combo-box.tsx b/frontend/components/nodes/filter-node/combo-box.tsx index 513540e..4505a0f 100644 --- a/frontend/components/nodes/filter-node/combo-box.tsx +++ b/frontend/components/nodes/filter-node/combo-box.tsx @@ -32,16 +32,15 @@ interface ComboBoxProps { export default function ComboBox({ value = 'lowpass', onValueChange, + lowCutoff, + highCutoff, + setLowCutoff, + setHighCutoff, isConnected = false, isDataStreamOn = false, }: ComboBoxProps) { const [isExpanded, setIsExpanded] = React.useState(false); const titleRef = React.useRef(null); - const [sliderValue, setSliderValue] = React.useState([75]); - const [cutoff, setCutoff] = React.useState([75]); - - const [lowCutoff, setLowCutoff] = React.useState([25]); - const [highCutoff, setHighCutoff] = React.useState([75]); const toggleExpanded = () => { setIsExpanded(!isExpanded); @@ -140,21 +139,17 @@ export default function ComboBox({ paddingRight: '60px', }} > - {value != 'bandpass' && ( + {value !== 'bandpass' && (
{ - setSliderValue(val); - if (value === 'lowpass') { - setHighCutoff(val); - } - - if (value === 'highpass') { - setLowCutoff(val); + setHighCutoff(val[0]); + } else { + setLowCutoff(val[0]); } - }} + }} max={100} min={0} step={1} @@ -167,29 +162,19 @@ export default function ComboBox({
)} - {/* Single slider for lowpass and highpass */} - {value == 'bandpass' && ( + {value === 'bandpass' && (
{/* Low cutoff */} -
- - { - // prevent low from going above high - const next = - val[0] >= highCutoff[0] - ? highCutoff[0] - 1 - : val[0]; - setLowCutoff([next]); - }} - min={0} - max={100} - step={1} - className="w-full mb-1" - /> -
- + { + setLowCutoff(Math.min(val[0], highCutoff - 1)); + }} + min={0} + max={100} + step={1} + className="w-full mb-1" + />
0 Low Cutoff @@ -197,24 +182,16 @@ export default function ComboBox({
{/* High cutoff */} -
- { - // prevent high from going below low - const next = - val[0] <= lowCutoff[0] - ? lowCutoff[0] + 1 - : val[0]; - setHighCutoff([next]); - }} - min={0} - max={100} - step={1} - className="w-full mb-1" - /> -
- + { + setHighCutoff(Math.max(val[0], lowCutoff + 1)); + }} + min={0} + max={100} + step={1} + className="w-full mb-1" + />
0 High Cutoff diff --git a/frontend/components/nodes/filter-node/filter-node.tsx b/frontend/components/nodes/filter-node/filter-node.tsx index 77c2df2..b1e21d1 100644 --- a/frontend/components/nodes/filter-node/filter-node.tsx +++ b/frontend/components/nodes/filter-node/filter-node.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Handle, Position, useReactFlow } from '@xyflow/react'; import { useGlobalContext } from '@/context/GlobalContext'; import ComboBox from './combo-box'; -import useWebsocket from '@/hooks/useWebsocket'; import { ProcessingConfig } from '@/lib/processing'; interface FilterNodeProps { @@ -23,8 +22,6 @@ export default function FilterNode({ id }: FilterNodeProps) { // Get data stream status from global context const { dataStreaming } = useGlobalContext(); - const { sendProcessingConfig } = useWebsocket(0, 0) - const buildConfig = (): ProcessingConfig => { if (!isConnected) { return { @@ -138,9 +135,13 @@ export default function FilterNode({ id }: FilterNodeProps) { }, [checkConnectionStatus]); React.useEffect(() => { - if (!dataStreaming) return - sendProcessingConfig(buildConfig()) - }, [selectedFilter, lowCutoff, highCutoff, isConnected, dataStreaming]) + if (!dataStreaming) return; + window.dispatchEvent( + new CustomEvent('processing-config-update', { + detail: buildConfig(), + }) + ); + }, [selectedFilter, lowCutoff, highCutoff, isConnected, dataStreaming]); return (
diff --git a/frontend/hooks/useWebsocket.tsx b/frontend/hooks/useWebsocket.tsx index edae134..6223e59 100644 --- a/frontend/hooks/useWebsocket.tsx +++ b/frontend/hooks/useWebsocket.tsx @@ -2,6 +2,16 @@ import { useEffect, useState, useRef } from 'react'; import { useGlobalContext } from '@/context/GlobalContext'; import { ProcessingConfig } from '@/lib/processing'; +const DEFAULT_PROCESSING_CONFIG: ProcessingConfig = { + apply_bandpass: false, + use_iir: false, + l_freq: null, + h_freq: null, + downsample_factor: null, + sfreq: 256, + n_channels: 4, +}; + export default function useWebsocket( chartSize: number, batchesPerSecond: number @@ -23,7 +33,16 @@ export default function useWebsocket( wsRef.current.send(JSON.stringify(config)) console.log('Sent processing config:', config) } - } + } + + useEffect(() => { + const handleConfigUpdate = (event: Event) => { + sendProcessingConfig((event as CustomEvent).detail); + }; + window.addEventListener('processing-config-update', handleConfigUpdate); + return () => window.removeEventListener('processing-config-update', handleConfigUpdate); + }, []); + const normalizeBatch = (batch: any) => { return batch.timestamps.map((time: number, i: number) => ({ time, @@ -65,10 +84,7 @@ export default function useWebsocket( ws.onopen = () => { console.log('WebSocket connection opened.'); - - if (processingConfigRef.current) { - ws.send(JSON.stringify(processingConfigRef.current)) - } + ws.send(JSON.stringify(processingConfigRef.current ?? DEFAULT_PROCESSING_CONFIG)); }; ws.onmessage = (event) => { From edfaea6aa0f4a9c51e76dee2b3daf2513217e1c3 Mon Sep 17 00:00:00 2001 From: Maddie Date: Tue, 10 Mar 2026 20:55:03 -0700 Subject: [PATCH 3/8] refactor websocket to websocket context --- frontend/app/home/page.tsx | 3 + .../signal-graph-node/signal-graph-node.tsx | 4 +- frontend/context/WebSocketContext.tsx | 151 +++++++++++++++ frontend/hooks/useNodeData.ts | 45 +++++ frontend/hooks/useWebsocket.tsx | 173 ------------------ 5 files changed, 201 insertions(+), 175 deletions(-) create mode 100644 frontend/context/WebSocketContext.tsx create mode 100644 frontend/hooks/useNodeData.ts delete mode 100644 frontend/hooks/useWebsocket.tsx diff --git a/frontend/app/home/page.tsx b/frontend/app/home/page.tsx index cadf0d9..1dca67a 100644 --- a/frontend/app/home/page.tsx +++ b/frontend/app/home/page.tsx @@ -1,6 +1,7 @@ 'use client'; import ReactFlowView from '@/components/ui-react-flow/react-flow-view'; import { GlobalProvider } from '@/context/GlobalContext'; +import { WebSocketProvider } from '@/context/WebSocketContext'; import AppHeader from '@/components/ui-header/app-header'; import SettingsBar from '@/components/ui-header/settings-bar'; import { NotificationsProvider } from '@/components/notifications'; @@ -8,6 +9,7 @@ import { NotificationsProvider } from '@/components/notifications'; export default function Home() { return ( +
{/* Top section for header and settings bar */} @@ -22,6 +24,7 @@ export default function Home() {
+ ); } diff --git a/frontend/components/nodes/signal-graph-node/signal-graph-node.tsx b/frontend/components/nodes/signal-graph-node/signal-graph-node.tsx index 5b33465..544e67d 100644 --- a/frontend/components/nodes/signal-graph-node/signal-graph-node.tsx +++ b/frontend/components/nodes/signal-graph-node/signal-graph-node.tsx @@ -1,6 +1,6 @@ import { Card } from '@/components/ui/card'; import { Handle, Position, useReactFlow } from '@xyflow/react'; -import useWebsocket from '@/hooks/useWebsocket'; +import useNodeData from '@/hooks/useNodeData'; import React from 'react'; import { useGlobalContext } from '@/context/GlobalContext'; import { ArrowUpRight } from 'lucide-react'; @@ -16,7 +16,7 @@ import { import SignalGraphView from './signal-graph-full'; export default function SignalGraphNode({ id }: { id?: string }) { - const { renderData } = useWebsocket(20, 10); + const { renderData } = useNodeData(20, 10); const processedData = renderData; const reactFlowInstance = useReactFlow(); diff --git a/frontend/context/WebSocketContext.tsx b/frontend/context/WebSocketContext.tsx new file mode 100644 index 0000000..019ca5d --- /dev/null +++ b/frontend/context/WebSocketContext.tsx @@ -0,0 +1,151 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useRef, useCallback, ReactNode } from 'react'; +import { useGlobalContext } from './GlobalContext'; +import { ProcessingConfig } from '@/lib/processing'; + +export type DataPoint = { + time: string; + signal1: number; + signal2: number; + signal3: number; + signal4: number; +}; + +type Subscriber = (points: DataPoint[]) => void; + +type WebSocketContextType = { + subscribe: (fn: Subscriber) => () => void; + sendProcessingConfig: (config: ProcessingConfig) => void; +}; + +const WebSocketContext = createContext(undefined); + +const DEFAULT_PROCESSING_CONFIG: ProcessingConfig = { + apply_bandpass: false, + use_iir: false, + l_freq: null, + h_freq: null, + downsample_factor: null, + sfreq: 256, + n_channels: 4, +}; + +function normalizeBatch(batch: any): DataPoint[] { + return batch.timestamps.map((time: number, i: number) => ({ + time: String(time), + signal1: batch.signals[0][i], + signal2: batch.signals[1][i], + signal3: batch.signals[2][i], + signal4: batch.signals[3][i], + })); +} + +export function WebSocketProvider({ children }: { children: ReactNode }) { + const { dataStreaming } = useGlobalContext(); + const wsRef = useRef(null); + const processingConfigRef = useRef(null); + const subscribersRef = useRef>(new Set()); + const closingTimeoutRef = useRef(null); + const isClosingGracefullyRef = useRef(false); + + const subscribe = useCallback((fn: Subscriber) => { + subscribersRef.current.add(fn); + return () => subscribersRef.current.delete(fn); + }, []); + + const sendProcessingConfig = useCallback((config: ProcessingConfig) => { + processingConfigRef.current = config; + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify(config)); + console.log('Sent processing config:', config); + } + }, []); + + // Forward processing-config-update events from filter node to backend + useEffect(() => { + const handler = (event: Event) => { + sendProcessingConfig((event as CustomEvent).detail); + }; + window.addEventListener('processing-config-update', handler); + return () => window.removeEventListener('processing-config-update', handler); + }, [sendProcessingConfig]); + + // Manage WebSocket lifecycle + useEffect(() => { + if (!dataStreaming) { + if (wsRef.current?.readyState === WebSocket.OPEN && !isClosingGracefullyRef.current) { + isClosingGracefullyRef.current = true; + wsRef.current.send('clientClosing'); + closingTimeoutRef.current = setTimeout(() => { + console.warn('Timeout: no confirmed closing received. Forcing close.'); + wsRef.current?.close(); + isClosingGracefullyRef.current = false; + }, 5000); + } + return; + } + + if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) return; + + console.log('Opening WebSocket connection...'); + const ws = new WebSocket('ws://localhost:8080'); + wsRef.current = ws; + + ws.onopen = () => { + console.log('WebSocket connection opened.'); + ws.send(JSON.stringify(processingConfigRef.current ?? DEFAULT_PROCESSING_CONFIG)); + }; + + ws.onmessage = (event) => { + const message = event.data; + if (message === 'confirmed closing') { + console.log("Received 'confirmed closing' from server."); + if (closingTimeoutRef.current) clearTimeout(closingTimeoutRef.current); + ws.close(); + isClosingGracefullyRef.current = false; + } else { + try { + const points = normalizeBatch(JSON.parse(message)); + subscribersRef.current.forEach((fn) => fn(points)); + } catch (e) { + console.error('Failed to parse WebSocket message:', e); + } + } + }; + + ws.onclose = (event) => { + console.log('WebSocket connection closed:', event.code, event.reason); + wsRef.current = null; + isClosingGracefullyRef.current = false; + }; + + ws.onerror = () => { + if (closingTimeoutRef.current) clearTimeout(closingTimeoutRef.current); + isClosingGracefullyRef.current = false; + }; + + return () => { + if (closingTimeoutRef.current) clearTimeout(closingTimeoutRef.current); + if (ws.readyState === WebSocket.OPEN && !isClosingGracefullyRef.current) { + ws.send('clientClosing'); + closingTimeoutRef.current = setTimeout(() => ws.close(), 5000); + } else if (ws.readyState !== WebSocket.CLOSED) { + ws.close(); + } + wsRef.current = null; + }; + }, [dataStreaming]); + + return ( + + {children} + + ); +} + +export function useWebSocketContext() { + const ctx = useContext(WebSocketContext); + if (!ctx) throw new Error('useWebSocketContext must be used within a WebSocketProvider'); + return ctx; +} diff --git a/frontend/hooks/useNodeData.ts b/frontend/hooks/useNodeData.ts new file mode 100644 index 0000000..0a432aa --- /dev/null +++ b/frontend/hooks/useNodeData.ts @@ -0,0 +1,45 @@ +import { useEffect, useRef, useState } from 'react'; +import { useWebSocketContext, DataPoint } from '@/context/WebSocketContext'; +import { useGlobalContext } from '@/context/GlobalContext'; + +export default function useNodeData(chartSize: number, batchesPerSecond: number) { + const { subscribe } = useWebSocketContext(); + const { dataStreaming } = useGlobalContext(); + const [renderData, setRenderData] = useState([]); + const bufferRef = useRef([]); + + // Subscribe to incoming data points from the shared WebSocket + useEffect(() => { + const unsubscribe = subscribe((points) => { + bufferRef.current.push(...points); + }); + return unsubscribe; + }, [subscribe]); + + // Drain the buffer into renderData at the node's own rate + useEffect(() => { + if (!dataStreaming || batchesPerSecond <= 0) return; + + const intervalTime = 1000 / batchesPerSecond; + const id = setInterval(() => { + if (bufferRef.current.length > 0) { + const batch = bufferRef.current.splice( + 0, + Math.min(bufferRef.current.length, chartSize) + ); + setRenderData((prev) => [...prev, ...batch].slice(-chartSize)); + } + }, intervalTime); + + return () => clearInterval(id); + }, [dataStreaming, batchesPerSecond, chartSize]); + + // Always clear the buffer on start/stop to prevent backlog buildup. + // renderData is never cleared so the chart holds its last frame when paused + // and new data naturally replaces it as it arrives. + useEffect(() => { + bufferRef.current = []; + }, [dataStreaming]); + + return { renderData }; +} diff --git a/frontend/hooks/useWebsocket.tsx b/frontend/hooks/useWebsocket.tsx deleted file mode 100644 index 6223e59..0000000 --- a/frontend/hooks/useWebsocket.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useEffect, useState, useRef } from 'react'; -import { useGlobalContext } from '@/context/GlobalContext'; -import { ProcessingConfig } from '@/lib/processing'; - -const DEFAULT_PROCESSING_CONFIG: ProcessingConfig = { - apply_bandpass: false, - use_iir: false, - l_freq: null, - h_freq: null, - downsample_factor: null, - sfreq: 256, - n_channels: 4, -}; - -export default function useWebsocket( - chartSize: number, - batchesPerSecond: number -) { - const { dataStreaming } = useGlobalContext(); - const [renderData, setRenderData] = useState([]); - const bufferRef = useRef([]); - const wsRef = useRef(null); - const closingTimeoutRef = useRef(null); - const [isClosingGracefully, setIsClosingGracefully] = useState(false); - const processingConfigRef = useRef(null); - - const intervalTime = 1000 / batchesPerSecond; - - const sendProcessingConfig = (config: ProcessingConfig) => { - processingConfigRef.current = config - - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify(config)) - console.log('Sent processing config:', config) - } - } - - useEffect(() => { - const handleConfigUpdate = (event: Event) => { - sendProcessingConfig((event as CustomEvent).detail); - }; - window.addEventListener('processing-config-update', handleConfigUpdate); - return () => window.removeEventListener('processing-config-update', handleConfigUpdate); - }, []); - - const normalizeBatch = (batch: any) => { - return batch.timestamps.map((time: number, i: number) => ({ - time, - signal1: batch.signals[0][i], - signal2: batch.signals[1][i], - signal3: batch.signals[2][i], - signal4: batch.signals[3][i], - })); - }; - - useEffect(() => { - console.log('data streaming:', dataStreaming); - - if (!dataStreaming && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - if (!isClosingGracefully) { - console.log("Initiating graceful close..."); - setIsClosingGracefully(true); - wsRef.current.send('clientClosing'); - - closingTimeoutRef.current = setTimeout(() => { - console.warn("Timeout: No 'confirmed closing' received. Forcing WebSocket close."); - if (wsRef.current) { - wsRef.current.close(); - } - setIsClosingGracefully(false); - }, 5000); - } - return; - } - - if (!dataStreaming && (!wsRef.current || wsRef.current.readyState === WebSocket.CLOSED)) { - return; - } - - if (dataStreaming && (!wsRef.current || wsRef.current.readyState === WebSocket.CLOSED)) { - console.log("Opening new WebSocket connection..."); - const ws = new WebSocket('ws://localhost:8080'); - wsRef.current = ws; - - ws.onopen = () => { - console.log('WebSocket connection opened.'); - ws.send(JSON.stringify(processingConfigRef.current ?? DEFAULT_PROCESSING_CONFIG)); - }; - - ws.onmessage = (event) => { - const message = event.data; - if (message === 'confirmed closing') { - console.log("Received 'confirmed closing' from server. Proceeding to close."); - if (closingTimeoutRef.current) { - clearTimeout(closingTimeoutRef.current); - } - if (wsRef.current) { - wsRef.current.close(); - } - setIsClosingGracefully(false); - } else { - try { - const parsedData = JSON.parse(message); - const normalizedPoints = normalizeBatch(parsedData); - bufferRef.current.push(...normalizedPoints); - } catch (e) { - console.error("Failed to parse non-confirmation message as JSON:", e, message); - } - } - }; - - ws.onclose = (event) => { - console.log('WebSocket connection closed:', event.code, event.reason); - wsRef.current = null; - setIsClosingGracefully(false); - }; - - ws.onerror = (error) => { - console.error('WebSocket error:', error); - if (closingTimeoutRef.current) { - clearTimeout(closingTimeoutRef.current); - } - setIsClosingGracefully(false); - }; - } - - const updateRenderData = () => { - if (bufferRef.current.length > 0) { - const nextBatch = bufferRef.current.splice( - 0, - Math.min(bufferRef.current.length, chartSize) - ); - setRenderData((prevData) => - [...(Array.isArray(prevData) ? prevData : []), ...nextBatch].slice(-chartSize) - ); - } - }; - - let intervalId: NodeJS.Timeout | null = null; - if (dataStreaming) { - intervalId = setInterval(updateRenderData, intervalTime); - } - - return () => { - console.log("Cleanup function running."); - if (intervalId) { - clearInterval(intervalId); - } - - if (closingTimeoutRef.current) { - clearTimeout(closingTimeoutRef.current); - } - - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isClosingGracefully) { - console.log("Component unmounting or dependencies changed: Initiating graceful close during cleanup."); - wsRef.current.send('clientClosing'); - closingTimeoutRef.current = setTimeout(() => { - console.warn("Timeout: No 'confirmed closing' received during cleanup. Forcing WebSocket close."); - if (wsRef.current) { - wsRef.current.close(); - } - }, 5000); - } else if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) { - console.log("Forcing immediate WebSocket close during cleanup."); - wsRef.current.close(); - } - wsRef.current = null; - setIsClosingGracefully(false); - }; - }, [chartSize, batchesPerSecond, dataStreaming, isClosingGracefully]); - - return { renderData, sendProcessingConfig }; -} \ No newline at end of file From 382ea2c378cdcfc9c9d7ab058338e4f6dd81574b Mon Sep 17 00:00:00 2001 From: Maddie Date: Tue, 10 Mar 2026 20:58:38 -0700 Subject: [PATCH 4/8] format frontend websocket context timestamps --- frontend/context/WebSocketContext.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/context/WebSocketContext.tsx b/frontend/context/WebSocketContext.tsx index 019ca5d..7e09eb4 100644 --- a/frontend/context/WebSocketContext.tsx +++ b/frontend/context/WebSocketContext.tsx @@ -31,9 +31,16 @@ const DEFAULT_PROCESSING_CONFIG: ProcessingConfig = { n_channels: 4, }; +function formatTimestamp(raw: any): string { + const s = String(raw); + // ISO 8601: "2026-03-11T03:55:22.715574979Z" - extract "03:55:22" + if (s.includes('T')) return s.slice(11, 19); + return s; +} + function normalizeBatch(batch: any): DataPoint[] { - return batch.timestamps.map((time: number, i: number) => ({ - time: String(time), + return batch.timestamps.map((time: any, i: number) => ({ + time: formatTimestamp(time), signal1: batch.signals[0][i], signal2: batch.signals[1][i], signal3: batch.signals[2][i], From e79b8780c2770a0614a5d1ad7baa20ba8812b21d Mon Sep 17 00:00:00 2001 From: Maddie Date: Tue, 10 Mar 2026 21:58:11 -0700 Subject: [PATCH 5/8] update new session button design and location --- .../components/ui-header/settings-bar.tsx | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/frontend/components/ui-header/settings-bar.tsx b/frontend/components/ui-header/settings-bar.tsx index 2ed8f38..7f4d95b 100644 --- a/frontend/components/ui-header/settings-bar.tsx +++ b/frontend/components/ui-header/settings-bar.tsx @@ -1,6 +1,7 @@ 'use client'; -import { Menubar, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar'; +import { Menubar } from '@/components/ui/menubar'; +import { Plus } from 'lucide-react'; import { ProgressBar } from '@/components/ui/progressbar'; import { Button } from '@/components/ui/button'; import { useGlobalContext } from '@/context/GlobalContext'; @@ -283,14 +284,22 @@ export default function SettingsBar() { return (
- {/* Session ID, Tutorial */} + {/* Session ID, New, Tutorials */} Session {activeSessionId ?? 'ID'} - - Tutorials - + + {/* slider */} @@ -345,29 +354,6 @@ export default function SettingsBar() { ? 'Preparing...' : 'Save'} - - - - - Start a new session? - - Your current session is unsaved. Hitting confirm will clear the current pipeline. Any unsaved changes will be lost. - - -
- - - - -
-
-
+ + + + Start a new session? + + Your current session is unsaved. Hitting confirm will clear the current pipeline. Any unsaved changes will be lost. + + +
+ + + + +
+
+
+ Date: Tue, 10 Mar 2026 22:03:41 -0700 Subject: [PATCH 6/8] new button v2 --- .../components/ui-header/settings-bar.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/components/ui-header/settings-bar.tsx b/frontend/components/ui-header/settings-bar.tsx index 7f4d95b..23ff966 100644 --- a/frontend/components/ui-header/settings-bar.tsx +++ b/frontend/components/ui-header/settings-bar.tsx @@ -284,24 +284,27 @@ export default function SettingsBar() { return (
- {/* Session ID, New, Tutorials */} + {/* Session ID, Tutorials */} Session {activeSessionId ?? 'ID'} - + {/* New session button */} + + {/* slider */}
From 176f4b0e488daf92c58a5288aa14e2f50008fbcd Mon Sep 17 00:00:00 2001 From: Maddie Date: Tue, 10 Mar 2026 22:05:10 -0700 Subject: [PATCH 7/8] fix logo/header alignment --- frontend/components/ui-header/app-header.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/components/ui-header/app-header.tsx b/frontend/components/ui-header/app-header.tsx index 737bcfc..70e64e7 100644 --- a/frontend/components/ui-header/app-header.tsx +++ b/frontend/components/ui-header/app-header.tsx @@ -22,14 +22,14 @@ export default function AppHeader() { Logo
{/* update, issues */} -
+