From cdff6732b07d96afddeb73da95f227ef57dd2924 Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Fri, 1 May 2026 07:40:10 -0400 Subject: [PATCH] feat: manual metrics editor + past-analyses inline viewer on daily status - AnalysisSummaryCard gains an "Edit manually" affordance powered by the new ManualMetricsForm (add/remove categories, edit counts, recompute total + success rate). - New PastAnalysesViewer renders a tab-style selector of saved CSV analyses with an inline AnalysisResultsView panel. - Reorganizes the page so the manual-metrics path is reachable when no analysis_summary exists yet. - Sync package-lock to the recharts ^2.15.4 already declared in package.json. --- frontend/package-lock.json | 2 +- frontend/src/pages/DailyStatusDetail.jsx | 261 ++++++++++++++++++----- 2 files changed, 209 insertions(+), 54 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a535f56..d950083 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.0", - "recharts": "^2.12.7" + "recharts": "^2.15.4" }, "devDependencies": { "@babel/core": "^7.24.7", diff --git a/frontend/src/pages/DailyStatusDetail.jsx b/frontend/src/pages/DailyStatusDetail.jsx index 284e7cc..c645777 100644 --- a/frontend/src/pages/DailyStatusDetail.jsx +++ b/frontend/src/pages/DailyStatusDetail.jsx @@ -27,7 +27,7 @@ function getFieldValue(status, field) { // ── Analysis summary card ───────────────────────────────────────────────────── -function AnalysisSummaryCard({ summary }) { +function AnalysisSummaryCard({ summary, statusId, onSummaryUpdated }) { const { counts = {}, total, success_rate, computed_at } = summary; const COLORS = { Success: 'bg-green-100 text-green-800', @@ -37,33 +37,60 @@ function AnalysisSummaryCard({ summary }) { const EXTRA = ['bg-indigo-100 text-indigo-800', 'bg-yellow-100 text-yellow-800', 'bg-purple-100 text-purple-800', 'bg-pink-100 text-pink-800']; const catList = Object.entries(counts).sort(([, a], [, b]) => b - a); let extraIdx = 0; + + const [showForm, setShowForm] = useState(false); + return (

Session metrics

- {computed_at &&

Updated {fmtDateTime(computed_at)}

} -
-
- {catList.map(([cat, n]) => { - const cls = COLORS[cat] ?? EXTRA[extraIdx++ % EXTRA.length]; - return ( -
- {cat} - {n} -
- ); - })} -
- Total - {total} +
+ {computed_at &&

Updated {fmtDateTime(computed_at)}

} + {!showForm && ( + + )}
- {success_rate != null && ( -
- Success rate - {(success_rate * 100).toFixed(1)}% -
- )}
+ + {showForm ? ( + { + onSummaryUpdated({ ...updated, computed_at: new Date().toISOString() }); + setShowForm(false); + }} + onClose={() => setShowForm(false)} + /> + ) : ( +
+ {catList.map(([cat, n]) => { + const cls = COLORS[cat] ?? EXTRA[extraIdx++ % EXTRA.length]; + return ( +
+ {cat} + {n} +
+ ); + })} +
+ Total + {total} +
+ {success_rate != null && ( +
+ Success rate + {(success_rate * 100).toFixed(1)}% +
+ )} +
+ )}
); } @@ -379,6 +406,142 @@ export function AnalysisResultsView({ analysis }) { ); } +// ── Past analyses inline viewer ─────────────────────────────────────────────── + +function PastAnalysesViewer({ analyses, onDelete, onNewAnalysis }) { + const [selectedId, setSelectedId] = useState(() => analyses[0]?.id ?? null); + const [cache, setCache] = useState({}); + const [loadingId, setLoadingId] = useState(null); + const [confirmDelete, setConfirmDelete] = useState(null); + const [deleting, setDeleting] = useState(false); + + // Load full data whenever the selected id changes + useEffect(() => { + if (!selectedId || cache[selectedId]) return; + setLoadingId(selectedId); + analysesApi.get(selectedId) + .then((data) => setCache((prev) => ({ ...prev, [selectedId]: data }))) + .catch(() => {}) + .finally(() => setLoadingId(null)); + }, [selectedId]); + + // If the list changes (new analysis added / deleted), keep selection valid + useEffect(() => { + if (!analyses.length) { setSelectedId(null); return; } + if (!analyses.find((a) => a.id === selectedId)) setSelectedId(analyses[0].id); + }, [analyses]); + + async function handleDelete(id) { + setDeleting(true); + try { + await analysesApi.delete(id); + setCache((prev) => { const n = { ...prev }; delete n[id]; return n; }); + onDelete(id); + } finally { + setDeleting(false); + setConfirmDelete(null); + } + } + + if (!analyses.length) return null; + + const selected = analyses.find((a) => a.id === selectedId) ?? null; + const fullData = selectedId ? (cache[selectedId] ?? null) : null; + const isLoading = loadingId === selectedId; + + const categorySummary = (a) => + Object.entries(a.category_counts ?? {}) + .sort(([, x], [, y]) => y.total - x.total) + .map(([cat, d]) => `${cat}: ${d.total}`) + .join(' · '); + + return ( +
+ {/* Header */} +
+

