feat: Add session management and global Claude terminal
Phase 1 - Accurate study status detection: - Add is_optimization_running() to check for active processes - Add get_accurate_study_status() with proper status logic - Status now: not_started, running, paused, completed - Add "paused" status styling (orange) to Home page Phase 2 - Global Claude terminal: - Create ClaudeTerminalContext for app-level state - Create GlobalClaudeTerminal floating component - Terminal persists across page navigation - Shows green indicator when connected - Remove inline terminal from Dashboard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,63 @@ def get_results_dir(study_dir: Path) -> Path:
|
|||||||
return results_dir
|
return results_dir
|
||||||
|
|
||||||
|
|
||||||
|
def is_optimization_running(study_id: str) -> bool:
|
||||||
|
"""Check if an optimization process is currently running for a study.
|
||||||
|
|
||||||
|
Looks for Python processes running run_optimization.py with the study_id in the command line.
|
||||||
|
"""
|
||||||
|
study_dir = STUDIES_DIR / study_id
|
||||||
|
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cwd']):
|
||||||
|
try:
|
||||||
|
cmdline = proc.info.get('cmdline') or []
|
||||||
|
cmdline_str = ' '.join(cmdline) if cmdline else ''
|
||||||
|
|
||||||
|
# Check if this is a Python process running run_optimization.py for this study
|
||||||
|
if 'python' in cmdline_str.lower() and 'run_optimization' in cmdline_str:
|
||||||
|
if study_id in cmdline_str or str(study_dir) in cmdline_str:
|
||||||
|
return True
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_accurate_study_status(study_id: str, trial_count: int, total_trials: int, has_db: bool) -> str:
|
||||||
|
"""Determine accurate study status based on multiple factors.
|
||||||
|
|
||||||
|
Status can be:
|
||||||
|
- not_started: No database or 0 trials
|
||||||
|
- running: Active process found
|
||||||
|
- paused: Has trials but no active process and not completed
|
||||||
|
- completed: Reached trial target
|
||||||
|
- failed: Has error indicators (future enhancement)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
study_id: The study identifier
|
||||||
|
trial_count: Number of completed trials
|
||||||
|
total_trials: Target number of trials from config
|
||||||
|
has_db: Whether the study database exists
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Status string: "not_started", "running", "paused", or "completed"
|
||||||
|
"""
|
||||||
|
# No database or no trials = not started
|
||||||
|
if not has_db or trial_count == 0:
|
||||||
|
return "not_started"
|
||||||
|
|
||||||
|
# Check if we've reached the target
|
||||||
|
if trial_count >= total_trials:
|
||||||
|
return "completed"
|
||||||
|
|
||||||
|
# Check if process is actively running
|
||||||
|
if is_optimization_running(study_id):
|
||||||
|
return "running"
|
||||||
|
|
||||||
|
# Has trials but not running and not complete = paused
|
||||||
|
return "paused"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/studies")
|
@router.get("/studies")
|
||||||
async def list_studies():
|
async def list_studies():
|
||||||
"""List all available optimization studies"""
|
"""List all available optimization studies"""
|
||||||
@@ -66,12 +123,13 @@ async def list_studies():
|
|||||||
study_db = results_dir / "study.db"
|
study_db = results_dir / "study.db"
|
||||||
history_file = results_dir / "optimization_history_incremental.json"
|
history_file = results_dir / "optimization_history_incremental.json"
|
||||||
|
|
||||||
status = "not_started"
|
|
||||||
trial_count = 0
|
trial_count = 0
|
||||||
best_value = None
|
best_value = None
|
||||||
|
has_db = False
|
||||||
|
|
||||||
# Protocol 10: Read from Optuna SQLite database
|
# Protocol 10: Read from Optuna SQLite database
|
||||||
if study_db.exists():
|
if study_db.exists():
|
||||||
|
has_db = True
|
||||||
try:
|
try:
|
||||||
# Use timeout to avoid blocking on locked databases
|
# Use timeout to avoid blocking on locked databases
|
||||||
conn = sqlite3.connect(str(study_db), timeout=2.0)
|
conn = sqlite3.connect(str(study_db), timeout=2.0)
|
||||||
@@ -97,19 +155,12 @@ async def list_studies():
|
|||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# Determine status
|
|
||||||
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
|
|
||||||
if trial_count >= total_trials:
|
|
||||||
status = "completed"
|
|
||||||
else:
|
|
||||||
status = "running" # Simplified - would need process check
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to read Optuna database for {study_dir.name}: {e}")
|
print(f"Warning: Failed to read Optuna database for {study_dir.name}: {e}")
|
||||||
status = "error"
|
|
||||||
|
|
||||||
# Legacy: Read from JSON history
|
# Legacy: Read from JSON history
|
||||||
elif history_file.exists():
|
elif history_file.exists():
|
||||||
|
has_db = True
|
||||||
with open(history_file) as f:
|
with open(history_file) as f:
|
||||||
history = json.load(f)
|
history = json.load(f)
|
||||||
trial_count = len(history)
|
trial_count = len(history)
|
||||||
@@ -118,19 +169,15 @@ async def list_studies():
|
|||||||
best_trial = min(history, key=lambda x: x['objective'])
|
best_trial = min(history, key=lambda x: x['objective'])
|
||||||
best_value = best_trial['objective']
|
best_value = best_trial['objective']
|
||||||
|
|
||||||
# Determine status
|
|
||||||
total_trials = config.get('trials', {}).get('n_trials', 50)
|
|
||||||
if trial_count >= total_trials:
|
|
||||||
status = "completed"
|
|
||||||
else:
|
|
||||||
status = "running" # Simplified - would need process check
|
|
||||||
|
|
||||||
# Get total trials from config (supports both formats)
|
# Get total trials from config (supports both formats)
|
||||||
total_trials = (
|
total_trials = (
|
||||||
config.get('optimization_settings', {}).get('n_trials') or
|
config.get('optimization_settings', {}).get('n_trials') or
|
||||||
config.get('trials', {}).get('n_trials', 50)
|
config.get('trials', {}).get('n_trials', 50)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get accurate status using process detection
|
||||||
|
status = get_accurate_study_status(study_dir.name, trial_count, total_trials, has_db)
|
||||||
|
|
||||||
# Get creation date from directory or config modification time
|
# Get creation date from directory or config modification time
|
||||||
created_at = None
|
created_at = None
|
||||||
try:
|
try:
|
||||||
@@ -240,7 +287,7 @@ async def get_study_status(study_id: str):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
|
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
|
||||||
status = "completed" if trial_count >= total_trials else "running"
|
status = get_accurate_study_status(study_id, trial_count, total_trials, True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"study_id": study_id,
|
"study_id": study_id,
|
||||||
@@ -639,6 +686,82 @@ async def get_pareto_front(study_id: str):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to get Pareto front: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to get Pareto front: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/studies/{study_id}/nn-pareto-front")
|
||||||
|
async def get_nn_pareto_front(study_id: str):
|
||||||
|
"""Get NN surrogate Pareto front from nn_pareto_front.json"""
|
||||||
|
try:
|
||||||
|
study_dir = STUDIES_DIR / study_id
|
||||||
|
results_dir = get_results_dir(study_dir)
|
||||||
|
nn_pareto_file = results_dir / "nn_pareto_front.json"
|
||||||
|
|
||||||
|
if not nn_pareto_file.exists():
|
||||||
|
return {"has_nn_results": False, "pareto_front": []}
|
||||||
|
|
||||||
|
with open(nn_pareto_file) as f:
|
||||||
|
nn_pareto = json.load(f)
|
||||||
|
|
||||||
|
# Transform to match Trial interface format
|
||||||
|
transformed = []
|
||||||
|
for trial in nn_pareto:
|
||||||
|
transformed.append({
|
||||||
|
"trial_number": trial.get("trial_number"),
|
||||||
|
"values": [trial.get("mass"), trial.get("frequency")],
|
||||||
|
"params": trial.get("params", {}),
|
||||||
|
"user_attrs": {
|
||||||
|
"source": "NN",
|
||||||
|
"feasible": trial.get("feasible", False),
|
||||||
|
"predicted_stress": trial.get("predicted_stress"),
|
||||||
|
"predicted_displacement": trial.get("predicted_displacement"),
|
||||||
|
"mass": trial.get("mass"),
|
||||||
|
"frequency": trial.get("frequency")
|
||||||
|
},
|
||||||
|
"constraint_satisfied": trial.get("feasible", False),
|
||||||
|
"source": "NN"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"has_nn_results": True,
|
||||||
|
"pareto_front": transformed,
|
||||||
|
"count": len(transformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get NN Pareto front: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/studies/{study_id}/nn-state")
|
||||||
|
async def get_nn_optimization_state(study_id: str):
|
||||||
|
"""Get NN optimization state/summary from nn_optimization_state.json"""
|
||||||
|
try:
|
||||||
|
study_dir = STUDIES_DIR / study_id
|
||||||
|
results_dir = get_results_dir(study_dir)
|
||||||
|
nn_state_file = results_dir / "nn_optimization_state.json"
|
||||||
|
|
||||||
|
if not nn_state_file.exists():
|
||||||
|
return {"has_nn_state": False}
|
||||||
|
|
||||||
|
with open(nn_state_file) as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"has_nn_state": True,
|
||||||
|
"total_fea_count": state.get("total_fea_count", 0),
|
||||||
|
"total_nn_count": state.get("total_nn_count", 0),
|
||||||
|
"pareto_front_size": state.get("pareto_front_size", 0),
|
||||||
|
"best_mass": state.get("best_mass"),
|
||||||
|
"best_frequency": state.get("best_frequency"),
|
||||||
|
"timestamp": state.get("timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get NN state: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/studies")
|
@router.post("/studies")
|
||||||
async def create_study(
|
async def create_study(
|
||||||
config: str = Form(...),
|
config: str = Form(...),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { StudyProvider } from './context/StudyContext';
|
import { StudyProvider } from './context/StudyContext';
|
||||||
|
import { ClaudeTerminalProvider } from './context/ClaudeTerminalContext';
|
||||||
import { MainLayout } from './components/layout/MainLayout';
|
import { MainLayout } from './components/layout/MainLayout';
|
||||||
|
import { GlobalClaudeTerminal } from './components/GlobalClaudeTerminal';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Results from './pages/Results';
|
import Results from './pages/Results';
|
||||||
@@ -19,19 +21,24 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<StudyProvider>
|
<StudyProvider>
|
||||||
<BrowserRouter>
|
<ClaudeTerminalProvider>
|
||||||
<Routes>
|
<BrowserRouter>
|
||||||
{/* Home page - no sidebar layout */}
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
{/* Home page - no sidebar layout */}
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
|
||||||
{/* Study pages - with sidebar layout */}
|
{/* Study pages - with sidebar layout */}
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
<Route path="results" element={<Results />} />
|
<Route path="results" element={<Results />} />
|
||||||
<Route path="analytics" element={<Dashboard />} />
|
<Route path="analytics" element={<Dashboard />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
|
||||||
|
{/* Global Claude Terminal - persists across navigation */}
|
||||||
|
<GlobalClaudeTerminal />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ClaudeTerminalProvider>
|
||||||
</StudyProvider>
|
</StudyProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
FolderOpen
|
FolderOpen
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useStudy } from '../context/StudyContext';
|
import { useStudy } from '../context/StudyContext';
|
||||||
|
import { useClaudeTerminal } from '../context/ClaudeTerminalContext';
|
||||||
|
|
||||||
interface ClaudeTerminalProps {
|
interface ClaudeTerminalProps {
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
@@ -26,17 +27,24 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
|
|||||||
onClose
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const { selectedStudy } = useStudy();
|
const { selectedStudy } = useStudy();
|
||||||
|
const { setIsConnected: setGlobalConnected } = useClaudeTerminal();
|
||||||
const terminalRef = useRef<HTMLDivElement>(null);
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
const xtermRef = useRef<Terminal | null>(null);
|
const xtermRef = useRef<Terminal | null>(null);
|
||||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnectedLocal] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [_error, setError] = useState<string | null>(null);
|
const [_error, setError] = useState<string | null>(null);
|
||||||
const [cliAvailable, setCliAvailable] = useState<boolean | null>(null);
|
const [cliAvailable, setCliAvailable] = useState<boolean | null>(null);
|
||||||
const [contextSet, setContextSet] = useState(false);
|
const [contextSet, setContextSet] = useState(false);
|
||||||
const [settingContext, setSettingContext] = useState(false);
|
const [settingContext, setSettingContext] = useState(false);
|
||||||
|
|
||||||
|
// Sync local connection state to global context
|
||||||
|
const setIsConnected = useCallback((connected: boolean) => {
|
||||||
|
setIsConnectedLocal(connected);
|
||||||
|
setGlobalConnected(connected);
|
||||||
|
}, [setGlobalConnected]);
|
||||||
|
|
||||||
// Check CLI availability
|
// Check CLI availability
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/terminal/status')
|
fetch('/api/terminal/status')
|
||||||
@@ -251,9 +259,13 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
|
|||||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !selectedStudy?.id) return;
|
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !selectedStudy?.id) return;
|
||||||
|
|
||||||
setSettingContext(true);
|
setSettingContext(true);
|
||||||
// Send context message - Claude should use CLAUDE.md and .claude/skills/ for guidance
|
// Send context message with POS bootstrap instructions and study context
|
||||||
const contextMessage = `Context: Working on study "${selectedStudy.id}" at studies/${selectedStudy.id}/. ` +
|
const contextMessage =
|
||||||
`Read .claude/skills/ for task protocols. Use atomizer conda env. Acknowledge briefly.`;
|
`You are helping with Atomizer optimization. ` +
|
||||||
|
`First read: .claude/skills/00_BOOTSTRAP.md for task routing. ` +
|
||||||
|
`Then follow the Protocol Execution Framework. ` +
|
||||||
|
`Study context: Working on "${selectedStudy.id}" at studies/${selectedStudy.id}/. ` +
|
||||||
|
`Use atomizer conda env. Acknowledge briefly.`;
|
||||||
wsRef.current.send(JSON.stringify({ type: 'input', data: contextMessage + '\n' }));
|
wsRef.current.send(JSON.stringify({ type: 'input', data: contextMessage + '\n' }));
|
||||||
|
|
||||||
// Mark as done after Claude has had time to process
|
// Mark as done after Claude has had time to process
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useClaudeTerminal } from '../context/ClaudeTerminalContext';
|
||||||
|
import { ClaudeTerminal } from './ClaudeTerminal';
|
||||||
|
import { Terminal } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlobalClaudeTerminal - A floating terminal that persists across page navigation
|
||||||
|
*
|
||||||
|
* This component renders at the App level and maintains the Claude Code session
|
||||||
|
* even when the user navigates between pages. It can be minimized to a floating
|
||||||
|
* button or expanded to a side panel.
|
||||||
|
*/
|
||||||
|
export const GlobalClaudeTerminal: React.FC = () => {
|
||||||
|
const { isOpen, setIsOpen, isExpanded, setIsExpanded, isConnected } = useClaudeTerminal();
|
||||||
|
|
||||||
|
// Floating button when terminal is closed
|
||||||
|
if (!isOpen) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className={`fixed bottom-6 right-6 p-4 rounded-full shadow-lg transition-all z-50 ${
|
||||||
|
isConnected
|
||||||
|
? 'bg-green-600 hover:bg-green-500'
|
||||||
|
: 'bg-primary-600 hover:bg-primary-500'
|
||||||
|
}`}
|
||||||
|
title={isConnected ? 'Claude Terminal (Connected)' : 'Open Claude Terminal'}
|
||||||
|
>
|
||||||
|
<Terminal className="w-6 h-6 text-white" />
|
||||||
|
{isConnected && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-dark-900 animate-pulse" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal panel
|
||||||
|
return (
|
||||||
|
<div className={`fixed z-50 transition-all duration-200 ${
|
||||||
|
isExpanded
|
||||||
|
? 'inset-4'
|
||||||
|
: 'bottom-6 right-6 w-[650px] h-[500px]'
|
||||||
|
}`}>
|
||||||
|
<ClaudeTerminal
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
onToggleExpand={() => setIsExpanded(!isExpanded)}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ClaudeTerminalContextType {
|
||||||
|
// Terminal visibility state
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (open: boolean) => void;
|
||||||
|
isExpanded: boolean;
|
||||||
|
setIsExpanded: (expanded: boolean) => void;
|
||||||
|
|
||||||
|
// Connection state (updated by the terminal component)
|
||||||
|
isConnected: boolean;
|
||||||
|
setIsConnected: (connected: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClaudeTerminalContext = createContext<ClaudeTerminalContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ClaudeTerminalProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClaudeTerminalContext.Provider value={{
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
isExpanded,
|
||||||
|
setIsExpanded,
|
||||||
|
isConnected,
|
||||||
|
setIsConnected
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</ClaudeTerminalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useClaudeTerminal = () => {
|
||||||
|
const context = useContext(ClaudeTerminalContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useClaudeTerminal must be used within a ClaudeTerminalProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -4,9 +4,9 @@ import { Terminal } from 'lucide-react';
|
|||||||
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
|
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
import { useStudy } from '../context/StudyContext';
|
import { useStudy } from '../context/StudyContext';
|
||||||
|
import { useClaudeTerminal } from '../context/ClaudeTerminalContext';
|
||||||
import { Card } from '../components/common/Card';
|
import { Card } from '../components/common/Card';
|
||||||
import { ControlPanel } from '../components/dashboard/ControlPanel';
|
import { ControlPanel } from '../components/dashboard/ControlPanel';
|
||||||
import { ClaudeTerminal } from '../components/ClaudeTerminal';
|
|
||||||
import { ParetoPlot } from '../components/ParetoPlot';
|
import { ParetoPlot } from '../components/ParetoPlot';
|
||||||
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
|
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
|
||||||
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
|
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
|
||||||
@@ -64,9 +64,8 @@ export default function Dashboard() {
|
|||||||
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
|
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
|
||||||
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
|
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
|
||||||
|
|
||||||
// Claude chat panel state
|
// Claude terminal from global context
|
||||||
const [chatOpen, setChatOpen] = useState(false);
|
const { isOpen: claudeTerminalOpen, setIsOpen: setClaudeTerminalOpen, isConnected: claudeConnected } = useClaudeTerminal();
|
||||||
const [chatExpanded, setChatExpanded] = useState(false);
|
|
||||||
|
|
||||||
const showAlert = (type: 'success' | 'warning', message: string) => {
|
const showAlert = (type: 'success' | 'warning', message: string) => {
|
||||||
const id = alertIdCounter;
|
const id = alertIdCounter;
|
||||||
@@ -353,16 +352,21 @@ export default function Dashboard() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* Claude Code Terminal Toggle Button */}
|
{/* Claude Code Terminal Toggle Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setChatOpen(!chatOpen)}
|
onClick={() => setClaudeTerminalOpen(!claudeTerminalOpen)}
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
||||||
chatOpen
|
claudeTerminalOpen
|
||||||
? 'bg-primary-600 text-white'
|
? 'bg-primary-600 text-white'
|
||||||
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
|
: claudeConnected
|
||||||
|
? 'bg-green-700 text-white border border-green-600'
|
||||||
|
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
|
||||||
}`}
|
}`}
|
||||||
title="Open Claude Code terminal"
|
title={claudeConnected ? 'Claude Terminal (Connected)' : 'Open Claude Code terminal'}
|
||||||
>
|
>
|
||||||
<Terminal className="w-4 h-4" />
|
<Terminal className="w-4 h-4" />
|
||||||
<span className="hidden sm:inline">Claude Code</span>
|
<span className="hidden sm:inline">Claude Code</span>
|
||||||
|
{claudeConnected && !claudeTerminalOpen && (
|
||||||
|
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
{selectedStudyId && (
|
{selectedStudyId && (
|
||||||
<StudyReportViewer studyId={selectedStudyId} />
|
<StudyReportViewer studyId={selectedStudyId} />
|
||||||
@@ -417,10 +421,10 @@ export default function Dashboard() {
|
|||||||
<ControlPanel onStatusChange={refreshStudies} horizontal />
|
<ControlPanel onStatusChange={refreshStudies} horizontal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Layout: Charts + Claude Terminal */}
|
{/* Main Layout: Charts (Claude Terminal is now global/floating) */}
|
||||||
<div className={`grid gap-4 ${chatOpen ? 'grid-cols-12' : 'grid-cols-1'}`}>
|
<div className="grid gap-4 grid-cols-1">
|
||||||
{/* Main Content - Charts stacked vertically */}
|
{/* Main Content - Charts stacked vertically */}
|
||||||
<main className={chatOpen ? 'col-span-7' : 'col-span-1'}>
|
<main>
|
||||||
{/* Study Name Header + Metrics in one row */}
|
{/* Study Name Header + Metrics in one row */}
|
||||||
<div className="mb-4 pb-3 border-b border-dark-600 flex items-center justify-between">
|
<div className="mb-4 pb-3 border-b border-dark-600 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -790,17 +794,6 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Claude Code Terminal - Right Sidebar (wider for better visibility) */}
|
|
||||||
{chatOpen && (
|
|
||||||
<aside className="col-span-5 h-[calc(100vh-12rem)] sticky top-4">
|
|
||||||
<ClaudeTerminal
|
|
||||||
isExpanded={chatExpanded}
|
|
||||||
onToggleExpand={() => setChatExpanded(!chatExpanded)}
|
|
||||||
onClose={() => setChatOpen(false)}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Play,
|
Play,
|
||||||
|
Pause,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -64,6 +65,8 @@ const Home: React.FC = () => {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case 'running':
|
case 'running':
|
||||||
return <Play className="w-3.5 h-3.5" />;
|
return <Play className="w-3.5 h-3.5" />;
|
||||||
|
case 'paused':
|
||||||
|
return <Pause className="w-3.5 h-3.5" />;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return <CheckCircle className="w-3.5 h-3.5" />;
|
return <CheckCircle className="w-3.5 h-3.5" />;
|
||||||
case 'not_started':
|
case 'not_started':
|
||||||
@@ -81,6 +84,12 @@ const Home: React.FC = () => {
|
|||||||
card: 'border-green-500/30 hover:border-green-500/50',
|
card: 'border-green-500/30 hover:border-green-500/50',
|
||||||
glow: 'shadow-green-500/10'
|
glow: 'shadow-green-500/10'
|
||||||
};
|
};
|
||||||
|
case 'paused':
|
||||||
|
return {
|
||||||
|
badge: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
|
||||||
|
card: 'border-orange-500/30 hover:border-orange-500/50',
|
||||||
|
glow: 'shadow-orange-500/10'
|
||||||
|
};
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return {
|
return {
|
||||||
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default defineConfig({
|
|||||||
strictPort: false, // Allow fallback to next available port
|
strictPort: false, // Allow fallback to next available port
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8001', // Use 127.0.0.1 instead of localhost
|
target: 'http://127.0.0.1:8000', // Use 127.0.0.1 instead of localhost
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user