From ceaa3be200d06b479a44577dc18589a3ca2f96dc Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Fri, 1 May 2026 07:56:22 -0400 Subject: [PATCH] refactor(frontend): unify CsvAnalysis state under buckets[] --- frontend/src/components/CsvAnalysis.jsx | 65 ++++++++++++++++--------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/CsvAnalysis.jsx b/frontend/src/components/CsvAnalysis.jsx index 6b6b730..a7698cb 100644 --- a/frontend/src/components/CsvAnalysis.jsx +++ b/frontend/src/components/CsvAnalysis.jsx @@ -6,7 +6,12 @@ import RunSequenceView from './RunSequenceView'; // ── Constants ────────────────────────────────────────────────────────────────── -const DEFAULT_CATEGORIES = ['Success', 'Failure', 'Other']; +const DEFAULT_BUCKETS = [ + { name: 'Success', type: 'note', notes: [] }, + { name: 'Failure', type: 'note', notes: [] }, + { name: 'Other', type: 'note', notes: [] }, +]; +const DEFAULT_BUCKET_NAMES = new Set(DEFAULT_BUCKETS.map((b) => b.name)); const CATEGORY_COLORS = { Success: { bg: 'bg-green-100', border: 'border-green-300', text: 'text-green-800', dot: 'bg-green-500' }, @@ -106,15 +111,22 @@ export default function CsvAnalysis({ dailyStatusId, onSaved, onSummaryPushed }) // Classify phase const [rows, setRows] = useState([]); const [uniqueNotes, setUniqueNotes] = useState([]); - const [categories, setCategories] = useState(DEFAULT_CATEGORIES); - const [customCategories, setCustomCategories] = useState([]); - const [classifications, setClassifications] = useState({}); + const [buckets, setBuckets] = useState(DEFAULT_BUCKETS); const [selectedNote, setSelectedNote] = useState(null); const [draggedNote, setDraggedNote] = useState(null); const [overBucket, setOverBucket] = useState(null); const [newCatName, setNewCatName] = useState(''); const [showNewCat, setShowNewCat] = useState(false); + // Derived from buckets (recomputed every render — cheap) + const categories = buckets.map((b) => b.name); + const customCategories = buckets.filter((b) => !DEFAULT_BUCKET_NAMES.has(b.name)).map((b) => b.name); + const noteBuckets = buckets.filter((b) => b.type === 'note'); + const numericalBuckets = buckets.filter((b) => b.type === 'numerical'); + const classifications = Object.fromEntries( + noteBuckets.flatMap((b) => b.notes.map((n) => [n, b.name])) + ); + // Results phase const [results, setResults] = useState(null); const [savedAnalysisId, setSavedAnalysisId] = useState(null); @@ -166,7 +178,7 @@ export default function CsvAnalysis({ dailyStatusId, onSaved, onSummaryPushed }) const uniq = getUniqueNoteValues(parsed, noteCol); setRows(parsed); setUniqueNotes(uniq); - setClassifications({}); + setBuckets((prev) => prev.map((b) => ({ ...b, notes: [] }))); setSelectedNote(null); if (frameCol) setFrameCheck(checkFrameConsecutive(parsed, frameCol)); setParsePending(false); @@ -177,19 +189,31 @@ export default function CsvAnalysis({ dailyStatusId, onSaved, onSummaryPushed }) // ── Classification helpers ─────────────────────────────────────────────────── function assign(note, category) { - setClassifications(prev => ({ ...prev, [note]: category })); + setBuckets((prev) => prev.map((b) => { + if (b.name === category) { + return b.notes.includes(note) ? b : { ...b, notes: [...b.notes, note] }; + } + // Remove from any other bucket so a note belongs to at most one + return b.notes.includes(note) ? { ...b, notes: b.notes.filter((n) => n !== note) } : b; + })); setSelectedNote(null); } function unassign(note) { - setClassifications(prev => { const n = { ...prev }; delete n[note]; return n; }); + setBuckets((prev) => prev.map((b) => + b.notes.includes(note) ? { ...b, notes: b.notes.filter((n) => n !== note) } : b + )); } + function addBucket(name, type) { + const trimmed = name.trim(); + if (!trimmed || buckets.some((b) => b.name === trimmed)) return; + setBuckets((prev) => [...prev, { name: trimmed, type, notes: [] }]); + } + + // Back-compat shim so the existing "+ New category" UI keeps working until Task 9 swaps the picker function addCategory() { - const name = newCatName.trim(); - if (!name || categories.includes(name)) return; - setCategories(prev => [...prev, name]); - setCustomCategories(prev => [...prev, name]); + addBucket(newCatName, 'note'); setNewCatName(''); setShowNewCat(false); } @@ -266,13 +290,11 @@ export default function CsvAnalysis({ dailyStatusId, onSaved, onSummaryPushed }) setHeaders([]); setRows([]); setUniqueNotes([]); - setClassifications({}); + setBuckets(DEFAULT_BUCKETS); setResults(null); setSavedAnalysisId(null); setSummaryPushed(false); setSummaryError(null); - setCategories(DEFAULT_CATEGORIES); - setCustomCategories([]); setFrameCheck(null); } @@ -408,18 +430,17 @@ export default function CsvAnalysis({ dailyStatusId, onSaved, onSummaryPushed }) {/* Category buckets */}
- {categories.map(cat => { - const colors = getCategoryColors(cat, customCategories); - const notesInCat = uniqueNotes.filter(n => classifications[n] === cat); + {buckets.map((b) => { + const colors = getCategoryColors(b.name, customCategories); return ( - setOverBucket(cat)} + setOverBucket(b.name)} onDragLeave={() => setOverBucket(null)} onDrop={() => { - if (draggedNote) { assign(draggedNote, cat); setDraggedNote(null); setOverBucket(null); } + if (draggedNote) { assign(draggedNote, b.name); setDraggedNote(null); setOverBucket(null); } }} - onClickAssign={() => { if (selectedNote) assign(selectedNote, cat); }} + onClickAssign={() => { if (selectedNote) assign(selectedNote, b.name); }} onRemoveNote={(note) => unassign(note)} /> );