Files
Atomizer/atomizer-dashboard/frontend/src/pages/Results.tsx
Anto01 7c700c4606 feat: Dashboard improvements and configuration updates
Dashboard:
- Enhanced terminal components (ClaudeTerminal, GlobalClaudeTerminal)
- Improved MarkdownRenderer for better documentation display
- Updated convergence plots (ConvergencePlot, PlotlyConvergencePlot)
- Refined Home, Analysis, Dashboard, Setup, Results pages
- Added StudyContext improvements
- Updated vite.config for better dev experience

Configuration:
- Updated CLAUDE.md with latest instructions
- Enhanced launch_dashboard.py
- Updated config.py settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 13:47:05 -05:00

571 lines
21 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../components/common/Card';
import { Button } from '../components/common/Button';
import {
Download,
FileText,
RefreshCw,
Sparkles,
Loader2,
AlertTriangle,
CheckCircle,
Copy,
Trophy,
TrendingUp,
FileJson,
FileSpreadsheet,
Settings,
ArrowRight,
ChevronDown,
ChevronUp,
Printer
} from 'lucide-react';
import { apiClient } from '../api/client';
import { useStudy } from '../context/StudyContext';
import { MarkdownRenderer } from '../components/MarkdownRenderer';
interface BestSolution {
best_trial: {
trial_number: number;
objective: number;
design_variables: Record<string, number>;
user_attrs?: Record<string, any>;
timestamp?: string;
} | null;
first_trial: {
trial_number: number;
objective: number;
design_variables: Record<string, number>;
} | null;
improvements: Record<string, {
initial: number;
final: number;
improvement_pct: number;
absolute_change: number;
}>;
total_trials: number;
}
export default function Results() {
const { selectedStudy, isInitialized } = useStudy();
const navigate = useNavigate();
const [reportContent, setReportContent] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [lastGenerated, setLastGenerated] = useState<string | null>(null);
const [bestSolution, setBestSolution] = useState<BestSolution | null>(null);
const [showAllParams, setShowAllParams] = useState(false);
const [exporting, setExporting] = useState<string | null>(null);
// Redirect if no study selected (but only after initialization completes)
useEffect(() => {
if (isInitialized && !selectedStudy) {
navigate('/');
}
}, [selectedStudy, navigate, isInitialized]);
// Load report and best solution when study changes
useEffect(() => {
if (selectedStudy) {
loadReport();
loadBestSolution();
}
}, [selectedStudy]);
// Show loading state while initializing (must be after all hooks)
if (!isInitialized) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-dark-400">Loading study...</p>
</div>
</div>
);
}
const loadReport = async () => {
if (!selectedStudy) return;
setLoading(true);
setError(null);
try {
const data = await apiClient.getStudyReport(selectedStudy.id);
setReportContent(data.content);
if (data.generated_at) {
setLastGenerated(data.generated_at);
}
} catch {
// No report yet - show placeholder
setReportContent(null);
} finally {
setLoading(false);
}
};
const loadBestSolution = async () => {
if (!selectedStudy) return;
try {
const data = await apiClient.getBestSolution(selectedStudy.id);
setBestSolution(data);
} catch {
setBestSolution(null);
}
};
const handleGenerate = async () => {
if (!selectedStudy) return;
setGenerating(true);
setError(null);
try {
const data = await apiClient.generateReport(selectedStudy.id);
setReportContent(data.content);
if (data.generated_at) {
setLastGenerated(data.generated_at);
}
} catch (err: any) {
setError(err.message || 'Failed to generate report');
} finally {
setGenerating(false);
}
};
const handleCopy = async () => {
if (reportContent) {
await navigator.clipboard.writeText(reportContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleDownload = () => {
if (!reportContent || !selectedStudy) return;
const blob = new Blob([reportContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedStudy.id}_report.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handlePrintPDF = () => {
if (!reportContent || !selectedStudy) return;
// Create a printable version of the report
const printWindow = window.open('', '_blank');
if (!printWindow) {
setError('Pop-up blocked. Please allow pop-ups to print PDF.');
return;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${selectedStudy.name} - Optimization Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 40px;
color: #1a1a1a;
line-height: 1.6;
}
h1 { color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 10px; }
h2 { color: #1e40af; margin-top: 30px; }
h3 { color: #3730a3; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f3f4f6; font-weight: 600; }
tr:nth-child(even) { background: #f9fafb; }
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-family: 'Monaco', monospace; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow-x: auto; }
pre code { background: transparent; padding: 0; }
blockquote { border-left: 4px solid #2563eb; margin: 20px 0; padding: 10px 20px; background: #eff6ff; }
.header-info { color: #666; margin-bottom: 30px; }
@media print {
body { padding: 20px; }
pre { white-space: pre-wrap; word-wrap: break-word; }
}
</style>
</head>
<body>
<div class="header-info">
<strong>Study:</strong> ${selectedStudy.name}<br>
<strong>Generated:</strong> ${new Date().toLocaleString()}<br>
<strong>Trials:</strong> ${selectedStudy.progress.current} / ${selectedStudy.progress.total}
</div>
${convertMarkdownToHTML(reportContent)}
</body>
</html>
`);
printWindow.document.close();
// Wait for content to load then print
printWindow.onload = () => {
printWindow.print();
};
};
// Simple markdown to HTML converter for print
const convertMarkdownToHTML = (md: string): string => {
return md
// Headers
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
// Bold and italic
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// Code blocks
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Lists
.replace(/^\s*[-*]\s+(.*)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)\n(?!<li>)/g, '</ul>$1\n')
.replace(/(?<!<\/ul>)(<li>)/g, '<ul>$1')
// Blockquotes
.replace(/^>\s*(.*)$/gm, '<blockquote>$1</blockquote>')
// Horizontal rules
.replace(/^---$/gm, '<hr>')
// Paragraphs
.replace(/\n\n/g, '</p><p>')
.replace(/^(.+)$/gm, (match) => {
if (match.startsWith('<')) return match;
return match;
});
};
const handleExport = async (format: 'csv' | 'json' | 'config') => {
if (!selectedStudy) return;
setExporting(format);
try {
const data = await apiClient.exportData(selectedStudy.id, format);
if (data.filename && data.content) {
const blob = new Blob([data.content], { type: data.content_type || 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = data.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} else if (format === 'json' && data.trials) {
// Direct JSON response
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedStudy.id}_data.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
} catch (err: any) {
setError(err.message || `Failed to export ${format}`);
} finally {
setExporting(null);
}
};
if (!selectedStudy) {
return null;
}
const paramEntries = bestSolution?.best_trial?.design_variables
? Object.entries(bestSolution.best_trial.design_variables)
: [];
const visibleParams = showAllParams ? paramEntries : paramEntries.slice(0, 6);
return (
<div className="h-full flex flex-col w-full">
{/* Header */}
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
<div>
<h1 className="text-2xl font-bold text-primary-400">Results</h1>
<p className="text-dark-400 text-sm">{selectedStudy.name}</p>
</div>
<div className="flex gap-2">
<Button
variant="primary"
icon={generating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
onClick={handleGenerate}
disabled={generating}
>
{generating ? 'Generating...' : reportContent ? 'Update Report' : 'Generate Report'}
</Button>
{reportContent && (
<>
<Button
variant="secondary"
icon={copied ? <CheckCircle className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
onClick={handleCopy}
>
{copied ? 'Copied!' : 'Copy'}
</Button>
<Button
variant="secondary"
icon={<Download className="w-4 h-4" />}
onClick={handleDownload}
>
Download
</Button>
</>
)}
</div>
</header>
{/* Error Message */}
{error && (
<div className="mb-4 p-4 bg-red-900/20 border border-red-800/30 rounded-lg">
<div className="flex items-center gap-2 text-red-400">
<AlertTriangle className="w-5 h-5" />
<span>{error}</span>
</div>
</div>
)}
{/* Best Solution Card */}
{bestSolution?.best_trial && (
<Card className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center">
<Trophy className="w-5 h-5 text-yellow-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Best Solution</h2>
<p className="text-sm text-dark-400">Trial #{bestSolution.best_trial.trial_number} of {bestSolution.total_trials}</p>
</div>
{bestSolution.improvements.objective && (
<div className="ml-auto flex items-center gap-2 px-4 py-2 bg-green-900/20 rounded-lg border border-green-800/30">
<TrendingUp className="w-5 h-5 text-green-400" />
<span className="text-green-400 font-bold text-lg">
{bestSolution.improvements.objective.improvement_pct > 0 ? '+' : ''}
{bestSolution.improvements.objective.improvement_pct.toFixed(1)}%
</span>
<span className="text-dark-400 text-sm">improvement</span>
</div>
)}
</div>
{/* Objective Value */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="bg-dark-700 rounded-lg p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Best Objective</div>
<div className="text-2xl font-bold text-primary-400">
{bestSolution.best_trial.objective.toExponential(4)}
</div>
</div>
{bestSolution.first_trial && (
<div className="bg-dark-700 rounded-lg p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Initial Value</div>
<div className="text-2xl font-bold text-dark-300">
{bestSolution.first_trial.objective.toExponential(4)}
</div>
</div>
)}
{bestSolution.improvements.objective && (
<div className="bg-dark-700 rounded-lg p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Absolute Change</div>
<div className="text-2xl font-bold text-green-400 flex items-center gap-2">
<ArrowRight className="w-5 h-5" />
{bestSolution.improvements.objective.absolute_change.toExponential(4)}
</div>
</div>
)}
</div>
{/* Design Variables */}
<div className="border-t border-dark-600 pt-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-dark-300">Optimal Design Variables</h3>
{paramEntries.length > 6 && (
<button
onClick={() => setShowAllParams(!showAllParams)}
className="text-xs text-primary-400 hover:text-primary-300 flex items-center gap-1"
>
{showAllParams ? (
<>Show Less <ChevronUp className="w-3 h-3" /></>
) : (
<>Show All ({paramEntries.length}) <ChevronDown className="w-3 h-3" /></>
)}
</button>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{visibleParams.map(([name, value]) => (
<div key={name} className="bg-dark-800 rounded px-3 py-2">
<div className="text-xs text-dark-400 truncate" title={name}>{name}</div>
<div className="text-sm font-mono text-white">
{typeof value === 'number' ? value.toFixed(4) : value}
</div>
</div>
))}
</div>
</div>
</Card>
)}
{/* Export Options */}
<Card className="mb-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Download className="w-5 h-5 text-primary-400" />
Export Data
</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<button
onClick={() => handleExport('csv')}
disabled={exporting !== null}
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50"
>
<FileSpreadsheet className="w-8 h-8 text-green-400" />
<div className="text-left">
<div className="text-sm font-medium text-white">CSV</div>
<div className="text-xs text-dark-400">Spreadsheet</div>
</div>
{exporting === 'csv' && <Loader2 className="w-4 h-4 animate-spin ml-auto" />}
</button>
<button
onClick={() => handleExport('json')}
disabled={exporting !== null}
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50"
>
<FileJson className="w-8 h-8 text-blue-400" />
<div className="text-left">
<div className="text-sm font-medium text-white">JSON</div>
<div className="text-xs text-dark-400">Full data</div>
</div>
{exporting === 'json' && <Loader2 className="w-4 h-4 animate-spin ml-auto" />}
</button>
<button
onClick={() => handleExport('config')}
disabled={exporting !== null}
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50"
>
<Settings className="w-8 h-8 text-purple-400" />
<div className="text-left">
<div className="text-sm font-medium text-white">Config</div>
<div className="text-xs text-dark-400">Settings</div>
</div>
{exporting === 'config' && <Loader2 className="w-4 h-4 animate-spin ml-auto" />}
</button>
<button
onClick={handleDownload}
disabled={!reportContent}
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<FileText className="w-8 h-8 text-orange-400" />
<div className="text-left">
<div className="text-sm font-medium text-white">Report</div>
<div className="text-xs text-dark-400">Markdown</div>
</div>
</button>
<button
onClick={handlePrintPDF}
disabled={!reportContent}
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Printer className="w-8 h-8 text-red-400" />
<div className="text-left">
<div className="text-sm font-medium text-white">PDF</div>
<div className="text-xs text-dark-400">Print report</div>
</div>
</button>
</div>
</Card>
{/* Main Content - Report */}
<div className="flex-1 min-h-0">
<Card className="h-full overflow-hidden flex flex-col">
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-primary-400" />
Report Content
</h2>
{lastGenerated && (
<span className="text-xs text-dark-400">
Last generated: {new Date(lastGenerated).toLocaleString()}
</span>
)}
</div>
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
{loading ? (
<div className="h-full flex flex-col items-center justify-center text-dark-300">
<RefreshCw className="w-8 h-8 animate-spin mb-3" />
<span>Loading report...</span>
</div>
) : reportContent ? (
<div className="p-2">
<MarkdownRenderer content={reportContent} />
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-dark-400">
<FileText className="w-16 h-16 mb-4 opacity-50" />
<h3 className="text-lg font-medium text-dark-300 mb-2">No Report Generated</h3>
<p className="text-sm text-center mb-6 max-w-md">
Click "Generate Report" to create an AI-generated analysis of your optimization results.
</p>
<Button
variant="primary"
icon={<Sparkles className="w-4 h-4" />}
onClick={handleGenerate}
disabled={generating}
>
Generate Report
</Button>
</div>
)}
</div>
</Card>
</div>
{/* Study Stats */}
<div className="mt-4 grid grid-cols-4 gap-4">
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
<div className="text-2xl font-bold text-white">{selectedStudy.progress.current}</div>
</div>
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Best Value</div>
<div className="text-2xl font-bold text-primary-400">
{selectedStudy.best_value?.toFixed(4) || 'N/A'}
</div>
</div>
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Target</div>
<div className="text-2xl font-bold text-dark-300">
{selectedStudy.target?.toFixed(4) || 'N/A'}
</div>
</div>
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Status</div>
<div className={`text-lg font-bold capitalize ${
selectedStudy.status === 'completed' ? 'text-green-400' :
selectedStudy.status === 'running' ? 'text-blue-400' : 'text-dark-400'
}`}>
{selectedStudy.status}
</div>
</div>
</div>
</div>
);
}