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>
This commit is contained in:
1 parent
cd807ef175
commit
5bace38e47
4 files changed
+112
-106
No files matched your search
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
@@ -10,16 +12,77 @@ interface ErrorBoundaryProps {
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
showDetails: boolean;
|
||||
}
|
||||
|
||||
/* ── Fallback UI (functional, so it can use hooks) ──────────── */
|
||||
|
||||
function ErrorFallback({
|
||||
error,
|
||||
onReset,
|
||||
}: {
|
||||
error: Error | null;
|
||||
onReset: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-background p-8">
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
||||
<CircleAlert size={24} />
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{t("error.title")}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("error.description")}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<button
|
||||
onClick={() => setShowDetails((s) => !s)}
|
||||
className="text-xs text-muted-foreground underline hover:text-foreground"
|
||||
>
|
||||
{showDetails ? t("error.hideDetails") : t("error.showDetails")}
|
||||
</button>
|
||||
)}
|
||||
{showDetails && error && (
|
||||
<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">
|
||||
{error.message}
|
||||
{error.stack && `\n\n${error.stack}`}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{t("error.tryAgain")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
|
||||
>
|
||||
{t("error.reloadApp")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Error Boundary (class component, required by React) ────── */
|
||||
|
||||
export class ErrorBoundary extends React.Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, showDetails: false };
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||
@@ -32,11 +95,7 @@ export class ErrorBoundary extends React.Component<
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: null, showDetails: false });
|
||||
};
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
this.setState({ hasError: false, error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -44,91 +103,10 @@ export class ErrorBoundary extends React.Component<
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-background p-8">
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
{/* Error icon */}
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
An unexpected error occurred. You can try again or reload the app.
|
||||
</p>
|
||||
|
||||
{/* Expandable error details */}
|
||||
{this.state.error && (
|
||||
<button
|
||||
onClick={() =>
|
||||
this.setState((s) => ({ showDetails: !s.showDetails }))
|
||||
}
|
||||
className="text-xs text-muted-foreground underline hover:text-foreground"
|
||||
>
|
||||
{this.state.showDetails ? "Hide details" : "Show details"}
|
||||
</button>
|
||||
)}
|
||||
{this.state.showDetails && this.state.error && (
|
||||
<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">
|
||||
{this.state.error.message}
|
||||
{this.state.error.stack && `\n\n${this.state.error.stack}`}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleReload}
|
||||
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
|
||||
>
|
||||
Reload App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorFallback error={this.state.error} onReset={this.handleReset} />
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience HOC to wrap any component with an ErrorBoundary */
|
||||
export function withErrorBoundary<P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
fallback?: React.ReactNode
|
||||
) {
|
||||
const displayName = Component.displayName || Component.name || "Component";
|
||||
|
||||
const Wrapped = (props: P) => (
|
||||
<ErrorBoundary fallback={fallback}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
Wrapped.displayName = `withErrorBoundary(${displayName})`;
|
||||
return Wrapped;
|
||||
}
|
||||
@@ -146,8 +146,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
{
|
||||
key: "glm-cn",
|
||||
name: "GLM (CN)",
|
||||
description: "Zhipu GLM — China region",
|
||||
descriptionZh: "智谱 GLM — 中国区",
|
||||
description: "Zhipu GLM Code Plan — China region",
|
||||
descriptionZh: "智谱 GLM 编程套餐 — 中国区",
|
||||
icon: <Zhipu size={18} />,
|
||||
provider_type: "custom",
|
||||
base_url: "https://open.bigmodel.cn/api/anthropic",
|
||||
@@ -157,8 +157,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
{
|
||||
key: "glm-global",
|
||||
name: "GLM (Global)",
|
||||
description: "Zhipu GLM — Global region",
|
||||
descriptionZh: "智谱 GLM — 国际区",
|
||||
description: "Zhipu GLM Code Plan — Global region",
|
||||
descriptionZh: "智谱 GLM 编程套餐 — 国际区",
|
||||
icon: <Zhipu size={18} />,
|
||||
provider_type: "custom",
|
||||
base_url: "https://api.z.ai/api/anthropic",
|
||||
@@ -190,8 +190,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
{
|
||||
key: "minimax-cn",
|
||||
name: "MiniMax (CN)",
|
||||
description: "MiniMax API — China region",
|
||||
descriptionZh: "MiniMax API — 中国区",
|
||||
description: "MiniMax Code Plan — China region",
|
||||
descriptionZh: "MiniMax 编程套餐 — 中国区",
|
||||
icon: <Minimax size={18} />,
|
||||
provider_type: "custom",
|
||||
base_url: "https://api.minimaxi.com/anthropic",
|
||||
@@ -201,8 +201,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
{
|
||||
key: "minimax-global",
|
||||
name: "MiniMax (Global)",
|
||||
description: "MiniMax API — Global region",
|
||||
descriptionZh: "MiniMax API — 国际区",
|
||||
description: "MiniMax Code Plan — Global region",
|
||||
descriptionZh: "MiniMax 编程套餐 — 国际区",
|
||||
icon: <Minimax size={18} />,
|
||||
provider_type: "custom",
|
||||
base_url: "https://api.minimax.io/anthropic",
|
||||
@@ -563,20 +563,30 @@ export function ProviderManager() {
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-2">
|
||||
<h3 className="text-sm font-medium mb-1">{t('provider.connectedProviders')}</h3>
|
||||
|
||||
{/* Environment variable detection */}
|
||||
{Object.keys(envDetected).length > 0 && (
|
||||
<div className="flex items-center gap-3 py-2.5 px-1 border-b border-border/30">
|
||||
{/* Claude Code default config */}
|
||||
<div className="border-b border-border/30 pb-2">
|
||||
<div className="flex items-center gap-3 py-2.5 px-1">
|
||||
<div className="shrink-0 w-[22px] flex justify-center">
|
||||
<Anthropic size={18} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium">{t('provider.environment')}</span>
|
||||
<Badge variant="outline" className="ml-2 text-[10px] px-1.5 py-0">
|
||||
ENV
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Claude Code</span>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||
{t('provider.default')}
|
||||
</Badge>
|
||||
{Object.keys(envDetected).length > 0 && (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 text-green-600 dark:text-green-400 border-green-500/30">
|
||||
ENV
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[11px] text-muted-foreground ml-[34px] leading-relaxed">
|
||||
{t('provider.ccSwitchHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Connected provider list */}
|
||||
{sorted.length > 0 ? (
|
||||
|
||||
@@ -145,6 +145,7 @@ const en = {
|
||||
'provider.disconnecting': 'Disconnecting...',
|
||||
'provider.disconnectProvider': 'Disconnect Provider',
|
||||
'provider.disconnectConfirm': 'Are you sure you want to disconnect "{name}"? This action cannot be undone.',
|
||||
'provider.ccSwitchHint': 'Claude Code configurations added via tools like cc switch may not be readable by CodePilot. We recommend re-adding your provider here.',
|
||||
'provider.addProviderSection': 'Add Provider',
|
||||
'provider.addProviderDesc': 'Select a provider to connect. Most presets only require an API key.',
|
||||
|
||||
@@ -290,6 +291,14 @@ const en = {
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
|
||||
// ── Error boundary ────────────────────────────────────────
|
||||
'error.title': 'Something went wrong',
|
||||
'error.description': 'An unexpected error occurred. You can try again or reload the app.',
|
||||
'error.showDetails': 'Show details',
|
||||
'error.hideDetails': 'Hide details',
|
||||
'error.tryAgain': 'Try Again',
|
||||
'error.reloadApp': 'Reload App',
|
||||
|
||||
// ── CLI dynamic field labels ──────────────────────────────
|
||||
'cli.loadingSettings': 'Loading settings...',
|
||||
'cli.field.skipDangerousModePermissionPrompt': 'Skip Dangerous Mode Permission Prompt',
|
||||
|
||||
@@ -142,6 +142,7 @@ const zh: Record<TranslationKey, string> = {
|
||||
'provider.disconnecting': '断开中...',
|
||||
'provider.disconnectProvider': '断开提供商',
|
||||
'provider.disconnectConfirm': '确定要断开"{name}"吗?此操作无法撤销。',
|
||||
'provider.ccSwitchHint': '通过类似 cc switch 等工具添加的 Claude Code 配置可能无法被 CodePilot 读取,建议在此处重新添加。',
|
||||
'provider.addProviderSection': '添加提供商',
|
||||
'provider.addProviderDesc': '选择要连接的提供商。大多数预设只需填写 API 密钥。',
|
||||
|
||||
@@ -287,6 +288,14 @@ const zh: Record<TranslationKey, string> = {
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
|
||||
// ── Error boundary ────────────────────────────────────────
|
||||
'error.title': '出错了',
|
||||
'error.description': '发生了意外错误。您可以重试或重新加载应用。',
|
||||
'error.showDetails': '显示详情',
|
||||
'error.hideDetails': '隐藏详情',
|
||||
'error.tryAgain': '重试',
|
||||
'error.reloadApp': '重新加载',
|
||||
|
||||
// ── CLI dynamic field labels ──────────────────────────────
|
||||
'cli.loadingSettings': '加载设置中...',
|
||||
'cli.field.skipDangerousModePermissionPrompt': '跳过危险模式权限提示',
|
||||
|
||||
Reference in new issue
Block a user