Skip to content

Commit a2e124d

Browse files
committed
feat: Introduce new Studio UI, CLI Sandbox components, and core backend infrastructure for monitoring, logging, and job management.
1 parent 993441a commit a2e124d

File tree

30 files changed

+4226
-85
lines changed

30 files changed

+4226
-85
lines changed

cli/src/components/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ChatView } from './ChatView.js';
88
import { TrackView } from './TrackView.js';
99
import { AnalyzeView } from './AnalyzeView.js';
1010
import { GenCodeView } from './GenCodeView.js';
11+
import { SandboxView } from './SandboxView.js';
1112
import { StatusBar } from './StatusBar.js';
1213
import { client } from '../utils/api.js';
1314

@@ -18,6 +19,7 @@ export interface AppFlags {
1819
abstract?: string;
1920
output?: string;
2021
stream?: boolean;
22+
runId?: string;
2123
}
2224

2325
interface AppProps {
@@ -59,6 +61,8 @@ export const App: React.FC<AppProps> = ({ command, flags }) => {
5961
);
6062
case 'review':
6163
return <AnalyzeView title={flags.title} doi={flags.doi} mode="review" />;
64+
case 'sandbox':
65+
return <SandboxView runId={flags.runId} />;
6266
case 'chat':
6367
default:
6468
return <ChatView />;

cli/src/components/SandboxView.tsx

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/**
2+
* SandboxView Component - Main sandbox management interface
3+
*
4+
* Features:
5+
* - Tab navigation (Queue, History, Submit)
6+
* - System status display
7+
* - Keyboard shortcuts
8+
*/
9+
10+
import React, { useState, useEffect } from 'react';
11+
import { Box, Text, useApp, useInput } from 'ink';
12+
import { client } from '../utils/api.js';
13+
import { QueuePanel } from './sandbox/QueuePanel.js';
14+
import { HistoryPanel } from './sandbox/HistoryPanel.js';
15+
import { SubmitPanel } from './sandbox/SubmitPanel.js';
16+
import { LogView } from './sandbox/LogView.js';
17+
18+
type TabType = 'queue' | 'history' | 'submit';
19+
type ViewMode = 'tabs' | 'logs';
20+
21+
interface SystemStatusData {
22+
e2b: { status: string; api_key_set?: boolean };
23+
docker: { status: string; containers_active?: number };
24+
queue: { status: string; redis_connected?: boolean };
25+
}
26+
27+
interface SandboxViewProps {
28+
initialTab?: TabType;
29+
runId?: string;
30+
}
31+
32+
export const SandboxView: React.FC<SandboxViewProps> = ({
33+
initialTab = 'queue',
34+
runId: initialRunId,
35+
}) => {
36+
const { exit } = useApp();
37+
const [activeTab, setActiveTab] = useState<TabType>(initialTab);
38+
const [viewMode, setViewMode] = useState<ViewMode>(initialRunId ? 'logs' : 'tabs');
39+
const [selectedRunId, setSelectedRunId] = useState<string | null>(initialRunId || null);
40+
const [systemStatus, setSystemStatus] = useState<SystemStatusData | null>(null);
41+
const [showHelp, setShowHelp] = useState(false);
42+
43+
// Fetch system status
44+
useEffect(() => {
45+
const fetchStatus = async () => {
46+
try {
47+
const status = await client.fetchJson('/api/sandbox/status');
48+
setSystemStatus(status);
49+
} catch {
50+
// Ignore errors
51+
}
52+
};
53+
54+
fetchStatus();
55+
const interval = setInterval(fetchStatus, 10000); // Every 10 seconds
56+
return () => clearInterval(interval);
57+
}, []);
58+
59+
// Keyboard shortcuts
60+
useInput((input, key) => {
61+
if (key.ctrl && input === 'c') {
62+
exit();
63+
}
64+
65+
// Help toggle
66+
if (input === '?') {
67+
setShowHelp(!showHelp);
68+
return;
69+
}
70+
71+
// Only handle tab navigation when in tabs mode
72+
if (viewMode === 'tabs') {
73+
if (key.tab || input === '\t') {
74+
// Cycle through tabs
75+
const tabs: TabType[] = ['queue', 'history', 'submit'];
76+
const currentIndex = tabs.indexOf(activeTab);
77+
const nextIndex = (currentIndex + 1) % tabs.length;
78+
setActiveTab(tabs[nextIndex]);
79+
}
80+
81+
// Number shortcuts for tabs
82+
if (input === '1') setActiveTab('queue');
83+
if (input === '2') setActiveTab('history');
84+
if (input === '3') setActiveTab('submit');
85+
}
86+
87+
// Back from logs view
88+
if (viewMode === 'logs' && (key.escape || input === 'q')) {
89+
setViewMode('tabs');
90+
setSelectedRunId(null);
91+
}
92+
});
93+
94+
// Handler to view logs for a run
95+
const handleViewLogs = (runId: string) => {
96+
setSelectedRunId(runId);
97+
setViewMode('logs');
98+
};
99+
100+
// Handler to go back to tabs
101+
const handleBack = () => {
102+
setViewMode('tabs');
103+
setSelectedRunId(null);
104+
};
105+
106+
// Render help overlay
107+
if (showHelp) {
108+
return (
109+
<Box flexDirection="column" padding={1}>
110+
<Text color="cyan" bold>
111+
Keyboard Shortcuts
112+
</Text>
113+
<Box flexDirection="column" marginTop={1}>
114+
<Text>
115+
<Text color="yellow">Tab</Text> - Cycle through tabs
116+
</Text>
117+
<Text>
118+
<Text color="yellow">1/2/3</Text> - Jump to Queue/History/Submit
119+
</Text>
120+
<Text>
121+
<Text color="yellow">j/k</Text> - Navigate up/down in lists
122+
</Text>
123+
<Text>
124+
<Text color="yellow">Enter</Text> - Select/View details
125+
</Text>
126+
<Text>
127+
<Text color="yellow">c</Text> - Cancel selected job
128+
</Text>
129+
<Text>
130+
<Text color="yellow">r</Text> - Retry selected job
131+
</Text>
132+
<Text>
133+
<Text color="yellow">q/Esc</Text> - Back/Exit
134+
</Text>
135+
<Text>
136+
<Text color="yellow">?</Text> - Toggle this help
137+
</Text>
138+
<Text>
139+
<Text color="yellow">Ctrl+C</Text> - Exit
140+
</Text>
141+
</Box>
142+
<Box marginTop={2}>
143+
<Text color="gray">Press ? to close help</Text>
144+
</Box>
145+
</Box>
146+
);
147+
}
148+
149+
return (
150+
<Box flexDirection="column" padding={1}>
151+
{/* Header */}
152+
<Box justifyContent="space-between">
153+
<Text color="cyan" bold>
154+
Sandbox Manager
155+
</Text>
156+
<SystemStatusBar status={systemStatus} />
157+
</Box>
158+
159+
{/* Tab Bar (only show in tabs mode) */}
160+
{viewMode === 'tabs' && (
161+
<Box marginY={1}>
162+
<TabBar
163+
tabs={[
164+
{ key: 'queue', label: '1. Queue' },
165+
{ key: 'history', label: '2. History' },
166+
{ key: 'submit', label: '3. Submit' },
167+
]}
168+
activeTab={activeTab}
169+
onSelect={setActiveTab}
170+
/>
171+
</Box>
172+
)}
173+
174+
{/* Content */}
175+
{viewMode === 'tabs' && (
176+
<Box flexDirection="column">
177+
{activeTab === 'queue' && <QueuePanel onViewLogs={handleViewLogs} />}
178+
{activeTab === 'history' && <HistoryPanel onViewLogs={handleViewLogs} />}
179+
{activeTab === 'submit' && <SubmitPanel onJobSubmitted={handleViewLogs} />}
180+
</Box>
181+
)}
182+
183+
{viewMode === 'logs' && selectedRunId && (
184+
<LogView runId={selectedRunId} onBack={handleBack} />
185+
)}
186+
187+
{/* Footer */}
188+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
189+
<Text color="gray">
190+
{viewMode === 'tabs'
191+
? 'Tab: switch | 1-3: jump | ?: help | Ctrl+C: exit'
192+
: 'q/Esc: back | c: cancel | r: retry | ?: help'}
193+
</Text>
194+
</Box>
195+
</Box>
196+
);
197+
};
198+
199+
// Tab Bar Component
200+
interface TabDef {
201+
key: TabType;
202+
label: string;
203+
}
204+
205+
interface TabBarProps {
206+
tabs: TabDef[];
207+
activeTab: TabType;
208+
onSelect: (tab: TabType) => void;
209+
}
210+
211+
const TabBar: React.FC<TabBarProps> = ({ tabs, activeTab }) => {
212+
return (
213+
<Box>
214+
{tabs.map((tab, index) => (
215+
<React.Fragment key={tab.key}>
216+
<Box
217+
paddingX={1}
218+
borderStyle={tab.key === activeTab ? 'single' : undefined}
219+
borderColor={tab.key === activeTab ? 'cyan' : undefined}
220+
>
221+
<Text color={tab.key === activeTab ? 'cyan' : 'gray'} bold={tab.key === activeTab}>
222+
{tab.label}
223+
</Text>
224+
</Box>
225+
{index < tabs.length - 1 && <Text color="gray"> </Text>}
226+
</React.Fragment>
227+
))}
228+
</Box>
229+
);
230+
};
231+
232+
// System Status Bar Component
233+
interface SystemStatusBarProps {
234+
status: SystemStatusData | null;
235+
}
236+
237+
const SystemStatusBar: React.FC<SystemStatusBarProps> = ({ status }) => {
238+
if (!status) {
239+
return <Text color="gray">Loading status...</Text>;
240+
}
241+
242+
const getStatusColor = (s: string) => {
243+
if (s === 'healthy' || s === 'available') return 'green';
244+
if (s === 'not_configured' || s === 'unavailable') return 'yellow';
245+
return 'red';
246+
};
247+
248+
return (
249+
<Box>
250+
<Text color="gray">E2B:</Text>
251+
<Text color={getStatusColor(status.e2b?.status || 'unknown')}>
252+
{status.e2b?.status === 'available' ? '●' : '○'}
253+
</Text>
254+
<Text color="gray"> Docker:</Text>
255+
<Text color={getStatusColor(status.docker?.status || 'unknown')}>
256+
{status.docker?.status === 'healthy' ? '●' : '○'}
257+
</Text>
258+
<Text color="gray"> Redis:</Text>
259+
<Text color={status.queue?.redis_connected ? 'green' : 'yellow'}>
260+
{status.queue?.redis_connected ? '●' : '○'}
261+
</Text>
262+
</Box>
263+
);
264+
};

0 commit comments

Comments
 (0)