feat: add TalibPatternPanel, TrainingPanel, ModelSelector UI components (tasks 5-8)
- TalibPatternPanel: pattern checkboxes, detect button, results summary, clear-all and per-pattern delete - TrainingPanel: model type selector, dataset info, start training, polling, run history - ModelSelector: dropdown of completed runs, wired into PredictionPanel for model switching - page.tsx: integrate all three panels into sidebar, wire callbacks (model load, annotations refresh) - tasks.md: mark all 39 tasks complete
This commit is contained in:
parent
2a02669222
commit
12a9603fce
7 changed files with 849 additions and 23 deletions
305
src/components/TalibPatternPanel.tsx
Normal file
305
src/components/TalibPatternPanel.tsx
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ChevronDown, Trash2 } from 'lucide-react';
|
||||
|
||||
interface PatternInfo {
|
||||
function_name: string;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
interface DetectionResult {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface TalibPatternPanelProps {
|
||||
activeChartId: number | null;
|
||||
onAnnotationsChanged: () => void;
|
||||
getCandles: () => any[] | undefined;
|
||||
}
|
||||
|
||||
export default function TalibPatternPanel({
|
||||
activeChartId,
|
||||
onAnnotationsChanged,
|
||||
getCandles,
|
||||
}: TalibPatternPanelProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [patterns, setPatterns] = useState<PatternInfo[]>([]);
|
||||
const [selectedPatterns, setSelectedPatterns] = useState<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFetchingPatterns, setIsFetchingPatterns] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [results, setResults] = useState<DetectionResult[]>([]);
|
||||
const [deletingLabel, setDeletingLabel] = useState<string | null>(null);
|
||||
const [isDeletingAll, setIsDeletingAll] = useState(false);
|
||||
|
||||
// Fetch available patterns when panel expands
|
||||
useEffect(() => {
|
||||
if (expanded && patterns.length === 0) {
|
||||
setIsFetchingPatterns(true);
|
||||
fetch('/api/patterns/available')
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const list: PatternInfo[] = data.patterns || data;
|
||||
setPatterns(list);
|
||||
})
|
||||
.catch(() => setError('Failed to load patterns'))
|
||||
.finally(() => setIsFetchingPatterns(false));
|
||||
}
|
||||
}, [expanded, patterns.length]);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setSelectedPatterns(new Set(patterns.map((p) => p.function_name)));
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
setSelectedPatterns(new Set());
|
||||
};
|
||||
|
||||
const handleTogglePattern = (fn: string) => {
|
||||
setSelectedPatterns((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(fn)) next.delete(fn);
|
||||
else next.add(fn);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDetect = useCallback(async () => {
|
||||
if (!activeChartId) {
|
||||
setError('Load a chart first');
|
||||
return;
|
||||
}
|
||||
const candles = getCandles();
|
||||
if (!candles || candles.length === 0) {
|
||||
setError('No candle data available');
|
||||
return;
|
||||
}
|
||||
if (selectedPatterns.size === 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setResults([]);
|
||||
|
||||
try {
|
||||
// Detect patterns
|
||||
const detectRes = await fetch('/api/patterns/detect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
candles,
|
||||
patterns: Array.from(selectedPatterns),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!detectRes.ok) {
|
||||
const err = await detectRes.json();
|
||||
throw new Error(err.detail || err.error || 'Detection failed');
|
||||
}
|
||||
|
||||
const detectData = await detectRes.json();
|
||||
const annotations: any[] = detectData.annotations || [];
|
||||
|
||||
if (annotations.length === 0) {
|
||||
setResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save each annotation via POST /api/span-annotations
|
||||
await Promise.all(
|
||||
annotations.map((ann: any) =>
|
||||
fetch('/api/span-annotations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chart_id: activeChartId,
|
||||
start_time: ann.start_time,
|
||||
end_time: ann.end_time,
|
||||
label: ann.label,
|
||||
confidence: ann.confidence ?? null,
|
||||
source: 'talib',
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Build results summary
|
||||
const counts: Record<string, number> = {};
|
||||
for (const ann of annotations) {
|
||||
counts[ann.label] = (counts[ann.label] || 0) + 1;
|
||||
}
|
||||
setResults(
|
||||
Object.entries(counts).map(([label, count]) => ({ label, count }))
|
||||
);
|
||||
|
||||
onAnnotationsChanged();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Detection failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [activeChartId, getCandles, selectedPatterns, onAnnotationsChanged]);
|
||||
|
||||
const handleClearAll = async () => {
|
||||
if (!activeChartId) return;
|
||||
setIsDeletingAll(true);
|
||||
try {
|
||||
await fetch(
|
||||
`/api/span-annotations?source=talib&chartId=${activeChartId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
setResults([]);
|
||||
onAnnotationsChanged();
|
||||
} catch {
|
||||
setError('Failed to clear TA-Lib annotations');
|
||||
} finally {
|
||||
setIsDeletingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteByLabel = async (label: string) => {
|
||||
if (!activeChartId) return;
|
||||
setDeletingLabel(label);
|
||||
try {
|
||||
await fetch(
|
||||
`/api/span-annotations?source=talib&label=${encodeURIComponent(label)}&chartId=${activeChartId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
setResults((prev) => prev.filter((r) => r.label !== label));
|
||||
onAnnotationsChanged();
|
||||
} catch {
|
||||
setError('Failed to delete pattern annotations');
|
||||
} finally {
|
||||
setDeletingLabel(null);
|
||||
}
|
||||
};
|
||||
|
||||
const totalDetected = results.reduce((s, r) => s + r.count, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center justify-between py-1"
|
||||
>
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
TA-Lib Patterns
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 text-muted-foreground transition-transform ${expanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-1 space-y-2">
|
||||
{isFetchingPatterns && (
|
||||
<p className="text-[10px] text-muted-foreground">Loading patterns...</p>
|
||||
)}
|
||||
|
||||
{!isFetchingPatterns && patterns.length > 0 && (
|
||||
<>
|
||||
{/* Select All / Deselect All */}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={handleSelectAll}
|
||||
className="flex-1 px-1.5 py-0.5 text-[10px] bg-secondary text-secondary-foreground rounded hover:opacity-80"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeselectAll}
|
||||
className="flex-1 px-1.5 py-0.5 text-[10px] bg-secondary text-secondary-foreground rounded hover:opacity-80"
|
||||
>
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Pattern checkboxes */}
|
||||
<div className="max-h-40 overflow-y-auto scrollbar-thin space-y-0.5 pr-1">
|
||||
{patterns.map((p) => (
|
||||
<label
|
||||
key={p.function_name}
|
||||
className="flex items-center gap-1.5 p-0.5 rounded hover:bg-secondary/50 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPatterns.has(p.function_name)}
|
||||
onChange={() => handleTogglePattern(p.function_name)}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<span className="text-[10px] text-foreground truncate">{p.display_name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Detect button */}
|
||||
<button
|
||||
onClick={handleDetect}
|
||||
disabled={isLoading || selectedPatterns.size === 0 || !activeChartId}
|
||||
className="w-full px-2 py-1.5 text-[10px] bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{isLoading
|
||||
? 'Detecting...'
|
||||
: selectedPatterns.size === 0
|
||||
? 'Detect Patterns'
|
||||
: `Detect Patterns (${selectedPatterns.size} selected)`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<p className="text-[10px] text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Results summary */}
|
||||
{results.length > 0 && (
|
||||
<div className="p-2 bg-secondary/30 rounded space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
Found: {totalDetected} pattern{totalDetected !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
disabled={isDeletingAll}
|
||||
className="text-[10px] text-destructive hover:opacity-80 disabled:opacity-50"
|
||||
title="Clear all TA-Lib annotations"
|
||||
>
|
||||
{isDeletingAll ? 'Clearing...' : 'Clear All'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{results.map((r) => (
|
||||
<div key={r.label} className="flex items-center justify-between gap-1">
|
||||
<span className="text-[10px] text-foreground truncate flex-1">{r.label}</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground">{r.count}</span>
|
||||
<button
|
||||
onClick={() => handleDeleteByLabel(r.label)}
|
||||
disabled={deletingLabel === r.label}
|
||||
className="text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50"
|
||||
title={`Delete all ${r.label} annotations`}
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Clear All button when results are empty but TA-Lib annotations may exist */}
|
||||
{results.length === 0 && !isLoading && (
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
disabled={isDeletingAll || !activeChartId}
|
||||
className="w-full px-2 py-1 text-[10px] text-destructive border border-destructive/30 rounded hover:bg-destructive/10 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDeletingAll ? 'Clearing...' : 'Clear All TA-Lib'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue