| | import React from 'react'; |
| | import { Button } from '@librechat/client'; |
| | import { useLocalize } from '~/hooks'; |
| | import { cn } from '~/utils'; |
| |
|
| | |
| | type ApiError = |
| | | string |
| | | Error |
| | | { |
| | message?: string; |
| | status?: number; |
| | code?: string; |
| | response?: { |
| | data?: { |
| | userMessage?: string; |
| | suggestion?: string; |
| | message?: string; |
| | }; |
| | status?: number; |
| | }; |
| | data?: { |
| | userMessage?: string; |
| | suggestion?: string; |
| | message?: string; |
| | }; |
| | }; |
| |
|
| | interface ErrorDisplayProps { |
| | error: ApiError; |
| | onRetry?: () => void; |
| | context?: { |
| | searchQuery?: string; |
| | category?: string; |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onRetry, context }) => { |
| | const localize = useLocalize(); |
| |
|
| | |
| | const isErrorObject = (err: ApiError): err is { [key: string]: unknown } => { |
| | return typeof err === 'object' && err !== null && !(err instanceof Error); |
| | }; |
| |
|
| | const isErrorInstance = (err: ApiError): err is Error => { |
| | return err instanceof Error; |
| | }; |
| |
|
| | |
| | const getErrorInfo = (): { title: string; message: string; suggestion: string } => { |
| | |
| | let errorData: unknown; |
| |
|
| | if (typeof error === 'string') { |
| | errorData = { message: error }; |
| | } else if (isErrorInstance(error)) { |
| | errorData = { message: error.message }; |
| | } else if (isErrorObject(error)) { |
| | |
| | errorData = (error as any)?.response?.data || (error as any)?.data || error; |
| | } else { |
| | errorData = error; |
| | } |
| |
|
| | |
| | let errorMessage = ''; |
| | if (isErrorInstance(error)) { |
| | errorMessage = error.message; |
| | } else if (isErrorObject(error) && (error as any)?.message) { |
| | errorMessage = (error as any).message; |
| | } |
| |
|
| | const errorCode = isErrorObject(error) ? (error as any)?.code : ''; |
| |
|
| | |
| | if (errorCode === 'ECONNABORTED' || errorMessage?.includes('timeout')) { |
| | return { |
| | title: localize('com_agents_error_timeout_title'), |
| | message: localize('com_agents_error_timeout_message'), |
| | suggestion: localize('com_agents_error_timeout_suggestion'), |
| | }; |
| | } |
| |
|
| | if (errorCode === 'NETWORK_ERROR' || errorMessage?.includes('Network Error')) { |
| | return { |
| | title: localize('com_agents_error_network_title'), |
| | message: localize('com_agents_error_network_message'), |
| | suggestion: localize('com_agents_error_network_suggestion'), |
| | }; |
| | } |
| |
|
| | |
| | const status = isErrorObject(error) ? (error as any)?.response?.status : null; |
| | if (status) { |
| | if (status === 404) { |
| | return { |
| | title: localize('com_agents_error_not_found_title'), |
| | message: getNotFoundMessage(), |
| | suggestion: localize('com_agents_error_not_found_suggestion'), |
| | }; |
| | } |
| |
|
| | if (status === 400) { |
| | return { |
| | title: localize('com_agents_error_invalid_request'), |
| | message: |
| | (errorData as any)?.userMessage || localize('com_agents_error_bad_request_message'), |
| | suggestion: |
| | (errorData as any)?.suggestion || localize('com_agents_error_bad_request_suggestion'), |
| | }; |
| | } |
| |
|
| | if (status >= 500) { |
| | return { |
| | title: localize('com_agents_error_server_title'), |
| | message: localize('com_agents_error_server_message'), |
| | suggestion: localize('com_agents_error_server_suggestion'), |
| | }; |
| | } |
| | } |
| |
|
| | |
| | if (errorData && typeof errorData === 'object' && (errorData as any)?.userMessage) { |
| | return { |
| | title: getContextualTitle(), |
| | message: (errorData as any).userMessage, |
| | suggestion: |
| | (errorData as any).suggestion || localize('com_agents_error_suggestion_generic'), |
| | }; |
| | } |
| |
|
| | |
| | return { |
| | title: getContextualTitle(), |
| | message: localize('com_agents_error_generic'), |
| | suggestion: localize('com_agents_error_suggestion_generic'), |
| | }; |
| | }; |
| |
|
| | |
| | |
| | |
| | const getContextualTitle = (): string => { |
| | if (context?.searchQuery) { |
| | return localize('com_agents_error_search_title'); |
| | } |
| |
|
| | if (context?.category) { |
| | return localize('com_agents_error_category_title'); |
| | } |
| |
|
| | return localize('com_agents_error_title'); |
| | }; |
| |
|
| | |
| | |
| | |
| | const getNotFoundMessage = (): string => { |
| | if (context?.searchQuery) { |
| | return localize('com_agents_search_no_results', { query: context.searchQuery }); |
| | } |
| |
|
| | if (context?.category && context.category !== 'all') { |
| | return localize('com_agents_category_empty', { category: context.category }); |
| | } |
| |
|
| | return localize('com_agents_error_not_found_message'); |
| | }; |
| |
|
| | const { title, message, suggestion } = getErrorInfo(); |
| |
|
| | return ( |
| | <div className="py-12 text-center" role="alert" aria-live="assertive" aria-atomic="true"> |
| | <div className="mx-auto max-w-md space-y-4"> |
| | {/* Error icon with proper accessibility */} |
| | <div className="flex justify-center"> |
| | <div |
| | className={cn( |
| | 'flex h-12 w-12 items-center justify-center rounded-full', |
| | 'bg-red-100 dark:bg-red-900/20', |
| | )} |
| | > |
| | <svg |
| | className="h-6 w-6 text-red-600 dark:text-red-400" |
| | fill="none" |
| | viewBox="0 0 24 24" |
| | strokeWidth={2} |
| | stroke="currentColor" |
| | aria-hidden="true" |
| | role="img" |
| | aria-label="Error icon" |
| | > |
| | <path |
| | strokeLinecap="round" |
| | strokeLinejoin="round" |
| | d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" |
| | /> |
| | </svg> |
| | </div> |
| | </div> |
| | |
| | {/* Error content with proper headings and structure */} |
| | <div className="space-y-3"> |
| | <h3 className="text-lg font-semibold text-gray-900 dark:text-white" id="error-title"> |
| | {title} |
| | </h3> |
| | <p |
| | className="text-gray-600 dark:text-gray-400" |
| | id="error-message" |
| | aria-describedby="error-title" |
| | > |
| | {message} |
| | </p> |
| | <p |
| | className="text-sm text-gray-500 dark:text-gray-500" |
| | id="error-suggestion" |
| | role="note" |
| | aria-label={`Suggestion: ${suggestion}`} |
| | > |
| | 💡 {suggestion} |
| | </p> |
| | </div> |
| | |
| | {/* Retry button with enhanced accessibility */} |
| | {onRetry && ( |
| | <div className="pt-2"> |
| | <Button |
| | onClick={onRetry} |
| | variant="outline" |
| | size="sm" |
| | className={cn( |
| | 'border-red-300 text-red-700 hover:bg-red-50 focus:ring-2 focus:ring-red-500', |
| | 'dark:border-red-600 dark:text-red-400 dark:hover:bg-red-900/20 dark:focus:ring-red-400', |
| | )} |
| | aria-describedby="error-message error-suggestion" |
| | aria-label={`Retry action. ${message}`} |
| | > |
| | {localize('com_agents_error_retry')} |
| | </Button> |
| | </div> |
| | )} |
| | </div> |
| | </div> |
| | ); |
| | }; |
| |
|
| | export default ErrorDisplay; |
| |
|