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