import { useState, useEffect, useRef, useMemo } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Badge } from "@/components/ui/badge" import { Toaster } from "@/components/ui/toaster" import { TooltipProvider } from "@/components/ui/tooltip" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { BrowserRouter, Routes, Route } from "react-router-dom" import { startAnalysis, getWorkflowStatus, getWorkflowResult, StockResult, WorkflowStatus, ActivityLogEntry, MCPStatus, LLMStatus, MetricEntry, UserApiKeys, } from "@/lib/api" import { AnalysisResponse } from "@/lib/types" import { TrendingUp, TrendingDown, Target, AlertTriangle, CheckCircle, XCircle, BarChart3, RefreshCw, Play, Copy, Download, Printer, Check, Pause, X, Loader2, Settings, Key, ChevronDown, ChevronUp, Database, } from "lucide-react" // Import new components import { ProcessFlow } from "@/components/ProcessFlow" import { StockSearch } from "@/components/StockSearch" import { ActivityLog } from "@/components/ActivityLog" import { MCPDataPanel } from "@/components/MCPDataPanel" const queryClient = new QueryClient() const App = () => ( } /> } /> ) export default App const defaultMCPStatus: MCPStatus = { fundamentals: 'idle', valuation: 'idle', volatility: 'idle', macro: 'idle', news: 'idle', sentiment: 'idle', } const defaultLLMStatus: LLMStatus = { groq: 'idle', gemini: 'idle', openrouter: 'idle', } // Helper to clean markdown formatting (strip asterisks, bold markers) const cleanMarkdown = (text: string): string => { return text .replace(/\*\*([^*]+)\*\*/g, '$1') // Remove **bold** .replace(/\*([^*]+)\*/g, '$1') // Remove *italic* .replace(/^\*\s*/gm, '') // Remove bullet asterisks at line start .trim() } // Parse structured SWOT line: "[M01] Revenue: $394.3B - Strong market position" // Returns { ref, metric, insight } or null if line doesn't match pattern interface SwotRow { ref: string metric: string insight: string } const parseSwotLine = (line: string): SwotRow | null => { // Clean markdown first const cleaned = cleanMarkdown(line) // Pattern: [M##] Metric: Value - Insight // Use " - " (space-hyphen-space) to avoid matching hyphens in dates like "2024-12-31" // Also handle: [M##] Metric: Value | Insight (pipe separator) const match = cleaned.match(/^\[?(M\d+)\]?\s*(.+?)\s+[-|]\s+(.+)$/i) if (match) { return { ref: match[1], metric: match[2].trim(), insight: match[3].trim() } } // Fallback: no ref pattern, just "Metric: Value - Insight" const fallback = cleaned.match(/^(.+?)\s+[-|]\s+(.+)$/) if (fallback) { return { ref: '', metric: fallback[1].trim(), insight: fallback[2].trim() } } // Last resort: just return the cleaned line as insight if (cleaned.length > 0) { return { ref: '', metric: '', insight: cleaned } } return null } const Index = () => { const [selectedStock, setSelectedStock] = useState(null) const [isLoading, setIsLoading] = useState(false) const [showResults, setShowResults] = useState(false) const [mainTab, setMainTab] = useState<"flow" | "results">("flow") const [analysisResult, setAnalysisResult] = useState(null) const [workflowId, setWorkflowId] = useState(null) // Workflow tracking const [currentStep, setCurrentStep] = useState('idle') const [completedSteps, setCompletedSteps] = useState([]) const [mcpStatus, setMcpStatus] = useState(defaultMCPStatus) const [llmStatus, setLlmStatus] = useState(defaultLLMStatus) const [activityLog, setActivityLog] = useState([]) const [metrics, setMetrics] = useState([]) const [revisionCount, setRevisionCount] = useState(0) const [score, setScore] = useState(0) const [llmProvider, setLlmProvider] = useState('') const [cacheHit, setCacheHit] = useState(false) const [isSearching, setIsSearching] = useState(false) const [isPaused, setIsPaused] = useState(false) const [hasError, setHasError] = useState(false) const [isAborted, setIsAborted] = useState(false) const [abortReason, setAbortReason] = useState('') const [userEvents, setUserEvents] = useState>([]) // User API keys (optional - for when server keys hit rate limits) const [userApiKeys, setUserApiKeys] = useState({}) const [showApiKeySettings, setShowApiKeySettings] = useState(false) // Cache dialog state const [showCacheDialog, setShowCacheDialog] = useState(false) const [pendingCacheWorkflowId, setPendingCacheWorkflowId] = useState(null) const [copied, setCopied] = useState(false) const pollingRef = useRef | null>(null) // Helper to add user events to log const addUserEvent = (message: string) => { setUserEvents(prev => [...prev, { timestamp: new Date().toISOString(), message }]) } // Browser Notification helper const notify = (title: string, body: string, type: 'success' | 'error' = 'success') => { if (Notification.permission === 'granted') { new Notification(title, { body, icon: type === 'success' ? '/favicon.ico' : undefined, tag: 'swot-notification', // Prevents duplicate notifications }) } } // Request notification permission on first stock selection const requestNotificationPermission = () => { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission() } } // Extracted polling logic to avoid duplication const startPolling = (workflowIdToUse: string) => { if (pollingRef.current) { clearInterval(pollingRef.current) } pollingRef.current = setInterval(async () => { try { const status = await getWorkflowStatus(workflowIdToUse) setRevisionCount(status.revision_count) setScore(status.score) setActivityLog(status.activity_log || []) setMetrics(status.metrics || []) // Merge MCP status - preserve failed/partial states (they persist for session) setMcpStatus(prev => { const newStatus = status.mcp_status || defaultMCPStatus return { fundamentals: prev.fundamentals === 'failed' || prev.fundamentals === 'partial' ? prev.fundamentals : newStatus.fundamentals, valuation: prev.valuation === 'failed' || prev.valuation === 'partial' ? prev.valuation : newStatus.valuation, volatility: prev.volatility === 'failed' || prev.volatility === 'partial' ? prev.volatility : newStatus.volatility, macro: prev.macro === 'failed' || prev.macro === 'partial' ? prev.macro : newStatus.macro, news: prev.news === 'failed' || prev.news === 'partial' ? prev.news : newStatus.news, sentiment: prev.sentiment === 'failed' || prev.sentiment === 'partial' ? prev.sentiment : newStatus.sentiment, } }) // Merge LLM status - preserve failed states (they persist for session) setLlmStatus(prev => { const newStatus = status.llm_status || defaultLLMStatus return { groq: prev.groq === 'failed' ? prev.groq : newStatus.groq, gemini: prev.gemini === 'failed' ? prev.gemini : newStatus.gemini, openrouter: prev.openrouter === 'failed' ? prev.openrouter : newStatus.openrouter, } }) if (status.provider_used) setLlmProvider(status.provider_used) // Update completed steps - accumulate rather than recalculate to handle loops const stepOrder = ['input', 'cache', 'researcher', 'analyzer', 'critic', 'output'] setCompletedSteps(prev => { const newCompleted = new Set(prev) const currentIdx = stepOrder.indexOf(status.current_step) // Mark all steps before current as completed for (let i = 0; i < currentIdx; i++) { newCompleted.add(stepOrder[i]) } return Array.from(newCompleted) }) // Only update currentStep for in-progress workflows to prevent output glow flash if (status.status !== 'completed') { setCurrentStep(status.current_step) } // Set cacheHit flag for ProcessFlow visualization if (status.data_source === 'cache') { setCacheHit(true) } if (status.status === "completed") { clearInterval(pollingRef.current!) pollingRef.current = null // Check if this was a cache hit - show dialog to let user choose if (status.data_source === 'cache') { setCacheHit(true) setCurrentStep('cache') setCompletedSteps(['input', 'cache']) setPendingCacheWorkflowId(workflowIdToUse) setShowCacheDialog(true) // Don't auto-proceed - wait for user choice return } // Normal flow - all steps completed // Set completed steps BEFORE the async fetch to prevent output from glowing prematurely setCompletedSteps(stepOrder) setCurrentStep('completed') const result = await getWorkflowResult(workflowIdToUse) setAnalysisResult(result) setIsLoading(false) setShowResults(true) setMainTab("results") // Notify user that report is ready (works even if tab is in background) notify("Analysis complete!", `SWOT report for ${selectedStock?.symbol || 'company'} is ready.`, 'success') } else if (status.status === "aborted") { clearInterval(pollingRef.current!) pollingRef.current = null setIsLoading(false) setIsAborted(true) setAbortReason(status.error || 'Critical failure - workflow aborted') // Notify user of abort (works even if tab is in background) notify("Analysis aborted", status.error || "Critical failure - workflow aborted", 'error') } else if (status.status === "error") { clearInterval(pollingRef.current!) pollingRef.current = null setIsLoading(false) setHasError(true) // Notify user of error (works even if tab is in background) notify("Analysis failed", "An error occurred during analysis.", 'error') } } catch (error) { console.error("Polling error:", error) } }, 700) } // Button state logic const buttonState = useMemo(() => { if (isAborted) return 'aborted' if (hasError) return 'error' if (analysisResult && !isLoading) return 'complete' if (isPaused) return 'paused' if (isLoading) return 'analyzing' return 'ready' }, [isAborted, hasError, analysisResult, isLoading, isPaused]) // Pause handler - stop polling const handlePause = () => { setIsPaused(true) if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } } // Resume handler - restart polling const handleResume = () => { if (!workflowId) return setIsPaused(false) startPolling(workflowId) } // Abort handler - cancel workflow const handleAbort = () => { if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } setIsLoading(false) setIsPaused(false) setHasError(false) setIsAborted(false) setAbortReason('') setCurrentStep('idle') setCompletedSteps([]) setAnalysisResult(null) setShowResults(false) setMcpStatus(defaultMCPStatus) setLlmStatus(defaultLLMStatus) } // Cache dialog: Use cached data const handleUseCached = async () => { if (!pendingCacheWorkflowId) return setShowCacheDialog(false) addUserEvent('Using cached analysis') // Animate cache → output transition setCurrentStep('output') setTimeout(async () => { setCompletedSteps(['input', 'cache', 'output']) setCurrentStep('completed') // Fetch both result and status (for metrics) const [result, status] = await Promise.all([ getWorkflowResult(pendingCacheWorkflowId), getWorkflowStatus(pendingCacheWorkflowId) ]) setAnalysisResult(result) setMetrics(status.metrics || []) setIsLoading(false) setShowResults(true) setMainTab("results") setPendingCacheWorkflowId(null) }, 800) } // Cache dialog: Run fresh analysis const handleRunFresh = async () => { if (!selectedStock) return setShowCacheDialog(false) setPendingCacheWorkflowId(null) addUserEvent('Running fresh analysis (cache bypassed)') // Reset state for fresh run setCurrentStep('input') setCompletedSteps([]) setMcpStatus(defaultMCPStatus) setLlmStatus(defaultLLMStatus) setActivityLog([]) setMetrics([]) setRevisionCount(0) setScore(0) setCacheHit(false) setAnalysisResult(null) try { const { workflow_id } = await startAnalysis( selectedStock.name, selectedStock.symbol, 'Competitive Position', true, // skipCache = true userApiKeys ) setWorkflowId(workflow_id) setCompletedSteps(['input']) setCurrentStep('cache') startPolling(workflow_id) } catch (error) { console.error("Error starting fresh analysis:", error) setIsLoading(false) setHasError(true) } } // Force dark mode useEffect(() => { document.documentElement.classList.add("dark") }, []) // Export functions const formatSwotForClipboard = () => { if (!analysisResult) return '' return `SWOT Analysis: ${analysisResult.company_name} Quality Score: ${analysisResult.score}/10 Revisions: ${analysisResult.revision_count} STRENGTHS: ${analysisResult.swot_data.strengths.map(s => `- ${cleanMarkdown(s)}`).join('\n')} WEAKNESSES: ${analysisResult.swot_data.weaknesses.map(w => `- ${cleanMarkdown(w)}`).join('\n')} OPPORTUNITIES: ${analysisResult.swot_data.opportunities.map(o => `- ${cleanMarkdown(o)}`).join('\n')} THREATS: ${analysisResult.swot_data.threats.map(t => `- ${cleanMarkdown(t)}`).join('\n')} QUALITY EVALUATION: ${analysisResult.critique} --- Generated by Instant SWOT Agent` } const copyToClipboard = async () => { try { await navigator.clipboard.writeText(formatSwotForClipboard()) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch (err) { console.error('Failed to copy:', err) } } const downloadAsJson = () => { if (!analysisResult) return const exportData = { ...analysisResult, exported_at: new Date().toISOString() } const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `swot-analysis-${analysisResult.company_name.toLowerCase().replace(/\s+/g, '-')}.json` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } const handleGenerate = async () => { if (!selectedStock) return addUserEvent(`Analysis started for ${selectedStock.symbol}`) setIsLoading(true) setShowResults(false) setCurrentStep('input') setCompletedSteps([]) setMcpStatus(defaultMCPStatus) setLlmStatus(defaultLLMStatus) setActivityLog([]) setMetrics([]) setRevisionCount(0) setScore(0) setCacheHit(false) setIsPaused(false) setHasError(false) setIsAborted(false) setAbortReason('') setAnalysisResult(null) try { const { workflow_id } = await startAnalysis( selectedStock.name, selectedStock.symbol, 'Competitive Position', false, // skipCache = false (check cache first) userApiKeys ) setWorkflowId(workflow_id) setCompletedSteps(['input']) setCurrentStep('cache') startPolling(workflow_id) } catch (error) { console.error("Error starting analysis:", error) setIsLoading(false) setHasError(true) } } useEffect(() => { return () => { if (pollingRef.current) { clearInterval(pollingRef.current) } } }, []) const getScoreColor = (score: number) => { if (score >= 7) return "text-emerald-400" if (score >= 5) return "text-yellow-400" return "text-red-400" } const getScoreBadge = (score: number) => { if (score >= 6) return { label: "", variant: "default" as const, icon: CheckCircle } return { label: "", variant: "destructive" as const, icon: XCircle } } const handleStockClear = () => { setSelectedStock(null) setShowResults(false) setAnalysisResult(null) setCurrentStep('idle') setCompletedSteps([]) setActivityLog([]) setMetrics([]) setUserEvents([]) setHasError(false) setIsAborted(false) setAbortReason('') setMcpStatus(defaultMCPStatus) setLlmStatus(defaultLLMStatus) } return ( setMainTab(v as "flow" | "results")} className="min-h-screen bg-background"> {/* Header */}

