import { Dispatch, SetStateAction } from "react"; import { BotData, ChatState } from "~/types/bot"; import { Message, ConversationStep } from "~/types/chat"; import { fetchBotDataBySharingID, validateThreadId } from "~/utils/api"; import { withAsyncErrorHandling, withErrorHandling, setSentryUserContext, addBreadcrumb, } from "~/utils/sentry"; import { clearLoadedFromStorageFlag, WidgetType, getDoriThreadId, clearDoriThreadId, loadBotDataWithETag, saveBotDataWithETag, } from "~/utils/session-storage"; import { parseSuggestedReply } from "../parsers/suggestion-parser"; import { createWelcomeMessage } from "../utils/message-builders"; /** * Creates a function to initialize the bot with data from API * @param setBotError - State setter for bot error * @param setIsLoadingBot - State setter for loading state * @param setBotData - State setter for bot data * @param setCurrentStep - State setter for conversation step * @param widgetType - Type of widget (floating or embedded) * @returns Async function to initialize bot */ export function createInitializeBot( setBotError: Dispatch>, setIsLoadingBot: Dispatch>, setBotData: Dispatch>, setCurrentStep: Dispatch>, widgetType: WidgetType, setChatState?: Dispatch> ) { return withAsyncErrorHandling("initializeBot", async (sharingId: string) => { if (!sharingId || sharingId.trim() === "") { setBotError("Invalid sharing ID provided"); return; } setIsLoadingBot(true); setBotError(null); let encounteredError = false; const fetchWithFallback = async () => { // 1. Check for cached data immediately (available if prefetch found cache) const cachedData = window.moriBotDataCached; // 2. If prefetch promise exists, await it for fresh/validated data if (window.moriBotDataPromise) { try { const freshData = await window.moriBotDataPromise; if (freshData) { return freshData; } } catch (error) { addBreadcrumb( "Prefetch promise rejected, using fallback", "widget.prefetch_fallback", "warning", { error_message: (error as Error).message } ); // Continue to cached data or fallback fetch } } // 3. Return cached data if prefetch failed but cache was available if (cachedData) { return cachedData; } // 4. Fallback: direct fetch const { botData: cachedBotData, etag: cachedEtag } = loadBotDataWithETag(widgetType, sharingId); const fallbackResult = await fetchBotDataBySharingID( sharingId, cachedEtag ?? undefined ); if (fallbackResult.status === 304) { if (cachedBotData) { return cachedBotData; } // Cache was cleared after load - fall through to error handling } if (fallbackResult.error) { setBotError(fallbackResult.error); encounteredError = true; return null; } if (fallbackResult.data) { // Persist fallback data to cache for next load if (fallbackResult.etag) { saveBotDataWithETag( fallbackResult.data, fallbackResult.etag, widgetType, sharingId ); } return fallbackResult.data; } encounteredError = true; return null; }; const botDataResult: BotData | null = await fetchWithFallback(); if (!botDataResult) { if (!encounteredError) { setBotError("No bot data found"); } return; } clearLoadedFromStorageFlag(widgetType); const botDataWithSharingId: BotData = { ...botDataResult, sharing_id: sharingId, suggestedReply: parseSuggestedReply(botDataResult.suggestedReply), }; setSentryUserContext(sharingId); addBreadcrumb("Bot initialized successfully", "widget.bot_init", "info", { sharing_id: sharingId, }); setBotData(botDataWithSharingId); setCurrentStep("initial-greeting"); const storedThreadId = getDoriThreadId(widgetType); if (storedThreadId && botDataWithSharingId.assistant_id) { addBreadcrumb( "Validating stored thread ID", "widget.thread_validation", "info", { thread_id: storedThreadId, assistant_id: botDataWithSharingId.assistant_id, } ); validateThreadId(storedThreadId, botDataWithSharingId.assistant_id) .then((isValid) => { if (!isValid) { addBreadcrumb( "Thread ID validation failed - clearing invalid thread", "widget.thread_validation_failed", "warning", { thread_id: storedThreadId, } ); clearDoriThreadId(widgetType); if (setChatState) { setChatState((prev) => ({ ...prev, threadId: null })); } } else { addBreadcrumb( "Thread ID validation successful", "widget.thread_validation_success", "info", { thread_id: storedThreadId, } ); } }) .catch((error) => { addBreadcrumb( "Thread ID validation error - clearing thread", "widget.thread_validation_error", "warning", { thread_id: storedThreadId, error_message: (error as Error)?.message || String(error), } ); clearDoriThreadId(widgetType); if (setChatState) { setChatState((prev) => ({ ...prev, threadId: null })); } }); } }); } /** * Creates a function to add welcome message to chat * @param botData - Bot data containing welcome message * @param dynamicWelcomeMessage - Dynamic welcome message from API (if available) * @param setMessages - State setter for messages * @returns Function to add welcome message */ export function createAddWelcomeMessage( botData: BotData | null, dynamicWelcomeMessage: string | null, setMessages: Dispatch> ) { return withErrorHandling("addWelcomeMessage", () => { // Prefer dynamic welcome message, fall back to static botData message const welcomeText = dynamicWelcomeMessage || botData?.wellcomeMessage || ""; // Only add welcome message if it exists and has content if (!welcomeText || welcomeText.trim().length === 0) { return; // Exit early, don't add any message } const welcomeMessage = createWelcomeMessage(welcomeText); setMessages((prev) => [...prev, welcomeMessage]); }); }