From 26c46088bc4a430a3fc999e9522de08a269650eb 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 d944865..7daffb6 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; @@ -114,6 +117,8 @@ export const BaseChat = React.forwardRef( clearDeployAlert, supabaseAlert, clearSupabaseAlert, + llmErrorAlert, + clearLlmErrorAlert, data, chatMode, setChatMode, @@ -416,6 +421,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; @@ -183,15 +185,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; @@ -269,6 +264,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; @@ -617,6 +686,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 e35e8c1..93ab6e8 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -375,16 +375,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;