mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-17 10:53:09 +00:00
Adding features work
This commit is contained in:
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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user