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 */}