feat: Add Claude Code terminal integration to dashboard
- Add embedded Claude Code terminal with xterm.js for full CLI experience - Create WebSocket PTY backend for real-time terminal communication - Add terminal status endpoint to check CLI availability - Update dashboard to use Claude Code terminal instead of API chat - Add optimization control panel with start/stop/validate actions - Add study context provider for global state management - Update frontend with new dependencies (xterm.js addons) - Comprehensive README documentation for all new features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
143
atomizer-dashboard/frontend/package-lock.json
generated
143
atomizer-dashboard/frontend/package-lock.json
generated
@@ -12,8 +12,12 @@
|
||||
"@react-three/fiber": "^9.4.0",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@types/react-plotly.js": "^2.6.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/three": "^0.181.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.25",
|
||||
"lucide-react": "^0.554.0",
|
||||
"plotly.js-basic-dist": "^3.3.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -21,13 +25,15 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"recharts": "^2.10.3",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.181.2"
|
||||
"three": "^0.181.2",
|
||||
"xterm": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
@@ -1660,6 +1666,12 @@
|
||||
"integrity": "sha512-FjmSFaLmHVgBIBL6H0yX5k/AB3a7FQzjKBlRUF8YT6HiXMArE+hbXYIZXZ/42SBrdL05LWEog0zPqEaIDNsAiw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
@@ -1705,6 +1717,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
|
||||
@@ -1994,6 +2015,24 @@
|
||||
"integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-web-links": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz",
|
||||
"integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -3188,6 +3227,19 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fault": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
@@ -3259,6 +3311,14 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
@@ -3615,6 +3675,21 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/highlightjs-vue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.15",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
|
||||
@@ -4094,6 +4169,20 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
||||
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -5542,6 +5631,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
@@ -5759,6 +5857,26 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz",
|
||||
"integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"highlight.js": "^10.4.1",
|
||||
"highlightjs-vue": "^1.0.0",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"refractor": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.20.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@@ -5851,6 +5969,22 @@
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
|
||||
"integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/prismjs": "^1.0.0",
|
||||
"hastscript": "^9.0.0",
|
||||
"parse-entities": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-katex": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
|
||||
@@ -6989,6 +7123,13 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
"@react-three/fiber": "^9.4.0",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@types/react-plotly.js": "^2.6.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/three": "^0.181.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.25",
|
||||
"lucide-react": "^0.554.0",
|
||||
"plotly.js-basic-dist": "^3.3.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -23,13 +27,15 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"recharts": "^2.10.3",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.181.2"
|
||||
"three": "^0.181.2",
|
||||
"xterm": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { StudyProvider } from './context/StudyContext';
|
||||
import { MainLayout } from './components/layout/MainLayout';
|
||||
import Home from './pages/Home';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Configurator from './pages/Configurator';
|
||||
import Results from './pages/Results';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5000,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="configurator" element={<Configurator />} />
|
||||
<Route path="results" element={<Results />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<StudyProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Home page - no sidebar layout */}
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
{/* Study pages - with sidebar layout */}
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="results" element={<Results />} />
|
||||
<Route path="analytics" element={<Dashboard />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</StudyProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,56 @@ import { StudyListResponse, HistoryResponse, PruningResponse, StudyStatus } from
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
export interface OptimizationControlResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
pid?: number;
|
||||
}
|
||||
|
||||
export interface ReadmeResponse {
|
||||
content: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ReportResponse {
|
||||
content: string;
|
||||
generated_at?: string;
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
config: Record<string, any>;
|
||||
objectives: Array<{
|
||||
name: string;
|
||||
direction: string;
|
||||
weight?: number;
|
||||
target?: number;
|
||||
units?: string;
|
||||
}>;
|
||||
design_variables: Array<{
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
baseline?: number;
|
||||
units?: string;
|
||||
}>;
|
||||
constraints?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
max_value?: number;
|
||||
min_value?: number;
|
||||
units?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ProcessStatus {
|
||||
is_running: boolean;
|
||||
pid?: number;
|
||||
start_time?: string;
|
||||
iteration?: number;
|
||||
fea_count?: number;
|
||||
nn_count?: number;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
async getStudies(): Promise<StudyListResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies`);
|
||||
@@ -37,12 +87,24 @@ class ApiClient {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyReport(studyId: string): Promise<{ content: string }> {
|
||||
async getStudyReport(studyId: string): Promise<ReportResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report`);
|
||||
if (!response.ok) throw new Error('Failed to fetch report');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyReadme(studyId: string): Promise<ReadmeResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/readme`);
|
||||
if (!response.ok) throw new Error('Failed to fetch README');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyConfig(studyId: string): Promise<ConfigResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/config`);
|
||||
if (!response.ok) throw new Error('Failed to fetch config');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getConsoleOutput(studyId: string, lines: number = 200): Promise<{
|
||||
lines: string[];
|
||||
total_lines: number;
|
||||
@@ -56,16 +118,81 @@ class ApiClient {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Future endpoints for control
|
||||
async startOptimization(studyId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/start`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error('Failed to start optimization');
|
||||
async getProcessStatus(studyId: string): Promise<ProcessStatus> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/process`);
|
||||
if (!response.ok) throw new Error('Failed to fetch process status');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async stopOptimization(studyId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/stop`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error('Failed to stop optimization');
|
||||
// Control operations
|
||||
async startOptimization(studyId: string, options?: {
|
||||
freshStart?: boolean;
|
||||
maxIterations?: number;
|
||||
feaBatchSize?: number;
|
||||
tuneTrials?: number;
|
||||
ensembleSize?: number;
|
||||
patience?: number;
|
||||
}): Promise<OptimizationControlResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(options || {}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to start optimization');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async stopOptimization(studyId: string): Promise<OptimizationControlResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/stop`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to stop optimization');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async validateOptimization(studyId: string, options?: {
|
||||
topN?: number;
|
||||
}): Promise<OptimizationControlResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(options || {}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to start validation');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async generateReport(studyId: string): Promise<ReportResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report/generate`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to generate report');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Optuna dashboard
|
||||
async launchOptunaDashboard(studyId: string): Promise<{ url: string; pid: number }> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/optuna-dashboard`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to launch Optuna dashboard');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
export const apiClient = new ApiClient();
|
||||
|
||||
450
atomizer-dashboard/frontend/src/components/ClaudeChat.tsx
Normal file
450
atomizer-dashboard/frontend/src/components/ClaudeChat.tsx
Normal file
@@ -0,0 +1,450 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Send,
|
||||
Bot,
|
||||
User,
|
||||
Sparkles,
|
||||
Loader2,
|
||||
X,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
AlertCircle,
|
||||
Wrench,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Trash2
|
||||
} from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
toolCalls?: Array<{
|
||||
tool: string;
|
||||
input: Record<string, any>;
|
||||
result_preview: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ClaudeChatProps {
|
||||
isExpanded?: boolean;
|
||||
onToggleExpand?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ClaudeChat: React.FC<ClaudeChatProps> = ({
|
||||
isExpanded = false,
|
||||
onToggleExpand,
|
||||
onClose
|
||||
}) => {
|
||||
const { selectedStudy } = useStudy();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiAvailable, setApiAvailable] = useState<boolean | null>(null);
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Check API status on mount
|
||||
useEffect(() => {
|
||||
checkApiStatus();
|
||||
}, []);
|
||||
|
||||
// Load suggestions when study changes
|
||||
useEffect(() => {
|
||||
loadSuggestions();
|
||||
}, [selectedStudy]);
|
||||
|
||||
// Scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
const checkApiStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/claude/status');
|
||||
const data = await response.json();
|
||||
setApiAvailable(data.available);
|
||||
if (!data.available) {
|
||||
setError(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
setApiAvailable(false);
|
||||
setError('Could not connect to Claude API');
|
||||
}
|
||||
};
|
||||
|
||||
const loadSuggestions = async () => {
|
||||
try {
|
||||
const url = selectedStudy
|
||||
? `/api/claude/suggestions?study_id=${selectedStudy.id}`
|
||||
: '/api/claude/suggestions';
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
setSuggestions(data.suggestions || []);
|
||||
} catch (err) {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (messageText?: string) => {
|
||||
const text = messageText || input.trim();
|
||||
if (!text || isLoading) return;
|
||||
|
||||
setError(null);
|
||||
const userMessage: Message = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/claude/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
study_id: selectedStudy?.id,
|
||||
conversation_history: messages.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to get response');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: data.response,
|
||||
timestamp: new Date(),
|
||||
toolCalls: data.tool_calls
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to send message');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const clearConversation = () => {
|
||||
setMessages([]);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const toggleToolExpand = (toolId: string) => {
|
||||
setExpandedTools(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(toolId)) {
|
||||
newSet.delete(toolId);
|
||||
} else {
|
||||
newSet.add(toolId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Render tool call indicator
|
||||
const renderToolCalls = (toolCalls: Message['toolCalls'], messageId: string) => {
|
||||
if (!toolCalls || toolCalls.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-2 space-y-1">
|
||||
{toolCalls.map((tool, index) => {
|
||||
const toolId = `${messageId}-tool-${index}`;
|
||||
const isExpanded = expandedTools.has(toolId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-dark-700/50 rounded-lg border border-dark-600 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleToolExpand(toolId)}
|
||||
className="w-full px-3 py-2 flex items-center justify-between text-xs hover:bg-dark-600/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Wrench className="w-3 h-3 text-primary-400" />
|
||||
<span className="text-dark-300">Used tool: </span>
|
||||
<span className="text-primary-400 font-mono">{tool.tool}</span>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-3 h-3 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-3 py-2 border-t border-dark-600 text-xs">
|
||||
<div className="text-dark-400 mb-1">Input:</div>
|
||||
<pre className="text-dark-300 bg-dark-800 p-2 rounded overflow-x-auto">
|
||||
{JSON.stringify(tool.input, null, 2)}
|
||||
</pre>
|
||||
<div className="text-dark-400 mt-2 mb-1">Result preview:</div>
|
||||
<pre className="text-dark-300 bg-dark-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{tool.result_preview}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col bg-dark-800 rounded-xl border border-dark-600 overflow-hidden ${
|
||||
isExpanded ? 'fixed inset-4 z-50' : 'h-full'
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b border-dark-600 flex items-center justify-between bg-dark-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-white">Claude Code</span>
|
||||
{selectedStudy && (
|
||||
<span className="ml-2 text-xs bg-dark-700 px-2 py-0.5 rounded text-dark-300">
|
||||
{selectedStudy.id}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{messages.length > 0 && (
|
||||
<button
|
||||
onClick={clearConversation}
|
||||
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
|
||||
title="Clear conversation"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{onToggleExpand && (
|
||||
<button
|
||||
onClick={onToggleExpand}
|
||||
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
|
||||
>
|
||||
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Status Warning */}
|
||||
{apiAvailable === false && (
|
||||
<div className="px-4 py-3 bg-yellow-900/20 border-b border-yellow-800/30 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-yellow-400 text-sm">
|
||||
{error || 'Claude API not available'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Sparkles className="w-12 h-12 mx-auto mb-4 text-primary-400 opacity-50" />
|
||||
<p className="text-dark-300 mb-2">Ask me anything about your optimization</p>
|
||||
<p className="text-dark-500 text-sm mb-6">
|
||||
I can analyze results, explain concepts, and help you improve your designs.
|
||||
</p>
|
||||
|
||||
{/* Suggestions */}
|
||||
{suggestions.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 justify-center max-w-md mx-auto">
|
||||
{suggestions.slice(0, 6).map((suggestion, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => sendMessage(suggestion)}
|
||||
disabled={isLoading || apiAvailable === false}
|
||||
className="px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:opacity-50
|
||||
rounded-lg text-sm text-dark-300 hover:text-white transition-colors
|
||||
border border-dark-600 hover:border-dark-500"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}
|
||||
>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`max-w-[85%] rounded-lg ${
|
||||
msg.role === 'user'
|
||||
? 'bg-primary-600 text-white px-4 py-2'
|
||||
: 'bg-dark-700 text-dark-200 px-4 py-3'
|
||||
}`}
|
||||
>
|
||||
{msg.role === 'assistant' ? (
|
||||
<>
|
||||
<div className="prose prose-sm prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// Simplified markdown styling for chat
|
||||
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
|
||||
ul: ({ children }) => <ul className="list-disc list-inside mb-2">{children}</ul>,
|
||||
ol: ({ children }) => <ol className="list-decimal list-inside mb-2">{children}</ol>,
|
||||
li: ({ children }) => <li className="mb-1">{children}</li>,
|
||||
code: ({ inline, children }: any) =>
|
||||
inline ? (
|
||||
<code className="px-1 py-0.5 bg-dark-600 rounded text-primary-400 text-xs">
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<pre className="p-2 bg-dark-800 rounded overflow-x-auto text-xs my-2">
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto my-2">
|
||||
<table className="text-xs border-collapse">{children}</table>
|
||||
</div>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="border border-dark-600 px-2 py-1 bg-dark-800 text-left">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="border border-dark-600 px-2 py-1">{children}</td>
|
||||
),
|
||||
strong: ({ children }) => <strong className="text-white">{children}</strong>,
|
||||
h1: ({ children }) => <h1 className="text-lg font-bold text-white mt-4 mb-2">{children}</h1>,
|
||||
h2: ({ children }) => <h2 className="text-base font-semibold text-white mt-3 mb-2">{children}</h2>,
|
||||
h3: ({ children }) => <h3 className="text-sm font-semibold text-white mt-2 mb-1">{children}</h3>,
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
{renderToolCalls(msg.toolCalls, msg.id)}
|
||||
</>
|
||||
) : (
|
||||
<p>{msg.content}</p>
|
||||
)}
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className="w-8 h-8 rounded-lg bg-dark-600 flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-4 h-4 text-dark-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoading && (
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
|
||||
<Loader2 className="w-4 h-4 text-white animate-spin" />
|
||||
</div>
|
||||
<div className="bg-dark-700 rounded-lg px-4 py-3 text-dark-400">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span>Thinking</span>
|
||||
<span className="flex gap-1">
|
||||
<span className="w-1.5 h-1.5 bg-dark-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<span className="w-1.5 h-1.5 bg-dark-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<span className="w-1.5 h-1.5 bg-dark-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{error && !isLoading && (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-red-900/20 border border-red-800/30 rounded-lg">
|
||||
<AlertCircle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-red-400 text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-dark-600 bg-dark-800">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
placeholder={apiAvailable === false ? 'API not available...' : 'Ask about your optimization...'}
|
||||
disabled={isLoading || apiAvailable === false}
|
||||
className="flex-1 px-4 py-2.5 bg-dark-700 border border-dark-600 rounded-lg
|
||||
text-white placeholder-dark-400 focus:outline-none focus:border-primary-500
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
onClick={() => sendMessage()}
|
||||
disabled={!input.trim() || isLoading || apiAvailable === false}
|
||||
className="px-4 py-2.5 bg-primary-600 hover:bg-primary-500 disabled:opacity-50
|
||||
disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Send className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-dark-500 mt-2 text-center">
|
||||
Claude can query your study data, analyze results, and help improve your optimization.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaudeChat;
|
||||
336
atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx
Normal file
336
atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import 'xterm/css/xterm.css';
|
||||
import {
|
||||
Terminal as TerminalIcon,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
X,
|
||||
RefreshCw,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
|
||||
interface ClaudeTerminalProps {
|
||||
isExpanded?: boolean;
|
||||
onToggleExpand?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
|
||||
isExpanded = false,
|
||||
onToggleExpand,
|
||||
onClose
|
||||
}) => {
|
||||
const { selectedStudy } = useStudy();
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const xtermRef = useRef<Terminal | null>(null);
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [_error, setError] = useState<string | null>(null);
|
||||
const [cliAvailable, setCliAvailable] = useState<boolean | null>(null);
|
||||
|
||||
// Check CLI availability
|
||||
useEffect(() => {
|
||||
fetch('/api/terminal/status')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setCliAvailable(data.available);
|
||||
if (!data.available) {
|
||||
setError(data.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setCliAvailable(false);
|
||||
setError('Could not check Claude Code CLI status');
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Initialize terminal
|
||||
useEffect(() => {
|
||||
if (!terminalRef.current || xtermRef.current) return;
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 13,
|
||||
fontFamily: '"JetBrains Mono", "Fira Code", Consolas, monospace',
|
||||
theme: {
|
||||
background: '#0f172a',
|
||||
foreground: '#e2e8f0',
|
||||
cursor: '#60a5fa',
|
||||
cursorAccent: '#0f172a',
|
||||
selectionBackground: '#334155',
|
||||
black: '#1e293b',
|
||||
red: '#ef4444',
|
||||
green: '#22c55e',
|
||||
yellow: '#eab308',
|
||||
blue: '#3b82f6',
|
||||
magenta: '#a855f7',
|
||||
cyan: '#06b6d4',
|
||||
white: '#f1f5f9',
|
||||
brightBlack: '#475569',
|
||||
brightRed: '#f87171',
|
||||
brightGreen: '#4ade80',
|
||||
brightYellow: '#facc15',
|
||||
brightBlue: '#60a5fa',
|
||||
brightMagenta: '#c084fc',
|
||||
brightCyan: '#22d3ee',
|
||||
brightWhite: '#f8fafc',
|
||||
},
|
||||
allowProposedApi: true,
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
const webLinksAddon = new WebLinksAddon();
|
||||
|
||||
term.loadAddon(fitAddon);
|
||||
term.loadAddon(webLinksAddon);
|
||||
term.open(terminalRef.current);
|
||||
|
||||
// Initial fit
|
||||
setTimeout(() => fitAddon.fit(), 0);
|
||||
|
||||
xtermRef.current = term;
|
||||
fitAddonRef.current = fitAddon;
|
||||
|
||||
// Welcome message
|
||||
term.writeln('\x1b[1;36m╔══════════════════════════════════════════════════════════╗\x1b[0m');
|
||||
term.writeln('\x1b[1;36m║\x1b[0m \x1b[1;37mClaude Code Terminal\x1b[0m \x1b[1;36m║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m║\x1b[0m \x1b[90mFull Claude Code experience in the Atomizer dashboard\x1b[0m \x1b[1;36m║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m╚══════════════════════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
|
||||
if (cliAvailable === false) {
|
||||
term.writeln('\x1b[1;31mError:\x1b[0m Claude Code CLI not found.');
|
||||
term.writeln('Install with: \x1b[1;33mnpm install -g @anthropic-ai/claude-code\x1b[0m');
|
||||
} else {
|
||||
term.writeln('\x1b[90mClick "Connect" to start a Claude Code session.\x1b[0m');
|
||||
term.writeln('\x1b[90mClaude will have access to CLAUDE.md and .claude/ skills.\x1b[0m');
|
||||
}
|
||||
term.writeln('');
|
||||
|
||||
return () => {
|
||||
term.dispose();
|
||||
xtermRef.current = null;
|
||||
};
|
||||
}, [cliAvailable]);
|
||||
|
||||
// Handle resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (fitAddonRef.current) {
|
||||
fitAddonRef.current.fit();
|
||||
// Send resize to backend
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN && xtermRef.current) {
|
||||
wsRef.current.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: xtermRef.current.cols,
|
||||
rows: xtermRef.current.rows
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
// Also fit when expanded state changes
|
||||
setTimeout(handleResize, 100);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [isExpanded]);
|
||||
|
||||
// Connect to terminal WebSocket
|
||||
const connect = useCallback(() => {
|
||||
if (!xtermRef.current || wsRef.current?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
setIsConnecting(true);
|
||||
setError(null);
|
||||
|
||||
// Determine working directory - use study path if available
|
||||
let workingDir = '';
|
||||
if (selectedStudy?.id) {
|
||||
// The study directory path
|
||||
workingDir = `?working_dir=C:/Users/Antoine/Atomizer`;
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude${workingDir}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false);
|
||||
xtermRef.current?.clear();
|
||||
xtermRef.current?.writeln('\x1b[1;32mConnected to Claude Code\x1b[0m');
|
||||
xtermRef.current?.writeln('');
|
||||
|
||||
// Send initial resize
|
||||
if (xtermRef.current) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: xtermRef.current.cols,
|
||||
rows: xtermRef.current.rows
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
switch (message.type) {
|
||||
case 'output':
|
||||
xtermRef.current?.write(message.data);
|
||||
break;
|
||||
case 'started':
|
||||
xtermRef.current?.writeln(`\x1b[90m${message.message}\x1b[0m`);
|
||||
break;
|
||||
case 'exit':
|
||||
xtermRef.current?.writeln('');
|
||||
xtermRef.current?.writeln(`\x1b[33mClaude Code exited with code ${message.code}\x1b[0m`);
|
||||
setIsConnected(false);
|
||||
break;
|
||||
case 'error':
|
||||
xtermRef.current?.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
|
||||
setError(message.message);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Raw output
|
||||
xtermRef.current?.write(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
setError('WebSocket connection error');
|
||||
setIsConnecting(false);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
xtermRef.current?.writeln('');
|
||||
xtermRef.current?.writeln('\x1b[90mDisconnected from Claude Code\x1b[0m');
|
||||
};
|
||||
|
||||
// Handle terminal input
|
||||
const disposable = xtermRef.current.onData((data) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'input', data }));
|
||||
}
|
||||
});
|
||||
|
||||
wsRef.current = ws;
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [selectedStudy]);
|
||||
|
||||
// Disconnect
|
||||
const disconnect = useCallback(() => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.send(JSON.stringify({ type: 'stop' }));
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
setIsConnected(false);
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col bg-dark-800 rounded-xl border border-dark-600 overflow-hidden ${
|
||||
isExpanded ? 'fixed inset-4 z-50' : 'h-full'
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b border-dark-600 flex items-center justify-between bg-dark-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center">
|
||||
<TerminalIcon className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-white">Claude Code</span>
|
||||
{selectedStudy && (
|
||||
<span className="ml-2 text-xs bg-dark-700 px-2 py-0.5 rounded text-dark-300">
|
||||
{selectedStudy.id}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Connection status indicator */}
|
||||
<div className={`w-2 h-2 rounded-full ml-2 ${
|
||||
isConnected ? 'bg-green-500' : 'bg-dark-500'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Connect/Disconnect button */}
|
||||
<button
|
||||
onClick={isConnected ? disconnect : connect}
|
||||
disabled={isConnecting || cliAvailable === false}
|
||||
className={`px-3 py-1.5 text-sm rounded-lg transition-colors flex items-center gap-2 ${
|
||||
isConnected
|
||||
? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
|
||||
: 'bg-green-600/20 text-green-400 hover:bg-green-600/30'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
) : null}
|
||||
{isConnected ? 'Disconnect' : 'Connect'}
|
||||
</button>
|
||||
|
||||
{onToggleExpand && (
|
||||
<button
|
||||
onClick={onToggleExpand}
|
||||
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
|
||||
>
|
||||
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CLI not available warning */}
|
||||
{cliAvailable === false && (
|
||||
<div className="px-4 py-3 bg-yellow-900/20 border-b border-yellow-800/30 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-yellow-400 text-sm">
|
||||
Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terminal */}
|
||||
<div className="flex-1 p-2 bg-[#0f172a]">
|
||||
<div ref={terminalRef} className="h-full" />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-2 border-t border-dark-600 bg-dark-800">
|
||||
<p className="text-xs text-dark-500 text-center">
|
||||
Claude Code has access to CLAUDE.md instructions and .claude/ skills for Atomizer optimization
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaudeTerminal;
|
||||
@@ -0,0 +1,355 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Play,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
AlertTriangle,
|
||||
Loader2,
|
||||
ExternalLink,
|
||||
Sliders,
|
||||
Skull
|
||||
} from 'lucide-react';
|
||||
import { apiClient, ProcessStatus } from '../../api/client';
|
||||
import { useStudy } from '../../context/StudyContext';
|
||||
|
||||
interface ControlPanelProps {
|
||||
onStatusChange?: () => void;
|
||||
}
|
||||
|
||||
export const ControlPanel: React.FC<ControlPanelProps> = ({ onStatusChange }) => {
|
||||
const { selectedStudy, refreshStudies } = useStudy();
|
||||
const [processStatus, setProcessStatus] = useState<ProcessStatus | null>(null);
|
||||
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
// Settings for starting optimization
|
||||
const [settings, setSettings] = useState({
|
||||
freshStart: false,
|
||||
maxIterations: 100,
|
||||
feaBatchSize: 5,
|
||||
tuneTrials: 30,
|
||||
ensembleSize: 3,
|
||||
patience: 5,
|
||||
});
|
||||
|
||||
// Validate top N
|
||||
const [validateTopN, setValidateTopN] = useState(5);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedStudy) {
|
||||
fetchProcessStatus();
|
||||
const interval = setInterval(fetchProcessStatus, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [selectedStudy]);
|
||||
|
||||
const fetchProcessStatus = async () => {
|
||||
if (!selectedStudy) return;
|
||||
try {
|
||||
const status = await apiClient.getProcessStatus(selectedStudy.id);
|
||||
setProcessStatus(status);
|
||||
} catch (err) {
|
||||
// Process status endpoint might not exist yet
|
||||
setProcessStatus(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = async () => {
|
||||
if (!selectedStudy) return;
|
||||
setActionInProgress('start');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await apiClient.startOptimization(selectedStudy.id, {
|
||||
freshStart: settings.freshStart,
|
||||
maxIterations: settings.maxIterations,
|
||||
feaBatchSize: settings.feaBatchSize,
|
||||
tuneTrials: settings.tuneTrials,
|
||||
ensembleSize: settings.ensembleSize,
|
||||
patience: settings.patience,
|
||||
});
|
||||
|
||||
await fetchProcessStatus();
|
||||
await refreshStudies();
|
||||
onStatusChange?.();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to start optimization');
|
||||
} finally {
|
||||
setActionInProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async () => {
|
||||
if (!selectedStudy) return;
|
||||
setActionInProgress('stop');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await apiClient.stopOptimization(selectedStudy.id);
|
||||
await fetchProcessStatus();
|
||||
await refreshStudies();
|
||||
onStatusChange?.();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to stop optimization');
|
||||
} finally {
|
||||
setActionInProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!selectedStudy) return;
|
||||
setActionInProgress('validate');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await apiClient.validateOptimization(selectedStudy.id, { topN: validateTopN });
|
||||
await fetchProcessStatus();
|
||||
await refreshStudies();
|
||||
onStatusChange?.();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to start validation');
|
||||
} finally {
|
||||
setActionInProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLaunchOptuna = async () => {
|
||||
if (!selectedStudy) return;
|
||||
setActionInProgress('optuna');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await apiClient.launchOptunaDashboard(selectedStudy.id);
|
||||
window.open(result.url, '_blank');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to launch Optuna dashboard');
|
||||
} finally {
|
||||
setActionInProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const isRunning = processStatus?.is_running || selectedStudy?.status === 'running';
|
||||
|
||||
return (
|
||||
<div className="bg-dark-800 rounded-xl border border-dark-600 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-dark-600 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Sliders className="w-5 h-5 text-primary-400" />
|
||||
Optimization Control
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
className={`p-2 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>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="px-6 py-4 border-b border-dark-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-dark-400 mb-1">Status</div>
|
||||
<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">Running</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-3 h-3 bg-dark-500 rounded-full" />
|
||||
<span className="text-dark-400">Stopped</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{processStatus && (
|
||||
<div className="text-right">
|
||||
{processStatus.iteration && (
|
||||
<div className="text-sm text-dark-400">
|
||||
Iteration: <span className="text-white">{processStatus.iteration}</span>
|
||||
</div>
|
||||
)}
|
||||
{processStatus.fea_count && (
|
||||
<div className="text-sm 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></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Panel */}
|
||||
{showSettings && (
|
||||
<div className="px-6 py-4 border-b border-dark-700 bg-dark-750">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-dark-400 mb-1">Max Iterations</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maxIterations}
|
||||
onChange={(e) => setSettings({ ...settings, maxIterations: parseInt(e.target.value) || 100 })}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-dark-400 mb-1">FEA Batch Size</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.feaBatchSize}
|
||||
onChange={(e) => setSettings({ ...settings, feaBatchSize: parseInt(e.target.value) || 5 })}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-dark-400 mb-1">Patience</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.patience}
|
||||
onChange={(e) => setSettings({ ...settings, patience: parseInt(e.target.value) || 5 })}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-dark-400 mb-1">Tuning Trials</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.tuneTrials}
|
||||
onChange={(e) => setSettings({ ...settings, tuneTrials: parseInt(e.target.value) || 30 })}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-dark-400 mb-1">Ensemble Size</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.ensembleSize}
|
||||
onChange={(e) => setSettings({ ...settings, ensembleSize: parseInt(e.target.value) || 3 })}
|
||||
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<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-sm text-dark-300">Fresh Start</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="px-6 py-3 bg-red-900/20 border-b border-red-800/30">
|
||||
<div className="flex items-center gap-2 text-red-400 text-sm">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Start / Kill Button */}
|
||||
{isRunning ? (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
disabled={actionInProgress !== null}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-600 hover:bg-red-500
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
text-white rounded-lg transition-colors font-medium"
|
||||
title="Force kill the optimization process and all child processes"
|
||||
>
|
||||
{actionInProgress === 'stop' ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Skull className="w-5 h-5" />
|
||||
)}
|
||||
Kill Process
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleStart}
|
||||
disabled={actionInProgress !== null}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-500
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
{actionInProgress === 'start' ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-5 h-5" />
|
||||
)}
|
||||
Start Optimization
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Validate Button */}
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
disabled={actionInProgress !== null || isRunning}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-primary-600 hover:bg-primary-500
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
{actionInProgress === 'validate' ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
)}
|
||||
Validate Top {validateTopN}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Validation Settings */}
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-sm text-dark-400">Validate top</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={validateTopN}
|
||||
onChange={(e) => setValidateTopN(parseInt(e.target.value) || 5)}
|
||||
className="w-16 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm text-center"
|
||||
/>
|
||||
<span className="text-sm text-dark-400">NN predictions with FEA</span>
|
||||
</div>
|
||||
|
||||
{/* Optuna Dashboard Button */}
|
||||
<button
|
||||
onClick={handleLaunchOptuna}
|
||||
disabled={actionInProgress !== null}
|
||||
className="w-full mt-4 flex items-center justify-center gap-2 px-4 py-2
|
||||
bg-dark-700 hover:bg-dark-600 border border-dark-600
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
text-dark-300 hover:text-white rounded-lg transition-colors text-sm"
|
||||
>
|
||||
{actionInProgress === 'optuna' ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
)}
|
||||
Launch Optuna Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlPanel;
|
||||
@@ -1,31 +1,109 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LayoutDashboard, Settings, FileText, Activity } from 'lucide-react';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Home,
|
||||
Activity,
|
||||
FileText,
|
||||
BarChart3,
|
||||
ChevronLeft,
|
||||
Play,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Zap
|
||||
} from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useStudy } from '../../context/StudyContext';
|
||||
|
||||
export const Sidebar = () => {
|
||||
const navItems = [
|
||||
{ to: '/dashboard', icon: Activity, label: 'Live Dashboard' },
|
||||
{ to: '/configurator', icon: Settings, label: 'Configurator' },
|
||||
{ to: '/results', icon: FileText, label: 'Results Viewer' },
|
||||
];
|
||||
const { selectedStudy, clearStudy } = useStudy();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBackToHome = () => {
|
||||
clearStudy();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Play className="w-3 h-3 text-green-400" />;
|
||||
case 'completed':
|
||||
return <CheckCircle className="w-3 h-3 text-blue-400" />;
|
||||
default:
|
||||
return <Clock className="w-3 h-3 text-dark-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'text-green-400';
|
||||
case 'completed':
|
||||
return 'text-blue-400';
|
||||
default:
|
||||
return 'text-dark-400';
|
||||
}
|
||||
};
|
||||
|
||||
// Navigation items depend on whether a study is selected
|
||||
const navItems = selectedStudy
|
||||
? [
|
||||
{ to: '/dashboard', icon: Activity, label: 'Live Tracker' },
|
||||
{ to: '/results', icon: FileText, label: 'Reports' },
|
||||
{ to: '/analytics', icon: BarChart3, label: 'Analytics' },
|
||||
]
|
||||
: [
|
||||
{ to: '/', icon: Home, label: 'Select Study' },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-dark-800 border-r border-dark-600 flex flex-col h-screen fixed left-0 top-0">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-dark-600">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
||||
<LayoutDashboard className="w-5 h-5 text-white" />
|
||||
<Zap className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-white tracking-tight">Atomizer</h1>
|
||||
</div>
|
||||
<p className="text-xs text-dark-300 mt-1 ml-11">Optimization Platform</p>
|
||||
</div>
|
||||
|
||||
{/* Selected Study Info */}
|
||||
{selectedStudy && (
|
||||
<div className="p-4 border-b border-dark-600">
|
||||
<button
|
||||
onClick={handleBackToHome}
|
||||
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white
|
||||
transition-colors mb-3 group"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
Change Study
|
||||
</button>
|
||||
|
||||
<div className="bg-dark-700 rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-dark-400 uppercase mb-1">Active Study</div>
|
||||
<div className="text-white font-medium truncate">{selectedStudy.name}</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`flex items-center gap-1 text-xs ${getStatusColor(selectedStudy.status)}`}>
|
||||
{getStatusIcon(selectedStudy.status)}
|
||||
{selectedStudy.status}
|
||||
</span>
|
||||
<span className="text-dark-500">|</span>
|
||||
<span className="text-xs text-dark-400">
|
||||
{selectedStudy.progress.current}/{selectedStudy.progress.total}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
'flex items-center gap-3 px-4 py-3 rounded-lg transition-colors duration-200',
|
||||
@@ -41,6 +119,7 @@ export const Sidebar = () => {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Footer Status */}
|
||||
<div className="p-4 border-t border-dark-600">
|
||||
<div className="bg-dark-700 rounded-lg p-4">
|
||||
<div className="text-xs font-medium text-dark-400 uppercase mb-2">System Status</div>
|
||||
@@ -48,8 +127,14 @@ export const Sidebar = () => {
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Backend Online
|
||||
</div>
|
||||
{selectedStudy && selectedStudy.status === 'running' && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-400 mt-1">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Optimization Running
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
93
atomizer-dashboard/frontend/src/context/StudyContext.tsx
Normal file
93
atomizer-dashboard/frontend/src/context/StudyContext.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Study } from '../types';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
interface StudyContextType {
|
||||
selectedStudy: Study | null;
|
||||
setSelectedStudy: (study: Study | null) => void;
|
||||
studies: Study[];
|
||||
refreshStudies: () => Promise<void>;
|
||||
isLoading: boolean;
|
||||
clearStudy: () => void;
|
||||
}
|
||||
|
||||
const StudyContext = createContext<StudyContextType | undefined>(undefined);
|
||||
|
||||
export const StudyProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [selectedStudy, setSelectedStudyState] = useState<Study | null>(null);
|
||||
const [studies, setStudies] = useState<Study[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const refreshStudies = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiClient.getStudies();
|
||||
setStudies(response.studies);
|
||||
|
||||
// If we have a selected study, refresh its data
|
||||
if (selectedStudy) {
|
||||
const updated = response.studies.find(s => s.id === selectedStudy.id);
|
||||
if (updated) {
|
||||
setSelectedStudyState(updated);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch studies:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectedStudy = (study: Study | null) => {
|
||||
setSelectedStudyState(study);
|
||||
if (study) {
|
||||
localStorage.setItem('selectedStudyId', study.id);
|
||||
} else {
|
||||
localStorage.removeItem('selectedStudyId');
|
||||
}
|
||||
};
|
||||
|
||||
const clearStudy = () => {
|
||||
setSelectedStudyState(null);
|
||||
localStorage.removeItem('selectedStudyId');
|
||||
};
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await refreshStudies();
|
||||
|
||||
// Restore last selected study
|
||||
const lastStudyId = localStorage.getItem('selectedStudyId');
|
||||
if (lastStudyId) {
|
||||
const response = await apiClient.getStudies();
|
||||
const study = response.studies.find(s => s.id === lastStudyId);
|
||||
if (study) {
|
||||
setSelectedStudyState(study);
|
||||
}
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StudyContext.Provider value={{
|
||||
selectedStudy,
|
||||
setSelectedStudy,
|
||||
studies,
|
||||
refreshStudies,
|
||||
isLoading,
|
||||
clearStudy
|
||||
}}>
|
||||
{children}
|
||||
</StudyContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useStudy = () => {
|
||||
const context = useContext(StudyContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useStudy must be used within a StudyProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
@import 'katex/dist/katex.min.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
LineChart, Line, ScatterChart, Scatter,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell
|
||||
} from 'recharts';
|
||||
import { Terminal } from 'lucide-react';
|
||||
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
|
||||
import { apiClient } from '../api/client';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import { Card } from '../components/common/Card';
|
||||
import { MetricCard } from '../components/dashboard/MetricCard';
|
||||
import { StudyCard } from '../components/dashboard/StudyCard';
|
||||
// import { OptimizerPanel } from '../components/OptimizerPanel'; // Not used currently
|
||||
import { ControlPanel } from '../components/dashboard/ControlPanel';
|
||||
import { ClaudeTerminal } from '../components/ClaudeTerminal';
|
||||
import { ParetoPlot } from '../components/ParetoPlot';
|
||||
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
|
||||
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
|
||||
@@ -16,7 +19,7 @@ import { ConvergencePlot } from '../components/ConvergencePlot';
|
||||
import { StudyReportViewer } from '../components/StudyReportViewer';
|
||||
import { ConsoleOutput } from '../components/ConsoleOutput';
|
||||
import { ExpandableChart } from '../components/ExpandableChart';
|
||||
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
|
||||
import type { Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
|
||||
|
||||
// Lazy load Plotly components for better initial load performance
|
||||
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
|
||||
@@ -32,8 +35,17 @@ const ChartLoading = () => (
|
||||
);
|
||||
|
||||
export default function Dashboard() {
|
||||
const [studies, setStudies] = useState<Study[]>([]);
|
||||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const { selectedStudy, refreshStudies } = useStudy();
|
||||
const selectedStudyId = selectedStudy?.id || null;
|
||||
|
||||
// Redirect to home if no study selected
|
||||
useEffect(() => {
|
||||
if (!selectedStudy) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [selectedStudy, navigate]);
|
||||
|
||||
const [allTrials, setAllTrials] = useState<Trial[]>([]);
|
||||
const [displayedTrials, setDisplayedTrials] = useState<Trial[]>([]);
|
||||
const [bestValue, setBestValue] = useState<number>(Infinity);
|
||||
@@ -55,26 +67,9 @@ export default function Dashboard() {
|
||||
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
|
||||
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
|
||||
|
||||
// Load studies on mount
|
||||
useEffect(() => {
|
||||
apiClient.getStudies()
|
||||
.then(data => {
|
||||
setStudies(data.studies);
|
||||
if (data.studies.length > 0) {
|
||||
// Check LocalStorage for last selected study
|
||||
const savedStudyId = localStorage.getItem('lastSelectedStudyId');
|
||||
const studyExists = data.studies.find(s => s.id === savedStudyId);
|
||||
|
||||
if (savedStudyId && studyExists) {
|
||||
setSelectedStudyId(savedStudyId);
|
||||
} else {
|
||||
const running = data.studies.find(s => s.status === 'running');
|
||||
setSelectedStudyId(running?.id || data.studies[0].id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
// Claude chat panel state
|
||||
const [chatOpen, setChatOpen] = useState(false);
|
||||
const [chatExpanded, setChatExpanded] = useState(false);
|
||||
|
||||
const showAlert = (type: 'success' | 'warning', message: string) => {
|
||||
const id = alertIdCounter;
|
||||
@@ -111,9 +106,6 @@ export default function Dashboard() {
|
||||
setPrunedCount(0);
|
||||
setExpandedTrials(new Set());
|
||||
|
||||
// Save to LocalStorage
|
||||
localStorage.setItem('lastSelectedStudyId', selectedStudyId);
|
||||
|
||||
apiClient.getStudyHistory(selectedStudyId)
|
||||
.then(data => {
|
||||
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
|
||||
@@ -331,6 +323,19 @@ export default function Dashboard() {
|
||||
<p className="text-dark-300 mt-1">Real-time optimization monitoring</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{/* Claude Code Terminal Toggle Button */}
|
||||
<button
|
||||
onClick={() => setChatOpen(!chatOpen)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
||||
chatOpen
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
|
||||
}`}
|
||||
title="Open Claude Code terminal"
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Claude Code</span>
|
||||
</button>
|
||||
{selectedStudyId && (
|
||||
<StudyReportViewer studyId={selectedStudyId} />
|
||||
)}
|
||||
@@ -380,24 +385,13 @@ export default function Dashboard() {
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
{/* Sidebar - Study List */}
|
||||
{/* Control Panel - Left Sidebar */}
|
||||
<aside className="col-span-3">
|
||||
<Card title="Active Studies">
|
||||
<div className="space-y-3 max-h-[calc(100vh-200px)] overflow-y-auto">
|
||||
{studies.map(study => (
|
||||
<StudyCard
|
||||
key={study.id}
|
||||
study={study}
|
||||
isActive={study.id === selectedStudyId}
|
||||
onClick={() => setSelectedStudyId(study.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<ControlPanel onStatusChange={refreshStudies} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="col-span-9">
|
||||
{/* Main Content - shrinks when chat is open */}
|
||||
<main className={chatOpen ? 'col-span-5' : 'col-span-9'}>
|
||||
{/* Study Name Header */}
|
||||
{selectedStudyId && (
|
||||
<div className="mb-4 pb-3 border-b border-dark-600">
|
||||
@@ -884,6 +878,17 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Claude Code Terminal - Right Sidebar */}
|
||||
{chatOpen && (
|
||||
<aside className="col-span-4 h-[calc(100vh-12rem)] sticky top-24">
|
||||
<ClaudeTerminal
|
||||
isExpanded={chatExpanded}
|
||||
onToggleExpand={() => setChatExpanded(!chatExpanded)}
|
||||
onClose={() => setChatOpen(false)}
|
||||
/>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
455
atomizer-dashboard/frontend/src/pages/Home.tsx
Normal file
455
atomizer-dashboard/frontend/src/pages/Home.tsx
Normal file
@@ -0,0 +1,455 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
FolderOpen,
|
||||
Play,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
ArrowRight,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
FileText,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Target,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import { Study } from '../types';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { studies, setSelectedStudy, refreshStudies, isLoading } = useStudy();
|
||||
const [selectedPreview, setSelectedPreview] = useState<Study | null>(null);
|
||||
const [readme, setReadme] = useState<string>('');
|
||||
const [readmeLoading, setReadmeLoading] = useState(false);
|
||||
const [showAllStudies, setShowAllStudies] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Load README when a study is selected for preview
|
||||
useEffect(() => {
|
||||
if (selectedPreview) {
|
||||
loadReadme(selectedPreview.id);
|
||||
} else {
|
||||
setReadme('');
|
||||
}
|
||||
}, [selectedPreview]);
|
||||
|
||||
const loadReadme = async (studyId: string) => {
|
||||
setReadmeLoading(true);
|
||||
try {
|
||||
const response = await apiClient.getStudyReadme(studyId);
|
||||
setReadme(response.content || 'No README found for this study.');
|
||||
} catch (error) {
|
||||
setReadme('No README found for this study.');
|
||||
} finally {
|
||||
setReadmeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectStudy = (study: Study) => {
|
||||
setSelectedStudy(study);
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Play className="w-3.5 h-3.5" />;
|
||||
case 'completed':
|
||||
return <CheckCircle className="w-3.5 h-3.5" />;
|
||||
case 'not_started':
|
||||
return <Clock className="w-3.5 h-3.5" />;
|
||||
default:
|
||||
return <AlertCircle className="w-3.5 h-3.5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusStyles = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return {
|
||||
badge: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
card: 'border-green-500/30 hover:border-green-500/50',
|
||||
glow: 'shadow-green-500/10'
|
||||
};
|
||||
case 'completed':
|
||||
return {
|
||||
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
card: 'border-blue-500/30 hover:border-blue-500/50',
|
||||
glow: 'shadow-blue-500/10'
|
||||
};
|
||||
case 'not_started':
|
||||
return {
|
||||
badge: 'bg-dark-600 text-dark-400 border-dark-500',
|
||||
card: 'border-dark-600 hover:border-dark-500',
|
||||
glow: ''
|
||||
};
|
||||
default:
|
||||
return {
|
||||
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
||||
card: 'border-yellow-500/30 hover:border-yellow-500/50',
|
||||
glow: 'shadow-yellow-500/10'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Sort studies: running first, then by trial count
|
||||
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;
|
||||
});
|
||||
|
||||
const displayedStudies = showAllStudies ? sortedStudies : sortedStudies.slice(0, 6);
|
||||
|
||||
return (
|
||||
<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="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">
|
||||
<Zap className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Atomizer</h1>
|
||||
<p className="text-dark-400 text-sm">FEA Optimization Platform</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refreshStudies()}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-dark-700 hover:bg-dark-600
|
||||
text-white rounded-lg transition-all disabled:opacity-50 border border-dark-600"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-[1600px] mx-auto px-6 py-8">
|
||||
{/* Study Selection Section */}
|
||||
<section className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<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>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading studies...
|
||||
</div>
|
||||
) : studies.length === 0 ? (
|
||||
<div className="text-center py-12 text-dark-400">
|
||||
<FolderOpen className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No studies found. Create a new study to get started.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{displayedStudies.map((study) => {
|
||||
const styles = getStatusStyles(study.status);
|
||||
const isSelected = selectedPreview?.id === study.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={study.id}
|
||||
onClick={() => setSelectedPreview(study)}
|
||||
className={`
|
||||
relative p-4 rounded-xl border cursor-pointer transition-all duration-200
|
||||
bg-dark-800 hover:bg-dark-750
|
||||
${styles.card} ${styles.glow}
|
||||
${isSelected ? 'ring-2 ring-primary-500 border-primary-500' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Status Badge */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0 pr-2">
|
||||
<h3 className="text-white font-medium truncate">{study.name || study.id}</h3>
|
||||
<p className="text-dark-500 text-xs truncate mt-0.5">{study.id}</p>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-full border ${styles.badge}`}>
|
||||
{getStatusIcon(study.status)}
|
||||
{study.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 text-sm mb-3">
|
||||
<div className="flex items-center gap-1.5 text-dark-400">
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
<span>{study.progress.current} trials</span>
|
||||
</div>
|
||||
{study.best_value !== null && (
|
||||
<div className="flex items-center gap-1.5 text-primary-400">
|
||||
<Target className="w-3.5 h-3.5" />
|
||||
<span>{study.best_value.toFixed(4)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-1.5 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${
|
||||
study.status === 'running' ? 'bg-green-500' :
|
||||
study.status === 'completed' ? 'bg-blue-500' : 'bg-primary-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min((study.progress.current / study.progress.total) * 100, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected Indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute -bottom-px left-1/2 -translate-x-1/2 w-12 h-1 bg-primary-500 rounded-t-full" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Study Documentation Section */}
|
||||
{selectedPreview && (
|
||||
<section className="animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
{/* Documentation Header */}
|
||||
<div className="bg-dark-800 rounded-t-xl border border-dark-600 border-b-0">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-dark-700 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">{selectedPreview.name || selectedPreview.id}</h2>
|
||||
<p className="text-dark-400 text-sm">Study Documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSelectStudy(selectedPreview)}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-primary-600 hover:bg-primary-500
|
||||
text-white rounded-lg transition-all font-medium shadow-lg shadow-primary-500/20
|
||||
hover:shadow-primary-500/30"
|
||||
>
|
||||
Open Dashboard
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* README Content */}
|
||||
<div className="bg-dark-850 rounded-b-xl border border-dark-600 border-t-0 overflow-hidden">
|
||||
{readmeLoading ? (
|
||||
<div className="flex items-center justify-center py-16 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading documentation...
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 overflow-x-auto">
|
||||
<article className="markdown-body max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
// Custom heading styles
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-3xl font-bold text-white mb-6 pb-3 border-b border-dark-600">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-2xl font-semibold text-white mt-10 mb-4 pb-2 border-b border-dark-700">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xl font-semibold text-white mt-8 mb-3">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-lg font-medium text-white mt-6 mb-2">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
// Paragraphs
|
||||
p: ({ children }) => (
|
||||
<p className="text-dark-300 leading-relaxed mb-4">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
// Strong/Bold
|
||||
strong: ({ children }) => (
|
||||
<strong className="text-white font-semibold">{children}</strong>
|
||||
),
|
||||
// Links
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-primary-400 hover:text-primary-300 underline underline-offset-2"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
// Lists
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-dark-300 mb-4 space-y-1.5 ml-2">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside text-dark-300 mb-4 space-y-1.5 ml-2">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-dark-300 leading-relaxed">{children}</li>
|
||||
),
|
||||
// Code blocks with syntax highlighting
|
||||
code: ({ inline, className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const language = match ? match[1] : '';
|
||||
|
||||
if (!inline && language) {
|
||||
return (
|
||||
<div className="my-4 rounded-lg overflow-hidden border border-dark-600">
|
||||
<div className="bg-dark-700 px-4 py-2 text-xs text-dark-400 font-mono border-b border-dark-600">
|
||||
{language}
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
style={oneDark}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
background: '#1a1d23',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!inline) {
|
||||
return (
|
||||
<pre className="my-4 p-4 bg-dark-700 rounded-lg border border-dark-600 overflow-x-auto">
|
||||
<code className="text-primary-400 text-sm font-mono">{children}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code className="px-1.5 py-0.5 bg-dark-700 text-primary-400 rounded text-sm font-mono">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
// Tables
|
||||
table: ({ children }) => (
|
||||
<div className="my-6 overflow-x-auto rounded-lg border border-dark-600">
|
||||
<table className="w-full text-sm">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-dark-700 text-white">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody className="divide-y divide-dark-600">
|
||||
{children}
|
||||
</tbody>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr className="hover:bg-dark-750 transition-colors">
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-3 text-left font-semibold text-white border-b border-dark-600">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-3 text-dark-300">
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
// Blockquotes
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="my-4 pl-4 border-l-4 border-primary-500 bg-dark-750 py-3 pr-4 rounded-r-lg">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
// Horizontal rules
|
||||
hr: () => (
|
||||
<hr className="my-8 border-dark-600" />
|
||||
),
|
||||
// Images
|
||||
img: ({ src, alt }) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="my-4 rounded-lg max-w-full h-auto border border-dark-600"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Empty State when no study selected */}
|
||||
{!selectedPreview && studies.length > 0 && (
|
||||
<section className="flex items-center justify-center py-16 text-dark-400">
|
||||
<div className="text-center">
|
||||
<FileText className="w-16 h-16 mx-auto mb-4 opacity-30" />
|
||||
<p className="text-lg">Select a study to view its documentation</p>
|
||||
<p className="text-sm mt-1 text-dark-500">Click on any study card above</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,151 +1,242 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card } from '../components/common/Card';
|
||||
import { Button } from '../components/common/Button';
|
||||
import { Download, FileText, Image, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
Download,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Copy
|
||||
} from 'lucide-react';
|
||||
import { apiClient } from '../api/client';
|
||||
import { Study } from '../types';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
export default function Results() {
|
||||
const [studies, setStudies] = useState<Study[]>([]);
|
||||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const { selectedStudy } = useStudy();
|
||||
const navigate = useNavigate();
|
||||
const [reportContent, setReportContent] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [lastGenerated, setLastGenerated] = useState<string | null>(null);
|
||||
|
||||
// Redirect if no study selected
|
||||
useEffect(() => {
|
||||
apiClient.getStudies()
|
||||
.then(data => {
|
||||
setStudies(data.studies);
|
||||
if (data.studies.length > 0) {
|
||||
const completed = data.studies.find(s => s.status === 'completed');
|
||||
setSelectedStudyId(completed?.id || data.studies[0].id);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedStudyId) {
|
||||
setLoading(true);
|
||||
apiClient.getStudyReport(selectedStudyId)
|
||||
.then(data => {
|
||||
setReportContent(data.content);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to fetch report:', err);
|
||||
// Fallback for demo if report doesn't exist
|
||||
setReportContent(`# Optimization Report: ${selectedStudyId}
|
||||
|
||||
## Executive Summary
|
||||
The optimization study successfully converged after 45 trials. The best design achieved a mass reduction of 15% while maintaining all constraints.
|
||||
|
||||
## Key Findings
|
||||
- **Best Objective Value**: 115.185 Hz
|
||||
- **Critical Parameter**: Plate Thickness (sensitivity: 0.85)
|
||||
- **Constraint Margins**: All safety factors > 1.2
|
||||
|
||||
## Recommendations
|
||||
Based on the results, we recommend proceeding with the design from Trial #45. Further refinement could be achieved by narrowing the bounds for 'thickness'.
|
||||
`);
|
||||
setLoading(false);
|
||||
});
|
||||
if (!selectedStudy) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [selectedStudyId]);
|
||||
}, [selectedStudy, navigate]);
|
||||
|
||||
// Load report when study changes
|
||||
useEffect(() => {
|
||||
if (selectedStudy) {
|
||||
loadReport();
|
||||
}
|
||||
}, [selectedStudy]);
|
||||
|
||||
const loadReport = async () => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
const handleRegenerate = () => {
|
||||
if (!selectedStudyId) return;
|
||||
setLoading(true);
|
||||
// In a real app, this would call an endpoint to trigger report generation
|
||||
setTimeout(() => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await apiClient.getStudyReport(selectedStudy.id);
|
||||
setReportContent(data.content);
|
||||
if (data.generated_at) {
|
||||
setLastGenerated(data.generated_at);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// No report yet - show placeholder
|
||||
setReportContent(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
setGenerating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await apiClient.generateReport(selectedStudy.id);
|
||||
setReportContent(data.content);
|
||||
if (data.generated_at) {
|
||||
setLastGenerated(data.generated_at);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to generate report');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (reportContent) {
|
||||
await navigator.clipboard.writeText(reportContent);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!reportContent || !selectedStudy) return;
|
||||
|
||||
const blob = new Blob([reportContent], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${selectedStudy.id}_report.md`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
if (!selectedStudy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-[calc(100vh-100px)] flex flex-col">
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-primary-400">Results Viewer</h1>
|
||||
<p className="text-dark-300 mt-1">Analyze completed optimization studies</p>
|
||||
<h1 className="text-2xl font-bold text-white">Optimization Report</h1>
|
||||
<p className="text-dark-400 mt-1">{selectedStudy.name}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />}
|
||||
onClick={handleRegenerate}
|
||||
disabled={loading || !selectedStudyId}
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={generating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button variant="secondary" icon={<Download className="w-4 h-4" />}>
|
||||
Export Data
|
||||
{generating ? 'Generating...' : reportContent ? 'Update Report' : 'Generate Report'}
|
||||
</Button>
|
||||
{reportContent && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={copied ? <CheckCircle className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 flex-1 min-h-0">
|
||||
{/* Sidebar - Study Selection */}
|
||||
<aside className="col-span-3 flex flex-col gap-4">
|
||||
<Card title="Select Study" className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="space-y-2 overflow-y-auto flex-1 pr-2">
|
||||
{studies.map(study => (
|
||||
<button
|
||||
key={study.id}
|
||||
onClick={() => setSelectedStudyId(study.id)}
|
||||
className={`w-full text-left p-3 rounded-lg transition-colors ${
|
||||
selectedStudyId === study.id
|
||||
? 'bg-primary-900/30 text-primary-100 border border-primary-700/50'
|
||||
: 'text-dark-300 hover:bg-dark-700'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium truncate">{study.name}</div>
|
||||
<div className="text-xs text-dark-400 mt-1 capitalize">{study.status}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</aside>
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-900/20 border border-red-800/30 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-red-400">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content - Report Viewer */}
|
||||
<main className="col-span-9 flex flex-col gap-6 overflow-hidden">
|
||||
<Card className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
Optimization Report
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-2 text-dark-300 hover:text-white hover:bg-dark-700 rounded-lg" title="View Charts">
|
||||
<Image className="w-5 h-5" />
|
||||
</button>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<Card className="h-full overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
Report Content
|
||||
</h2>
|
||||
{lastGenerated && (
|
||||
<span className="text-xs text-dark-400">
|
||||
Last generated: {new Date(lastGenerated).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
|
||||
{loading ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-dark-300">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mb-3" />
|
||||
<span>Loading report...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-dark-300">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mb-2" />
|
||||
<span className="ml-2">Loading report...</span>
|
||||
</div>
|
||||
) : reportContent ? (
|
||||
<div className="prose prose-invert max-w-none">
|
||||
{/* Simple markdown rendering for now */}
|
||||
{reportContent.split('\n').map((line, i) => {
|
||||
if (line.startsWith('# ')) return <h1 key={i} className="text-2xl font-bold text-white mt-6 mb-4">{line.substring(2)}</h1>;
|
||||
if (line.startsWith('## ')) return <h2 key={i} className="text-xl font-bold text-primary-200 mt-6 mb-3">{line.substring(3)}</h2>;
|
||||
if (line.startsWith('- ')) return <li key={i} className="ml-4 text-dark-100">{line.substring(2)}</li>;
|
||||
return <p key={i} className="text-dark-200 mb-2">{line}</p>;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-dark-300">
|
||||
Select a study to view results
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
) : reportContent ? (
|
||||
<div className="prose prose-invert prose-sm max-w-none
|
||||
prose-headings:text-white prose-headings:font-semibold
|
||||
prose-p:text-dark-300 prose-strong:text-white
|
||||
prose-code:text-primary-400 prose-code:bg-dark-700 prose-code:px-1 prose-code:rounded
|
||||
prose-pre:bg-dark-700 prose-pre:border prose-pre:border-dark-600
|
||||
prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline
|
||||
prose-ul:text-dark-300 prose-ol:text-dark-300
|
||||
prose-li:text-dark-300
|
||||
prose-table:border-collapse prose-th:border prose-th:border-dark-600 prose-th:p-2 prose-th:bg-dark-700
|
||||
prose-td:border prose-td:border-dark-600 prose-td:p-2
|
||||
prose-hr:border-dark-600">
|
||||
<ReactMarkdown>{reportContent}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-dark-400">
|
||||
<FileText className="w-16 h-16 mb-4 opacity-50" />
|
||||
<h3 className="text-lg font-medium text-dark-300 mb-2">No Report Generated</h3>
|
||||
<p className="text-sm text-center mb-6 max-w-md">
|
||||
Click "Generate Report" to create an AI-generated analysis of your optimization results.
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={<Sparkles className="w-4 h-4" />}
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
Generate Report
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Study Stats */}
|
||||
<div className="mt-4 grid grid-cols-4 gap-4">
|
||||
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
|
||||
<div className="text-2xl font-bold text-white">{selectedStudy.progress.current}</div>
|
||||
</div>
|
||||
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Best Value</div>
|
||||
<div className="text-2xl font-bold text-primary-400">
|
||||
{selectedStudy.best_value?.toFixed(4) || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Target</div>
|
||||
<div className="text-2xl font-bold text-dark-300">
|
||||
{selectedStudy.target?.toFixed(4) || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Status</div>
|
||||
<div className={`text-lg font-bold capitalize ${
|
||||
selectedStudy.status === 'completed' ? 'text-green-400' :
|
||||
selectedStudy.status === 'running' ? 'text-blue-400' : 'text-dark-400'
|
||||
}`}>
|
||||
{selectedStudy.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ export default {
|
||||
500: '#334155',
|
||||
600: '#1e293b',
|
||||
700: '#0f172a',
|
||||
750: '#0a1120',
|
||||
800: '#020617',
|
||||
850: '#010410',
|
||||
900: '#000000',
|
||||
},
|
||||
primary: {
|
||||
|
||||
Reference in New Issue
Block a user