g3 console initial cut + error doesnt kill auto

This commit is contained in:
Dhanji Prasanna
2025-11-04 11:39:26 +11:00
parent 6913c5f72e
commit aaf918828f
53 changed files with 6796 additions and 23 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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>,
)

View 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

View 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

View 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;
}