From 9d6ff741d983c09263762bcb8c1142c852ead849 Mon Sep 17 00:00:00 2001 From: xKevIsDev Date: Thu, 3 Jul 2025 11:43:58 +0100 Subject: [PATCH] feat: enhance error handling for LLM API calls Add LLM error alert functionality to display specific error messages based on API responses. Introduce new LlmErrorAlertType interface for structured error alerts. Update chat components to manage and display LLM error alerts effectively, improving user feedback during error scenarios. --- app/components/chat/BaseChat.tsx | 8 +- app/components/chat/Chat.client.tsx | 91 ++++++++++++++++++++--- app/components/chat/LLMApiAlert.tsx | 109 ++++++++++++++++++++++++++++ app/routes/api.chat.ts | 32 ++++++-- app/routes/api.llmcall.ts | 32 ++++++-- app/types/actions.ts | 9 +++ 6 files changed, 256 insertions(+), 25 deletions(-) create mode 100644 app/components/chat/LLMApiAlert.tsx diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index f027abe..4a0c177 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -19,7 +19,7 @@ import { ExamplePrompts } from '~/components/chat/ExamplePrompts'; import GitCloneButton from './GitCloneButton'; import type { ProviderInfo } from '~/types/model'; import StarterTemplates from './StarterTemplates'; -import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions'; +import type { ActionAlert, SupabaseAlert, DeployAlert, LlmErrorAlertType } from '~/types/actions'; import DeployChatAlert from '~/components/deploy/DeployAlert'; import ChatAlert from './ChatAlert'; import type { ModelInfo } from '~/lib/modules/llm/types'; @@ -32,6 +32,7 @@ import { StickToBottom, useStickToBottomContext } from '~/lib/hooks'; import { ChatBox } from './ChatBox'; import type { DesignScheme } from '~/types/design-scheme'; import type { ElementInfo } from '~/components/workbench/Inspector'; +import LlmErrorAlert from './LLMApiAlert'; const TEXTAREA_MIN_HEIGHT = 76; @@ -69,6 +70,8 @@ interface BaseChatProps { clearSupabaseAlert?: () => void; deployAlert?: DeployAlert; clearDeployAlert?: () => void; + llmErrorAlert?: LlmErrorAlertType; + clearLlmErrorAlert?: () => void; data?: JSONValue[] | undefined; chatMode?: 'discuss' | 'build'; setChatMode?: (mode: 'discuss' | 'build') => void; @@ -113,6 +116,8 @@ export const BaseChat = React.forwardRef( clearDeployAlert, supabaseAlert, clearSupabaseAlert, + llmErrorAlert, + clearLlmErrorAlert, data, chatMode, setChatMode, @@ -411,6 +416,7 @@ export const BaseChat = React.forwardRef( }} /> )} + {llmErrorAlert && clearLlmErrorAlert?.()} />} {progressAnnotations && } (defaultDesignScheme); const actionAlert = useStore(workbenchStore.alert); const deployAlert = useStore(workbenchStore.deployAlert); - const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection + const supabaseConn = useStore(supabaseConnection); const selectedProject = supabaseConn.stats?.projects?.find( (project) => project.id === supabaseConn.selectedProjectId, ); const supabaseAlert = useStore(workbenchStore.supabaseAlert); const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings(); + const [llmErrorAlert, setLlmErrorAlert] = useState(undefined); const [model, setModel] = useState(() => { const savedModel = Cookies.get('selectedModel'); return savedModel || DEFAULT_MODEL; @@ -181,15 +183,8 @@ export const ChatImpl = memo( }, sendExtraMessageFields: true, onError: (e) => { - logger.error('Request failed\n\n', e, error); - logStore.logError('Chat request failed', e, { - component: 'Chat', - action: 'request', - error: e.message, - }); - toast.error( - 'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'), - ); + setFakeLoading(false); + handleError(e, 'chat'); }, onFinish: (message, response) => { const usage = response.usage; @@ -272,6 +267,80 @@ export const ChatImpl = memo( }); }; + const handleError = useCallback( + (error: any, context: 'chat' | 'template' | 'llmcall' = 'chat') => { + logger.error(`${context} request failed`, error); + + stop(); + setFakeLoading(false); + + let errorInfo = { + message: 'An unexpected error occurred', + isRetryable: true, + statusCode: 500, + provider: provider.name, + type: 'unknown' as const, + retryDelay: 0, + }; + + if (error.message) { + try { + const parsed = JSON.parse(error.message); + + if (parsed.error || parsed.message) { + errorInfo = { ...errorInfo, ...parsed }; + } else { + errorInfo.message = error.message; + } + } catch { + errorInfo.message = error.message; + } + } + + let errorType: LlmErrorAlertType['errorType'] = 'unknown'; + let title = 'Request Failed'; + + if (errorInfo.statusCode === 401 || errorInfo.message.toLowerCase().includes('api key')) { + errorType = 'authentication'; + title = 'Authentication Error'; + } else if (errorInfo.statusCode === 429 || errorInfo.message.toLowerCase().includes('rate limit')) { + errorType = 'rate_limit'; + title = 'Rate Limit Exceeded'; + } else if (errorInfo.message.toLowerCase().includes('quota')) { + errorType = 'quota'; + title = 'Quota Exceeded'; + } else if (errorInfo.statusCode >= 500) { + errorType = 'network'; + title = 'Server Error'; + } + + logStore.logError(`${context} request failed`, error, { + component: 'Chat', + action: 'request', + error: errorInfo.message, + context, + retryable: errorInfo.isRetryable, + errorType, + provider: provider.name, + }); + + // Create API error alert + setLlmErrorAlert({ + type: 'error', + title, + description: errorInfo.message, + provider: provider.name, + errorType, + }); + setData([]); + }, + [provider.name, stop], + ); + + const clearApiErrorAlert = useCallback(() => { + setLlmErrorAlert(undefined); + }, []); + useEffect(() => { const textarea = textareaRef.current; @@ -571,6 +640,8 @@ export const ChatImpl = memo( clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()} deployAlert={deployAlert} clearDeployAlert={() => workbenchStore.clearDeployAlert()} + llmErrorAlert={llmErrorAlert} + clearLlmErrorAlert={clearApiErrorAlert} data={chatData} chatMode={chatMode} setChatMode={setChatMode} diff --git a/app/components/chat/LLMApiAlert.tsx b/app/components/chat/LLMApiAlert.tsx new file mode 100644 index 0000000..12c5ada --- /dev/null +++ b/app/components/chat/LLMApiAlert.tsx @@ -0,0 +1,109 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import type { LlmErrorAlertType } from '~/types/actions'; +import { classNames } from '~/utils/classNames'; + +interface Props { + alert: LlmErrorAlertType; + clearAlert: () => void; +} + +export default function LlmErrorAlert({ alert, clearAlert }: Props) { + const { title, description, provider, errorType } = alert; + + const getErrorIcon = () => { + switch (errorType) { + case 'authentication': + return 'i-ph:key-duotone'; + case 'rate_limit': + return 'i-ph:clock-duotone'; + case 'quota': + return 'i-ph:warning-circle-duotone'; + default: + return 'i-ph:warning-duotone'; + } + }; + + const getErrorMessage = () => { + switch (errorType) { + case 'authentication': + return `Authentication failed with ${provider}. Please check your API key.`; + case 'rate_limit': + return `Rate limit exceeded for ${provider}. Please wait before retrying.`; + case 'quota': + return `Quota exceeded for ${provider}. Please check your account limits.`; + default: + return 'An error occurred while processing your request.'; + } + }; + + return ( + + +
+ +
+
+ +
+ + {title} + + + +

