/** * An internal context to separate the messages state (which is constantly changing) from the rest of VN SDK context */ import { ReactNode, useEffect, useState, useRef, useCallback, useMemo, createContext, useContext, } from "react"; import { AiMessagesContext } from "../../context/ai-messages-context"; import { loadMessagesFromJsonRepresentation, Message, GraphQLError, } from "@vn-sdk/runtime-client-gql"; import { useAiContext } from "../../context/ai-context"; import { useToast } from "../toast/toast-provider"; import { shouldShowDevConsole } from "../../utils/dev-console"; import { ErrorVisibility, AiSDKApiDiscoveryError, AiSDKRemoteEndpointDiscoveryError, AiSDKAgentDiscoveryError, AiSDKError, AiSDKErrorCode, } from "@vn-sdk/shared"; import { SuggestionItem } from "../../utils/suggestions"; // Helper to determine if error should show as banner based on visibility and legacy patterns function shouldShowAsBanner(gqlError: GraphQLError): boolean { const extensions = gqlError.extensions; if (!extensions) return false; // Priority 1: Check error code for discovery errors (these should always be banners) const code = extensions.code as AiSDKErrorCode; if ( code === AiSDKErrorCode.AGENT_NOT_FOUND || code === AiSDKErrorCode.API_NOT_FOUND || code === AiSDKErrorCode.REMOTE_ENDPOINT_NOT_FOUND || code === AiSDKErrorCode.CONFIGURATION_ERROR || code === AiSDKErrorCode.MISSING_PUBLIC_API_KEY_ERROR || code === AiSDKErrorCode.UPGRADE_REQUIRED_ERROR ) { return true; } // Priority 2: Check banner visibility if (extensions.visibility === ErrorVisibility.BANNER) { return true; } // Priority 3: Check for critical errors that should be banners regardless of formal classification const errorMessage = gqlError.message.toLowerCase(); if ( errorMessage.includes("api key") || errorMessage.includes("401") || errorMessage.includes("unauthorized") || errorMessage.includes("authentication") || errorMessage.includes("incorrect api key") ) { return true; } // Priority 4: Legacy stack trace detection for discovery errors const originalError = extensions.originalError as any; if (originalError?.stack) { return ( originalError.stack.includes("AiApiDiscoveryError") || originalError.stack.includes("AiSDKRemoteEndpointDiscoveryError") || originalError.stack.includes("AiSDKAgentDiscoveryError") ); } return false; } /** * MessagesTap is used to mitigate performance issues when we only need * a snapshot of the messages, not a continuously updating stream of messages. */ export type MessagesTap = { getMessagesFromTap: () => Message[]; updateTapMessages: (messages: Message[]) => void; }; const MessagesTapContext = createContext(null); export function useMessagesTap() { const tap = useContext(MessagesTapContext); if (!tap) throw new Error("useMessagesTap must be used inside "); return tap; } export function MessagesTapProvider({ children }: { children: React.ReactNode }) { const messagesRef = useRef([]); const tapRef = useRef({ getMessagesFromTap: () => messagesRef.current, updateTapMessages: (messages: Message[]) => { messagesRef.current = messages; }, }); return ( {children} ); } /** * VN SDK messages context. */ export function AiMessages({ children }: { children: ReactNode }) { const [messages, setMessages] = useState([]); const lastLoadedThreadId = useRef(); const lastLoadedAgentName = useRef(); const lastLoadedMessages = useRef(); const { updateTapMessages } = useMessagesTap(); const { threadId, agentSession, runtimeClient, showDevConsole, onError, aiApiConfig } = useAiContext(); const { setBannerError } = useToast(); // Helper function to trace UI errors (similar to useAiRuntimeClient) const traceUIError = useCallback( async (error: AiSDKError, originalError?: any) => { // Just check if onError and publicApiKey are defined if (!onError || !aiApiConfig.publicApiKey) return; try { const traceEvent = { type: "error" as const, timestamp: Date.now(), context: { source: "ui" as const, request: { operation: "loadAgentState", url: aiApiConfig.chatApiEndpoint, startTime: Date.now(), }, technical: { environment: "browser", userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined, stackTrace: originalError instanceof Error ? originalError.stack : undefined, }, }, error, }; await onError(traceEvent); } catch (traceError) { console.error("Error in AiMessages onError handler:", traceError); } }, [onError, aiApiConfig.publicApiKey, aiApiConfig.chatApiEndpoint], ); const createStructuredError = (gqlError: GraphQLError): AiSDKError | null => { const extensions = gqlError.extensions; const originalError = extensions?.originalError as any; // Priority: Check stack trace for discovery errors first if (originalError?.stack) { if (originalError.stack.includes("AiApiDiscoveryError")) { return new AiSDKApiDiscoveryError({ message: originalError.message }); } if (originalError.stack.includes("AiSDKRemoteEndpointDiscoveryError")) { return new AiSDKRemoteEndpointDiscoveryError({ message: originalError.message }); } if (originalError.stack.includes("AiSDKAgentDiscoveryError")) { return new AiSDKAgentDiscoveryError({ agentName: "", availableAgents: [], }); } } // Fallback: Use the formal error code if available const message = originalError?.message || gqlError.message; const code = extensions?.code as AiSDKErrorCode; if (code) { return new AiSDKError({ message, code }); } return null; }; const handleGraphQLErrors = useCallback( (error: any) => { if (error.graphQLErrors?.length) { const graphQLErrors = error.graphQLErrors as GraphQLError[]; // Route all errors to banners for consistent UI const routeError = (gqlError: GraphQLError) => { const extensions = gqlError.extensions; const visibility = extensions?.visibility as ErrorVisibility; const isDev = shouldShowDevConsole(showDevConsole); if (!isDev) { console.error("VN SDK Error (hidden in production):", gqlError.message); return; } // Silent errors - just log if (visibility === ErrorVisibility.SILENT) { console.error("VN SDK Silent Error:", gqlError.message); return; } // All other errors (including DEV_ONLY) show as banners for consistency const ckError = createStructuredError(gqlError); if (ckError) { setBannerError(ckError); // Trace the structured error traceUIError(ckError, gqlError); } else { // Fallback: create a generic error for unstructured GraphQL errors const fallbackError = new AiSDKError({ message: gqlError.message, code: AiSDKErrorCode.UNKNOWN, }); setBannerError(fallbackError); // Trace the fallback error traceUIError(fallbackError, gqlError); } }; // Process all errors as banners graphQLErrors.forEach(routeError); } else { const isDev = shouldShowDevConsole(showDevConsole); if (!isDev) { console.error("VN SDK Error (hidden in production):", error); } else { // Route non-GraphQL errors to banner as well const fallbackError = new AiSDKError({ message: error?.message || String(error), code: AiSDKErrorCode.UNKNOWN, }); setBannerError(fallbackError); // Trace the non-GraphQL error traceUIError(fallbackError, error); } } }, [setBannerError, showDevConsole, traceUIError], ); useEffect(() => { if (!threadId || threadId === lastLoadedThreadId.current) return; if ( threadId === lastLoadedThreadId.current && agentSession?.agentName === lastLoadedAgentName.current ) { return; } const fetchMessages = async () => { if (!agentSession?.agentName) return; const result = await runtimeClient.loadAgentState({ threadId, agentName: agentSession?.agentName, }); // Check for GraphQL errors and manually trigger error handling if (result.error) { // Update refs to prevent infinite retries of the same failed request lastLoadedThreadId.current = threadId; lastLoadedAgentName.current = agentSession?.agentName; handleGraphQLErrors(result.error); return; // Don't try to process the data if there's an error } const newMessages = result.data?.loadAgentState?.messages; if (newMessages === lastLoadedMessages.current) return; if (result.data?.loadAgentState) { lastLoadedMessages.current = newMessages; lastLoadedThreadId.current = threadId; lastLoadedAgentName.current = agentSession?.agentName; const messages = loadMessagesFromJsonRepresentation(JSON.parse(newMessages || "[]")); setMessages(messages); } }; void fetchMessages(); }, [threadId, agentSession?.agentName]); useEffect(() => { updateTapMessages(messages); }, [messages, updateTapMessages]); const memoizedChildren = useMemo(() => children, [children]); const [suggestions, setSuggestions] = useState([]); return ( {memoizedChildren} ); }