refactor(frontend): unify CsvAnalysis state under buckets[]

This commit is contained in:
Experiments DB Dev
2026-05-01 07:56:22 -04:00
parent b9b8a4ef1d
commit ceaa3be200
+43 -22
View File
@@ -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 */}
<div className="grid grid-cols-3 gap-3">
{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 (
<CategoryBucket key={cat} name={cat} notes={notesInCat} colors={colors}
isOver={overBucket === cat}
onDragOver={() => setOverBucket(cat)}
<CategoryBucket key={b.name} name={b.name} notes={b.notes} colors={colors}
isOver={overBucket === b.name}
onDragOver={() => 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)}
/>
);