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:
@@ -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)}
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
Reference in New Issue
Block a user