import { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Eye, RefreshCw, Maximize2, X, Activity, Thermometer, Waves, Grid3X3, Box, AlertCircle, CheckCircle, Lightbulb, ChevronRight, ChevronDown, Folder, Play, ExternalLink, Zap, List, LucideIcon } from 'lucide-react'; import { useStudy } from '../context/StudyContext'; import { Card } from '../components/common/Card'; import Plot from 'react-plotly.js'; // ============================================================================ // Types // ============================================================================ interface IterationInfo { id: string; path: string; op2_file: string; modified: number; type: 'iteration' | 'best'; label?: string; } interface InsightInfo { type: string; name: string; description: string; category?: string; category_label?: string; applicable_to: string[]; } interface GeneratedFile { filename: string; insight_type: string; timestamp: string | null; size_kb: number; modified: number; } interface InsightResult { success: boolean; insight_type: string; insight_name?: string; linked_objective?: string | null; iteration?: string; plotly_figure: any; summary: Record; html_path: string | null; error?: string; } // ============================================================================ // Constants // ============================================================================ const INSIGHT_ICONS: Record = { zernike_wfe: Waves, zernike_opd_comparison: Waves, msf_zernike: Waves, stress_field: Activity, modal: Box, thermal: Thermometer, design_space: Grid3X3 }; const INSIGHT_COLORS: Record = { zernike_wfe: 'text-blue-400 bg-blue-500/10 border-blue-500/30', zernike_opd_comparison: 'text-blue-400 bg-blue-500/10 border-blue-500/30', msf_zernike: 'text-cyan-400 bg-cyan-500/10 border-cyan-500/30', stress_field: 'text-red-400 bg-red-500/10 border-red-500/30', modal: 'text-purple-400 bg-purple-500/10 border-purple-500/30', thermal: 'text-orange-400 bg-orange-500/10 border-orange-500/30', design_space: 'text-green-400 bg-green-500/10 border-green-500/30' }; const CATEGORY_COLORS: Record = { optical: 'border-blue-500/50 bg-blue-500/5', structural_static: 'border-red-500/50 bg-red-500/5', structural_dynamic: 'border-orange-500/50 bg-orange-500/5', structural_modal: 'border-purple-500/50 bg-purple-500/5', thermal: 'border-yellow-500/50 bg-yellow-500/5', kinematic: 'border-teal-500/50 bg-teal-500/5', design_exploration: 'border-green-500/50 bg-green-500/5', other: 'border-gray-500/50 bg-gray-500/5' }; // ============================================================================ // Main Component // ============================================================================ export default function Insights() { const navigate = useNavigate(); const { selectedStudy, isInitialized } = useStudy(); // State - Step-based workflow const [step, setStep] = useState<'select-iteration' | 'select-insight' | 'view-result'>('select-iteration'); // Data const [iterations, setIterations] = useState([]); const [availableInsights, setAvailableInsights] = useState([]); const [generatedFiles, setGeneratedFiles] = useState([]); // Selections const [selectedIteration, setSelectedIteration] = useState(null); const [selectedInsightType, setSelectedInsightType] = useState(null); // Result const [activeInsight, setActiveInsight] = useState(null); const [fullscreen, setFullscreen] = useState(false); // Loading states const [loadingIterations, setLoadingIterations] = useState(true); const [loadingInsights, setLoadingInsights] = useState(false); const [generating, setGenerating] = useState(false); const [error, setError] = useState(null); // Redirect if no study useEffect(() => { if (isInitialized && !selectedStudy) { navigate('/'); } }, [selectedStudy, navigate, isInitialized]); // Load iterations on mount (single fast API call + generated files for quick access) const loadIterations = useCallback(async () => { if (!selectedStudy) return; setLoadingIterations(true); setError(null); try { // Load iterations and generated files in parallel const [iterRes, genRes] = await Promise.all([ fetch(`/api/insights/studies/${selectedStudy.id}/iterations`), fetch(`/api/insights/studies/${selectedStudy.id}/generated`) ]); const [iterData, genData] = await Promise.all([ iterRes.json(), genRes.json() ]); const iters = iterData.iterations || []; setIterations(iters); setGeneratedFiles(genData.files || []); // Auto-select best design if available if (iters.length > 0) { const best = iters.find((i: IterationInfo) => i.type === 'best'); if (best) { setSelectedIteration(best); } else { setSelectedIteration(iters[0]); } } } catch (err) { console.error('Failed to load iterations:', err); setError('Failed to load iterations'); } finally { setLoadingIterations(false); } }, [selectedStudy]); // Load available insights (lazy - only when needed) const loadAvailableInsights = useCallback(async () => { if (!selectedStudy) return; setLoadingInsights(true); try { const [availRes, genRes] = await Promise.all([ fetch(`/api/insights/studies/${selectedStudy.id}/available`), fetch(`/api/insights/studies/${selectedStudy.id}/generated`) ]); const [availData, genData] = await Promise.all([ availRes.json(), genRes.json() ]); setAvailableInsights(availData.insights || []); setGeneratedFiles(genData.files || []); } catch (err) { console.error('Failed to load insights:', err); } finally { setLoadingInsights(false); } }, [selectedStudy]); // Initial load useEffect(() => { loadIterations(); }, [loadIterations]); // Load insights when moving to step 2 useEffect(() => { if (step === 'select-insight' && availableInsights.length === 0) { loadAvailableInsights(); } }, [step, availableInsights.length, loadAvailableInsights]); // Generate insight const handleGenerate = async () => { if (!selectedStudy || !selectedIteration || !selectedInsightType || generating) return; setGenerating(true); setError(null); try { const res = await fetch( `/api/insights/studies/${selectedStudy.id}/generate/${selectedInsightType}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ iteration: selectedIteration.id }) } ); if (!res.ok) { const errData = await res.json(); throw new Error(errData.detail || 'Generation failed'); } const result: InsightResult = await res.json(); const insightInfo = availableInsights.find(i => i.type === selectedInsightType); result.insight_name = insightInfo?.name || result.insight_type; setActiveInsight(result); setStep('view-result'); // Refresh generated files list loadAvailableInsights(); } catch (err: any) { setError(err.message || 'Failed to generate insight'); } finally { setGenerating(false); } }; // Quick view existing generated file const handleQuickView = async (file: GeneratedFile) => { if (!selectedStudy) return; window.open(`/api/insights/studies/${selectedStudy.id}/view/${file.insight_type}`, '_blank'); }; // Format timestamp const formatTime = (ts: number) => { const date = new Date(ts * 1000); return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; // Group insights by category const groupedInsights = useMemo(() => { const groups: Record = {}; availableInsights.forEach(insight => { const cat = insight.category || 'other'; if (!groups[cat]) { groups[cat] = []; } groups[cat].push(insight); }); return groups; }, [availableInsights]); const getIcon = (type: string): LucideIcon => INSIGHT_ICONS[type] || Eye; const getColorClass = (type: string) => INSIGHT_COLORS[type] || 'text-gray-400 bg-gray-500/10 border-gray-500/30'; // ============================================================================ // Render // ============================================================================ if (!isInitialized || !selectedStudy) { return (

