feat: Improve dashboard layout and Claude terminal context

- Reorganize dashboard: control panel on top, charts stacked vertically
- Add Set Context button to Claude terminal for study awareness
- Add conda environment instructions to CLAUDE.md
- Fix STUDY_REPORT.md location in generate-report.md skill
- Claude terminal now sends study context with skills reminder

🤖 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 20:59:31 -05:00
parent f8b90156b3
commit fb2d06236a
5 changed files with 309 additions and 211 deletions

View File

@@ -9,7 +9,8 @@ import {
Minimize2,
X,
RefreshCw,
AlertCircle
AlertCircle,
FolderOpen
} from 'lucide-react';
import { useStudy } from '../context/StudyContext';
@@ -33,6 +34,8 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
const [isConnecting, setIsConnecting] = useState(false);
const [_error, setError] = useState<string | null>(null);
const [cliAvailable, setCliAvailable] = useState<boolean | null>(null);
const [contextSet, setContextSet] = useState(false);
const [settingContext, setSettingContext] = useState(false);
// Check CLI availability
useEffect(() => {
@@ -165,8 +168,7 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
xtermRef.current?.clear();
xtermRef.current?.writeln('\x1b[1;32mConnected to Claude Code\x1b[0m');
if (selectedStudy?.id) {
xtermRef.current?.writeln(`\x1b[90mStudy context: \x1b[1;33m${selectedStudy.id}\x1b[0m`);
xtermRef.current?.writeln('\x1b[90mTip: Tell Claude about your study, e.g. "Help me with study ' + selectedStudy.id + '"\x1b[0m');
xtermRef.current?.writeln(`\x1b[90mStudy: \x1b[1;33m${selectedStudy.id}\x1b[0m \x1b[90m- Click "Set Context" to initialize\x1b[0m`);
}
xtermRef.current?.writeln('');
@@ -241,8 +243,26 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
wsRef.current = null;
}
setIsConnected(false);
setContextSet(false);
}, []);
// Set study context - sends context message to Claude silently
const setStudyContext = useCallback(() => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !selectedStudy?.id) return;
setSettingContext(true);
// Send context message - Claude should use CLAUDE.md and .claude/skills/ for guidance
const contextMessage = `Context: Working on study "${selectedStudy.id}" at studies/${selectedStudy.id}/. ` +
`Read .claude/skills/ for task protocols. Use atomizer conda env. Acknowledge briefly.`;
wsRef.current.send(JSON.stringify({ type: 'input', data: contextMessage + '\n' }));
// Mark as done after Claude has had time to process
setTimeout(() => {
setSettingContext(false);
setContextSet(true);
}, 500);
}, [selectedStudy]);
// Cleanup on unmount
useEffect(() => {
return () => {
@@ -293,6 +313,35 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
{isConnected ? 'Disconnect' : 'Connect'}
</button>
{/* Set Context button - always show, with different states */}
<button
onClick={setStudyContext}
disabled={!selectedStudy?.id || !isConnected || settingContext || contextSet}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors flex items-center gap-2 ${
contextSet
? 'bg-green-600/20 text-green-400'
: !selectedStudy?.id || !isConnected
? 'bg-dark-600 text-dark-400'
: 'bg-primary-600/20 text-primary-400 hover:bg-primary-600/30'
} disabled:opacity-50 disabled:cursor-not-allowed`}
title={
!selectedStudy?.id
? 'No study selected - select a study from Home page'
: !isConnected
? 'Connect first to set study context'
: contextSet
? 'Context already set'
: `Set context to study: ${selectedStudy.id}`
}
>
{settingContext ? (
<RefreshCw className="w-3 h-3 animate-spin" />
) : (
<FolderOpen className="w-3 h-3" />
)}
{contextSet ? 'Context Set' : selectedStudy?.id ? 'Set Context' : 'No Study'}
</button>
{onToggleExpand && (
<button
onClick={onToggleExpand}

View File

@@ -14,9 +14,10 @@ import { useStudy } from '../../context/StudyContext';
interface ControlPanelProps {
onStatusChange?: () => void;
horizontal?: boolean;
}
export const ControlPanel: React.FC<ControlPanelProps> = ({ onStatusChange }) => {
export const ControlPanel: React.FC<ControlPanelProps> = ({ onStatusChange, horizontal = false }) => {
const { selectedStudy, refreshStudies } = useStudy();
const [processStatus, setProcessStatus] = useState<ProcessStatus | null>(null);
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
@@ -131,6 +132,177 @@ export const ControlPanel: React.FC<ControlPanelProps> = ({ onStatusChange }) =>
const isRunning = processStatus?.is_running || selectedStudy?.status === 'running';
// Horizontal layout for top of page
if (horizontal) {
return (
<div className="bg-dark-800 rounded-xl border border-dark-600 overflow-hidden">
<div className="px-4 py-3 flex items-center gap-6">
{/* Status */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{isRunning ? (
<>
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse" />
<span className="text-green-400 font-medium text-sm">Running</span>
</>
) : (
<>
<div className="w-3 h-3 bg-dark-500 rounded-full" />
<span className="text-dark-400 text-sm">Stopped</span>
</>
)}
</div>
{processStatus?.fea_count && (
<span className="text-xs text-dark-400">
FEA: <span className="text-primary-400">{processStatus.fea_count}</span>
{processStatus.nn_count && (
<> | NN: <span className="text-orange-400">{processStatus.nn_count}</span></>
)}
</span>
)}
</div>
{/* Main Actions */}
<div className="flex items-center gap-2">
{isRunning ? (
<button
onClick={handleStop}
disabled={actionInProgress !== null}
className="flex items-center gap-2 px-3 py-1.5 bg-red-600 hover:bg-red-500
disabled:opacity-50 text-white rounded-lg text-sm font-medium"
>
{actionInProgress === 'stop' ? <Loader2 className="w-4 h-4 animate-spin" /> : <Skull className="w-4 h-4" />}
Kill
</button>
) : (
<button
onClick={handleStart}
disabled={actionInProgress !== null}
className="flex items-center gap-2 px-3 py-1.5 bg-green-600 hover:bg-green-500
disabled:opacity-50 text-white rounded-lg text-sm font-medium"
>
{actionInProgress === 'start' ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
Start
</button>
)}
<button
onClick={handleValidate}
disabled={actionInProgress !== null || isRunning}
className="flex items-center gap-2 px-3 py-1.5 bg-primary-600 hover:bg-primary-500
disabled:opacity-50 text-white rounded-lg text-sm font-medium"
>
{actionInProgress === 'validate' ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
Validate
</button>
<button
onClick={handleLaunchOptuna}
disabled={actionInProgress !== null}
className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 hover:bg-dark-600
border border-dark-600 disabled:opacity-50 text-dark-300 hover:text-white rounded-lg text-sm"
>
<ExternalLink className="w-4 h-4" />
Optuna
</button>
</div>
{/* Settings Toggle */}
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-1.5 rounded-lg transition-colors ${
showSettings ? 'bg-primary-600 text-white' : 'bg-dark-700 text-dark-300 hover:text-white'
}`}
>
<Settings className="w-4 h-4" />
</button>
{/* Error */}
{error && (
<div className="flex items-center gap-2 text-red-400 text-sm">
<AlertTriangle className="w-4 h-4" />
{error}
</div>
)}
</div>
{/* Collapsible Settings */}
{showSettings && (
<div className="px-4 py-3 border-t border-dark-700 bg-dark-750">
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<label className="text-xs text-dark-400">Iterations:</label>
<input
type="number"
value={settings.maxIterations}
onChange={(e) => setSettings({ ...settings, maxIterations: parseInt(e.target.value) || 100 })}
className="w-16 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-dark-400">FEA Batch:</label>
<input
type="number"
value={settings.feaBatchSize}
onChange={(e) => setSettings({ ...settings, feaBatchSize: parseInt(e.target.value) || 5 })}
className="w-12 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-dark-400">Patience:</label>
<input
type="number"
value={settings.patience}
onChange={(e) => setSettings({ ...settings, patience: parseInt(e.target.value) || 5 })}
className="w-12 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-dark-400">Tune:</label>
<input
type="number"
value={settings.tuneTrials}
onChange={(e) => setSettings({ ...settings, tuneTrials: parseInt(e.target.value) || 30 })}
className="w-12 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-dark-400">Ensemble:</label>
<input
type="number"
value={settings.ensembleSize}
onChange={(e) => setSettings({ ...settings, ensembleSize: parseInt(e.target.value) || 3 })}
className="w-12 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-dark-400">Validate Top:</label>
<input
type="number"
min={1}
max={20}
value={validateTopN}
onChange={(e) => setValidateTopN(parseInt(e.target.value) || 5)}
className="w-12 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm"
/>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings.freshStart}
onChange={(e) => setSettings({ ...settings, freshStart: e.target.checked })}
className="w-4 h-4 rounded border-dark-600 bg-dark-700 text-primary-600"
/>
<span className="text-xs text-dark-300">Fresh Start</span>
</label>
</div>
</div>
)}
</div>
);
}
// Vertical layout (original sidebar layout)
return (
<div className="bg-dark-800 rounded-xl border border-dark-600 overflow-hidden">
{/* Header */}