diff --git a/gadget-code/frontend/src/pages/ChatSessionView.tsx b/gadget-code/frontend/src/pages/ChatSessionView.tsx index 8fa4b9c..e4ab486 100644 --- a/gadget-code/frontend/src/pages/ChatSessionView.tsx +++ b/gadget-code/frontend/src/pages/ChatSessionView.tsx @@ -1,27 +1,14 @@ -import { useState, useEffect, useRef, useContext } from 'react'; +import { useState, useEffect, useRef, useContext, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { socketClient } from '../lib/socket'; -import { chatSessionApi, projectApi, type ChatSession, type ChatTurn, type Project } from '../lib/api'; +import { chatSessionApi, projectApi, type ChatSession, type ChatTurn } from '../lib/api'; import { WorkspaceMode } from '../lib/types'; import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator'; import FilesPanel from '../components/FilesPanel'; import LogPanel from '../components/LogPanel'; +import ChatTurnComponent from '../components/ChatTurn'; import { AppContext } from '../App'; -interface ChatMessage { - id: string; - role: 'user' | 'assistant' | 'system'; - content: string; - thinking?: string; - toolCalls?: Array<{ - callId: string; - name: string; - parameters?: string; - response?: string; - }>; - timestamp: Date; -} - interface LogEntry { id: string; timestamp: Date; @@ -37,7 +24,7 @@ export default function ChatSessionView() { const [project, setProject] = useState(null); const [session, setSession] = useState(null); - const [messages, setMessages] = useState([]); + const [turns, setTurns] = useState([]); const [promptInput, setPromptInput] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [loading, setLoading] = useState(true); @@ -51,6 +38,9 @@ export default function ChatSessionView() { const messagesEndRef = useRef(null); const inputRef = useRef(null); const toastTimerRef = useRef | null>(null); + const pendingUpdatesRef = useRef>>(new Map()); + const updateRafRef = useRef(null); + const currentTurnIdRef = useRef(null); useEffect(() => { loadSessionData(); @@ -63,13 +53,16 @@ export default function ChatSessionView() { useEffect(() => { scrollToBottom(); - }, [messages]); + }, [turns]); useEffect(() => { return () => { if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); } + if (updateRafRef.current) { + cancelAnimationFrame(updateRafRef.current); + } }; }, []); @@ -86,28 +79,8 @@ export default function ChatSessionView() { setProject(projectData); } - const turns = await chatSessionApi.getTurns(sessionId); - const chatMessages: ChatMessage[] = turns.map((turn: ChatTurn) => ({ - id: turn._id, - role: 'user', - content: turn.prompts.user, - timestamp: new Date(turn.createdAt), - })); - - turns.forEach((turn: ChatTurn) => { - if (turn.thinking || turn.response || turn.toolCalls?.length) { - chatMessages.push({ - id: `${turn._id}-response`, - role: 'assistant', - content: turn.response || '', - thinking: turn.thinking, - toolCalls: turn.toolCalls, - timestamp: new Date(turn.createdAt), - }); - } - }); - - setMessages(chatMessages); + const turnsData = await chatSessionApi.getTurns(sessionId); + setTurns(turnsData); setSessionLocked(true); } } catch (err) { @@ -141,68 +114,102 @@ export default function ChatSessionView() { socket.off('status', handleStatus); }; - const handleThinking = (content: string) => { - setMessages(prev => { - const lastMessage = prev[prev.length - 1]; - if (lastMessage && lastMessage.role === 'assistant') { - return [ - ...prev.slice(0, -1), - { - ...lastMessage, - thinking: (lastMessage.thinking || '') + content, - }, - ]; + const scheduleUpdate = useCallback(() => { + if (updateRafRef.current) return; + + updateRafRef.current = requestAnimationFrame(() => { + const pendingEntries = Array.from(pendingUpdatesRef.current.entries()); + if (pendingEntries.length === 0) { + updateRafRef.current = null; + return; } - return prev; - }); - }; - const handleResponse = (content: string) => { - setMessages(prev => { - const lastMessage = prev[prev.length - 1]; - if (lastMessage && lastMessage.role === 'assistant') { - return [ - ...prev.slice(0, -1), - { - ...lastMessage, - content: lastMessage.content + content, - }, - ]; - } - return prev; - }); - }; + setTurns(prevTurns => { + const newTurns = [...prevTurns]; + let changed = false; - const handleToolCall = (callId: string, name: string, params: string, response: string) => { - setMessages(prev => { - const lastMessage = prev[prev.length - 1]; - if (lastMessage && lastMessage.role === 'assistant') { - const toolCalls = lastMessage.toolCalls || []; - toolCalls.push({ callId, name, parameters: params, response }); - return [ - ...prev.slice(0, -1), - { - ...lastMessage, - toolCalls, - }, - ]; - } - return prev; - }); - }; + for (const [turnId, turnUpdates] of pendingEntries) { + const turnIndex = newTurns.findIndex(t => t._id === turnId); + if (turnIndex === -1) continue; - const handleWorkOrderComplete = (turnId: string, success: boolean, message?: string) => { + const oldTurn = newTurns[turnIndex]; + const newTurn = { ...oldTurn, ...turnUpdates }; + + if (turnUpdates.thinking !== undefined) { + newTurn.thinking = (oldTurn.thinking || '') + turnUpdates.thinking; + } + if (turnUpdates.response !== undefined) { + newTurn.response = (oldTurn.response || '') + turnUpdates.response; + } + if (turnUpdates.toolCalls !== undefined) { + newTurn.toolCalls = [...(oldTurn.toolCalls || []), ...turnUpdates.toolCalls]; + newTurn.stats = { + ...oldTurn.stats, + toolCallCount: newTurn.toolCalls.length + }; + } + if (turnUpdates.status !== undefined) { + newTurn.status = turnUpdates.status; + } + + newTurns[turnIndex] = newTurn; + changed = true; + } + + return changed ? newTurns : prevTurns; + }); + + pendingUpdatesRef.current.clear(); + updateRafRef.current = null; + }); + }, []); + + const handleThinking = useCallback((content: string) => { + const turnId = currentTurnIdRef.current; + if (!turnId) return; + + pendingUpdatesRef.current.set(turnId, { thinking: content }); + scheduleUpdate(); + }, [scheduleUpdate]); + + const handleResponse = useCallback((content: string) => { + const turnId = currentTurnIdRef.current; + if (!turnId) return; + + pendingUpdatesRef.current.set(turnId, { response: content }); + scheduleUpdate(); + }, [scheduleUpdate]); + + const handleToolCall = useCallback((callId: string, name: string, params: string, response: string) => { + const turnId = currentTurnIdRef.current; + if (!turnId) return; + + pendingUpdatesRef.current.set(turnId, { + toolCalls: [{ callId, name, parameters: params, response }] + }); + scheduleUpdate(); + }, [scheduleUpdate]); + + const handleWorkOrderComplete = useCallback((turnId: string, success: boolean, message?: string) => { + setTurns(prevTurns => + prevTurns.map(turn => + turn._id === turnId + ? { ...turn, status: success ? 'finished' : 'error', response: message && !success ? message : turn.response } + : turn + ) + ); setIsProcessing(false); + currentTurnIdRef.current = null; if (!success) { setError(message || 'Work order failed'); } - }; + }, []); - const handleWorkspaceModeChanged = (mode: string) => { + const handleWorkspaceModeChanged = useCallback((mode: string) => { setWorkspaceMode(mode as WorkspaceMode); - }; + }, []); - const handleLogEntry = (data: { level: string; message: string; timestamp: number }) => { + const handleLogEntry = useCallback((data: { level: string; message: string; timestamp: number }) => { setLogs(prev => [ ...prev, { @@ -212,13 +219,13 @@ export default function ChatSessionView() { message: data.message, }, ]); - }; + }, []); - const handleStatus = (content: string) => { + const handleStatus = useCallback((content: string) => { appContext?.setStatusMessage(content); - }; + }, [appContext]); - const showToast = (message: string) => { + const showToast = useCallback((message: string) => { setToast(message); if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); @@ -226,7 +233,7 @@ export default function ChatSessionView() { toastTimerRef.current = setTimeout(() => { setToast(null); }, 4000); - }; + }, []); const handleWorkspaceModeChange = async (mode: WorkspaceMode) => { if (!session || !project) return; @@ -260,25 +267,37 @@ export default function ChatSessionView() { if (!promptInput.trim() || isProcessing || !socket || !sessionLocked) return; if (workspaceMode !== WorkspaceMode.Agent) return; - const userMessage: ChatMessage = { - id: `temp-${Date.now()}`, - role: 'user', - content: promptInput.trim(), - timestamp: new Date(), + const userTurn: ChatTurn = { + _id: `temp-${Date.now()}`, + createdAt: new Date().toISOString(), + user: session?.user?._id || '', + project: session?.project?._id || projectId || '', + session: sessionId || '', + provider: session?.provider?._id || '', + llm: session?.selectedModel || '', + mode: session?.mode || 'develop', + status: 'processing', + prompts: { user: promptInput.trim() }, + thinking: '', + response: '', + toolCalls: [], + subagents: [], + stats: { + toolCallCount: 0, + inputTokens: 0, + thinkingTokenCount: 0, + responseTokens: 0, + durationMs: 0, + durationLabel: '0ms' + } }; - setMessages(prev => [...prev, userMessage]); + setTurns(prev => [...prev, userTurn]); + currentTurnIdRef.current = userTurn._id; setPromptInput(''); setIsProcessing(true); setError(''); - setMessages(prev => [...prev, { - id: `response-${Date.now()}`, - role: 'assistant', - content: '', - timestamp: new Date(), - }]); - socketClient.emitServer('submitPrompt', promptInput.trim()); }; @@ -286,6 +305,14 @@ export default function ChatSessionView() { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; + const handleTurnUpdate = useCallback((turnId: string, updates: Partial) => { + setTurns(prevTurns => + prevTurns.map(turn => + turn._id === turnId ? { ...turn, ...updates } : turn + ) + ); + }, []); + const promptDisabled = isProcessing || workspaceMode !== WorkspaceMode.Agent || !sessionLocked; const promptPlaceholder = workspaceMode !== WorkspaceMode.Agent ? 'Select Agent mode to enter a prompt.' @@ -330,8 +357,12 @@ export default function ChatSessionView() {
{/* Messages */}
- {messages.map((message) => ( - + {turns.map((turn) => ( + ))}
@@ -435,64 +466,3 @@ export default function ChatSessionView() {
); } - -function ChatMessageBubble({ message }: { message: ChatMessage }) { - const [thinkingExpanded, setThinkingExpanded] = useState(false); - - return ( -
-
- {message.role === 'assistant' && message.thinking && ( -
- - {thinkingExpanded && ( -
- {message.thinking} -
- )} -
- )} - -
{message.content}
- - {message.toolCalls && message.toolCalls.length > 0 && ( -
- {message.toolCalls.map((toolCall, idx) => ( -
-
- {toolCall.name} - {toolCall.response && ( - - )} -
-
- ))} -
- )} - -
- {message.timestamp.toLocaleTimeString()} -
-
-
- ); -} \ No newline at end of file