Loading...

); } return (
{/* Header with Breadcrumb */}
1. Select Iteration 2. Choose Insight 3. View Result

Study Insights

{selectedStudy.name || selectedStudy.id}

{/* Error Banner */} {error && (

{error}

)} {/* Step 1: Select Iteration */} {step === 'select-iteration' && (
{loadingIterations ? (
Scanning for iterations...
) : iterations.length === 0 ? (

No Iterations Found

Run some optimization trials first to generate data.

) : (
{/* Best Design - Primary Option */} {(() => { const bestIter = iterations.find((i) => i.type === 'best'); if (!bestIter) return null; return ( ); })()} {/* Separator */}
or select specific iteration
{/* Dropdown for other iterations */} {(() => { const regularIters = iterations.filter((i) => i.type !== 'best'); if (regularIters.length === 0) return null; const selectedRegular = regularIters.find((i) => i.id === selectedIteration?.id); return (
{/* Show selected iteration details */} {selectedRegular && (
{selectedRegular.label || selectedRegular.id}
{selectedRegular.op2_file} • {formatTime(selectedRegular.modified)}
)}
); })()}
)}
{/* Continue Button */} {selectedIteration && (
)} {/* Previously Generated - Quick Access */} {generatedFiles.length > 0 && (

Quick access to existing visualizations

{generatedFiles.slice(0, 5).map((file) => { const Icon = getIcon(file.insight_type); return ( ); })}
)}
)} {/* Step 2: Select Insight Type */} {step === 'select-insight' && (
{/* Selection Summary */}
Selected Data Source
{selectedIteration?.label || selectedIteration?.id}
{loadingInsights ? (
Loading available insights...
) : availableInsights.length === 0 ? (

No Insights Available

The selected iteration may not have compatible data.

) : (
{Object.entries(groupedInsights).map(([category, insights]) => (

{category.replace(/_/g, ' ')}

{insights.map((insight) => { const Icon = getIcon(insight.type); const colorClass = getColorClass(insight.type); const isSelected = selectedInsightType === insight.type; return ( ); })}
))}
)}
{/* Generate Button */} {selectedInsightType && (
)}
)} {/* Step 3: View Result */} {step === 'view-result' && activeInsight && (
{/* Result Header */}

{activeInsight.insight_name || activeInsight.insight_type.replace(/_/g, ' ')}

Generated from {selectedIteration?.label || selectedIteration?.id}

{activeInsight.html_path && ( )}
{/* Summary Stats */} {activeInsight.summary && Object.keys(activeInsight.summary).length > 0 && (
{Object.entries(activeInsight.summary) .filter(([key]) => !key.startsWith('html_')) .slice(0, 8) .map(([key, value]) => (
{key.replace(/_/g, ' ')}
{typeof value === 'number' ? value.toFixed(2) : String(value) }
))}
)} {/* Plotly Figure */} {activeInsight.plotly_figure ? (
) : (

Insight Generated Successfully

This insight generates HTML files. Click "Open Full View" to see the visualization.

{activeInsight.summary?.html_files && (

Generated files:

    {(activeInsight.summary.html_files as string[]).slice(0, 4).map((f: string, i: number) => (
  • {f.split(/[/\\]/).pop()}
  • ))}
)}
)}
{/* Generate Another */}
)} {/* Fullscreen Modal */} {fullscreen && activeInsight?.plotly_figure && (

{activeInsight.insight_name || activeInsight.insight_type.replace(/_/g, ' ')}

)}
); }