import { AiRuntimeClient, AiRuntimeClientOptions, GraphQLError, } from "@vn-sdk/runtime-client-gql"; import { useToast } from "../components/toast/toast-provider"; import { useMemo, useRef } from "react"; import { ErrorVisibility, AiSDKApiDiscoveryError, AiSDKRemoteEndpointDiscoveryError, AiSDKAgentDiscoveryError, AiSDKError, AiSDKErrorCode, AiErrorHandler, AiErrorEvent, } from "@vn-sdk/shared"; import { shouldShowDevConsole } from "../utils/dev-console"; export interface AiRuntimeClientHookOptions extends AiRuntimeClientOptions { showDevConsole?: boolean; onError: AiErrorHandler; } export const useAiRuntimeClient = (options: AiRuntimeClientHookOptions) => { const { setBannerError } = useToast(); const { showDevConsole, onError, ...runtimeOptions } = options; // Deduplication state for structured errors const lastStructuredErrorRef = useRef<{ message: string; timestamp: number } | null>(null); // Helper function to trace UI errors const traceUIError = async (error: AiSDKError, originalError?: any) => { try { const errorEvent: AiErrorEvent = { type: "error", timestamp: Date.now(), context: { source: "ui", request: { operation: "runtimeClient", url: runtimeOptions.url, startTime: Date.now(), }, technical: { environment: "browser", userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined, stackTrace: originalError instanceof Error ? originalError.stack : undefined, }, }, error, }; await onError(errorEvent); } catch (error) { console.error("Error in onError handler:", error); } }; const runtimeClient = useMemo(() => { return new AiRuntimeClient({ ...runtimeOptions, handleGQLErrors: (error) => { if ((error as any).graphQLErrors?.length) { const graphQLErrors = (error as any).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 ?? false); // Silent errors - just log if (visibility === ErrorVisibility.SILENT) { console.error("VN SDK Silent Error:", gqlError.message); return; } if (!isDev) { console.error("VN SDK Error (hidden in production):", gqlError.message); return; } // All errors (including DEV_ONLY) show as banners for consistency // Deduplicate to prevent spam const now = Date.now(); const errorMessage = gqlError.message; if ( lastStructuredErrorRef.current && lastStructuredErrorRef.current.message === errorMessage && now - lastStructuredErrorRef.current.timestamp < 150 ) { return; // Skip duplicate } lastStructuredErrorRef.current = { message: errorMessage, timestamp: now }; const ckError = createStructuredError(gqlError); if (ckError) { setBannerError(ckError); // Trace the error traceUIError(ckError, gqlError); // TODO: if onError & renderError should work without key, insert here } else { // Fallback for unstructured errors const fallbackError = new AiSDKError({ message: gqlError.message, code: AiSDKErrorCode.UNKNOWN, }); setBannerError(fallbackError); // Trace the fallback error traceUIError(fallbackError, gqlError); // TODO: if onError & renderError should work without key, insert here } }; // Process all errors as banners graphQLErrors.forEach(routeError); } else { const isDev = shouldShowDevConsole(showDevConsole ?? false); 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); // TODO: if onError & renderError should work without key, insert here } } }, handleGQLWarning: (message: string) => { console.warn(message); // Show warnings as banners too for consistency const warningError = new AiSDKError({ message, code: AiSDKErrorCode.UNKNOWN, }); setBannerError(warningError); }, }); }, [runtimeOptions, setBannerError, showDevConsole, onError]); return runtimeClient; }; // Create appropriate structured error from GraphQL error function createStructuredError(gqlError: GraphQLError): AiSDKError | null { const extensions = gqlError.extensions; const originalError = extensions?.originalError as any; const message = originalError?.message || gqlError.message; const code = extensions?.code as AiSDKErrorCode; if (code) { return new AiSDKError({ message, code }); } // Legacy error detection by stack trace if (originalError?.stack?.includes("AiApiDiscoveryError")) { return new AiSDKApiDiscoveryError({ message }); } if (originalError?.stack?.includes("AiSDKRemoteEndpointDiscoveryError")) { return new AiSDKRemoteEndpointDiscoveryError({ message }); } if (originalError?.stack?.includes("AiSDKAgentDiscoveryError")) { return new AiSDKAgentDiscoveryError({ agentName: "", availableAgents: [], }); } return null; }