mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
show thought
This commit is contained in:
@@ -13,6 +13,7 @@ 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'
|
||||
|
||||
@@ -182,6 +183,12 @@ function App() {
|
||||
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 &&
|
||||
|
||||
157
ui/src/components/AgentThought.tsx
Normal file
157
ui/src/components/AgentThought.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useMemo, useState, useEffect } from 'react'
|
||||
import { Brain, Sparkles } from 'lucide-react'
|
||||
import type { AgentStatus } from '../lib/types'
|
||||
|
||||
interface AgentThoughtProps {
|
||||
logs: Array<{ line: string; timestamp: string }>
|
||||
agentStatus: AgentStatus
|
||||
}
|
||||
|
||||
const IDLE_TIMEOUT = 30000 // 30 seconds
|
||||
|
||||
/**
|
||||
* Determines if a log line is an agent "thought" (narrative text)
|
||||
* vs. tool mechanics that should be hidden
|
||||
*/
|
||||
function isAgentThought(line: string): boolean {
|
||||
const trimmed = line.trim()
|
||||
|
||||
// Skip tool mechanics
|
||||
if (/^\[Tool:/.test(trimmed)) return false
|
||||
if (/^\s*Input:\s*\{/.test(trimmed)) return false
|
||||
if (/^\[(Done|Error)\]/.test(trimmed)) return false
|
||||
if (/^\[Error\]/.test(trimmed)) return false
|
||||
if (/^Output:/.test(trimmed)) return false
|
||||
|
||||
// Skip JSON and very short lines
|
||||
if (/^[\[\{]/.test(trimmed)) return false
|
||||
if (trimmed.length < 15) return false
|
||||
|
||||
// Skip lines that are just paths or technical output
|
||||
if (/^[A-Za-z]:\\/.test(trimmed)) return false
|
||||
if (/^\/[a-z]/.test(trimmed)) return false
|
||||
|
||||
// Keep narrative text (starts with capital, looks like a sentence)
|
||||
return /^[A-Z]/.test(trimmed) && trimmed.length > 20
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the latest agent thought from logs
|
||||
*/
|
||||
function getLatestThought(logs: Array<{ line: string; timestamp: string }>): string | null {
|
||||
// Search from most recent
|
||||
for (let i = logs.length - 1; i >= 0; i--) {
|
||||
if (isAgentThought(logs[i].line)) {
|
||||
return logs[i].line.trim()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function AgentThought({ logs, agentStatus }: AgentThoughtProps) {
|
||||
const thought = useMemo(() => getLatestThought(logs), [logs])
|
||||
const [displayedThought, setDisplayedThought] = useState<string | null>(null)
|
||||
const [textVisible, setTextVisible] = useState(true)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
// Get last log timestamp for idle detection
|
||||
const lastLogTimestamp = logs.length > 0
|
||||
? new Date(logs[logs.length - 1].timestamp).getTime()
|
||||
: 0
|
||||
|
||||
// Determine if component should be visible
|
||||
const shouldShow = useMemo(() => {
|
||||
if (!thought) return false
|
||||
if (agentStatus === 'running') return true
|
||||
if (agentStatus === 'paused') {
|
||||
return Date.now() - lastLogTimestamp < IDLE_TIMEOUT
|
||||
}
|
||||
return false
|
||||
}, [thought, agentStatus, lastLogTimestamp])
|
||||
|
||||
// Animate text changes using CSS transitions
|
||||
useEffect(() => {
|
||||
if (thought !== displayedThought && thought) {
|
||||
// Fade out
|
||||
setTextVisible(false)
|
||||
// After fade out, update text and fade in
|
||||
const timeout = setTimeout(() => {
|
||||
setDisplayedThought(thought)
|
||||
setTextVisible(true)
|
||||
}, 150) // Match transition duration
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [thought, displayedThought])
|
||||
|
||||
// Handle visibility transitions
|
||||
useEffect(() => {
|
||||
if (shouldShow) {
|
||||
setIsVisible(true)
|
||||
} else {
|
||||
// Delay hiding to allow exit animation
|
||||
const timeout = setTimeout(() => setIsVisible(false), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [shouldShow])
|
||||
|
||||
if (!isVisible || !displayedThought) return null
|
||||
|
||||
const isRunning = agentStatus === 'running'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
transition-all duration-300 ease-out overflow-hidden
|
||||
${shouldShow ? 'opacity-100 max-h-20' : 'opacity-0 max-h-0'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
relative
|
||||
bg-[var(--color-neo-card)]
|
||||
border-3 border-[var(--color-neo-border)]
|
||||
shadow-[var(--shadow-neo-sm)]
|
||||
px-4 py-3
|
||||
flex items-center gap-3
|
||||
${isRunning ? 'animate-pulse-neo' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Brain Icon with subtle glow */}
|
||||
<div className="relative shrink-0">
|
||||
<Brain
|
||||
size={22}
|
||||
className="text-[var(--color-neo-progress)]"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
{isRunning && (
|
||||
<Sparkles
|
||||
size={10}
|
||||
className="absolute -top-1 -right-1 text-[var(--color-neo-pending)] animate-pulse"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thought text with fade transition */}
|
||||
<p
|
||||
className="font-mono text-sm text-[var(--color-neo-text)] truncate transition-all duration-150 ease-out"
|
||||
style={{
|
||||
opacity: textVisible ? 1 : 0,
|
||||
transform: textVisible ? 'translateY(0)' : 'translateY(-4px)',
|
||||
}}
|
||||
>
|
||||
{displayedThought?.replace(/:$/, '')}
|
||||
</p>
|
||||
|
||||
{/* Subtle running indicator bar */}
|
||||
{isRunning && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-neo-progress)] opacity-50">
|
||||
<div
|
||||
className="h-full bg-[var(--color-neo-progress)] animate-pulse"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -332,6 +332,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes thoughtFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Utilities Layer
|
||||
============================================================================ */
|
||||
@@ -353,6 +366,10 @@
|
||||
animation: completePop 0.5s var(--transition-neo-fast);
|
||||
}
|
||||
|
||||
.animate-thought-fade {
|
||||
animation: thoughtFade 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--font-neo-display);
|
||||
}
|
||||
|
||||
@@ -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/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/usecelebration.ts","./src/hooks/usefeaturesound.ts","./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/agentthought.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/usecelebration.ts","./src/hooks/usefeaturesound.ts","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}
|
||||
Reference in New Issue
Block a user