import { Dispatch, SetStateAction } from "react"; import { BotData, ChatState, StreamChatResponse } from "~/types/bot"; import { Message } from "~/types/chat"; import { ProductCardData } from "~/types/product"; import { withErrorHandling, withAsyncErrorHandling } from "~/utils/sentry"; import { storeDoriThreadId, storeThreadInWooCommerceSession, clearLoadedFromStorageFlag, WidgetType, } from "~/utils/session-storage"; import { trackDoriSession } from "~/utils/api"; import { getLoadingMessage } from "../constants"; import { createBotMessage, createLoadingMessage, createProductMessage, } from "../utils/message-builders"; /** * Creates a streaming chunk handler function * This handles real-time streaming responses from the API */ export function createChunkHandler( botData: BotData, chatState: ChatState, widgetType: WidgetType, shouldTrackSession: boolean, setChatState: Dispatch>, setMessages: Dispatch>, setShouldTrackSession: Dispatch>, // Mutable state that persists across chunk calls fullMessageRef: { current: string }, currentBotMessageRef: { current: Message | null }, accumulatedProductsRef: { current: ProductCardData[] }, pendingSuggestionsRef: { current: string[] } ) { return (chunk: StreamChatResponse) => { withErrorHandling("streaming chunk handler", () => { // Handle thread ID update if (chunk.thread_id) { if (chunk.thread_id !== chatState.threadId) { clearLoadedFromStorageFlag(widgetType); } storeDoriThreadId(chunk.thread_id, widgetType); storeThreadInWooCommerceSession(chunk.thread_id, widgetType); setChatState((prev) => ({ ...prev, threadId: chunk.thread_id || null, })); if (shouldTrackSession && botData && chunk.thread_id) { withAsyncErrorHandling("trackDoriSession", async () => { await trackDoriSession( botData.sharing_id, chunk.thread_id!, "wordpress", widgetType ); setShouldTrackSession(false); })().catch((error) => { console.error("Failed to track session:", error); // Error already logged by withAsyncErrorHandling }); } } // Handle wait state (tool execution) else if (chunk.wait) { if (currentBotMessageRef.current && fullMessageRef.current.trim()) { setMessages((prev) => prev.map((msg) => msg.id === currentBotMessageRef.current?.id ? { ...msg, content: fullMessageRef.current } : msg ) ); } currentBotMessageRef.current = null; fullMessageRef.current = ""; setMessages((prev) => { const filteredMessages = prev.filter( (msg) => msg.type !== "loading" ); return [ ...filteredMessages, createLoadingMessage(getLoadingMessage(chunk.tool_name)), ]; }); } // Handle text streaming else if (chunk.text) { fullMessageRef.current += chunk.text; if (!currentBotMessageRef.current) { const newBotMessage = createBotMessage(fullMessageRef.current); setMessages((prev) => { const filteredMessages = prev.filter( (msg) => msg.type !== "loading" ); return [...filteredMessages, newBotMessage]; }); currentBotMessageRef.current = newBotMessage; } else { setMessages((prev) => prev.map((msg) => msg.id === currentBotMessageRef.current?.id ? { ...msg, content: fullMessageRef.current } : msg ) ); } } // Handle product results else if (chunk.products && chunk.products.length > 0) { clearLoadedFromStorageFlag(widgetType); accumulatedProductsRef.current = [ ...accumulatedProductsRef.current, ...chunk.products, ]; setMessages((prev) => { const filteredMessages = prev.filter( (msg) => msg.type !== "loading" ); const newMessages = [...filteredMessages]; const currentMessageIndex = newMessages.findIndex( (msg) => msg.id === currentBotMessageRef.current?.id ); const productMessage = createProductMessage( accumulatedProductsRef.current, chunk.products?.[0]?.tool_call_id ); if (currentMessageIndex !== -1) { newMessages.splice(currentMessageIndex + 1, 0, productMessage); } else { newMessages.push(productMessage); } return newMessages; }); } // Handle suggestions else if (chunk.suggestions && chunk.suggestions.length > 0) { pendingSuggestionsRef.current = [ ...pendingSuggestionsRef.current, ...chunk.suggestions, ]; } })(); }; }