'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([]); const [selectedPatterns, setSelectedPatterns] = useState>(new Set()); const [isLoading, setIsLoading] = useState(false); const [isFetchingPatterns, setIsFetchingPatterns] = useState(false); const [error, setError] = useState(null); const [results, setResults] = useState([]); const [deletingLabel, setDeletingLabel] = useState(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: null, source: 'talib', }), }) ) ); // Build results summary const counts: Record = {}; 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 (
{expanded && (
{isFetchingPatterns && (

Loading patterns...

)} {!isFetchingPatterns && patterns.length > 0 && ( <> {/* Select All / Deselect All */}
{/* Pattern checkboxes */}
{patterns.map((p) => ( ))}
{/* Detect button */} )} {/* Error */} {error && (

{error}

)} {/* Results summary */} {results.length > 0 && (
Found: {totalDetected} pattern{totalDetected !== 1 ? 's' : ''}
{results.map((r) => (
{r.label} {r.count}
))}
)} {/* Clear All button when results are empty but TA-Lib annotations may exist */} {results.length === 0 && !isLoading && ( )}
)}
); }