mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 14:22:04 +00:00
250 lines
8.5 KiB
TypeScript
250 lines
8.5 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { useProjects, useFeatures } from './hooks/useProjects'
|
|
import { useProjectWebSocket } from './hooks/useWebSocket'
|
|
import { useFeatureSound } from './hooks/useFeatureSound'
|
|
import { useCelebration } from './hooks/useCelebration'
|
|
|
|
const STORAGE_KEY = 'autonomous-coder-selected-project'
|
|
import { ProjectSelector } from './components/ProjectSelector'
|
|
import { KanbanBoard } from './components/KanbanBoard'
|
|
import { AgentControl } from './components/AgentControl'
|
|
import { ProgressDashboard } from './components/ProgressDashboard'
|
|
import { SetupWizard } from './components/SetupWizard'
|
|
import { AddFeatureForm } from './components/AddFeatureForm'
|
|
import { FeatureModal } from './components/FeatureModal'
|
|
import { DebugLogViewer } from './components/DebugLogViewer'
|
|
import { AgentThought } from './components/AgentThought'
|
|
import { Plus, Loader2 } from 'lucide-react'
|
|
import type { Feature } from './lib/types'
|
|
|
|
function App() {
|
|
// Initialize selected project from localStorage
|
|
const [selectedProject, setSelectedProject] = useState<string | null>(() => {
|
|
try {
|
|
return localStorage.getItem(STORAGE_KEY)
|
|
} catch {
|
|
return null
|
|
}
|
|
})
|
|
const [showAddFeature, setShowAddFeature] = useState(false)
|
|
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
|
|
const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
|
|
const [debugOpen, setDebugOpen] = useState(false)
|
|
const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
|
|
|
|
const { data: projects, isLoading: projectsLoading } = useProjects()
|
|
const { data: features } = useFeatures(selectedProject)
|
|
const wsState = useProjectWebSocket(selectedProject)
|
|
|
|
// Play sounds when features move between columns
|
|
useFeatureSound(features)
|
|
|
|
// Celebrate when all features are complete
|
|
useCelebration(features, selectedProject)
|
|
|
|
// Persist selected project to localStorage
|
|
const handleSelectProject = useCallback((project: string | null) => {
|
|
setSelectedProject(project)
|
|
try {
|
|
if (project) {
|
|
localStorage.setItem(STORAGE_KEY, project)
|
|
} else {
|
|
localStorage.removeItem(STORAGE_KEY)
|
|
}
|
|
} catch {
|
|
// localStorage not available
|
|
}
|
|
}, [])
|
|
|
|
// Validate stored project exists (clear if project was deleted)
|
|
useEffect(() => {
|
|
if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
|
|
handleSelectProject(null)
|
|
}
|
|
}, [selectedProject, projects, handleSelectProject])
|
|
|
|
// Keyboard shortcuts
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
// Ignore if user is typing in an input
|
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
return
|
|
}
|
|
|
|
// D : Toggle debug window
|
|
if (e.key === 'd' || e.key === 'D') {
|
|
e.preventDefault()
|
|
setDebugOpen(prev => !prev)
|
|
}
|
|
|
|
// N : Add new feature (when project selected)
|
|
if ((e.key === 'n' || e.key === 'N') && selectedProject) {
|
|
e.preventDefault()
|
|
setShowAddFeature(true)
|
|
}
|
|
|
|
// Escape : Close modals
|
|
if (e.key === 'Escape') {
|
|
if (showAddFeature) {
|
|
setShowAddFeature(false)
|
|
} else if (selectedFeature) {
|
|
setSelectedFeature(null)
|
|
} else if (debugOpen) {
|
|
setDebugOpen(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
}, [selectedProject, showAddFeature, selectedFeature, debugOpen])
|
|
|
|
// Combine WebSocket progress with feature data
|
|
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
|
passing: features?.done.length ?? 0,
|
|
total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
|
|
percentage: 0,
|
|
}
|
|
|
|
if (progress.total > 0 && progress.percentage === 0) {
|
|
progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
|
|
}
|
|
|
|
if (!setupComplete) {
|
|
return <SetupWizard onComplete={() => setSetupComplete(true)} />
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[var(--color-neo-bg)]">
|
|
{/* Header */}
|
|
<header className="bg-[var(--color-neo-text)] text-white border-b-4 border-[var(--color-neo-border)]">
|
|
<div className="max-w-7xl mx-auto px-4 py-4">
|
|
<div className="flex items-center justify-between">
|
|
{/* Logo and Title */}
|
|
<h1 className="font-display text-2xl font-bold tracking-tight uppercase">
|
|
Autonomous Coder
|
|
</h1>
|
|
|
|
{/* Controls */}
|
|
<div className="flex items-center gap-4">
|
|
<ProjectSelector
|
|
projects={projects ?? []}
|
|
selectedProject={selectedProject}
|
|
onSelectProject={handleSelectProject}
|
|
isLoading={projectsLoading}
|
|
/>
|
|
|
|
{selectedProject && (
|
|
<>
|
|
<button
|
|
onClick={() => setShowAddFeature(true)}
|
|
className="neo-btn neo-btn-primary text-sm"
|
|
title="Press N"
|
|
>
|
|
<Plus size={18} />
|
|
Add Feature
|
|
<kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
|
|
N
|
|
</kbd>
|
|
</button>
|
|
|
|
<AgentControl
|
|
projectName={selectedProject}
|
|
status={wsState.agentStatus}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main
|
|
className="max-w-7xl mx-auto px-4 py-8"
|
|
style={{ paddingBottom: debugOpen ? debugPanelHeight + 32 : undefined }}
|
|
>
|
|
{!selectedProject ? (
|
|
<div className="neo-empty-state mt-12">
|
|
<h2 className="font-display text-2xl font-bold mb-2">
|
|
Welcome to Autonomous Coder
|
|
</h2>
|
|
<p className="text-[var(--color-neo-text-secondary)] mb-4">
|
|
Select a project from the dropdown above or create a new one to get started.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-8">
|
|
{/* Progress Dashboard */}
|
|
<ProgressDashboard
|
|
passing={progress.passing}
|
|
total={progress.total}
|
|
percentage={progress.percentage}
|
|
isConnected={wsState.isConnected}
|
|
/>
|
|
|
|
{/* Agent Thought - shows latest agent narrative */}
|
|
<AgentThought
|
|
logs={wsState.logs}
|
|
agentStatus={wsState.agentStatus}
|
|
/>
|
|
|
|
{/* Initializing Features State - show when agent is running but no features yet */}
|
|
{features &&
|
|
features.pending.length === 0 &&
|
|
features.in_progress.length === 0 &&
|
|
features.done.length === 0 &&
|
|
wsState.agentStatus === 'running' && (
|
|
<div className="neo-card p-8 text-center">
|
|
<Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />
|
|
<h3 className="font-display font-bold text-xl mb-2">
|
|
Initializing Features...
|
|
</h3>
|
|
<p className="text-[var(--color-neo-text-secondary)]">
|
|
The agent is reading your spec and creating features. This may take a moment.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Kanban Board */}
|
|
<KanbanBoard
|
|
features={features}
|
|
onFeatureClick={setSelectedFeature}
|
|
/>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
{/* Add Feature Modal */}
|
|
{showAddFeature && selectedProject && (
|
|
<AddFeatureForm
|
|
projectName={selectedProject}
|
|
onClose={() => setShowAddFeature(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Feature Detail Modal */}
|
|
{selectedFeature && selectedProject && (
|
|
<FeatureModal
|
|
feature={selectedFeature}
|
|
projectName={selectedProject}
|
|
onClose={() => setSelectedFeature(null)}
|
|
/>
|
|
)}
|
|
|
|
{/* Debug Log Viewer - fixed to bottom */}
|
|
{selectedProject && (
|
|
<DebugLogViewer
|
|
logs={wsState.logs}
|
|
isOpen={debugOpen}
|
|
onToggle={() => setDebugOpen(!debugOpen)}
|
|
onClear={wsState.clearLogs}
|
|
onHeightChange={setDebugPanelHeight}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App
|