+ Past Analyses ({analyses.length}) +

+
+ + {/* Tab selector — scrollable if many */} + {analyses.length > 1 && ( +
+ {analyses.map((a) => { + const active = a.id === selectedId; + return ( + + ); + })} +
+ )} + + {/* Selected analysis panel */} + {selected && ( +
+ {/* Panel header */} +
+
+

+ {selected.file_name ?? 'Unnamed file'} +

+

+ {fmtDateTime(selected.created_at)} + {' · '} + {selected.note_col} + {' · '} + {selected.total_note_rows} note rows + {categorySummary(selected) && ( + {categorySummary(selected)} + )} +

+
+ +
+ + {/* Panel body */} +
+ {isLoading && ( +

Loading…

+ )} + {!isLoading && fullData && } +
+
+ )} + + setConfirmDelete(null)} + onConfirm={() => handleDelete(confirmDelete)} + loading={deleting} + title="Delete Analysis" + message="Remove this analysis record? The raw CSV is not affected." + /> +
+ ); +} + // ── Page ────────────────────────────────────────────────────────────────────── export default function DailyStatusDetail() { @@ -498,18 +661,11 @@ export default function DailyStatusDetail() {
{/* Analysis summary (pinned metrics) */} - {status.analysis_summary && !showManualForm && ( - - )} - {showManualForm ? ( - { - setStatus((prev) => ({ ...prev, analysis_summary: { ...summary, computed_at: new Date().toISOString() } })); - setShowManualForm(false); - }} - onClose={() => setShowManualForm(false)} + onSummaryUpdated={(summary) => setStatus((prev) => ({ ...prev, analysis_summary: summary }))} /> ) : (
@@ -518,36 +674,35 @@ export default function DailyStatusDetail() { onClick={() => setShowManualForm(true)} className="text-xs text-gray-400 hover:text-indigo-600 border border-dashed border-gray-200 hover:border-indigo-300 rounded-lg px-3 py-1.5 transition-colors" > - {status.analysis_summary ? 'Edit metrics manually' : 'Set metrics manually'} + Set metrics manually
)} + {showManualForm && !status.analysis_summary && ( + { + setStatus((prev) => ({ ...prev, analysis_summary: { ...summary, computed_at: new Date().toISOString() } })); + setShowManualForm(false); + }} + onClose={() => setShowManualForm(false)} + /> + )} - {/* CSV Analysis — new analysis */} + {/* Past analyses — inline viewer */} + setAnalyses((prev) => prev.filter((x) => x.id !== deletedId))} + /> + + {/* CSV Analysis — after past analyses */} setAnalyses((prev) => [saved, ...prev])} onSummaryPushed={(summary) => setStatus((prev) => ({ ...prev, analysis_summary: { ...summary, computed_at: new Date().toISOString() } }))} /> - {/* Past analyses */} - {analyses.length > 0 && ( -
-

- Past Analyses ({analyses.length}) -

-
- {analyses.map((a) => ( - setAnalyses((prev) => prev.filter((x) => x.id !== deletedId))} - /> - ))} -
-
- )} - {/* Session file registry */}