Skip to content

Commit 5bace38

Browse files
7418claude
andcommitted
fix: improve ErrorBoundary i18n and update provider preset descriptions
ErrorBoundary (PR #49 follow-up): - Replace inline SVG with lucide-react CircleAlert icon for consistency - Add i18n support: extract fallback UI into functional ErrorFallback component so it can use useTranslation hook - Remove unused withErrorBoundary HOC - Add error.* translation keys to en.ts and zh.ts ProviderManager: - Add Claude Code default config entry in connected providers section with cc switch hint explaining configurations may not be readable - Update GLM preset descriptions: "GLM" → "GLM Code Plan / 编程套餐" - Update MiniMax preset descriptions: "MiniMax API" → "MiniMax Code Plan / 编程套餐" - Add provider.ccSwitchHint translation key to en.ts and zh.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cd807ef commit 5bace38

File tree

4 files changed

+112
-106
lines changed

4 files changed

+112
-106
lines changed
Lines changed: 68 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useState } from "react";
4+
import { CircleAlert } from "lucide-react";
5+
import { useTranslation } from "@/hooks/useTranslation";
46

57
interface ErrorBoundaryProps {
68
children: React.ReactNode;
@@ -10,16 +12,77 @@ interface ErrorBoundaryProps {
1012
interface ErrorBoundaryState {
1113
hasError: boolean;
1214
error: Error | null;
13-
showDetails: boolean;
1415
}
1516

17+
/* ── Fallback UI (functional, so it can use hooks) ──────────── */
18+
19+
function ErrorFallback({
20+
error,
21+
onReset,
22+
}: {
23+
error: Error | null;
24+
onReset: () => void;
25+
}) {
26+
const { t } = useTranslation();
27+
const [showDetails, setShowDetails] = useState(false);
28+
29+
return (
30+
<div className="flex h-full w-full items-center justify-center bg-background p-8">
31+
<div className="flex max-w-md flex-col items-center gap-4 text-center">
32+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
33+
<CircleAlert size={24} />
34+
</div>
35+
36+
<h2 className="text-lg font-semibold text-foreground">
37+
{t("error.title")}
38+
</h2>
39+
<p className="text-sm text-muted-foreground">
40+
{t("error.description")}
41+
</p>
42+
43+
{error && (
44+
<button
45+
onClick={() => setShowDetails((s) => !s)}
46+
className="text-xs text-muted-foreground underline hover:text-foreground"
47+
>
48+
{showDetails ? t("error.hideDetails") : t("error.showDetails")}
49+
</button>
50+
)}
51+
{showDetails && error && (
52+
<pre className="max-h-40 w-full overflow-auto rounded-md border border-border/50 bg-muted/30 p-3 text-left text-xs text-muted-foreground">
53+
{error.message}
54+
{error.stack && `\n\n${error.stack}`}
55+
</pre>
56+
)}
57+
58+
<div className="flex gap-2">
59+
<button
60+
onClick={onReset}
61+
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
62+
>
63+
{t("error.tryAgain")}
64+
</button>
65+
<button
66+
onClick={() => window.location.reload()}
67+
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
68+
>
69+
{t("error.reloadApp")}
70+
</button>
71+
</div>
72+
</div>
73+
</div>
74+
);
75+
}
76+
77+
/* ── Error Boundary (class component, required by React) ────── */
78+
1679
export class ErrorBoundary extends React.Component<
1780
ErrorBoundaryProps,
1881
ErrorBoundaryState
1982
> {
2083
constructor(props: ErrorBoundaryProps) {
2184
super(props);
22-
this.state = { hasError: false, error: null, showDetails: false };
85+
this.state = { hasError: false, error: null };
2386
}
2487

2588
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
@@ -32,103 +95,18 @@ export class ErrorBoundary extends React.Component<
3295
}
3396

3497
handleReset = () => {
35-
this.setState({ hasError: false, error: null, showDetails: false });
36-
};
37-
38-
handleReload = () => {
39-
window.location.reload();
98+
this.setState({ hasError: false, error: null });
4099
};
41100

42101
render() {
43102
if (this.state.hasError) {
44103
if (this.props.fallback) {
45104
return this.props.fallback;
46105
}
47-
48106
return (
49-
<div className="flex h-full w-full items-center justify-center bg-background p-8">
50-
<div className="flex max-w-md flex-col items-center gap-4 text-center">
51-
{/* Error icon */}
52-
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
53-
<svg
54-
xmlns="http://www.w3.org/2000/svg"
55-
width="24"
56-
height="24"
57-
viewBox="0 0 24 24"
58-
fill="none"
59-
stroke="currentColor"
60-
strokeWidth="2"
61-
strokeLinecap="round"
62-
strokeLinejoin="round"
63-
>
64-
<circle cx="12" cy="12" r="10" />
65-
<line x1="12" y1="8" x2="12" y2="12" />
66-
<line x1="12" y1="16" x2="12.01" y2="16" />
67-
</svg>
68-
</div>
69-
70-
<h2 className="text-lg font-semibold text-foreground">
71-
Something went wrong
72-
</h2>
73-
<p className="text-sm text-muted-foreground">
74-
An unexpected error occurred. You can try again or reload the app.
75-
</p>
76-
77-
{/* Expandable error details */}
78-
{this.state.error && (
79-
<button
80-
onClick={() =>
81-
this.setState((s) => ({ showDetails: !s.showDetails }))
82-
}
83-
className="text-xs text-muted-foreground underline hover:text-foreground"
84-
>
85-
{this.state.showDetails ? "Hide details" : "Show details"}
86-
</button>
87-
)}
88-
{this.state.showDetails && this.state.error && (
89-
<pre className="max-h-40 w-full overflow-auto rounded-md border border-border/50 bg-muted/30 p-3 text-left text-xs text-muted-foreground">
90-
{this.state.error.message}
91-
{this.state.error.stack && `\n\n${this.state.error.stack}`}
92-
</pre>
93-
)}
94-
95-
{/* Action buttons */}
96-
<div className="flex gap-2">
97-
<button
98-
onClick={this.handleReset}
99-
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
100-
>
101-
Try Again
102-
</button>
103-
<button
104-
onClick={this.handleReload}
105-
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
106-
>
107-
Reload App
108-
</button>
109-
</div>
110-
</div>
111-
</div>
107+
<ErrorFallback error={this.state.error} onReset={this.handleReset} />
112108
);
113109
}
114-
115110
return this.props.children;
116111
}
117112
}
118-
119-
/** Convenience HOC to wrap any component with an ErrorBoundary */
120-
export function withErrorBoundary<P extends object>(
121-
Component: React.ComponentType<P>,
122-
fallback?: React.ReactNode
123-
) {
124-
const displayName = Component.displayName || Component.name || "Component";
125-
126-
const Wrapped = (props: P) => (
127-
<ErrorBoundary fallback={fallback}>
128-
<Component {...props} />
129-
</ErrorBoundary>
130-
);
131-
132-
Wrapped.displayName = `withErrorBoundary(${displayName})`;
133-
return Wrapped;
134-
}

src/components/settings/ProviderManager.tsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ const QUICK_PRESETS: QuickPreset[] = [
146146
{
147147
key: "glm-cn",
148148
name: "GLM (CN)",
149-
description: "Zhipu GLM — China region",
150-
descriptionZh: "智谱 GLM — 中国区",
149+
description: "Zhipu GLM Code Plan — China region",
150+
descriptionZh: "智谱 GLM 编程套餐 — 中国区",
151151
icon: <Zhipu size={18} />,
152152
provider_type: "custom",
153153
base_url: "https://open.bigmodel.cn/api/anthropic",
@@ -157,8 +157,8 @@ const QUICK_PRESETS: QuickPreset[] = [
157157
{
158158
key: "glm-global",
159159
name: "GLM (Global)",
160-
description: "Zhipu GLM — Global region",
161-
descriptionZh: "智谱 GLM — 国际区",
160+
description: "Zhipu GLM Code Plan — Global region",
161+
descriptionZh: "智谱 GLM 编程套餐 — 国际区",
162162
icon: <Zhipu size={18} />,
163163
provider_type: "custom",
164164
base_url: "https://api.z.ai/api/anthropic",
@@ -190,8 +190,8 @@ const QUICK_PRESETS: QuickPreset[] = [
190190
{
191191
key: "minimax-cn",
192192
name: "MiniMax (CN)",
193-
description: "MiniMax API — China region",
194-
descriptionZh: "MiniMax API — 中国区",
193+
description: "MiniMax Code Plan — China region",
194+
descriptionZh: "MiniMax 编程套餐 — 中国区",
195195
icon: <Minimax size={18} />,
196196
provider_type: "custom",
197197
base_url: "https://api.minimaxi.com/anthropic",
@@ -201,8 +201,8 @@ const QUICK_PRESETS: QuickPreset[] = [
201201
{
202202
key: "minimax-global",
203203
name: "MiniMax (Global)",
204-
description: "MiniMax API — Global region",
205-
descriptionZh: "MiniMax API — 国际区",
204+
description: "MiniMax Code Plan — Global region",
205+
descriptionZh: "MiniMax 编程套餐 — 国际区",
206206
icon: <Minimax size={18} />,
207207
provider_type: "custom",
208208
base_url: "https://api.minimax.io/anthropic",
@@ -563,20 +563,30 @@ export function ProviderManager() {
563563
<div className="rounded-lg border border-border/50 p-4 space-y-2">
564564
<h3 className="text-sm font-medium mb-1">{t('provider.connectedProviders')}</h3>
565565

566-
{/* Environment variable detection */}
567-
{Object.keys(envDetected).length > 0 && (
568-
<div className="flex items-center gap-3 py-2.5 px-1 border-b border-border/30">
566+
{/* Claude Code default config */}
567+
<div className="border-b border-border/30 pb-2">
568+
<div className="flex items-center gap-3 py-2.5 px-1">
569569
<div className="shrink-0 w-[22px] flex justify-center">
570570
<Anthropic size={18} />
571571
</div>
572572
<div className="flex-1 min-w-0">
573-
<span className="text-sm font-medium">{t('provider.environment')}</span>
574-
<Badge variant="outline" className="ml-2 text-[10px] px-1.5 py-0">
575-
ENV
576-
</Badge>
573+
<div className="flex items-center gap-2">
574+
<span className="text-sm font-medium">Claude Code</span>
575+
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
576+
{t('provider.default')}
577+
</Badge>
578+
{Object.keys(envDetected).length > 0 && (
579+
<Badge variant="outline" className="text-[10px] px-1.5 py-0 text-green-600 dark:text-green-400 border-green-500/30">
580+
ENV
581+
</Badge>
582+
)}
583+
</div>
577584
</div>
578585
</div>
579-
)}
586+
<p className="text-[11px] text-muted-foreground ml-[34px] leading-relaxed">
587+
{t('provider.ccSwitchHint')}
588+
</p>
589+
</div>
580590

581591
{/* Connected provider list */}
582592
{sorted.length > 0 ? (

src/i18n/en.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ const en = {
145145
'provider.disconnecting': 'Disconnecting...',
146146
'provider.disconnectProvider': 'Disconnect Provider',
147147
'provider.disconnectConfirm': 'Are you sure you want to disconnect "{name}"? This action cannot be undone.',
148+
'provider.ccSwitchHint': 'Claude Code configurations added via tools like cc switch may not be readable by CodePilot. We recommend re-adding your provider here.',
148149
'provider.addProviderSection': 'Add Provider',
149150
'provider.addProviderDesc': 'Select a provider to connect. Most presets only require an API key.',
150151

@@ -290,6 +291,14 @@ const en = {
290291
'common.enabled': 'Enabled',
291292
'common.disabled': 'Disabled',
292293

294+
// ── Error boundary ────────────────────────────────────────
295+
'error.title': 'Something went wrong',
296+
'error.description': 'An unexpected error occurred. You can try again or reload the app.',
297+
'error.showDetails': 'Show details',
298+
'error.hideDetails': 'Hide details',
299+
'error.tryAgain': 'Try Again',
300+
'error.reloadApp': 'Reload App',
301+
293302
// ── CLI dynamic field labels ──────────────────────────────
294303
'cli.loadingSettings': 'Loading settings...',
295304
'cli.field.skipDangerousModePermissionPrompt': 'Skip Dangerous Mode Permission Prompt',

src/i18n/zh.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const zh: Record<TranslationKey, string> = {
142142
'provider.disconnecting': '断开中...',
143143
'provider.disconnectProvider': '断开提供商',
144144
'provider.disconnectConfirm': '确定要断开"{name}"吗?此操作无法撤销。',
145+
'provider.ccSwitchHint': '通过类似 cc switch 等工具添加的 Claude Code 配置可能无法被 CodePilot 读取,建议在此处重新添加。',
145146
'provider.addProviderSection': '添加提供商',
146147
'provider.addProviderDesc': '选择要连接的提供商。大多数预设只需填写 API 密钥。',
147148

@@ -287,6 +288,14 @@ const zh: Record<TranslationKey, string> = {
287288
'common.enabled': '已启用',
288289
'common.disabled': '已禁用',
289290

291+
// ── Error boundary ────────────────────────────────────────
292+
'error.title': '出错了',
293+
'error.description': '发生了意外错误。您可以重试或重新加载应用。',
294+
'error.showDetails': '显示详情',
295+
'error.hideDetails': '隐藏详情',
296+
'error.tryAgain': '重试',
297+
'error.reloadApp': '重新加载',
298+
290299
// ── CLI dynamic field labels ──────────────────────────────
291300
'cli.loadingSettings': '加载设置中...',
292301
'cli.field.skipDangerousModePermissionPrompt': '跳过危险模式权限提示',

0 commit comments

Comments
 (0)