Instant SWOT Agent

with self-correcting feedback

{ setSelectedStock(stock) addUserEvent(`Selected: ${stock.name} (${stock.symbol})`) requestNotificationPermission() }} selectedStock={selectedStock} onClear={handleStockClear} disabled={isLoading} onSearchChange={setIsSearching} />
{/* Dynamic Submit/Control Buttons */}
{buttonState === 'ready' && ( )} {buttonState === 'analyzing' && ( <> )} {buttonState === 'paused' && ( <> )} {buttonState === 'complete' && ( )} {buttonState === 'error' && ( )} {buttonState === 'aborted' && ( )}
{/* Cache Hit Dialog */} {showCacheDialog && (
Cached Analysis Found

A recent analysis for {selectedStock?.symbol} was found in cache. Would you like to use the cached result or run a fresh analysis?

)} {/* API Key Settings (Expandable) */}
{showApiKeySettings && (

Provide your own API keys if server keys hit rate limits. Keys are not stored.

setUserApiKeys(prev => ({ ...prev, groq: e.target.value || undefined }))} className="w-full px-2 py-1 text-xs bg-background border rounded focus:outline-none focus:ring-1 focus:ring-ring" />
setUserApiKeys(prev => ({ ...prev, gemini: e.target.value || undefined }))} className="w-full px-2 py-1 text-xs bg-background border rounded focus:outline-none focus:ring-1 focus:ring-ring" />
setUserApiKeys(prev => ({ ...prev, openrouter: e.target.value || undefined }))} className="w-full px-2 py-1 text-xs bg-background border rounded focus:outline-none focus:ring-1 focus:ring-ring" />
)}
{/* Process Flow + Metrics Panel */}
{/* Results Tab - SWOT cards + metrics */} {(isLoading || showResults) && ( {analysisResult && (
{/* Results Header */}

{analysisResult.company_name} ({selectedStock?.symbol})

{selectedStock?.exchange}

{analysisResult.business_address && (

{analysisResult.business_address}

)}
{/* Metrics */}

Score

{analysisResult.score}/10

Revisions

{analysisResult.revision_count}

{(() => { const BadgeIcon = getScoreBadge(analysisResult.score).icon return })()} {getScoreBadge(analysisResult.score).label}
{/* Export Buttons */}
{/* MCP Source Data */} {metrics.length > 0 && ( )} {/* SWOT Analysis - Tables */}
{/* Strengths Table */}

Strengths

{analysisResult.swot_data.strengths.map((item, i) => { const parsed = parseSwotLine(item) if (!parsed) return null return ( ) })}
Ref Metric Insight
{parsed.ref} {parsed.metric} {parsed.insight}
{/* Weaknesses Table */}

Weaknesses

{analysisResult.swot_data.weaknesses.map((item, i) => { const parsed = parseSwotLine(item) if (!parsed) return null return ( ) })}
Ref Metric Insight
{parsed.ref} {parsed.metric} {parsed.insight}
{/* Opportunities Table */}

Opportunities

{analysisResult.swot_data.opportunities.map((item, i) => { const parsed = parseSwotLine(item) if (!parsed) return null return ( ) })}
Ref Metric Insight
{parsed.ref} {parsed.metric} {parsed.insight}
{/* Threats Table */}

Threats

{analysisResult.swot_data.threats.map((item, i) => { const parsed = parseSwotLine(item) if (!parsed) return null return ( ) })}
Ref Metric Insight
{parsed.ref} {parsed.metric} {parsed.insight}
{/* Data Quality Notes - Hidden for now, will revisit later */}
)}
)}
) } const NotFound = () => (

404

Page Not Found

)