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)}