Skip to content

Commit d32eeee

Browse files
committed
feat: add error handling for AI generation failures
1 parent c81b95c commit d32eeee

File tree

15 files changed

+235
-48
lines changed

15 files changed

+235
-48
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
## Key Rules
2121

22+
- Ignore code formatting and css class sorting lint issues, focus on functionality implementation
2223
- Pure SPA (Single Page Application) - no SSR
2324
- TypeScript required, implement proper error handling
2425
- Use `POST` + JSON for all API endpoints (not RESTful)

drizzle/migrations/0000_known_glorian.sql renamed to drizzle/migrations/0000_talented_sinister_six.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ CREATE TABLE `generations` (
8181
`model` text NOT NULL,
8282
`status` text DEFAULT 'pending',
8383
`file_ids` text,
84-
`error_message` text,
84+
`error_reason` text,
8585
`generation_time` integer,
8686
`cost` real,
8787
`created_at` text NOT NULL,

drizzle/migrations/meta/0000_snapshot.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"version": "6",
33
"dialect": "sqlite",
4-
"id": "82d26b97-e90c-4c86-bdd8-6215bf6402ca",
4+
"id": "2505c767-2fae-4df3-a6de-f59692802ad9",
55
"prevId": "00000000-0000-0000-0000-000000000000",
66
"tables": {
77
"account": {
@@ -545,8 +545,8 @@
545545
"notNull": false,
546546
"autoincrement": false
547547
},
548-
"error_message": {
549-
"name": "error_message",
548+
"error_reason": {
549+
"name": "error_reason",
550550
"type": "text",
551551
"primaryKey": false,
552552
"notNull": false,

drizzle/migrations/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
{
66
"idx": 0,
77
"version": "6",
8-
"when": 1754554963016,
9-
"tag": "0000_known_glorian",
8+
"when": 1754622954861,
9+
"tag": "0000_talented_sinister_six",
1010
"breakpoints": true
1111
}
1212
]

src/app/i18n/locales/en.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@
4343
"createChat": "Failed to create chat - no available AI models",
4444
"noModel": "No available AI models - please configure providers first",
4545
"createNewChat": "Failed to create new chat - please select a provider and model"
46+
},
47+
"generation": {
48+
"failed": "Generation failed",
49+
"configError": "Provider configuration error",
50+
"configErrorDesc": "Please check your provider settings and try again",
51+
"apiError": "API request failed",
52+
"apiErrorDesc": "The service is temporarily unavailable, please try again later",
53+
"timeoutError": "Request timeout",
54+
"timeoutErrorDesc": "The request took too long, please try again",
55+
"unknownError": "Unknown error",
56+
"unknownErrorDesc": "An unexpected error occurred, please try again",
57+
"goToSettings": "Go to Settings",
58+
"retry": "Retry"
4659
}
4760
},
4861
"navigation": {

src/app/i18n/locales/zh.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@
4343
"createChat": "创建对话失败 - 没有可用的AI模型",
4444
"noModel": "没有可用的AI模型 - 请先配置提供商",
4545
"createNewChat": "创建新对话失败 - 请选择提供商和模型"
46+
},
47+
"generation": {
48+
"failed": "生成失败",
49+
"configError": "提供商配置有误",
50+
"configErrorDesc": "请检查您的提供商设置后重试",
51+
"apiError": "API请求失败",
52+
"apiErrorDesc": "服务暂时不可用,请稍后重试",
53+
"timeoutError": "请求超时",
54+
"timeoutErrorDesc": "请求时间过长,请重试",
55+
"unknownError": "未知错误",
56+
"unknownErrorDesc": "发生了意外错误,请重试",
57+
"goToSettings": "前往设置",
58+
"retry": "重试"
4659
}
4760
},
4861
"navigation": {

src/app/routes/chat/-components/chat/ChatMessageItem.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { cn } from "@/app/lib/utils";
77
import type { chatService } from "@/server/service/chat";
88
import { useEffect, useRef, useState } from "react";
99
import { useTranslation } from "react-i18next";
10+
import { GenerationErrorItem } from "./GenerationErrorItem";
1011

1112
// Type inference from service functions
1213
type ChatData = NonNullable<Awaited<ReturnType<typeof chatService.getChatById>>>;
@@ -70,6 +71,7 @@ export function ChatMessageItem({ message, user, allMessages, onMessageUpdate }:
7071
return date;
7172
})();
7273
const isMessageGenerating = message.generation?.status === "pending" || message.generation?.status === "generating";
74+
const isMessageFailed = message.generation?.status === "failed";
7375

