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>
This commit is contained in:
@@ -227,7 +227,7 @@ export default function Analysis() {
|
||||
const isMultiObjective = (metadata?.objectives?.length || 0) > 1;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[2400px] mx-auto px-4">
|
||||
<div className="w-full">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||
<div>
|
||||
|
||||
@@ -375,7 +375,7 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[2400px] mx-auto px-4">
|
||||
<div className="w-full">
|
||||
{/* Alerts */}
|
||||
<div className="fixed top-4 right-4 z-50 space-y-2">
|
||||
{alerts.map(alert => (
|
||||
@@ -436,13 +436,21 @@ export default function Dashboard() {
|
||||
<StudyReportViewer studyId={selectedStudyId} />
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
// Open Optuna dashboard on port 8081
|
||||
// Note: The dashboard needs to be started separately with the correct study database
|
||||
window.open('http://localhost:8081', '_blank');
|
||||
onClick={async () => {
|
||||
if (!selectedStudyId) return;
|
||||
try {
|
||||
// Launch Optuna dashboard via API, then open the returned URL
|
||||
const result = await apiClient.launchOptunaDashboard(selectedStudyId);
|
||||
window.open(result.url || 'http://localhost:8081', '_blank');
|
||||
} catch (err) {
|
||||
// If launch fails (maybe already running), try opening directly
|
||||
console.warn('Failed to launch dashboard:', err);
|
||||
window.open('http://localhost:8081', '_blank');
|
||||
}
|
||||
}}
|
||||
className="btn-secondary"
|
||||
title="Open Optuna Dashboard (runs on port 8081)"
|
||||
title="Launch Optuna Dashboard for this study"
|
||||
disabled={!selectedStudyId}
|
||||
>
|
||||
Optuna Dashboard
|
||||
</button>
|
||||
|
||||
@@ -10,11 +10,16 @@ import {
|
||||
FileText,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ChevronRight,
|
||||
Target,
|
||||
Activity,
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
ArrowRight
|
||||
ArrowRight,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Maximize2,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import { Study } from '../types';
|
||||
@@ -28,8 +33,64 @@ const Home: React.FC = () => {
|
||||
const [readmeLoading, setReadmeLoading] = useState(false);
|
||||
const [sortField, setSortField] = useState<'name' | 'status' | 'trials' | 'bestValue'>('trials');
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||
const [expandedTopics, setExpandedTopics] = useState<Set<string>>(new Set());
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Group studies by topic, sorted by most recent first
|
||||
const studiesByTopic = useMemo(() => {
|
||||
const grouped: Record<string, Study[]> = {};
|
||||
studies.forEach(study => {
|
||||
const topic = study.topic || 'Other';
|
||||
if (!grouped[topic]) grouped[topic] = [];
|
||||
grouped[topic].push(study);
|
||||
});
|
||||
|
||||
// Sort studies within each topic by last_modified (most recent first)
|
||||
Object.keys(grouped).forEach(topic => {
|
||||
grouped[topic].sort((a, b) => {
|
||||
const aTime = a.last_modified ? new Date(a.last_modified).getTime() : 0;
|
||||
const bTime = b.last_modified ? new Date(b.last_modified).getTime() : 0;
|
||||
return bTime - aTime; // Descending (most recent first)
|
||||
});
|
||||
});
|
||||
|
||||
// Get most recent study time for each topic (for topic sorting)
|
||||
const topicMostRecent: Record<string, number> = {};
|
||||
Object.keys(grouped).forEach(topic => {
|
||||
const mostRecent = grouped[topic][0]?.last_modified;
|
||||
topicMostRecent[topic] = mostRecent ? new Date(mostRecent).getTime() : 0;
|
||||
});
|
||||
|
||||
// Sort topics by most recent study (most recent first), 'Other' always last
|
||||
const sortedTopics = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === 'Other') return 1;
|
||||
if (b === 'Other') return -1;
|
||||
return topicMostRecent[b] - topicMostRecent[a]; // Descending (most recent first)
|
||||
});
|
||||
|
||||
const result: Record<string, Study[]> = {};
|
||||
sortedTopics.forEach(topic => {
|
||||
result[topic] = grouped[topic];
|
||||
});
|
||||
return result;
|
||||
}, [studies]);
|
||||
|
||||
// Topics start collapsed by default - no initialization needed
|
||||
// Users can expand topics by clicking on them
|
||||
|
||||
const toggleTopic = (topic: string) => {
|
||||
setExpandedTopics(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(topic)) {
|
||||
next.delete(topic);
|
||||
} else {
|
||||
next.add(topic);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Load README when a study is selected for preview
|
||||
useEffect(() => {
|
||||
if (selectedPreview) {
|
||||
@@ -235,113 +296,105 @@ const Home: React.FC = () => {
|
||||
<p className="text-sm mt-1 text-dark-500">Create a new study to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-dark-750 z-10">
|
||||
<tr className="border-b border-dark-600">
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Study Name
|
||||
{sortField === 'name' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('status')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Status
|
||||
{sortField === 'status' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('trials')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Progress
|
||||
{sortField === 'trials' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('bestValue')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Best
|
||||
{sortField === 'bestValue' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedStudies.map((study) => {
|
||||
const completionPercent = study.progress.total > 0
|
||||
? Math.round((study.progress.current / study.progress.total) * 100)
|
||||
: 0;
|
||||
<div className="max-h-[500px] overflow-y-auto">
|
||||
{Object.entries(studiesByTopic).map(([topic, topicStudies]) => {
|
||||
const isExpanded = expandedTopics.has(topic);
|
||||
const topicTrials = topicStudies.reduce((sum, s) => sum + s.progress.current, 0);
|
||||
const runningCount = topicStudies.filter(s => s.status === 'running').length;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={study.id}
|
||||
onClick={() => setSelectedPreview(study)}
|
||||
className={`border-b border-dark-700 hover:bg-dark-750 transition-colors cursor-pointer ${
|
||||
selectedPreview?.id === study.id ? 'bg-primary-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white font-medium truncate max-w-[200px]">
|
||||
{study.name || study.id}
|
||||
</span>
|
||||
{study.name && (
|
||||
<span className="text-xs text-dark-500 truncate max-w-[200px]">{study.id}</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(study.status)}`}>
|
||||
{getStatusIcon(study.status)}
|
||||
{study.status}
|
||||
return (
|
||||
<div key={topic} className="border-b border-dark-600 last:border-b-0">
|
||||
{/* Topic Header */}
|
||||
<button
|
||||
onClick={() => toggleTopic(topic)}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="w-5 h-5 text-primary-400" />
|
||||
) : (
|
||||
<Folder className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
<span className="text-white font-medium">{topic.replace(/_/g, ' ')}</span>
|
||||
<span className="text-dark-500 text-sm">({topicStudies.length})</span>
|
||||
{runningCount > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-400 bg-green-500/10 px-2 py-0.5 rounded-full">
|
||||
<Play className="w-3 h-3" />
|
||||
{runningCount} running
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-dark-600 rounded-full overflow-hidden max-w-[80px]">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
completionPercent >= 100 ? 'bg-green-500' :
|
||||
completionPercent >= 50 ? 'bg-primary-500' :
|
||||
'bg-yellow-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(completionPercent, 100)}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-dark-400 text-sm">{topicTrials.toLocaleString()} trials</span>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-dark-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-dark-400" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Topic Studies */}
|
||||
{isExpanded && (
|
||||
<div className="bg-dark-850">
|
||||
{topicStudies.map((study) => {
|
||||
const completionPercent = study.progress.total > 0
|
||||
? Math.round((study.progress.current / study.progress.total) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={study.id}
|
||||
onClick={() => setSelectedPreview(study)}
|
||||
className={`px-4 py-3 pl-12 flex items-center gap-4 border-t border-dark-700 hover:bg-dark-700 transition-colors cursor-pointer ${
|
||||
selectedPreview?.id === study.id ? 'bg-primary-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Study Name */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-white font-medium truncate block">
|
||||
{study.name || study.id}
|
||||
</span>
|
||||
{study.name && (
|
||||
<span className="text-xs text-dark-500 truncate block">{study.id}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(study.status)}`}>
|
||||
{getStatusIcon(study.status)}
|
||||
{study.status}
|
||||
</span>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="flex items-center gap-2 w-32">
|
||||
<div className="flex-1 h-2 bg-dark-600 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
completionPercent >= 100 ? 'bg-green-500' :
|
||||
completionPercent >= 50 ? 'bg-primary-500' :
|
||||
'bg-yellow-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(completionPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-dark-400 text-xs font-mono w-14 text-right">
|
||||
{study.progress.current}/{study.progress.total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Best Value */}
|
||||
<span className={`font-mono text-sm w-20 text-right ${study.best_value !== null ? 'text-primary-400' : 'text-dark-500'}`}>
|
||||
{study.best_value !== null ? study.best_value.toExponential(2) : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-dark-400 text-sm font-mono w-16">
|
||||
{study.progress.current}/{study.progress.total}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`font-mono text-sm ${study.best_value !== null ? 'text-primary-400' : 'text-dark-500'}`}>
|
||||
{study.best_value !== null ? study.best_value.toExponential(3) : 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -375,21 +428,30 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Study Quick Stats */}
|
||||
<div className="px-6 py-3 border-b border-dark-600 flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(selectedPreview.status)}
|
||||
<span className="text-dark-300 capitalize">{selectedPreview.status}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-dark-400">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span>{selectedPreview.progress.current} / {selectedPreview.progress.total} trials</span>
|
||||
</div>
|
||||
{selectedPreview.best_value !== null && (
|
||||
<div className="flex items-center gap-2 text-primary-400">
|
||||
<Target className="w-4 h-4" />
|
||||
<span>Best: {selectedPreview.best_value.toExponential(4)}</span>
|
||||
<div className="px-6 py-3 border-b border-dark-600 flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(selectedPreview.status)}
|
||||
<span className="text-dark-300 capitalize">{selectedPreview.status}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-dark-400">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span>{selectedPreview.progress.current} / {selectedPreview.progress.total} trials</span>
|
||||
</div>
|
||||
{selectedPreview.best_value !== null && (
|
||||
<div className="flex items-center gap-2 text-primary-400">
|
||||
<Target className="w-4 h-4" />
|
||||
<span>Best: {selectedPreview.best_value.toExponential(4)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
className="p-2 hover:bg-dark-700 rounded-lg transition-colors text-dark-400 hover:text-white"
|
||||
title="View fullscreen"
|
||||
>
|
||||
<Maximize2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* README Content */}
|
||||
@@ -400,7 +462,7 @@ const Home: React.FC = () => {
|
||||
Loading documentation...
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownRenderer content={readme} />
|
||||
<MarkdownRenderer content={readme} studyId={selectedPreview.id} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -416,6 +478,59 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Fullscreen README Modal */}
|
||||
{isFullscreen && selectedPreview && (
|
||||
<div className="fixed inset-0 z-50 bg-dark-900/95 backdrop-blur-sm overflow-hidden">
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Modal Header */}
|
||||
<div className="flex-shrink-0 px-8 py-4 border-b border-dark-700 flex items-center justify-between bg-dark-800">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-dark-700 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
{selectedPreview.name || selectedPreview.id}
|
||||
</h2>
|
||||
<p className="text-dark-400 text-sm">Study Documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => handleSelectStudy(selectedPreview)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-500
|
||||
text-white rounded-lg transition-all font-medium"
|
||||
>
|
||||
Open Study
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsFullscreen(false)}
|
||||
className="p-2 hover:bg-dark-700 rounded-lg transition-colors text-dark-400 hover:text-white"
|
||||
title="Close fullscreen"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{readmeLoading ? (
|
||||
<div className="flex items-center justify-center py-16 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading documentation...
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownRenderer content={readme} studyId={selectedPreview.id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -295,7 +295,7 @@ export default function Results() {
|
||||
const visibleParams = showAllParams ? paramEntries : paramEntries.slice(0, 6);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col max-w-[2400px] mx-auto px-4">
|
||||
<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>
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function Setup() {
|
||||
}, 1) || 0;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[2400px] mx-auto px-4">
|
||||
<div className="w-full">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user