mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
refactor chat container
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { toast } from "sonner";
|
||||
import type { StreamChunk } from "@/hooks/useChatStream";
|
||||
import type { HandlerDependencies } from "./useChatContainer.handlers";
|
||||
import {
|
||||
handleTextChunk,
|
||||
handleTextEnded,
|
||||
handleToolCallStart,
|
||||
handleToolResponse,
|
||||
handleLoginNeeded,
|
||||
handleStreamEnd,
|
||||
handleError,
|
||||
} from "./useChatContainer.handlers";
|
||||
|
||||
/**
|
||||
* Creates a stream event dispatcher that routes stream chunks to appropriate handlers.
|
||||
*
|
||||
* This dispatcher pattern separates event routing logic from individual handler implementations,
|
||||
* making the code more maintainable and testable.
|
||||
*
|
||||
* @param deps - Handler dependencies (state setters and refs)
|
||||
* @returns A function that processes StreamChunk events
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const dispatcher = createStreamEventDispatcher({
|
||||
* setMessages,
|
||||
* setStreamingChunks,
|
||||
* streamingChunksRef,
|
||||
* setHasTextChunks,
|
||||
* sessionId,
|
||||
* });
|
||||
*
|
||||
* // Use with streaming hook
|
||||
* await sendStreamMessage(sessionId, content, dispatcher);
|
||||
* ```
|
||||
*/
|
||||
export function createStreamEventDispatcher(
|
||||
deps: HandlerDependencies,
|
||||
): (chunk: StreamChunk) => void {
|
||||
return function dispatchStreamEvent(chunk: StreamChunk): void {
|
||||
switch (chunk.type) {
|
||||
case "text_chunk":
|
||||
handleTextChunk(chunk, deps);
|
||||
break;
|
||||
|
||||
case "text_ended":
|
||||
handleTextEnded(chunk, deps);
|
||||
break;
|
||||
|
||||
case "tool_call_start":
|
||||
handleToolCallStart(chunk, deps);
|
||||
break;
|
||||
|
||||
case "tool_response":
|
||||
handleToolResponse(chunk, deps);
|
||||
break;
|
||||
|
||||
case "login_needed":
|
||||
case "need_login":
|
||||
handleLoginNeeded(chunk, deps);
|
||||
break;
|
||||
|
||||
case "stream_end":
|
||||
handleStreamEnd(chunk, deps);
|
||||
break;
|
||||
|
||||
case "error":
|
||||
handleError(chunk, deps);
|
||||
// Show toast at dispatcher level to avoid circular dependencies
|
||||
toast.error("Chat Error", {
|
||||
description: chunk.message || chunk.content || "An error occurred",
|
||||
});
|
||||
break;
|
||||
|
||||
case "usage":
|
||||
// TODO: Handle usage for display
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Unknown stream chunk type:", chunk);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,36 @@
|
||||
import type { ChatMessageData } from "@/components/molecules/ChatMessage/useChatMessage";
|
||||
import type { ToolResult } from "@/types/chat";
|
||||
|
||||
/**
|
||||
* Creates a user message object with current timestamp.
|
||||
*
|
||||
* @param content - The message content
|
||||
* @returns A ChatMessageData object of type "message" with role "user"
|
||||
*/
|
||||
export function createUserMessage(content: string): ChatMessageData {
|
||||
return {
|
||||
type: "message",
|
||||
role: "user",
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out authentication-related messages (credentials_needed, login_needed).
|
||||
* Used when sending a new message to remove stale authentication prompts.
|
||||
*
|
||||
* @param messages - Array of chat messages
|
||||
* @returns Filtered array without authentication prompt messages
|
||||
*/
|
||||
export function filterAuthMessages(
|
||||
messages: ChatMessageData[],
|
||||
): ChatMessageData[] {
|
||||
return messages.filter(
|
||||
(msg) => msg.type !== "credentials_needed" && msg.type !== "login_needed",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to validate message structure from backend.
|
||||
*
|
||||
@@ -188,7 +218,7 @@ export function parseToolResponse(
|
||||
type: "tool_response",
|
||||
toolId,
|
||||
toolName,
|
||||
result: parsedResult.message || "No results found",
|
||||
result: (parsedResult.message as string) || "No results found",
|
||||
success: true,
|
||||
timestamp: timestamp || new Date(),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import type { Dispatch, SetStateAction, MutableRefObject } from "react";
|
||||
import type { StreamChunk } from "@/hooks/useChatStream";
|
||||
import type { ChatMessageData } from "@/components/molecules/ChatMessage/useChatMessage";
|
||||
import { parseToolResponse, extractCredentialsNeeded } from "./helpers";
|
||||
|
||||
/**
|
||||
* Handler dependencies - all state setters and refs needed by handlers.
|
||||
*/
|
||||
export interface HandlerDependencies {
|
||||
setHasTextChunks: Dispatch<SetStateAction<boolean>>;
|
||||
setStreamingChunks: Dispatch<SetStateAction<string[]>>;
|
||||
streamingChunksRef: MutableRefObject<string[]>;
|
||||
setMessages: Dispatch<SetStateAction<ChatMessageData[]>>;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles text_chunk events by accumulating streaming text.
|
||||
* Updates both the state and ref to prevent stale closures.
|
||||
*/
|
||||
export function handleTextChunk(
|
||||
chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
): void {
|
||||
if (!chunk.content) return;
|
||||
|
||||
deps.setHasTextChunks(true);
|
||||
deps.setStreamingChunks((prev) => {
|
||||
const updated = [...prev, chunk.content!];
|
||||
deps.streamingChunksRef.current = updated;
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles text_ended events by saving completed text as assistant message.
|
||||
* Clears streaming state after saving the message.
|
||||
*/
|
||||
export function handleTextEnded(
|
||||
_chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
): void {
|
||||
console.log("[Text Ended] Saving streamed text as assistant message");
|
||||
const completedText = deps.streamingChunksRef.current.join("");
|
||||
|
||||
if (completedText.trim()) {
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: completedText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
deps.setMessages((prev) => [...prev, assistantMessage]);
|
||||
}
|
||||
|
||||
// Clear streaming state
|
||||
deps.setStreamingChunks([]);
|
||||
deps.streamingChunksRef.current = [];
|
||||
deps.setHasTextChunks(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles tool_call_start events by adding a ToolCallMessage to the UI.
|
||||
* Shows a loading state while the tool executes.
|
||||
*/
|
||||
export function handleToolCallStart(
|
||||
chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
): void {
|
||||
const toolCallMessage: ChatMessageData = {
|
||||
type: "tool_call",
|
||||
toolId: chunk.tool_id || `tool-${Date.now()}-${chunk.idx || 0}`,
|
||||
toolName: chunk.tool_name || "Executing...",
|
||||
arguments: chunk.arguments || {},
|
||||
timestamp: new Date(),
|
||||
};
|
||||
deps.setMessages((prev) => [...prev, toolCallMessage]);
|
||||
|
||||
console.log("[Tool Call Start]", {
|
||||
toolId: toolCallMessage.toolId,
|
||||
toolName: toolCallMessage.toolName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles tool_response events by replacing the matching tool_call message.
|
||||
* Parses the response and handles special cases like credential requirements.
|
||||
*/
|
||||
export function handleToolResponse(
|
||||
chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
): void {
|
||||
console.log("[Tool Response] Received:", {
|
||||
toolId: chunk.tool_id,
|
||||
toolName: chunk.tool_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Find the matching tool_call to get the tool name if missing
|
||||
let toolName = chunk.tool_name || "unknown";
|
||||
if (!chunk.tool_name || chunk.tool_name === "unknown") {
|
||||
deps.setMessages((prev) => {
|
||||
const matchingToolCall = [...prev]
|
||||
.reverse()
|
||||
.find(
|
||||
(msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
|
||||
);
|
||||
if (matchingToolCall && matchingToolCall.type === "tool_call") {
|
||||
toolName = matchingToolCall.toolName;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Use helper function to parse tool response
|
||||
const responseMessage = parseToolResponse(
|
||||
chunk.result!,
|
||||
chunk.tool_id!,
|
||||
toolName,
|
||||
new Date(),
|
||||
);
|
||||
|
||||
// If helper returns null (setup_requirements), handle credentials
|
||||
if (!responseMessage) {
|
||||
// Parse for credentials check
|
||||
let parsedResult: Record<string, unknown> | null = null;
|
||||
try {
|
||||
parsedResult =
|
||||
typeof chunk.result === "string"
|
||||
? JSON.parse(chunk.result)
|
||||
: (chunk.result as Record<string, unknown>);
|
||||
} catch {
|
||||
parsedResult = null;
|
||||
}
|
||||
|
||||
// Check if this is get_required_setup_info with missing credentials
|
||||
if (
|
||||
chunk.tool_name === "get_required_setup_info" &&
|
||||
chunk.success &&
|
||||
parsedResult
|
||||
) {
|
||||
const credentialsMessage = extractCredentialsNeeded(parsedResult);
|
||||
if (credentialsMessage) {
|
||||
deps.setMessages((prev) => [...prev, credentialsMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add message if setup_requirements
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the tool_call message with matching tool_id
|
||||
deps.setMessages((prev) => {
|
||||
// Find the tool_call with the matching tool_id
|
||||
const toolCallIndex = prev.findIndex(
|
||||
(msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
|
||||
);
|
||||
|
||||
if (toolCallIndex !== -1) {
|
||||
const newMessages = [...prev];
|
||||
newMessages[toolCallIndex] = responseMessage;
|
||||
|
||||
console.log(
|
||||
"[Tool Response] Replaced tool_call with matching tool_id:",
|
||||
chunk.tool_id,
|
||||
"at index:",
|
||||
toolCallIndex,
|
||||
);
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"[Tool Response] No tool_call found with tool_id:",
|
||||
chunk.tool_id,
|
||||
"appending instead",
|
||||
);
|
||||
return [...prev, responseMessage];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles login_needed events by adding a login prompt message.
|
||||
*/
|
||||
export function handleLoginNeeded(
|
||||
chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
): void {
|
||||
const loginNeededMessage: ChatMessageData = {
|
||||
type: "login_needed",
|
||||
message:
|
||||
chunk.message || "Please sign in to use chat and agent features",
|
||||
sessionId: chunk.session_id || deps.sessionId,
|
||||
agentInfo: chunk.agent_info,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
deps.setMessages((prev) => [...prev, loginNeededMessage]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles stream_end events by finalizing the streaming session.
|
||||
* Converts any remaining streaming chunks into a completed assistant message.
|
||||
*/
|
||||
export function handleStreamEnd(
|
||||
_chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
): void {
|
||||
// Convert streaming chunks into a completed assistant message
|
||||
// Use ref to get the latest chunks value (not stale closure value)
|
||||
const completedContent = deps.streamingChunksRef.current.join("");
|
||||
|
||||
if (completedContent) {
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: completedContent,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// Add the completed assistant message to local state
|
||||
deps.setMessages((prev) => {
|
||||
const updated = [...prev, assistantMessage];
|
||||
|
||||
// Log final state using current messages from state
|
||||
console.log("[Stream End] Final state:", {
|
||||
localMessages: updated.map((m) => ({
|
||||
type: m.type,
|
||||
...(m.type === "message" && {
|
||||
role: m.role,
|
||||
contentLength: m.content.length,
|
||||
}),
|
||||
...(m.type === "tool_call" && {
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
}),
|
||||
...(m.type === "tool_response" && {
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
success: m.success,
|
||||
}),
|
||||
})),
|
||||
streamingChunks: deps.streamingChunksRef.current,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
// Clear streaming state immediately now that we have the message
|
||||
deps.setStreamingChunks([]);
|
||||
deps.streamingChunksRef.current = [];
|
||||
deps.setHasTextChunks(false);
|
||||
|
||||
// Messages are now in local state and will be displayed
|
||||
console.log("[Stream End] Stream complete, messages in local state");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles error events by logging and showing error toast.
|
||||
*/
|
||||
export function handleError(chunk: StreamChunk, _deps: HandlerDependencies): void {
|
||||
const errorMessage = chunk.message || chunk.content || "An error occurred";
|
||||
console.error("Stream error:", errorMessage);
|
||||
// Note: Toast import removed to avoid circular dependencies
|
||||
// Error toasts should be shown at the hook level
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useState, useCallback, useRef, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useChatStream, type StreamChunk } from "@/hooks/useChatStream";
|
||||
import { useChatStream } from "@/hooks/useChatStream";
|
||||
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
|
||||
import type { ChatMessageData } from "@/components/molecules/ChatMessage/useChatMessage";
|
||||
import {
|
||||
parseToolResponse,
|
||||
extractCredentialsNeeded,
|
||||
isValidMessage,
|
||||
isToolCallArray,
|
||||
createUserMessage,
|
||||
filterAuthMessages,
|
||||
} from "./helpers";
|
||||
import { createStreamEventDispatcher } from "./createStreamEventDispatcher";
|
||||
|
||||
interface UseChatContainerArgs {
|
||||
sessionId: string | null;
|
||||
@@ -145,265 +147,32 @@ export function useChatContainer({
|
||||
return;
|
||||
}
|
||||
|
||||
// Add user message immediately (only if it's a user message)
|
||||
const userMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "user",
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// Remove any pending credentials_needed or login_needed messages when user sends a new message
|
||||
// This prevents them from persisting after the user has taken action
|
||||
// Only add user message to UI if isUserMessage is true
|
||||
// Update message state: add user message and remove stale auth prompts
|
||||
if (isUserMessage) {
|
||||
setMessages((prev) => {
|
||||
const filtered = prev.filter(
|
||||
(msg) =>
|
||||
msg.type !== "credentials_needed" && msg.type !== "login_needed",
|
||||
);
|
||||
return [...filtered, userMessage];
|
||||
});
|
||||
const userMessage = createUserMessage(content);
|
||||
setMessages((prev) => [...filterAuthMessages(prev), userMessage]);
|
||||
} else {
|
||||
// For system messages, just remove the login/credentials prompts
|
||||
setMessages((prev) =>
|
||||
prev.filter(
|
||||
(msg) =>
|
||||
msg.type !== "credentials_needed" && msg.type !== "login_needed",
|
||||
),
|
||||
);
|
||||
setMessages((prev) => filterAuthMessages(prev));
|
||||
}
|
||||
|
||||
// Clear streaming chunks and reset text flag
|
||||
// Clear streaming state
|
||||
setStreamingChunks([]);
|
||||
streamingChunksRef.current = [];
|
||||
setHasTextChunks(false);
|
||||
|
||||
// Create event dispatcher with all handler dependencies
|
||||
const dispatcher = createStreamEventDispatcher({
|
||||
setHasTextChunks,
|
||||
setStreamingChunks,
|
||||
streamingChunksRef,
|
||||
setMessages,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
try {
|
||||
// Stream the response
|
||||
await sendStreamMessage(
|
||||
sessionId,
|
||||
content,
|
||||
function handleChunk(chunk: StreamChunk) {
|
||||
if (chunk.type === "text_chunk" && chunk.content) {
|
||||
setHasTextChunks(true); // Mark that we have text chunks
|
||||
setStreamingChunks((prev) => {
|
||||
const updated = [...prev, chunk.content!];
|
||||
streamingChunksRef.current = updated;
|
||||
return updated;
|
||||
});
|
||||
} else if (chunk.type === "text_ended") {
|
||||
// Save the completed text as an assistant message before clearing
|
||||
console.log("[Text Ended] Saving streamed text as assistant message");
|
||||
const completedText = streamingChunksRef.current.join("");
|
||||
|
||||
if (completedText.trim()) {
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: completedText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
}
|
||||
|
||||
// Clear streaming state
|
||||
setStreamingChunks([]);
|
||||
streamingChunksRef.current = [];
|
||||
setHasTextChunks(false);
|
||||
} else if (chunk.type === "tool_call_start") {
|
||||
// Show ToolCallMessage immediately when tool execution starts
|
||||
// Use the tool_id from the backend to match with tool_response later
|
||||
const toolCallMessage: ChatMessageData = {
|
||||
type: "tool_call",
|
||||
toolId: chunk.tool_id || `tool-${Date.now()}-${chunk.idx || 0}`,
|
||||
toolName: chunk.tool_name || "Executing...",
|
||||
arguments: chunk.arguments || {},
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, toolCallMessage]);
|
||||
|
||||
console.log("[Tool Call Start]", {
|
||||
toolId: toolCallMessage.toolId,
|
||||
toolName: toolCallMessage.toolName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else if (chunk.type === "tool_response") {
|
||||
console.log("[Tool Response] Received:", {
|
||||
toolId: chunk.tool_id,
|
||||
toolName: chunk.tool_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Find the matching tool_call to get the tool name if missing
|
||||
let toolName = chunk.tool_name || "unknown";
|
||||
if (!chunk.tool_name || chunk.tool_name === "unknown") {
|
||||
setMessages((prev) => {
|
||||
const matchingToolCall = [...prev]
|
||||
.reverse()
|
||||
.find(
|
||||
(msg) =>
|
||||
msg.type === "tool_call" &&
|
||||
msg.toolId === chunk.tool_id,
|
||||
);
|
||||
if (
|
||||
matchingToolCall &&
|
||||
matchingToolCall.type === "tool_call"
|
||||
) {
|
||||
toolName = matchingToolCall.toolName;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Use helper function to parse tool response
|
||||
const responseMessage = parseToolResponse(
|
||||
chunk.result!,
|
||||
chunk.tool_id!,
|
||||
toolName,
|
||||
new Date(),
|
||||
);
|
||||
|
||||
// If helper returns null (setup_requirements), handle credentials
|
||||
if (!responseMessage) {
|
||||
// Parse for credentials check
|
||||
let parsedResult: Record<string, unknown> | null = null;
|
||||
try {
|
||||
parsedResult =
|
||||
typeof chunk.result === "string"
|
||||
? JSON.parse(chunk.result)
|
||||
: (chunk.result as Record<string, unknown>);
|
||||
} catch {
|
||||
parsedResult = null;
|
||||
}
|
||||
|
||||
// Check if this is get_required_setup_info with missing credentials
|
||||
if (
|
||||
chunk.tool_name === "get_required_setup_info" &&
|
||||
chunk.success &&
|
||||
parsedResult
|
||||
) {
|
||||
const credentialsMessage =
|
||||
extractCredentialsNeeded(parsedResult);
|
||||
if (credentialsMessage) {
|
||||
setMessages((prev) => [...prev, credentialsMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add message if setup_requirements
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the tool_call message with matching tool_id
|
||||
setMessages((prev) => {
|
||||
// Find the tool_call with the matching tool_id
|
||||
const toolCallIndex = prev.findIndex(
|
||||
(msg) =>
|
||||
msg.type === "tool_call" && msg.toolId === chunk.tool_id,
|
||||
);
|
||||
|
||||
if (toolCallIndex !== -1) {
|
||||
const newMessages = [...prev];
|
||||
newMessages[toolCallIndex] = responseMessage;
|
||||
|
||||
console.log(
|
||||
"[Tool Response] Replaced tool_call with matching tool_id:",
|
||||
chunk.tool_id,
|
||||
"at index:",
|
||||
toolCallIndex,
|
||||
);
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"[Tool Response] No tool_call found with tool_id:",
|
||||
chunk.tool_id,
|
||||
"appending instead",
|
||||
);
|
||||
return [...prev, responseMessage];
|
||||
});
|
||||
} else if (
|
||||
chunk.type === "login_needed" ||
|
||||
chunk.type === "need_login"
|
||||
) {
|
||||
// Add login needed message
|
||||
const loginNeededMessage: ChatMessageData = {
|
||||
type: "login_needed",
|
||||
message:
|
||||
chunk.message ||
|
||||
"Please sign in to use chat and agent features",
|
||||
sessionId: chunk.session_id || sessionId,
|
||||
agentInfo: chunk.agent_info,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, loginNeededMessage]);
|
||||
} else if (chunk.type === "stream_end") {
|
||||
// Convert streaming chunks into a completed assistant message
|
||||
// This provides seamless transition without flash or resize
|
||||
// Use ref to get the latest chunks value (not stale closure value)
|
||||
const completedContent = streamingChunksRef.current.join("");
|
||||
|
||||
if (completedContent) {
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: completedContent,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// Add the completed assistant message to local state
|
||||
// It will be visible immediately while backend updates
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev, assistantMessage];
|
||||
|
||||
// Log final state using current messages from state
|
||||
console.log("[Stream End] Final state:", {
|
||||
localMessages: updated.map((m) => ({
|
||||
type: m.type,
|
||||
...(m.type === "message" && {
|
||||
role: m.role,
|
||||
contentLength: m.content.length,
|
||||
}),
|
||||
...(m.type === "tool_call" && {
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
}),
|
||||
...(m.type === "tool_response" && {
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
success: m.success,
|
||||
}),
|
||||
})),
|
||||
streamingChunks: streamingChunksRef.current,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
// Clear streaming state immediately now that we have the message
|
||||
setStreamingChunks([]);
|
||||
streamingChunksRef.current = [];
|
||||
setHasTextChunks(false);
|
||||
|
||||
// Messages are now in local state and will be displayed
|
||||
// No need to refresh from backend - local state is the source of truth
|
||||
console.log(
|
||||
"[Stream End] Stream complete, messages in local state",
|
||||
);
|
||||
} else if (chunk.type === "error") {
|
||||
const errorMessage =
|
||||
chunk.message || chunk.content || "An error occurred";
|
||||
console.error("Stream error:", errorMessage);
|
||||
toast.error("Chat Error", {
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
// TODO: Handle usage for display
|
||||
},
|
||||
isUserMessage,
|
||||
);
|
||||
// Stream the response using the event dispatcher
|
||||
await sendStreamMessage(sessionId, content, dispatcher, isUserMessage);
|
||||
} catch (err) {
|
||||
console.error("Failed to send message:", err);
|
||||
const errorMessage =
|
||||
@@ -413,7 +182,7 @@ export function useChatContainer({
|
||||
});
|
||||
}
|
||||
},
|
||||
[sessionId, sendStreamMessage, onRefreshSession],
|
||||
[sessionId, sendStreamMessage],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user