import { GraphQLError } from "@copilotkit/runtime-client-gql"; import React, { createContext, useContext, useState, useCallback } from "react"; import { PartialBy, CopilotKitError, Severity } from "@copilotkit/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: CopilotKitError | null; setBannerError: (error: CopilotKitError | 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: CopilotKitError): 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; } function formatBannerMessage(message: string): string { // Try to extract the useful message from JSON first const jsonMatch = message.match(/'message':\s*'([^']+)'/); if (jsonMatch) { return jsonMatch[1]; } // Strip technical garbage but keep the meaningful message let cleaned = message.split(" - ")[0]; cleaned = cleaned.split(": Error code")[0]; cleaned = cleaned.replace(/:\s*\d{3}$/, ""); cleaned = cleaned.replace(/See more:.*$/g, ""); cleaned = cleaned.trim(); return cleaned || "An error occurred."; } function extractUrl(message: string): { url: string; text: string } | null { const markdownMatch = /\[([^\]]+)\]\(([^)]+)\)/.exec(message); if (markdownMatch) { return { url: markdownMatch[2], text: "See More" }; } const plainMatch = /(https?:\/\/[^\s)]+)/.exec(message); if (plainMatch) { return { url: plainMatch[0].replace(/[.,;:'"]*$/, ""), text: "See More", }; } return null; } function BannerErrorDisplay({ bannerError, onDismiss, }: { bannerError: CopilotKitError; onDismiss: () => void; }) { const [detailsExpanded, setDetailsExpanded] = useState(false); const severity = getErrorSeverity(bannerError); const colors = getErrorColors(severity); // Extract optional error details attached by CopilotListeners const details = (bannerError as any).details as | { code?: string; context?: Record; stack?: string; originalMessage?: string; } | undefined; const link = extractUrl(bannerError.message); return (
{formatBannerMessage(bannerError.message)}
{link && ( )} {details && ( )}
{detailsExpanded && details && (
{details.code && (
Code: {details.code}
)} {details.originalMessage && (
Message: {details.originalMessage}
)} {details.context && Object.keys(details.context).length > 0 && (
Context:{" "} {JSON.stringify(details.context, null, 2)}
)} {details.stack && (
Stack: {"\n"} {details.stack}
)}
)}
); } 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: CopilotKitError | 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 && ( setBannerError(null)} /> )} {/* Toast Display - Deprecated: All errors now show as banners */} {children} ); } // Toast component removed - all errors now show as banners for consistency