7476
// Get current message's images for display
7577
const currentMessageImages = message.generation?.resultUrls;
@@ -199,6 +201,22 @@ export function ChatMessageItem({ message, user, allMessages, onMessageUpdate }:
199201
<Skeleton className="h-4 w-3/4" />
200202
</div>
201203
</div>
204+
) : isMessageFailed && !isUser ? (
205+
<div className="space-y-3">
206+
{/* Show original prompt if available */}
207+
{message.content && (
208+
<p className="whitespace-pre-wrap break-words leading-relaxed">{message.content}</p>
209+
)}
210+
{/* Show error card */}
211+
<GenerationErrorItem
212+
errorReason={message.generation?.errorReason || "UNKNOWN"}
213+
provider={message.generation?.provider}
214+
onRetry={() => {
215+
// TODO: Implement retry functionality
216+
console.log("Retry generation for message:", message.id);
217+
}}
218+
/>
219+
</div>
202220
) : (
203221
<>
204222
{message.content && (
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Button } from "@/app/components/ui/button";
2+
import { cn } from "@/app/lib/utils";
3+
import type { ErrorReason } from "@/server/db/schemas/chat";
4+
import { ProviderIcon } from "@lobehub/icons";
5+
import { useNavigate } from "@tanstack/react-router";
6+
import { RefreshCw, Settings } from "lucide-react";
7+
import { useTranslation } from "react-i18next";
8+
9+
interface GenerationErrorItemProps {
10+
errorReason: ErrorReason;
11+
provider?: string;
12+
onRetry?: () => void;
13+
className?: string;
14+
}
15+
16+
export function GenerationErrorItem({ errorReason, provider, onRetry, className }: GenerationErrorItemProps) {
17+
const { t } = useTranslation();
18+
const navigate = useNavigate();
19+
20+
// Determine error type and corresponding messages
21+
const getErrorInfo = (reason: ErrorReason) => {
22+
switch (reason) {
23+
case "CONFIG_INVALID":
24+
case "CONFIG_ERROR":
25+
return {
26+
title: t("chat.generation.configError"),
27+
description: t("chat.generation.configErrorDesc"),
28+
buttonText: t("chat.generation.goToSettings"),
29+
buttonIcon: Settings,
30+
buttonAction: "config" as const,
31+
};
32+
case "API_ERROR":
33+
return {
34+
title: t("chat.generation.apiError"),
35+
description: t("chat.generation.apiErrorDesc"),
36+
buttonText: t("chat.generation.retry"),
37+
buttonIcon: RefreshCw,
38+
buttonAction: "retry" as const,
39+
};
40+
case "TIMEOUT":
41+
return {
42+
title: t("chat.generation.timeoutError"),
43+
description: t("chat.generation.timeoutErrorDesc"),
44+
buttonText: t("chat.generation.retry"),
45+
buttonIcon: RefreshCw,
46+
buttonAction: "retry" as const,
47+
};
48+
default:
49+
return {
50+
title: t("chat.generation.unknownError"),
51+
description: t("chat.generation.unknownErrorDesc"),
52+
buttonText: t("chat.generation.retry"),
53+
buttonIcon: RefreshCw,
54+
buttonAction: "retry" as const,
55+
};
56+
}
57+
};
58+
59+
const errorInfo = getErrorInfo(errorReason);
60+
61+
const handleButtonClick = () => {
62+
if (errorInfo.buttonAction === "config") {
63+
// Navigate to provider settings
64+
if (provider) {
65+
navigate({
66+
to: "/settings/provider/$providerId",
67+
params: { providerId: provider },
68+
});
69+
} else {
70+
navigate({
71+
to: "/settings/provider",
72+
});
73+
}
74+
} else {
75+
// Retry action
76+
onRetry?.();
77+
}
78+
};
79+
80+
const ButtonIcon = errorInfo.buttonIcon;
81+
82+
return (
83+
<div className={cn("flex h-48 w-80 flex-col px-2 text-center", className)}>
84+
{/* Provider Icon Block */}
85+
{provider && (
86+
<div className="mb-6 flex flex-1 items-center justify-center">
87+
<ProviderIcon provider={provider} size={44} type="avatar" />
88+
</div>
89+
)}
90+
91+
{/* Error Content Block */}
92+
<div className="mb-6 flex flex-1 flex-col items-center justify-center space-y-2 px-2">
93+
<h3 className="font-semibold text-foreground text-lg leading-tight">{errorInfo.title}</h3>
94+
<p className="text-muted-foreground text-sm leading-relaxed">{errorInfo.description}</p>
95+
</div>
96+
97+
{/* Action Button Block */}
98+
<div className="flex flex-1 items-center justify-center">
99+
<Button onClick={handleButtonClick} className="h-10 gap-2 px-6 font-medium text-sm">
100+
<ButtonIcon className="h-4 w-4" />
101+
{errorInfo.buttonText}
102+
</Button>
103+
</div>
104+
</div>
105+
);
106+
}

src/server/ai/provider/cloudflare.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ const Cloudflare: AiProvider = {
114114
});
115115

116116
if (!resp.ok) {
117+
if (resp.status === 401 || resp.status === 404) {
118+
return {
119+
errorReason: "CONFIG_ERROR",
120+
images: [],
121+
};
122+
}
123+
117124
const errorText = await resp.text();
118125
throw new Error(`Cloudflare API error: ${resp.status} ${resp.statusText} - ${errorText}`);
119126
}

src/server/ai/provider/fal.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { ServiceException } from "@/server/lib/exception";
21
import { fetchUrlToDataURI } from "@/server/lib/util";
3-
import { fal } from "@fal-ai/client";
2+
import { ApiError, fal } from "@fal-ai/client";
43
import type { AiProvider, ApiProviderSettings, ApiProviderSettingsItem } from "../types/provider";
54
import { type ProviderSettingsType, chooseAblility, doParseSettings, findModel } from "../types/provider";
65

@@ -56,15 +55,27 @@ const Fal: AiProvider = {
5655
}
5756

5857
fal.config({ credentials: apiKey });
59-
const resp = await fal.run(request.modelId + endpoint, {
60-
input: {
61-
prompt: request.prompt,
62-
image_url: genType === "i2i" ? request.images?.[0] : undefined,
63-
image_urls: genType === "mi2i" ? request.images : undefined,
64-
},
65-
});
6658

67-
console.log("Fal response:", resp);
59+
let resp: Awaited<ReturnType<typeof fal.run>>;
60+
try {
61+
resp = await fal.run(request.modelId + endpoint, {
62+
input: {
63+
prompt: request.prompt,
64+
image_url: genType === "i2i" ? request.images?.[0] : undefined,
65+
image_urls: genType === "mi2i" ? request.images : undefined,
66+
},
67+
});
68+
} catch (error) {
69+
if (error instanceof ApiError) {
70+
if (error.status === 401 || error.status === 404) {
71+
return {
72+
errorReason: "CONFIG_ERROR",
73+
images: [],
74+
};
75+
}
76+
}
77+
throw error;
78+
}
6879

6980
return {
7081
images: await Promise.all(

0 commit comments

Comments
 (0)