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;