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.}
-
);