mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
Adding features work
This commit is contained in:
@@ -8,7 +8,8 @@ import { ProgressDashboard } from './components/ProgressDashboard'
|
||||
import { SetupWizard } from './components/SetupWizard'
|
||||
import { AddFeatureForm } from './components/AddFeatureForm'
|
||||
import { FeatureModal } from './components/FeatureModal'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { DebugLogViewer } from './components/DebugLogViewer'
|
||||
import { Plus, Loader2 } from 'lucide-react'
|
||||
import type { Feature } from './lib/types'
|
||||
|
||||
function App() {
|
||||
@@ -16,6 +17,7 @@ function App() {
|
||||
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 { data: projects, isLoading: projectsLoading } = useProjects()
|
||||
const { data: features } = useFeatures(selectedProject)
|
||||
@@ -78,7 +80,7 @@ function App() {
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<main className={`max-w-7xl mx-auto px-4 py-8 ${debugOpen ? 'pb-80' : ''}`}>
|
||||
{!selectedProject ? (
|
||||
<div className="neo-empty-state mt-12">
|
||||
<h2 className="font-display text-2xl font-bold mb-2">
|
||||
@@ -98,6 +100,23 @@ function App() {
|
||||
isConnected={wsState.isConnected}
|
||||
/>
|
||||
|
||||
{/* 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}
|
||||
@@ -123,6 +142,16 @@ function App() {
|
||||
onClose={() => setSelectedFeature(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Debug Log Viewer - fixed to bottom */}
|
||||
{selectedProject && (
|
||||
<DebugLogViewer
|
||||
logs={wsState.logs}
|
||||
isOpen={debugOpen}
|
||||
onToggle={() => setDebugOpen(!debugOpen)}
|
||||
onClear={wsState.clearLogs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
177
ui/src/components/DebugLogViewer.tsx
Normal file
177
ui/src/components/DebugLogViewer.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Debug Log Viewer Component
|
||||
*
|
||||
* Collapsible panel at the bottom of the screen showing real-time
|
||||
* agent output (tool calls, results, steps). Similar to browser DevTools.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronUp, ChevronDown, Trash2, Terminal } from 'lucide-react'
|
||||
|
||||
interface DebugLogViewerProps {
|
||||
logs: Array<{ line: string; timestamp: string }>
|
||||
isOpen: boolean
|
||||
onToggle: () => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
type LogLevel = 'error' | 'warn' | 'debug' | 'info'
|
||||
|
||||
export function DebugLogViewer({
|
||||
logs,
|
||||
isOpen,
|
||||
onToggle,
|
||||
onClear,
|
||||
}: DebugLogViewerProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [autoScroll, setAutoScroll] = useState(true)
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive (if user hasn't scrolled up)
|
||||
useEffect(() => {
|
||||
if (autoScroll && scrollRef.current && isOpen) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}, [logs, autoScroll, isOpen])
|
||||
|
||||
// Detect if user scrolled up
|
||||
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const el = e.currentTarget
|
||||
const isAtBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 50
|
||||
setAutoScroll(isAtBottom)
|
||||
}
|
||||
|
||||
// Parse log level from line content
|
||||
const getLogLevel = (line: string): LogLevel => {
|
||||
const lowerLine = line.toLowerCase()
|
||||
if (lowerLine.includes('error') || lowerLine.includes('exception') || lowerLine.includes('traceback')) {
|
||||
return 'error'
|
||||
}
|
||||
if (lowerLine.includes('warn') || lowerLine.includes('warning')) {
|
||||
return 'warn'
|
||||
}
|
||||
if (lowerLine.includes('debug')) {
|
||||
return 'debug'
|
||||
}
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// Get color class for log level
|
||||
const getLogColor = (level: LogLevel): string => {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
return 'text-red-400'
|
||||
case 'warn':
|
||||
return 'text-yellow-400'
|
||||
case 'debug':
|
||||
return 'text-gray-400'
|
||||
case 'info':
|
||||
default:
|
||||
return 'text-green-400'
|
||||
}
|
||||
}
|
||||
|
||||
// Format timestamp to HH:MM:SS
|
||||
const formatTimestamp = (timestamp: string): string => {
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-0 left-0 right-0 z-40 transition-all duration-200 ${
|
||||
isOpen ? 'h-72' : 'h-10'
|
||||
}`}
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div
|
||||
className="flex items-center justify-between h-10 px-4 bg-[#1a1a1a] border-t-3 border-black cursor-pointer"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal size={16} className="text-green-400" />
|
||||
<span className="font-mono text-sm text-white font-bold">
|
||||
Debug
|
||||
</span>
|
||||
{logs.length > 0 && (
|
||||
<span className="px-2 py-0.5 text-xs font-mono bg-[#333] text-gray-300 rounded">
|
||||
{logs.length}
|
||||
</span>
|
||||
)}
|
||||
{!autoScroll && isOpen && (
|
||||
<span className="px-2 py-0.5 text-xs font-mono bg-yellow-600 text-white rounded">
|
||||
Paused
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isOpen && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClear()
|
||||
}}
|
||||
className="p-1.5 hover:bg-[#333] rounded transition-colors"
|
||||
title="Clear logs"
|
||||
>
|
||||
<Trash2 size={14} className="text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
<div className="p-1">
|
||||
{isOpen ? (
|
||||
<ChevronDown size={16} className="text-gray-400" />
|
||||
) : (
|
||||
<ChevronUp size={16} className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log content area */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[calc(100%-2.5rem)] overflow-y-auto bg-[#1a1a1a] p-2 font-mono text-sm"
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
No logs yet. Start the agent to see output.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{logs.map((log, index) => {
|
||||
const level = getLogLevel(log.line)
|
||||
const colorClass = getLogColor(level)
|
||||
const timestamp = formatTimestamp(log.timestamp)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${log.timestamp}-${index}`}
|
||||
className="flex gap-2 hover:bg-[#2a2a2a] px-1 py-0.5 rounded"
|
||||
>
|
||||
<span className="text-gray-500 select-none shrink-0">
|
||||
{timestamp}
|
||||
</span>
|
||||
<span className={`${colorClass} whitespace-pre-wrap break-all`}>
|
||||
{log.line}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import { useState } from 'react'
|
||||
import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2 } from 'lucide-react'
|
||||
import { useCreateProject } from '../hooks/useProjects'
|
||||
import { SpecCreationChat } from './SpecCreationChat'
|
||||
import { startAgent } from '../lib/api'
|
||||
|
||||
type InitializerStatus = 'idle' | 'starting' | 'error'
|
||||
|
||||
type Step = 'name' | 'method' | 'chat' | 'complete'
|
||||
type SpecMethod = 'claude' | 'manual'
|
||||
@@ -31,6 +34,8 @@ export function NewProjectModal({
|
||||
const [projectName, setProjectName] = useState('')
|
||||
const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
|
||||
const [initializerError, setInitializerError] = useState<string | null>(null)
|
||||
|
||||
// Suppress unused variable warning - specMethod may be used in future
|
||||
void _specMethod
|
||||
@@ -89,12 +94,27 @@ export function NewProjectModal({
|
||||
}
|
||||
}
|
||||
|
||||
const handleSpecComplete = () => {
|
||||
setStep('complete')
|
||||
setTimeout(() => {
|
||||
onProjectCreated(projectName.trim())
|
||||
handleClose()
|
||||
}, 1500)
|
||||
const handleSpecComplete = async () => {
|
||||
// Auto-start the initializer agent
|
||||
setInitializerStatus('starting')
|
||||
try {
|
||||
await startAgent(projectName.trim())
|
||||
// Success - navigate to project
|
||||
setStep('complete')
|
||||
setTimeout(() => {
|
||||
onProjectCreated(projectName.trim())
|
||||
handleClose()
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
setInitializerStatus('error')
|
||||
setInitializerError(err instanceof Error ? err.message : 'Failed to start agent')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetryInitializer = () => {
|
||||
setInitializerError(null)
|
||||
setInitializerStatus('idle')
|
||||
handleSpecComplete()
|
||||
}
|
||||
|
||||
const handleChatCancel = () => {
|
||||
@@ -108,6 +128,8 @@ export function NewProjectModal({
|
||||
setProjectName('')
|
||||
setSpecMethod(null)
|
||||
setError(null)
|
||||
setInitializerStatus('idle')
|
||||
setInitializerError(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
@@ -126,6 +148,9 @@ export function NewProjectModal({
|
||||
projectName={projectName.trim()}
|
||||
onComplete={handleSpecComplete}
|
||||
onCancel={handleChatCancel}
|
||||
initializerStatus={initializerStatus}
|
||||
initializerError={initializerError}
|
||||
onRetryInitializer={handleRetryInitializer}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,22 +6,30 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw } from 'lucide-react'
|
||||
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight } from 'lucide-react'
|
||||
import { useSpecChat } from '../hooks/useSpecChat'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import { QuestionOptions } from './QuestionOptions'
|
||||
import { TypingIndicator } from './TypingIndicator'
|
||||
|
||||
type InitializerStatus = 'idle' | 'starting' | 'error'
|
||||
|
||||
interface SpecCreationChatProps {
|
||||
projectName: string
|
||||
onComplete: (specPath: string) => void
|
||||
onCancel: () => void
|
||||
initializerStatus?: InitializerStatus
|
||||
initializerError?: string | null
|
||||
onRetryInitializer?: () => void
|
||||
}
|
||||
|
||||
export function SpecCreationChat({
|
||||
projectName,
|
||||
onComplete,
|
||||
onCancel,
|
||||
initializerStatus = 'idle',
|
||||
initializerError = null,
|
||||
onRetryInitializer,
|
||||
}: SpecCreationChatProps) {
|
||||
const [input, setInput] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -241,18 +249,50 @@ export function SpecCreationChat({
|
||||
|
||||
{/* Completion footer */}
|
||||
{isComplete && (
|
||||
<div className="p-4 border-t-3 border-[var(--color-neo-border)] bg-[var(--color-neo-done)]">
|
||||
<div className={`p-4 border-t-3 border-[var(--color-neo-border)] ${
|
||||
initializerStatus === 'error' ? 'bg-[var(--color-neo-danger)]' : 'bg-[var(--color-neo-done)]'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 size={20} />
|
||||
<span className="font-bold">Specification created successfully!</span>
|
||||
{initializerStatus === 'starting' ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span className="font-bold">Starting agent...</span>
|
||||
</>
|
||||
) : initializerStatus === 'error' ? (
|
||||
<>
|
||||
<AlertCircle size={20} className="text-white" />
|
||||
<span className="font-bold text-white">
|
||||
{initializerError || 'Failed to start agent'}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 size={20} />
|
||||
<span className="font-bold">Specification created successfully!</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{initializerStatus === 'error' && onRetryInitializer && (
|
||||
<button
|
||||
onClick={onRetryInitializer}
|
||||
className="neo-btn bg-white"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
{initializerStatus === 'idle' && (
|
||||
<button
|
||||
onClick={() => onComplete('')}
|
||||
className="neo-btn neo-btn-primary"
|
||||
>
|
||||
Continue to Project
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onComplete('')}
|
||||
className="neo-btn bg-white"
|
||||
>
|
||||
Continue to Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -203,7 +203,9 @@ export function useSpecChat({
|
||||
},
|
||||
])
|
||||
|
||||
onComplete?.(data.path)
|
||||
// NOTE: Do NOT auto-call onComplete here!
|
||||
// User should click "Continue to Project" button to start the agent.
|
||||
// This matches the CLI behavior where user closes the chat manually.
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}
|
||||
@@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite'
|
||||
import path from 'path'
|
||||
|
||||
// Backend port - can be overridden via VITE_API_PORT env var
|
||||
const apiPort = process.env.VITE_API_PORT || '8000'
|
||||
const apiPort = process.env.VITE_API_PORT || '8888'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
|
||||
Reference in New Issue
Block a user