agent's progress on ChatTurn
This commit is contained in:
parent
3a8f2e4f44
commit
09ebacc711
@ -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<Project | null>(null);
|
||||
const [session, setSession] = useState<ChatSession | null>(null);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [turns, setTurns] = useState<ChatTurn[]>([]);
|
||||
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<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingUpdatesRef = useRef<Map<string, Partial<ChatTurn>>>(new Map());
|
||||
const updateRafRef = useRef<number | null>(null);
|
||||
const currentTurnIdRef = useRef<string | null>(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,
|
||||
},
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
const scheduleUpdate = useCallback(() => {
|
||||
if (updateRafRef.current) return;
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
updateRafRef.current = requestAnimationFrame(() => {
|
||||
const pendingEntries = Array.from(pendingUpdatesRef.current.entries());
|
||||
if (pendingEntries.length === 0) {
|
||||
updateRafRef.current = null;
|
||||
return;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
setTurns(prevTurns => {
|
||||
const newTurns = [...prevTurns];
|
||||
let changed = false;
|
||||
|
||||
for (const [turnId, turnUpdates] of pendingEntries) {
|
||||
const turnIndex = newTurns.findIndex(t => t._id === turnId);
|
||||
if (turnIndex === -1) continue;
|
||||
|
||||
const oldTurn = newTurns[turnIndex];
|
||||
const newTurn = { ...oldTurn, ...turnUpdates };
|
||||
|
||||
if (turnUpdates.thinking !== undefined) {
|
||||
newTurn.thinking = (oldTurn.thinking || '') + turnUpdates.thinking;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
const handleWorkOrderComplete = (turnId: string, success: boolean, message?: string) => {
|
||||
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<ChatTurn>) => {
|
||||
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() {
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map((message) => (
|
||||
<ChatMessageBubble key={message.id} message={message} />
|
||||
{turns.map((turn) => (
|
||||
<ChatTurnComponent
|
||||
key={turn._id}
|
||||
turn={turn}
|
||||
onUpdate={handleTurnUpdate}
|
||||
/>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
@ -435,64 +466,3 @@ export default function ChatSessionView() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatMessageBubble({ message }: { message: ChatMessage }) {
|
||||
const [thinkingExpanded, setThinkingExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`max-w-3xl ${
|
||||
message.role === 'user' ? 'ml-auto' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded p-4 ${
|
||||
message.role === 'user'
|
||||
? 'bg-brand text-white'
|
||||
: 'bg-bg-tertiary border border-border-default'
|
||||
}`}
|
||||
>
|
||||
{message.role === 'assistant' && message.thinking && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setThinkingExpanded(!thinkingExpanded)}
|
||||
className="text-xs text-text-muted hover:text-text-secondary flex items-center gap-1"
|
||||
>
|
||||
<span className={`transform transition-transform ${thinkingExpanded ? 'rotate-90' : ''}`}>▶</span>
|
||||
Thinking ({message.thinking.length} chars)
|
||||
</button>
|
||||
{thinkingExpanded && (
|
||||
<div className="mt-2 p-3 bg-bg-secondary rounded text-sm text-text-secondary whitespace-pre-wrap">
|
||||
{message.thinking}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||
|
||||
{message.toolCalls && message.toolCalls.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{message.toolCalls.map((toolCall, idx) => (
|
||||
<div
|
||||
key={toolCall.callId || idx}
|
||||
className="p-2 bg-bg-secondary rounded border border-border-default"
|
||||
>
|
||||
<div className="text-xs font-mono text-text-secondary">
|
||||
<span className="text-brand">⚡</span> {toolCall.name}
|
||||
{toolCall.response && (
|
||||
<span className="ml-2 text-green-500">✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 text-xs text-text-muted">
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user