diff --git a/frontend/src/logs/index.tsx b/frontend/src/logs/index.tsx index 191bc450..a7b08eee 100644 --- a/frontend/src/logs/index.tsx +++ b/frontend/src/logs/index.tsx @@ -6,6 +6,7 @@ import { SERVER_URL } from '@/api'; interface Log { username: string; event: string; + interaction?: string; prompt: string; result: string; completion: string; @@ -14,6 +15,11 @@ interface Log { generation_type?: string; } +interface LogWithAnnotatedTimestamp extends Log { + secondsSinceLast: number; + secondsSinceStart: number; +} + // Collapsible component for prompt/result/completion function Collapsible({ text, maxWidth = 200 }: { text: any, maxWidth?: number }) { // If text is an object, render as JSON @@ -41,11 +47,11 @@ function secondsToHMS(seconds: number) { function EntriesTable({ entries }: { entries: Log[] }) { let lastTimestamp: number | null = null; const annotatedEntries = entries.map((entry) => { - const newEntry = { ...entry } as any; + const newEntry = { ...entry } as LogWithAnnotatedTimestamp; if (lastTimestamp !== null) { - newEntry.secondsSinceLast = (entry.timestamp - lastTimestamp) / 1000; + newEntry.secondsSinceLast = (entry.timestamp - lastTimestamp); } - newEntry.secondsSinceStart = (entry.timestamp - entries[0].timestamp) / 1000; + newEntry.secondsSinceStart = (entry.timestamp - entries[0].timestamp); lastTimestamp = entry.timestamp; return newEntry; }).reverse(); @@ -62,7 +68,7 @@ function EntriesTable({ entries }: { entries: Log[] }) { - {annotatedEntries.map((entry: any, i: number) => ( + {annotatedEntries.map((entry: LogWithAnnotatedTimestamp, i: number) => ( {secondsToHMS(entry.secondsSinceStart)} {entry.event}{entry.interaction && ` (${entry.interaction})`} @@ -76,12 +82,16 @@ function EntriesTable({ entries }: { entries: Log[] }) { ); } + function App() { const [logs, setLogs] = useState([]); const [username, setUsername] = useState(''); const [logSecret, setLogSecret] = useState(() => localStorage.getItem('logSecret') || ''); const logsRef = useRef([]); const pollingRef = useRef(null); + const [dragActive, setDragActive] = useState(false); + const [dragError, setDragError] = useState(null); + const [fileMode, setFileMode] = useState(false); // Helper: get log counts per username const getLogCounts = (logs: Log[]) => { @@ -92,9 +102,22 @@ function App() { return counts; }; - // Poll logs from server + // Helper: parse a log object (normalize timestamp, isBackend) + const parseLog = (x: any): Log => { + const ts = typeof x.timestamp === 'string' ? new Date(x.timestamp).getTime() / 1000 : x.timestamp; + const isBackend = ['suggestion_generated', 'reflection_generated', 'reflection_generated'].includes(x.event); + return { ...x, timestamp: ts, isBackend }; + }; + + // Helper: parse a JSONL string into Log[] + const parseLogFile = (text: string): Log[] => { + const lines = text.split(/\r?\n/).filter(Boolean); + return lines.map(line => parseLog(JSON.parse(line))); + }; + + // Poll logs from server (disabled if fileMode) useEffect(() => { - if (!logSecret) return; + if (!logSecret || fileMode) return; let stopped = false; async function pollLogs() { if (stopped) return; @@ -110,16 +133,7 @@ function App() { }); if (resp.ok) { const updates = await resp.json(); - let newLogs: Log[] = updates.map((log: { logs: Log[] }) => log.logs).flat(); - newLogs = newLogs.map((x) => { - const ts = typeof x.timestamp === 'string' ? new Date(x.timestamp).getTime() / 1000 : x.timestamp; - const isBackend = ['suggestion_generated', 'reflection_generated', 'reflection_generated'].includes(x.event); - return { - ...x, - timestamp: ts, - isBackend, - }; - }); + const newLogs: Log[] = updates.map((log: { logs: Log[] }) => log.logs).flat().map(parseLog); // Just append new logs (no deduplication) const allLogs = [...logsRef.current, ...newLogs]; logsRef.current = allLogs; @@ -137,8 +151,38 @@ function App() { stopped = true; if (pollingRef.current) clearTimeout(pollingRef.current); }; - }, [logSecret]); + }, [logSecret, fileMode]); + // Drag and drop file handler + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setDragActive(true); + }; + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setDragActive(false); + }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setDragActive(false); + setDragError(null); + const file = e.dataTransfer.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (event) => { + try { + // Try to parse as JSONL (one JSON per line) + const text = event.target?.result as string; + const parsed: Log[] = parseLogFile(text); + setLogs(parsed); + logsRef.current = parsed; + setFileMode(true); + } catch (err) { + setDragError('Failed to parse file: ' + (err as Error).message); + } + }; + reader.readAsText(file); + }; // Username datalist const availableUsernames = useMemo(() => { return Array.from(new Set(logs.map(x => x.username))).sort(); @@ -164,7 +208,21 @@ function App() { }, [desiredEntries]); return ( -
+
+ {dragActive && ( +
+ Drop a log file to view it +
+ )} + {dragError && ( +
{dragError}
+ )}
+ {fileMode && Viewing logs from file. Drag a new file to replace, or reload to return to server mode.}
-
);