g3 console initial cut + error doesnt kill auto
This commit is contained in:
42
crates/g3-console/web/src/App.jsx
Normal file
42
crates/g3-console/web/src/App.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from 'react'
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import Home from './pages/Home'
|
||||
import Detail from './pages/Detail'
|
||||
|
||||
function App() {
|
||||
const [theme, setTheme] = useState('dark')
|
||||
|
||||
React.useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<header className="bg-white dark:bg-gray-800 shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">G3 Console</h1>
|
||||
<button
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
>
|
||||
{theme === 'dark' ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/instance/:id" element={<Detail />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
71
crates/g3-console/web/src/components/ChatView.jsx
Normal file
71
crates/g3-console/web/src/components/ChatView.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github-dark.css'
|
||||
import ToolCall from './ToolCall'
|
||||
|
||||
function ChatView({ messages, toolCalls }) {
|
||||
const renderMessage = (message) => {
|
||||
const html = marked(message.content)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`p-4 rounded-lg mb-4 ${
|
||||
message.agent === 'coach'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500'
|
||||
: message.agent === 'player'
|
||||
? 'bg-gray-50 dark:bg-gray-800 border-l-4 border-gray-500'
|
||||
: 'bg-white dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-semibold text-gray-600 dark:text-gray-400">
|
||||
{message.agent.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="markdown prose dark:prose-invert max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
// Highlight code blocks after render
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block)
|
||||
})
|
||||
}, [messages])
|
||||
|
||||
if (messages.length === 0 && toolCalls.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-gray-600 dark:text-gray-400 py-8">
|
||||
No messages yet
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-h-[600px] overflow-y-auto">
|
||||
{messages.map(renderMessage)}
|
||||
|
||||
{toolCalls.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Tool Calls
|
||||
</h4>
|
||||
{toolCalls.map((toolCall) => (
|
||||
<ToolCall key={toolCall.id} toolCall={toolCall} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatView
|
||||
62
crates/g3-console/web/src/components/GitStatus.jsx
Normal file
62
crates/g3-console/web/src/components/GitStatus.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
|
||||
function GitStatus({ status }) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Git Status</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Branch:</span>
|
||||
<span className="ml-2 font-mono text-gray-900 dark:text-white">{status.branch}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Uncommitted changes:</span>
|
||||
<span className="ml-2 font-semibold text-gray-900 dark:text-white">
|
||||
{status.uncommitted_changes}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{status.modified_files.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-yellow-600 dark:text-yellow-400 mb-1">
|
||||
Modified ({status.modified_files.length})
|
||||
</div>
|
||||
<ul className="text-xs text-gray-700 dark:text-gray-300 space-y-1">
|
||||
{status.modified_files.map((file, i) => (
|
||||
<li key={i} className="font-mono">• {file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status.added_files.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-green-600 dark:text-green-400 mb-1">
|
||||
Added ({status.added_files.length})
|
||||
</div>
|
||||
<ul className="text-xs text-gray-700 dark:text-gray-300 space-y-1">
|
||||
{status.added_files.map((file, i) => (
|
||||
<li key={i} className="font-mono">• {file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status.deleted_files.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-red-600 dark:text-red-400 mb-1">
|
||||
Deleted ({status.deleted_files.length})
|
||||
</div>
|
||||
<ul className="text-xs text-gray-700 dark:text-gray-300 space-y-1">
|
||||
{status.deleted_files.map((file, i) => (
|
||||
<li key={i} className="font-mono">• {file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitStatus
|
||||
99
crates/g3-console/web/src/components/InstancePanel.jsx
Normal file
99
crates/g3-console/web/src/components/InstancePanel.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react'
|
||||
import StatusBadge from './StatusBadge'
|
||||
import ProgressBar from './ProgressBar'
|
||||
|
||||
function InstancePanel({ instance, onClick, onKill, onRestart }) {
|
||||
const { instance: inst, stats, latest_message } = instance
|
||||
|
||||
const handleKill = (e) => {
|
||||
e.stopPropagation()
|
||||
if (window.confirm('Are you sure you want to kill this instance?')) {
|
||||
onKill()
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestart = (e) => {
|
||||
e.stopPropagation()
|
||||
onRestart()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="hero-card p-6 cursor-pointer"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{inst.workspace.split('/').pop() || 'Unknown'}
|
||||
</h3>
|
||||
<StatusBadge status={inst.status} />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{inst.instance_type === 'ensemble' ? 'Coach + Player' : 'Single Agent'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
PID: {inst.pid} | Started: {new Date(inst.start_time).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{inst.status === 'running' && (
|
||||
<button
|
||||
onClick={handleKill}
|
||||
className="hero-button hero-button-danger text-sm"
|
||||
>
|
||||
Kill
|
||||
</button>
|
||||
)}
|
||||
{inst.status === 'terminated' && (
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
className="hero-button hero-button-secondary text-sm"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressBar
|
||||
instanceType={inst.instance_type}
|
||||
durationSecs={stats.duration_secs}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mt-4">
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">Tokens</div>
|
||||
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{stats.total_tokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">Tool Calls</div>
|
||||
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{stats.tool_calls}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">Errors</div>
|
||||
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{stats.errors}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{latest_message && (
|
||||
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400 truncate">
|
||||
<strong>Latest:</strong> {latest_message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 text-xs text-gray-500 dark:text-gray-500">
|
||||
{inst.workspace}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstancePanel
|
||||
179
crates/g3-console/web/src/components/NewRunModal.jsx
Normal file
179
crates/g3-console/web/src/components/NewRunModal.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
function NewRunModal({ onClose, onLaunch }) {
|
||||
const [prompt, setPrompt] = useState('')
|
||||
const [workspace, setWorkspace] = useState('')
|
||||
const [provider, setProvider] = useState('databricks')
|
||||
const [model, setModel] = useState('databricks-claude-sonnet-4-5')
|
||||
const [mode, setMode] = useState('single')
|
||||
const [g3BinaryPath, setG3BinaryPath] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
|
||||
const request = {
|
||||
prompt,
|
||||
workspace,
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
g3_binary_path: g3BinaryPath || null,
|
||||
}
|
||||
|
||||
await onLaunch(request)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const isValid = prompt.trim() && workspace.trim()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="hero-card p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
New Run
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Initial Prompt *
|
||||
</label>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Describe what you want g3 to build..."
|
||||
className="hero-input"
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Workspace Directory *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={workspace}
|
||||
onChange={(e) => setWorkspace(e.target.value)}
|
||||
placeholder="/path/to/workspace"
|
||||
className="hero-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
G3 Binary Path (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={g3BinaryPath}
|
||||
onChange={(e) => setG3BinaryPath(e.target.value)}
|
||||
placeholder="g3 (default) or /path/to/g3"
|
||||
className="hero-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Provider
|
||||
</label>
|
||||
<select
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
className="hero-input"
|
||||
>
|
||||
<option value="databricks">Databricks</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="local">Local</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
className="hero-input"
|
||||
>
|
||||
{provider === 'databricks' && (
|
||||
<>
|
||||
<option value="databricks-claude-sonnet-4-5">Claude Sonnet 4.5</option>
|
||||
<option value="databricks-meta-llama-3-1-405b-instruct">Llama 3.1 405B</option>
|
||||
</>
|
||||
)}
|
||||
{provider === 'anthropic' && (
|
||||
<>
|
||||
<option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
|
||||
<option value="claude-3-opus-20240229">Claude 3 Opus</option>
|
||||
</>
|
||||
)}
|
||||
{provider === 'local' && (
|
||||
<option value="local-model">Local Model</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Execution Mode
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
value="single"
|
||||
checked={mode === 'single'}
|
||||
onChange={(e) => setMode(e.target.value)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Single-shot (one agent, one task)
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
value="ensemble"
|
||||
checked={mode === 'ensemble'}
|
||||
onChange={(e) => setMode(e.target.value)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Coach + Player Ensemble (autonomous mode)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="hero-button hero-button-secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="hero-button hero-button-primary"
|
||||
disabled={!isValid || loading}
|
||||
>
|
||||
{loading ? 'Starting...' : 'Start'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewRunModal
|
||||
34
crates/g3-console/web/src/components/ProgressBar.jsx
Normal file
34
crates/g3-console/web/src/components/ProgressBar.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
|
||||
function ProgressBar({ instanceType, durationSecs }) {
|
||||
const formatDuration = (secs) => {
|
||||
const hours = Math.floor(secs / 3600)
|
||||
const minutes = Math.floor((secs % 3600) / 60)
|
||||
const seconds = secs % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${seconds}s`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`
|
||||
} else {
|
||||
return `${seconds}s`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Duration: {formatDuration(durationSecs)}</span>
|
||||
{instanceType === 'single' && <span>Running...</span>}
|
||||
</div>
|
||||
<div className="hero-progress">
|
||||
<div
|
||||
className="hero-progress-bar"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgressBar
|
||||
28
crates/g3-console/web/src/components/StatusBadge.jsx
Normal file
28
crates/g3-console/web/src/components/StatusBadge.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
const getStatusClass = () => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'hero-badge hero-badge-success'
|
||||
case 'completed':
|
||||
return 'hero-badge hero-badge-success'
|
||||
case 'failed':
|
||||
return 'hero-badge hero-badge-error'
|
||||
case 'idle':
|
||||
return 'hero-badge hero-badge-warning'
|
||||
case 'terminated':
|
||||
return 'hero-badge hero-badge-error'
|
||||
default:
|
||||
return 'hero-badge hero-badge-info'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={getStatusClass()}>
|
||||
{status.toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusBadge
|
||||
70
crates/g3-console/web/src/components/ToolCall.jsx
Normal file
70
crates/g3-console/web/src/components/ToolCall.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
function ToolCall({ toolCall }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-3">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{toolCall.tool_name}
|
||||
</span>
|
||||
{toolCall.success ? (
|
||||
<span className="hero-badge hero-badge-success">SUCCESS</span>
|
||||
) : (
|
||||
<span className="hero-badge hero-badge-error">FAILED</span>
|
||||
)}
|
||||
{toolCall.execution_time_ms && (
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{toolCall.execution_time_ms}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button className="text-gray-600 dark:text-gray-400">
|
||||
{expanded ? '▼' : '▶'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-1">
|
||||
Parameters
|
||||
</div>
|
||||
<pre className="text-xs bg-white dark:bg-gray-900 p-2 rounded overflow-x-auto">
|
||||
{JSON.stringify(toolCall.parameters, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{toolCall.result && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-1">
|
||||
Result
|
||||
</div>
|
||||
<pre className="text-xs bg-white dark:bg-gray-900 p-2 rounded overflow-x-auto">
|
||||
{JSON.stringify(toolCall.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolCall.error && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-red-600 dark:text-red-400 mb-1">
|
||||
Error
|
||||
</div>
|
||||
<pre className="text-xs bg-red-50 dark:bg-red-900/20 p-2 rounded text-red-800 dark:text-red-200">
|
||||
{toolCall.error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolCall
|
||||
10
crates/g3-console/web/src/main.jsx
Normal file
10
crates/g3-console/web/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './styles/hero-ui.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
167
crates/g3-console/web/src/pages/Detail.jsx
Normal file
167
crates/g3-console/web/src/pages/Detail.jsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import StatusBadge from '../components/StatusBadge'
|
||||
import ChatView from '../components/ChatView'
|
||||
import GitStatus from '../components/GitStatus'
|
||||
import ProgressBar from '../components/ProgressBar'
|
||||
|
||||
function Detail() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [instance, setInstance] = useState(null)
|
||||
const [logs, setLogs] = useState({ messages: [], tool_calls: [] })
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const fetchInstance = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/instances/${id}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setInstance(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch instance:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/instances/${id}/logs`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setLogs(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch logs:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstance()
|
||||
fetchLogs()
|
||||
const interval = setInterval(() => {
|
||||
fetchInstance()
|
||||
fetchLogs()
|
||||
}, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [id])
|
||||
|
||||
if (loading || !instance) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-gray-600 dark:text-gray-400">Loading instance details...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="mb-4 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
← Back to instances
|
||||
</button>
|
||||
|
||||
{/* Summary Section */}
|
||||
<div className="hero-card p-6 mb-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Instance {instance.instance.id}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusBadge status={instance.instance.status} />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{instance.instance.instance_type === 'ensemble' ? 'Coach + Player' : 'Single Agent'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressBar
|
||||
instanceType={instance.instance.instance_type}
|
||||
durationSecs={instance.stats.duration_secs}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mt-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Tokens</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{instance.stats.total_tokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Tool Calls</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{instance.stats.tool_calls}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Errors</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{instance.stats.errors}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div><strong>Workspace:</strong> {instance.instance.workspace}</div>
|
||||
<div><strong>Provider:</strong> {instance.instance.provider || 'N/A'}</div>
|
||||
<div><strong>Model:</strong> {instance.instance.model || 'N/A'}</div>
|
||||
<div><strong>Started:</strong> {new Date(instance.instance.start_time).toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Context Section */}
|
||||
<div className="hero-card p-6 mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">Project Context</h3>
|
||||
|
||||
{/* Project Files */}
|
||||
<div className="space-y-4">
|
||||
{instance.project_files.requirements && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">requirements.md</h4>
|
||||
<pre className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{instance.project_files.requirements}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{instance.project_files.readme && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">README.md</h4>
|
||||
<pre className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{instance.project_files.readme}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{instance.project_files.agents && (
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">AGENTS.md</h4>
|
||||
<pre className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{instance.project_files.agents}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Git Status */}
|
||||
{instance.git_status && (
|
||||
<div className="mt-6">
|
||||
<GitStatus status={instance.git_status} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chat View Section */}
|
||||
<div className="hero-card p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">Chat History</h3>
|
||||
<ChatView messages={logs.messages} toolCalls={logs.tool_calls} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
132
crates/g3-console/web/src/pages/Home.jsx
Normal file
132
crates/g3-console/web/src/pages/Home.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import InstancePanel from '../components/InstancePanel'
|
||||
import NewRunModal from '../components/NewRunModal'
|
||||
|
||||
function Home() {
|
||||
const [instances, setInstances] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const fetchInstances = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/instances')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setInstances(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch instances:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstances()
|
||||
const interval = setInterval(fetchInstances, 5000) // Poll every 5 seconds
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const handleInstanceClick = (id) => {
|
||||
navigate(`/instance/${id}`)
|
||||
}
|
||||
|
||||
const handleKill = async (id) => {
|
||||
try {
|
||||
const response = await fetch(`/api/instances/${id}/kill`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (response.ok) {
|
||||
fetchInstances()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to kill instance:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestart = async (id) => {
|
||||
try {
|
||||
const response = await fetch(`/api/instances/${id}/restart`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (response.ok) {
|
||||
fetchInstances()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restart instance:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLaunch = async (request) => {
|
||||
try {
|
||||
const response = await fetch('/api/instances/launch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
if (response.ok) {
|
||||
setShowModal(false)
|
||||
setTimeout(fetchInstances, 2000) // Refresh after 2 seconds
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to launch instance:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-gray-600 dark:text-gray-400">Loading instances...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Running Instances ({instances.length})
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="hero-button hero-button-primary"
|
||||
>
|
||||
+ New Run
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{instances.length === 0 ? (
|
||||
<div className="hero-card p-8 text-center">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
No running instances. Click "New Run" to start a g3 instance.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{instances.map((instance) => (
|
||||
<InstancePanel
|
||||
key={instance.instance.id}
|
||||
instance={instance}
|
||||
onClick={() => handleInstanceClick(instance.instance.id)}
|
||||
onKill={() => handleKill(instance.instance.id)}
|
||||
onRestart={() => handleRestart(instance.instance.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<NewRunModal
|
||||
onClose={() => setShowModal(false)}
|
||||
onLaunch={handleLaunch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
113
crates/g3-console/web/src/styles/hero-ui.css
Normal file
113
crates/g3-console/web/src/styles/hero-ui.css
Normal file
@@ -0,0 +1,113 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Hero UI inspired styles */
|
||||
.hero-card {
|
||||
@apply bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200;
|
||||
}
|
||||
|
||||
.hero-button {
|
||||
@apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;
|
||||
}
|
||||
|
||||
.hero-button-primary {
|
||||
@apply bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600;
|
||||
}
|
||||
|
||||
.hero-button-secondary {
|
||||
@apply bg-gray-200 text-gray-900 hover:bg-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600;
|
||||
}
|
||||
|
||||
.hero-button-danger {
|
||||
@apply bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.hero-badge-success {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200;
|
||||
}
|
||||
|
||||
.hero-badge-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200;
|
||||
}
|
||||
|
||||
.hero-badge-warning {
|
||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200;
|
||||
}
|
||||
|
||||
.hero-badge-info {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200;
|
||||
}
|
||||
|
||||
.hero-input {
|
||||
@apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white;
|
||||
}
|
||||
|
||||
.hero-progress {
|
||||
@apply w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700;
|
||||
}
|
||||
|
||||
.hero-progress-bar {
|
||||
@apply bg-blue-600 h-2.5 rounded-full transition-all duration-300;
|
||||
}
|
||||
|
||||
/* Code highlighting */
|
||||
pre {
|
||||
@apply bg-gray-100 dark:bg-gray-800 rounded-lg p-4 overflow-x-auto;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply font-mono text-sm;
|
||||
}
|
||||
|
||||
/* Markdown styles */
|
||||
.markdown {
|
||||
@apply prose dark:prose-invert max-w-none;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
@apply text-2xl font-bold mb-4;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
@apply text-xl font-bold mb-3;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
@apply text-lg font-bold mb-2;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.markdown ul {
|
||||
@apply list-disc list-inside mb-4;
|
||||
}
|
||||
|
||||
.markdown ol {
|
||||
@apply list-decimal list-inside mb-4;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
@apply text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300;
|
||||
}
|
||||
Reference in New Issue
Block a user