show thought

This commit is contained in:
Auto
2025-12-31 08:07:36 +02:00
parent f1cabd2d49
commit 5cb78a6b43
4 changed files with 182 additions and 1 deletions

View File

@@ -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 &&

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

View File

@@ -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);
}

View File

@@ -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"}