g3 console initial cut + error doesnt kill auto
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user