{getErrorMessage()}

+ + {description && ( +
+ Error Details: {description} +
+ )} +
+ + +
+ +
+
+
+
+
+
+ ); +} diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index d375a1a..c0579be 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -361,16 +361,34 @@ async function chatAction({ context, request }: ActionFunctionArgs) { } catch (error: any) { logger.error(error); + const errorResponse = { + error: true, + message: error.message || 'An unexpected error occurred', + statusCode: error.statusCode || 500, + isRetryable: error.isRetryable !== false, // Default to retryable unless explicitly false + provider: error.provider || 'unknown', + }; + if (error.message?.includes('API key')) { - throw new Response('Invalid or missing API key', { - status: 401, - statusText: 'Unauthorized', - }); + return new Response( + JSON.stringify({ + ...errorResponse, + message: 'Invalid or missing API key', + statusCode: 401, + isRetryable: false, + }), + { + status: 401, + headers: { 'Content-Type': 'application/json' }, + statusText: 'Unauthorized', + }, + ); } - throw new Response(null, { - status: 500, - statusText: 'Internal Server Error', + return new Response(JSON.stringify(errorResponse), { + status: errorResponse.statusCode, + headers: { 'Content-Type': 'application/json' }, + statusText: 'Error', }); } } diff --git a/app/routes/api.llmcall.ts b/app/routes/api.llmcall.ts index cf75e49..167f9ef 100644 --- a/app/routes/api.llmcall.ts +++ b/app/routes/api.llmcall.ts @@ -139,16 +139,34 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) { } catch (error: unknown) { console.log(error); + const errorResponse = { + error: true, + message: error instanceof Error ? error.message : 'An unexpected error occurred', + statusCode: (error as any).statusCode || 500, + isRetryable: (error as any).isRetryable !== false, + provider: (error as any).provider || 'unknown', + }; + if (error instanceof Error && error.message?.includes('API key')) { - throw new Response('Invalid or missing API key', { - status: 401, - statusText: 'Unauthorized', - }); + return new Response( + JSON.stringify({ + ...errorResponse, + message: 'Invalid or missing API key', + statusCode: 401, + isRetryable: false, + }), + { + status: 401, + headers: { 'Content-Type': 'application/json' }, + statusText: 'Unauthorized', + }, + ); } - throw new Response(null, { - status: 500, - statusText: 'Internal Server Error', + return new Response(JSON.stringify(errorResponse), { + status: errorResponse.statusCode, + headers: { 'Content-Type': 'application/json' }, + statusText: 'Error', }); } } diff --git a/app/types/actions.ts b/app/types/actions.ts index 0e1411d..95c75ba 100644 --- a/app/types/actions.ts +++ b/app/types/actions.ts @@ -62,6 +62,15 @@ export interface DeployAlert { source?: 'vercel' | 'netlify' | 'github'; } +export interface LlmErrorAlertType { + type: 'error' | 'warning'; + title: string; + description: string; + content?: string; + provider?: string; + errorType?: 'authentication' | 'rate_limit' | 'quota' | 'network' | 'unknown'; +} + export interface FileHistory { originalContent: string; lastModified: number;