import { GraphQLError } from "@vn-sdk/runtime-client-gql"; import React, { createContext, useContext, useState, useCallback } from "react"; import { PartialBy, AiSDKError, Severity } from "@vn-sdk/shared"; interface Toast { id: string; message: string | React.ReactNode; type: "info" | "success" | "warning" | "error"; duration?: number; } interface ToastContextValue { toasts: Toast[]; addToast: (toast: PartialBy) => void; addGraphQLErrorsToast: (errors: GraphQLError[]) => void; removeToast: (id: string) => void; enabled: boolean; // Banner management bannerError: AiSDKError | null; setBannerError: (error: AiSDKError | null) => void; } const ToastContext = createContext(undefined); // Helper functions for error banner styling type ErrorSeverity = "critical" | "warning" | "info"; interface ErrorColors { background: string; border: string; text: string; icon: string; } function getErrorSeverity(error: AiSDKError): ErrorSeverity { // Use structured error severity if available if (error.severity) { switch (error.severity) { case Severity.CRITICAL: return "critical"; case Severity.WARNING: return "warning"; case Severity.INFO: return "info"; default: return "info"; } } // Fallback: Check for API key errors which should always be critical const message = error.message.toLowerCase(); if ( message.includes("api key") || message.includes("401") || message.includes("unauthorized") || message.includes("authentication") || message.includes("incorrect api key") ) { return "critical"; } // Default to info level return "info"; } function getErrorColors(severity: ErrorSeverity): ErrorColors { switch (severity) { case "critical": return { background: "#fee2e2", border: "#dc2626", text: "#7f1d1d", icon: "#dc2626", }; case "warning": return { background: "#fef3c7", border: "#d97706", text: "#78350f", icon: "#d97706", }; case "info": return { background: "#dbeafe", border: "#2563eb", text: "#1e3a8a", icon: "#2563eb", }; } } export function useToast() { const context = useContext(ToastContext); if (!context) { throw new Error("useToast must be used within a ToastProvider"); } return context; } export function ToastProvider({ enabled, children, }: { enabled: boolean; children: React.ReactNode; }) { const [toasts, setToasts] = useState([]); const [bannerError, setBannerErrorState] = useState(null); const removeToast = useCallback((id: string) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }, []); const addToast = useCallback( (toast: PartialBy) => { // Respect the enabled flag for ALL toasts if (!enabled) { return; } const id = toast.id ?? Math.random().toString(36).substring(2, 9); setToasts((currentToasts) => { if (currentToasts.find((toast) => toast.id === id)) return currentToasts; return [...currentToasts, { ...toast, id }]; }); if (toast.duration) { setTimeout(() => { removeToast(id); }, toast.duration); } }, [enabled, removeToast], ); const setBannerError = useCallback( (error: AiSDKError | null) => { // Respect the enabled flag for ALL errors if (!enabled && error !== null) { return; } setBannerErrorState(error); }, [enabled], ); const addGraphQLErrorsToast = useCallback((errors: GraphQLError[]) => { // DEPRECATED: All errors now route to banners for consistency console.warn("addGraphQLErrorsToast is deprecated. All errors now show as banners."); // Function kept for backward compatibility - does nothing }, []); const value = { toasts, addToast, addGraphQLErrorsToast, removeToast, enabled, bannerError, setBannerError, }; return ( {/* Banner Error Display */} {bannerError && (() => { const severity = getErrorSeverity(bannerError); const colors = getErrorColors(severity); return (
{(() => { let message = bannerError.message; // Try to extract the useful message from JSON first const jsonMatch = message.match(/'message':\s*'([^']+)'/); if (jsonMatch) { return jsonMatch[1]; // Return the actual error message } // Strip technical garbage but keep the meaningful message message = message.split(" - ")[0]; // Remove everything after " - {" message = message.split(": Error code")[0]; // Remove ": Error code: 401" message = message.replace(/:\s*\d{3}$/, ""); // Remove trailing ": 401" message = message.replace(/See more:.*$/g, ""); // Remove "See more" links message = message.trim(); // If it's still garbage (contains { or '), use fallback // if (message.includes("{") || message.includes("'")) { // return "Configuration error.... Please check your setup."; // } return message || "Configuration error occurred."; })()}
{(() => { const message = bannerError.message; const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; const plainUrlRegex = /(https?:\/\/[^\s)]+)/g; // Extract the first URL found let url = null; let buttonText = "See More"; // Check for markdown links first const markdownMatch = markdownLinkRegex.exec(message); if (markdownMatch) { url = markdownMatch[2]; buttonText = "See More"; } else { // Check for plain URLs const urlMatch = plainUrlRegex.exec(message); if (urlMatch) { url = urlMatch[0].replace(/[.,;:'"]*$/, ""); // Remove trailing punctuation buttonText = "See More"; } } if (!url) return null; return ( ); })()}
); })()} {/* Toast Display - Deprecated: All errors now show as banners */} {children} ); } // Toast component removed - all errors now show as banners for consistency