feat: Improve dashboard performance and Claude terminal context

- Add trial limiting (300 max) and reduce polling to 15s for large studies
- Make dashboard layout wider with col-span adjustments
- Claude terminal now runs from Atomizer root for CLAUDE.md/skills access
- Add study context display in terminal on connect
- Add KaTeX math rendering styles for study reports
- Add surrogate tuner module for hyperparameter optimization
- Fix backend proxy to port 8001

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-04 17:36:00 -05:00
parent 9eed4d81eb
commit f8b90156b3
13 changed files with 1481 additions and 141 deletions

View File

@@ -54,6 +54,8 @@ export default function Dashboard() {
const [alertIdCounter, setAlertIdCounter] = useState(0);
const [expandedTrials, setExpandedTrials] = useState<Set<number>>(new Set());
const [sortBy, setSortBy] = useState<'performance' | 'chronological'>('performance');
const [trialsPage, setTrialsPage] = useState(0);
const trialsPerPage = 50; // Limit trials per page for performance
// Parameter Space axis selection
const [paramXIndex, setParamXIndex] = useState(0);
@@ -99,6 +101,9 @@ export default function Dashboard() {
});
// Load initial trial history when study changes
// PERFORMANCE: Use limit to avoid loading thousands of trials at once
const MAX_TRIALS_LOAD = 300;
useEffect(() => {
if (selectedStudyId) {
setAllTrials([]);
@@ -106,74 +111,63 @@ export default function Dashboard() {
setPrunedCount(0);
setExpandedTrials(new Set());
apiClient.getStudyHistory(selectedStudyId)
// Single history fetch with limit - used for both trial list and charts
// This replaces the duplicate fetch calls
fetch(`/api/optimization/studies/${selectedStudyId}/history?limit=${MAX_TRIALS_LOAD}`)
.then(res => res.json())
.then(data => {
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
// Set trials for the trial list
const validTrials = data.trials.filter((t: any) => t.objective !== null && t.objective !== undefined);
setAllTrials(validTrials);
if (validTrials.length > 0) {
const minObj = Math.min(...validTrials.map(t => t.objective));
const minObj = Math.min(...validTrials.map((t: any) => t.objective));
setBestValue(minObj);
}
})
.catch(console.error);
apiClient.getStudyPruning(selectedStudyId)
.then(data => {
// Use count if available (new API), fallback to array length (legacy)
setPrunedCount(data.count ?? data.pruned_trials?.length ?? 0);
})
.catch(console.error);
// Protocol 13: Fetch metadata
fetch(`/api/optimization/studies/${selectedStudyId}/metadata`)
.then(res => res.json())
.then(data => {
setStudyMetadata(data);
})
.catch(err => console.error('Failed to load metadata:', err));
// Protocol 13: Fetch Pareto front (raw format for Protocol 13 components)
fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`)
.then(res => res.json())
.then(paretoData => {
console.log('[Dashboard] Pareto front data:', paretoData);
if (paretoData.is_multi_objective && paretoData.pareto_front) {
console.log('[Dashboard] Setting Pareto front with', paretoData.pareto_front.length, 'trials');
setParetoFront(paretoData.pareto_front);
} else {
console.log('[Dashboard] No Pareto front or not multi-objective');
setParetoFront([]);
}
})
.catch(err => console.error('Failed to load Pareto front:', err));
// Fetch ALL trials (not just Pareto) for parallel coordinates and charts
fetch(`/api/optimization/studies/${selectedStudyId}/history`)
.then(res => res.json())
.then(data => {
// Transform to match the format expected by charts
// API returns 'objectives' (array) for multi-objective, 'objective' (number) for single
// Transform for charts (parallel coordinates, etc.)
const trialsData = data.trials.map((t: any) => {
// Build values array: use objectives if available, otherwise wrap single objective
let values: number[] = [];
if (t.objectives && Array.isArray(t.objectives)) {
values = t.objectives;
} else if (t.objective !== null && t.objective !== undefined) {
values = [t.objective];
}
return {
trial_number: t.trial_number,
values,
params: t.design_variables || {},
user_attrs: t.user_attrs || {},
constraint_satisfied: t.constraint_satisfied !== false,
source: t.source || t.user_attrs?.source || 'FEA' // FEA vs NN differentiation
source: t.source || t.user_attrs?.source || 'FEA'
};
});
setAllTrialsRaw(trialsData);
})
.catch(err => console.error('Failed to load all trials:', err));
.catch(console.error);
apiClient.getStudyPruning(selectedStudyId)
.then(data => {
setPrunedCount(data.count ?? data.pruned_trials?.length ?? 0);
})
.catch(console.error);
// Fetch metadata (small payload)
fetch(`/api/optimization/studies/${selectedStudyId}/metadata`)
.then(res => res.json())
.then(data => setStudyMetadata(data))
.catch(err => console.error('Failed to load metadata:', err));
// Fetch Pareto front (usually small)
fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`)
.then(res => res.json())
.then(paretoData => {
if (paretoData.is_multi_objective && paretoData.pareto_front) {
setParetoFront(paretoData.pareto_front);
} else {
setParetoFront([]);
}
})
.catch(err => console.error('Failed to load Pareto front:', err));
}
}, [selectedStudyId]);
@@ -194,41 +188,77 @@ export default function Dashboard() {
setDisplayedTrials(sorted);
}, [allTrials, sortBy]);
// Auto-refresh polling (every 3 seconds) for trial history
// Auto-refresh polling for trial history
// PERFORMANCE: Use limit and longer interval for large studies
useEffect(() => {
if (!selectedStudyId) return;
const refreshInterval = setInterval(() => {
apiClient.getStudyHistory(selectedStudyId)
// Only fetch latest trials, not the entire history
fetch(`/api/optimization/studies/${selectedStudyId}/history?limit=${MAX_TRIALS_LOAD}`)
.then(res => res.json())
.then(data => {
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
const validTrials = data.trials.filter((t: any) => t.objective !== null && t.objective !== undefined);
setAllTrials(validTrials);
if (validTrials.length > 0) {
const minObj = Math.min(...validTrials.map(t => t.objective));
const minObj = Math.min(...validTrials.map((t: any) => t.objective));
setBestValue(minObj);
}
// Also update chart data
const trialsData = data.trials.map((t: any) => {
let values: number[] = [];
if (t.objectives && Array.isArray(t.objectives)) {
values = t.objectives;
} else if (t.objective !== null && t.objective !== undefined) {
values = [t.objective];
}
return {
trial_number: t.trial_number,
values,
params: t.design_variables || {},
user_attrs: t.user_attrs || {},
constraint_satisfied: t.constraint_satisfied !== false,
source: t.source || t.user_attrs?.source || 'FEA'
};
});
setAllTrialsRaw(trialsData);
})
.catch(err => console.error('Auto-refresh failed:', err));
}, 3000); // Poll every 3 seconds
}, 15000); // Poll every 15 seconds for performance
return () => clearInterval(refreshInterval);
}, [selectedStudyId]);
// Prepare chart data with proper null/undefined handling
const convergenceData: ConvergenceDataPoint[] = allTrials
.filter(t => t.objective !== null && t.objective !== undefined)
.sort((a, b) => a.trial_number - b.trial_number)
.map((trial, idx, arr) => {
const previousTrials = arr.slice(0, idx + 1);
const validObjectives = previousTrials.map(t => t.objective).filter(o => o !== null && o !== undefined);
return {
trial_number: trial.trial_number,
objective: trial.objective,
best_so_far: validObjectives.length > 0 ? Math.min(...validObjectives) : trial.objective,
};
});
// Sample data for charts when there are too many trials (performance optimization)
const MAX_CHART_POINTS = 200; // Reduced for better performance
const sampleData = <T,>(data: T[], maxPoints: number): T[] => {
if (data.length <= maxPoints) return data;
const step = Math.ceil(data.length / maxPoints);
return data.filter((_, i) => i % step === 0 || i === data.length - 1);
};
const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials
// Prepare chart data with proper null/undefined handling
const allValidTrials = allTrials
.filter(t => t.objective !== null && t.objective !== undefined)
.sort((a, b) => a.trial_number - b.trial_number);
// Calculate best_so_far for each trial
let runningBest = Infinity;
const convergenceDataFull: ConvergenceDataPoint[] = allValidTrials.map(trial => {
if (trial.objective < runningBest) {
runningBest = trial.objective;
}
return {
trial_number: trial.trial_number,
objective: trial.objective,
best_so_far: runningBest,
};
});
// Sample for chart rendering performance
const convergenceData = sampleData(convergenceDataFull, MAX_CHART_POINTS);
const parameterSpaceDataFull: ParameterSpaceDataPoint[] = allTrials
.filter(t => t.objective !== null && t.objective !== undefined && t.design_variables)
.map(trial => {
const params = Object.values(trial.design_variables);
@@ -241,6 +271,9 @@ export default function Dashboard() {
};
});
// Sample for chart rendering performance
const parameterSpaceData = sampleData(parameterSpaceDataFull, MAX_CHART_POINTS);
// Calculate average objective
const validObjectives = allTrials.filter(t => t.objective !== null && t.objective !== undefined).map(t => t.objective);
const avgObjective = validObjectives.length > 0
@@ -384,14 +417,14 @@ export default function Dashboard() {
</div>
</header>
<div className="grid grid-cols-12 gap-6">
{/* Control Panel - Left Sidebar */}
<aside className="col-span-3">
<div className="grid grid-cols-12 gap-4">
{/* Control Panel - Left Sidebar (smaller) */}
<aside className="col-span-2">
<ControlPanel onStatusChange={refreshStudies} />
</aside>
{/* Main Content - shrinks when chat is open */}
<main className={chatOpen ? 'col-span-5' : 'col-span-9'}>
{/* Main Content - takes most of the space */}
<main className={chatOpen ? 'col-span-6' : 'col-span-10'}>
{/* Study Name Header */}
{selectedStudyId && (
<div className="mb-4 pb-3 border-b border-dark-600">
@@ -694,12 +727,12 @@ export default function Dashboard() {
</ExpandableChart>
</div>
{/* Trial History with Sort Controls */}
{/* Trial History with Sort Controls and Pagination */}
<Card
title={
<div className="flex items-center justify-between w-full">
<span>Trial History ({displayedTrials.length} trials)</span>
<div className="flex gap-2">
<div className="flex gap-2 items-center">
<button
onClick={() => setSortBy('performance')}
className={`px-3 py-1 rounded text-sm ${
@@ -720,13 +753,35 @@ export default function Dashboard() {
>
Newest First
</button>
{/* Pagination controls */}
{displayedTrials.length > trialsPerPage && (
<div className="flex items-center gap-1 ml-2 border-l border-dark-500 pl-2">
<button
onClick={() => setTrialsPage(Math.max(0, trialsPage - 1))}
disabled={trialsPage === 0}
className="px-2 py-1 text-sm bg-dark-500 rounded disabled:opacity-50 hover:bg-dark-400"
>
</button>
<span className="text-xs text-dark-300 px-2">
{trialsPage + 1}/{Math.ceil(displayedTrials.length / trialsPerPage)}
</span>
<button
onClick={() => setTrialsPage(Math.min(Math.ceil(displayedTrials.length / trialsPerPage) - 1, trialsPage + 1))}
disabled={trialsPage >= Math.ceil(displayedTrials.length / trialsPerPage) - 1}
className="px-2 py-1 text-sm bg-dark-500 rounded disabled:opacity-50 hover:bg-dark-400"
>
</button>
</div>
)}
</div>
</div>
}
>
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{displayedTrials.length > 0 ? (
displayedTrials.map(trial => {
displayedTrials.slice(trialsPage * trialsPerPage, (trialsPage + 1) * trialsPerPage).map(trial => {
const isExpanded = expandedTrials.has(trial.trial_number);
const isBest = trial.objective === bestValue;
@@ -879,9 +934,9 @@ export default function Dashboard() {
</div>
</main>
{/* Claude Code Terminal - Right Sidebar */}
{/* Claude Code Terminal - Right Sidebar (taller for better visibility) */}
{chatOpen && (
<aside className="col-span-4 h-[calc(100vh-12rem)] sticky top-24">
<aside className="col-span-4 h-[calc(100vh-8rem)] sticky top-20">
<ClaudeTerminal
isExpanded={chatExpanded}
onToggleExpand={() => setChatExpanded(!chatExpanded)}

View File

@@ -21,6 +21,7 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { apiClient } from '../api/client';
@@ -101,11 +102,24 @@ const Home: React.FC = () => {
}
};
// Sort studies: running first, then by trial count
// Study sort options
const [studySort, setStudySort] = useState<'date' | 'running' | 'trials'>('date');
// Sort studies based on selected sort option
const sortedStudies = [...studies].sort((a, b) => {
if (a.status === 'running' && b.status !== 'running') return -1;
if (b.status === 'running' && a.status !== 'running') return 1;
return b.progress.current - a.progress.current;
if (studySort === 'running') {
// Running first, then by date
if (a.status === 'running' && b.status !== 'running') return -1;
if (b.status === 'running' && a.status !== 'running') return 1;
}
if (studySort === 'trials') {
// By trial count (most trials first)
return b.progress.current - a.progress.current;
}
// Default: sort by date (newest first)
const aDate = a.last_modified || a.created_at || '';
const bDate = b.last_modified || b.created_at || '';
return bDate.localeCompare(aDate);
});
const displayedStudies = showAllStudies ? sortedStudies : sortedStudies.slice(0, 6);
@@ -114,7 +128,7 @@ const Home: React.FC = () => {
<div className="min-h-screen bg-dark-900">
{/* Header */}
<header className="bg-dark-800/50 border-b border-dark-700 backdrop-blur-sm sticky top-0 z-10">
<div className="max-w-[1600px] mx-auto px-6 py-4">
<div className="max-w-[1920px] mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-11 h-11 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center shadow-lg shadow-primary-500/20">
@@ -138,7 +152,7 @@ const Home: React.FC = () => {
</div>
</header>
<main className="max-w-[1600px] mx-auto px-6 py-8">
<main className="max-w-[1920px] mx-auto px-6 py-8">
{/* Study Selection Section */}
<section className="mb-8">
<div className="flex items-center justify-between mb-4">
@@ -146,18 +160,56 @@ const Home: React.FC = () => {
<FolderOpen className="w-5 h-5 text-primary-400" />
Select a Study
</h2>
{studies.length > 6 && (
<button
onClick={() => setShowAllStudies(!showAllStudies)}
className="text-sm text-primary-400 hover:text-primary-300 flex items-center gap-1"
>
{showAllStudies ? (
<>Show Less <ChevronUp className="w-4 h-4" /></>
) : (
<>Show All ({studies.length}) <ChevronDown className="w-4 h-4" /></>
)}
</button>
)}
<div className="flex items-center gap-4">
{/* Sort Controls */}
<div className="flex items-center gap-2">
<span className="text-sm text-dark-400">Sort:</span>
<div className="flex rounded-lg overflow-hidden border border-dark-600">
<button
onClick={() => setStudySort('date')}
className={`px-3 py-1.5 text-sm transition-colors ${
studySort === 'date'
? 'bg-primary-500 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Newest
</button>
<button
onClick={() => setStudySort('running')}
className={`px-3 py-1.5 text-sm transition-colors ${
studySort === 'running'
? 'bg-primary-500 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Running
</button>
<button
onClick={() => setStudySort('trials')}
className={`px-3 py-1.5 text-sm transition-colors ${
studySort === 'trials'
? 'bg-primary-500 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Most Trials
</button>
</div>
</div>
{studies.length > 6 && (
<button
onClick={() => setShowAllStudies(!showAllStudies)}
className="text-sm text-primary-400 hover:text-primary-300 flex items-center gap-1"
>
{showAllStudies ? (
<>Show Less <ChevronUp className="w-4 h-4" /></>
) : (
<>Show All ({studies.length}) <ChevronDown className="w-4 h-4" /></>
)}
</button>
)}
</div>
</div>
{isLoading ? (
@@ -273,8 +325,8 @@ const Home: React.FC = () => {
<div className="p-8 overflow-x-auto">
<article className="markdown-body max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
rehypePlugins={[[rehypeKatex, { strict: false, trust: true, output: 'html' }]]}
components={{
// Custom heading styles
h1: ({ children }) => (