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>
571 lines
21 KiB
TypeScript
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>
|
|
);
|
|
}
|