-
Notifications
You must be signed in to change notification settings - Fork 146
refactor(UI): UX Improvements #637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ation - Refactor monolithic provider-form.tsx into modular architecture - Implement vertical scroll navigation replacing tab switching - Remove deprecated provider types: claude-auth, gemini-cli - Make openai-compatible permanently available (no feature flag) - Add framer-motion for smooth animations - Remove asterisks from i18n labels (SmartInputWrapper handles required markers) - Update provider types in vendor view and keys list - Add sections i18n for new form structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthrough新增并重构提供商表单及上下文、引入完整仪表盘 Bento 组件集、扩展概览对比指标、添加 framer-motion 依赖,并在大量设置页面中进行 UI/样式与多语言文案更新(呈现层为主,多处新增导出/类型)。 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly overhauls the provider form's user interface and underlying architecture. The primary goal was to transform a large, single-file component into a highly modular and user-friendly experience. This was achieved by introducing a vertical scroll navigation system, breaking down the form into logical sections, and implementing new UI components for better organization and visual appeal. Additionally, it streamlines provider type management by removing deprecated options and making a key provider type universally accessible. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
40 files reviewed, 2 comments
| providerType?: | ||
| | "claude" | ||
| | "claude-auth" | ||
| | "codex" | ||
| | "gemini" | ||
| | "gemini-cli" | ||
| | "openai-compatible"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntax: Hardcoded deprecated provider types in preset definition. According to the PR description, claude-auth and gemini-cli were removed. This inline type definition should use the imported ProviderType instead.
| providerType?: | |
| | "claude" | |
| | "claude-auth" | |
| | "codex" | |
| | "gemini" | |
| | "gemini-cli" | |
| | "openai-compatible"; | |
| } | |
| providerType?: ProviderType; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Line: 22:29
Comment:
**syntax:** Hardcoded deprecated provider types in preset definition. According to the PR description, `claude-auth` and `gemini-cli` were removed. This inline type definition should use the imported `ProviderType` instead.
```suggestion
providerType?: ProviderType;
```
How can I resolve this? If you propose a fix, please make it concise.| providerType?: | ||
| | "claude" | ||
| | "claude-auth" | ||
| | "codex" | ||
| | "gemini" | ||
| | "gemini-cli" | ||
| | "openai-compatible"; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntax: Duplicate hardcoded deprecated provider types. Same issue as above - should use imported ProviderType.
| providerType?: | |
| | "claude" | |
| | "claude-auth" | |
| | "codex" | |
| | "gemini" | |
| | "gemini-cli" | |
| | "openai-compatible"; | |
| }; | |
| providerType?: ProviderType; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Line: 299:306
Comment:
**syntax:** Duplicate hardcoded deprecated provider types. Same issue as above - should use imported `ProviderType`.
```suggestion
providerType?: ProviderType;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is an excellent refactoring of the provider form. The modular architecture, use of React Context for state management, and the new vertical scroll navigation significantly improve maintainability and user experience. The code is well-structured and follows modern React practices. I've identified a few issues, including a functional bug in the URL resolver logic and some minor areas for improvement regarding consistency and clarity. Overall, this is a high-quality contribution.
| urlResolver, | ||
| allowedProviderTypes, | ||
| }: ProviderFormProps) { | ||
| const [groupSuggestions, setGroupSuggestions] = useState<string[]>([]); | ||
| const [autoUrlPending, setAutoUrlPending] = useState(false); | ||
|
|
||
| // Fetch group suggestions | ||
| useEffect(() => { | ||
| const fetchGroups = async () => { | ||
| try { | ||
| const res = await getDistinctProviderGroupsAction(); | ||
| if (res.ok && res.data) { | ||
| setGroupSuggestions(res.data); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This useEffect hook appears to be non-functional. It calls urlResolver but the resolved URL is never used to update the form's state. This means the URL field won't be auto-populated as intended when a preset is used without a URL. This is a bug that breaks the intended functionality.
To fix this, the resolved URL needs to be dispatched to the form's context to update the state, or the preset object passed to the context provider should be updated.
| AlertDialogTitle as AlertTitle, | ||
| } from "@/components/ui/alert-dialog"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { PROVIDER_DEFAULTS } from "@/lib/constants/provider.constants"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import { isValidUrl } from "@/lib/utils/validation"; | ||
| import type { ProviderDisplay, ProviderType } from "@/types/provider"; | ||
| import { FormTabNav, TAB_CONFIG } from "./components/form-tab-nav"; | ||
| import { createInitialState, ProviderFormProvider, useProviderForm } from "./provider-form-context"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| "use client"; | ||
|
|
||
| import { createContext, useContext, useReducer, type ReactNode } from "react"; | ||
| import { PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| name?: string; | ||
| url?: string; | ||
| websiteUrl?: string; | ||
| providerType?: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type definition for providerType includes claude-auth and gemini-cli, which are deprecated and have been removed from other parts of the application in this pull request. To maintain consistency, they should be removed from this type as well.
| providerType?: | |
| providerType?: "claude" | "codex" | "gemini" | "openai-compatible"; |
| : "bg-muted text-muted-foreground" | ||
| )} | ||
| > | ||
| <Icon className="h-4.5 w-4.5" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Tailwind CSS classes h-4.5 and w-4.5 are used here. These are not standard Tailwind classes and are likely typos, which may result in incorrect icon sizing. Please use a standard size like h-5 w-5 for consistency with other icons in the application.
| <Icon className="h-4.5 w-4.5" /> | |
| <Icon className="h-5 w-5" /> |
| </SmartInputWrapper> | ||
| </div> | ||
| </SectionCard> | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SectionCard for advanced settings is titled "Forward client IP", which is the label for the first field within it. This is confusing as the section contains other settings like "Cache TTL" and "1M Context Window". A more general title like "Advanced Settings" would be more appropriate. This would likely require adding a new translation key.
| title={t("sections.routing.advancedSettings.title")} |
| | "codex" | ||
| | "gemini" | ||
| | "gemini-cli" | ||
| | "openai-compatible"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[HIGH] [TYPE-WEAK-INVARIANT] Hardcoded deprecated provider types in preset parameter
Why this is a problem: The preset parameter type definition includes deprecated provider types "claude-auth" and "gemini-cli" that were intentionally removed from the UI in this PR (as stated in the PR description). This creates a type inconsistency where:
- The preset can accept deprecated types that are no longer selectable in the UI
- The
routing-section.tsxcorrectly uses only the 4 valid types:["claude", "codex", "gemini", "openai-compatible"] - The global
ProviderTypeunion insrc/types/provider.tsstill includes all 6 types (including deprecated ones)
This violates the principle of making invalid states unrepresentable and could lead to runtime issues if a preset with deprecated types is passed.
Suggested fix:
// Import the ProviderType from types and use it directly, OR
// Define a new type that excludes deprecated types
type ActiveProviderType = "claude" | "codex" | "gemini" | "openai-compatible";
export function createInitialState(
mode: FormMode,
provider?: ProviderDisplay,
cloneProvider?: ProviderDisplay,
preset?: {
name?: string;
url?: string;
websiteUrl?: string;
providerType?: ActiveProviderType; // Use the restricted type
}
): ProviderFormState {
// ... rest of implementation
}
// Apply the same fix to the ProviderFormProvider props (lines 295-306)Alternative approach: If the global ProviderType is being kept for backward compatibility with existing database records, consider creating a separate ActiveProviderType type that excludes deprecated values and use it consistently across the new form components.
Location: Lines 22-28 and 299-306
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Summary
This is a well-executed refactoring that successfully breaks down a monolithic 2,215-line form into a modular architecture with proper separation of concerns. The implementation demonstrates strong architectural decisions with React Context + useReducer for state management, smooth scroll navigation, and comprehensive i18n support across 5 languages.
PR Size: XL
- Lines changed: 3,282 (3,254 additions + 28 deletions)
- Files changed: 40
Split Recommendation: While this PR is large (XL), it represents a cohesive refactoring effort that would be difficult to split without losing context. The changes are well-organized into logical sections (basic info, routing, limits, network, testing) and the legacy form is preserved for reference. Future similar refactorings should consider incremental migration strategies.
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 1 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
High Priority Issues (Should Fix)
- Type Inconsistency with Deprecated Provider Types (provider-form-context.tsx:22-28, 299-306)
- The
presetparameter hardcodes deprecated types"claude-auth"and"gemini-cli"that were removed from the UI - Creates type mismatch where preset can accept types no longer selectable in the UI
- Violates principle of making invalid states unrepresentable
- Fix: Define
ActiveProviderTypeexcluding deprecated values and use consistently
- The
Positive Observations
Architecture & Design:
- Clean separation of concerns with dedicated section components
- Proper use of React Context + useReducer pattern for complex state management
- Glassmorphism card components with reusable variants
- Smooth scroll navigation with proper intersection detection
Code Quality:
- Comprehensive error handling with user feedback via toast notifications
- All errors are logged to console (lines 244, 280) - no silent failures
- Proper validation before form submission
- Confirmation dialog for dangerous circuit breaker thresholds
i18n Compliance:
- All user-facing strings use i18n (CLAUDE.md requirement met)
- Consistent translation keys across all 5 languages
- Removed hardcoded asterisks from labels (SmartInputWrapper handles required markers)
Type Safety:
- Strong TypeScript usage throughout
- Proper type definitions in provider-form-types.ts
- Type-safe reducer actions with discriminated unions
UX Improvements:
- Vertical scroll navigation replacing tab content switching
- Auto-scroll to sections when clicking tabs
- Auto-update active tab when scrolling
- Responsive tab navigation (desktop/tablet/mobile)
- Smooth animations via framer-motion
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - Excellent (all errors logged and surfaced to user)
- Type safety - One inconsistency found (see inline comment)
- Documentation accuracy - Clean
- Test coverage - Existing test file preserved (provider-form-total-limit-ui.test.tsx)
- Code clarity - Excellent modular structure
Validation Notes
Error Handling Analysis:
- All 3 catch blocks (lines 243, 279, 504) properly log errors and show user feedback
- No silent failures detected
- Proper fallback behavior with toast notifications
Type System Analysis:
- Comprehensive type definitions in provider-form-types.ts
- Proper use of discriminated unions for reducer actions
- One type inconsistency found with deprecated provider types (see inline comment)
Standards Compliance:
- Follows CLAUDE.md guidelines (no emoji, i18n required, proper formatting)
- Uses Biome formatting conventions (double quotes, trailing commas, 2-space indent)
- Proper path alias usage (@/)
- Named exports preferred over default exports
Recommendation
APPROVE with minor fix required: This is a high-quality refactoring that significantly improves code maintainability and user experience. The single type inconsistency issue should be addressed before merging, but it's a straightforward fix that doesn't affect runtime behavior. The rest of the implementation is solid with proper error handling, validation, and type safety.
Automated review by Claude AI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
messages/en/settings/providers/form/sections.json (1)
259-261: Unicode 特殊字符违反编码规范。
modelWhitelist.allowAll中使用了✓符号。根据编码规范,不应在代码、注释或字符串字面量中使用 emoji 字符。建议移除或替换为纯文本。建议修复
- "allowAll": "✓ Allow all models (recommended)", + "allowAll": "Allow all models (recommended)",messages/zh-CN/settings/providers/form/sections.json (1)
49-49: Unicode 特殊字符违反编码规范。与英文版本相同,
modelWhitelist.allowAll中使用了✓符号,应移除。messages/ja/settings/providers/form/sections.json (1)
261-261: Unicode 特殊字符违反编码规范。与其他语言版本相同,
modelWhitelist.allowAll中使用了✓符号,应移除。messages/zh-TW/settings/providers/form/sections.json (1)
132-183: 避免 rateLimit 内重复的 title 键。Line 133 与 Line 182 同时定义了 title,会导致前者被后者覆盖,易引发维护混乱。建议保留一个即可。
建议修改
- "summary": { + "summary": { "concurrent": "並發:{count}", "daily": "日: {amount}(重置 {resetTime})", "fiveHour": "5h:${amount}", "monthly": "月:${amount}", "none": "無限制", "total": "總:${amount}", "weekly": "週:${amount}" - }, - "title": "流量限制" + }
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 78: 在 package.json 中将依赖项 "framer-motion": "^12.28.1" 改为 "framer-motion":
"^12.27.1"(修正不存在的版本号),然后重新安装依赖(如运行 npm install 或 yarn install)以确保锁文件和
node_modules 同步更新;检查 package.json 中仅此处出现该键并更新相应的 lockfile(package-lock.json 或
yarn.lock)。
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts:
- Around line 156-172: The ProviderFormProps interface is duplicated between
provider-form-types.ts and index.tsx; remove the duplicate in index.tsx and
import the shared type from provider-form-types.ts instead. Update the index.tsx
import line to include ProviderFormProps alongside TabId (e.g., import type {
ProviderFormProps, TabId } from "./provider-form-types"), delete the local
ProviderFormProps declaration in index.tsx, and ensure any usages in index.tsx
reference the imported ProviderFormProps type.
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx:
- Around line 143-149: The visibility toggle button for the key (the button
using showKey, setShowKey and rendering <Eye>/<EyeOff>) lacks accessible naming
and state; update that button to include an i18n-driven aria-label (e.g., "show
key" / "hide key") and an aria-pressed attribute bound to showKey so screen
readers get the label and state, and use the same i18n strings for any visually
hidden text if needed.
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx:
- Around line 295-401: The number inputs for circuit breaker fields
(state.circuitBreaker.failureThreshold, openDurationMinutes,
halfOpenSuccessThreshold, maxRetryAttempts) directly parse strings with parseInt
and allow negative/decimal values; before dispatching the actions
(SET_FAILURE_THRESHOLD, SET_OPEN_DURATION_MINUTES,
SET_HALF_OPEN_SUCCESS_THRESHOLD, SET_MAX_RETRY_ATTEMPTS) validate and normalize
the input: reject or coerce non-integers by taking Math.floor or using an
integer parse check, enforce min/max bounds (e.g., >= 0 for failureThreshold,
>=1 for openDurationMinutes/halfOpenSuccessThreshold/maxRetryAttempts) and
convert empty string to undefined/null as currently intended, then dispatch only
the sanitized integer value so the UI min/max attributes and state stay
consistent.
- Around line 150-275: Replace hard-coded user-visible strings (USD, min, 00:00)
with i18n translation keys in the UI components: update LimitCard props (unit
for LimitCard instances using "USD" and the min unit passed to any
LimitCard/props elsewhere), the Input placeholder and default value for daily
reset time in the Input used by daily reset (currently "00:00"), and any
hardcoded step/unit labels passed to components in this file; use t(...)
translation keys (e.g., t("units.usd"), t("units.min"),
t("placeholders.dailyResetTime")) instead of literals so translations cover
those strings, and ensure the reducer/initial state still supplies the same
logical defaults but UI strings come from t(...) when rendering (refer to
LimitCard, Input, and the Select/SmartInputWrapper usages to find all
instances).
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx:
- Around line 95-96: The visible "s" timeout unit is hardcoded; replace the
literal in the span inside the NetworkSection component with a call to the i18n
translation function (e.g. t('settings.network.timeout_unit_seconds')) and
update the component to import/use the translation hook if it doesn't already,
then add the corresponding translation keys/values for all supported locales
(e.g. en, zh, etc.) in the locale resource files so the unit is localized and
has a sensible fallback.
- Around line 81-84: The onChange handler currently uses parseInt directly and
can write NaN into state when the input is in an intermediate form (e.g. "-");
update the onChange in the input so you compute const parsed = parseInt(val, 10)
(or Number(val)) then check for Number.isNaN(parsed) and call
onChange(undefined) for empty/invalid input, otherwise call onChange(parsed);
keep the existing behavior of treating "" as undefined and ensure you don't
write NaN into state (refer to the onChange handler, val variable, and parseInt
usage).
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx:
- Around line 341-343: Replace hardcoded SelectValue placeholder "inherit" with
a localized string via the component's translation function (e.g.,
t("common.inherit")); import and call the i18n hook used in this component
(e.g., const t = useTranslations(...) or const { t } = useTranslation()) at the
top of the RoutingSection component, then change each SelectValue placeholder to
use t("common.inherit") (applies to the SelectValue instances currently using
"inherit" such as those inside SelectTrigger blocks). Ensure the translation key
matches your locale files and keep the rest of the Select markup unchanged.
In
`@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx:
- Around line 79-81: The computed defaultProviderType may be a
historical/removed type not present in vendorAllowedTypes; change the logic that
computes defaultProviderType (currently using props.providers[0]?.providerType
?? "claude") to validate it against vendorAllowedTypes and if it's not included,
fall back to a safe value (e.g., vendorAllowedTypes[0] or "claude"). Update the
code that reads props.providers and sets defaultProviderType so it first checks
vendorAllowedTypes.includes(candidate) before using it, ensuring form
initialization and validation use an allowed provider type.
♻️ Duplicate comments (4)
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx (1)
309-312: 高级设置卡片标题不够准确该卡片内包含 Cache TTL、1M Context 等多项配置,仅用 “Forward client IP” 作为标题容易误导。建议增加更通用的标题(如 “Advanced Settings”)并新增翻译键。
src/app/[locale]/settings/providers/_components/forms/provider-form/components/section-card.tsx (1)
69-71: Icon 尺寸类名可能无效
h-4.5 w-4.5不是默认 Tailwind 尺寸,可能导致图标尺寸异常。建议改为标准尺寸(如h-5 w-5)。建议修改
- <Icon className="h-4.5 w-4.5" /> + <Icon className="h-5 w-5" />src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx (1)
18-28: 预设 providerType 类型仍包含已弃用值
claude-auth与gemini-cli已移除,preset 的类型定义仍保留它们会让无效类型进入表单。建议直接复用ProviderType保持一致性。建议修改
-import type { ProviderDisplay } from "@/types/provider"; +import type { ProviderDisplay, ProviderType } from "@/types/provider"; @@ - providerType?: - | "claude" - | "claude-auth" - | "codex" - | "gemini" - | "gemini-cli" - | "openai-compatible"; + providerType?: ProviderType; @@ - providerType?: - | "claude" - | "claude-auth" - | "codex" - | "gemini" - | "gemini-cli" - | "openai-compatible"; + providerType?: ProviderType;Also applies to: 295-305
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (1)
511-524: urlResolver 结果未写回表单状态当前仅调用
urlResolver,但没有把resolvedUrl写回表单状态或重新初始化 ProviderFormProvider,导致预设缺少 URL 时不会自动填充。建议将该逻辑移入ProviderFormContent并在解析成功后dispatch({ type: "SET_URL", payload: resolvedUrl }),或通过重新初始化 provider state 来写回。
🧹 Nitpick comments (5)
messages/en/settings/index.ts (1)
48-70:providersFormCommon同时展开和嵌套。
providersFormCommon在第 50 行被展开到providersForm根级别,同时在第 53 行又作为common属性嵌套。这意味着相同内容可通过form.core、form.tabs和form.common.core、form.common.tabs两种路径访问。如果这是为了向后兼容,建议添加注释说明;否则可考虑移除冗余的展开或嵌套。
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (1)
547-547: 建议移除默认导出当前既有具名导出又有默认导出,建议移除默认导出并同步更新引用,以保持导出风格一致。根据编码规范,优先使用具名导出。
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts (3)
1-1: 未使用的导入:Context1mPreference
Context1mPreference已导入但未在文件中使用。如果 Line 45 的context1mPreference字段应该使用此类型,建议直接引用导入的类型以保持一致性;否则请移除此未使用的导入。建议的修复
如果类型值匹配,可以在 Line 45 使用导入的类型:
- context1mPreference: "inherit" | "force_enable" | "disabled"; + context1mPreference: Context1mPreference;或者移除未使用的导入:
-import type { Context1mPreference } from "@/lib/special-attributes";
64-69:null与undefined语义不一致
CircuitBreakerState中混合使用了number | undefined和number | null:
failureThreshold,openDurationMinutes,halfOpenSuccessThreshold:number | undefinedmaxRetryAttempts:number | null根据项目惯例,
null通常表示"明确设置为无限制",而undefined表示"继承默认值"。建议添加注释说明每个字段的语义差异,或统一使用模式以保持一致性。
175-184: 建议显式导入Dispatch类型
React.Dispatch在 Line 177 使用,但文件中未显式导入 React。虽然在大多数配置下通过全局类型声明可以正常工作,但显式导入更清晰且更易于维护。建议的修复
+import type { Dispatch } from "react"; import type { Context1mPreference } from "@/lib/special-attributes";然后将 Line 177 改为:
- dispatch: React.Dispatch<ProviderFormAction>; + dispatch: Dispatch<ProviderFormAction>;
| "dotenv": "^17", | ||
| "drizzle-orm": "^0.44", | ||
| "fetch-socks": "^1.3.2", | ||
| "framer-motion": "^12.28.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
framer-motion npm latest version 2026
💡 Result:
As of January 21, 2026 the latest published framer-motion (motion) release is 12.27.1 (published January 19, 2026). [1][2]
Sources:
- Changelog / package (Yarn classic) showing 12.27.1 — Jan 19, 2026. [1]
- Project repository (motiondivision/motion) on GitHub. [2]
修正 framer-motion 版本号。
当前指定的版本 ^12.28.1 不存在。截至 2026 年 1 月 21 日,最新发布的版本是 12.27.1(2026 年 1 月 19 日发布)。请将依赖版本更新为 ^12.27.1。
🤖 Prompt for AI Agents
In `@package.json` at line 78, 在 package.json 中将依赖项 "framer-motion": "^12.28.1" 改为
"framer-motion": "^12.27.1"(修正不存在的版本号),然后重新安装依赖(如运行 npm install 或 yarn
install)以确保锁文件和 node_modules 同步更新;检查 package.json 中仅此处出现该键并更新相应的
lockfile(package-lock.json 或 yarn.lock)。
| export interface ProviderFormProps { | ||
| mode: FormMode; | ||
| onSuccess?: () => void; | ||
| provider?: ProviderDisplay; | ||
| cloneProvider?: ProviderDisplay; | ||
| enableMultiProviderTypes: boolean; | ||
| hideUrl?: boolean; | ||
| hideWebsiteUrl?: boolean; | ||
| preset?: { | ||
| name?: string; | ||
| url?: string; | ||
| websiteUrl?: string; | ||
| providerType?: ProviderType; | ||
| }; | ||
| urlResolver?: (providerType: ProviderType) => Promise<string | null>; | ||
| allowedProviderTypes?: ProviderType[]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, find the index.tsx file in the provider-form directory
find src/app/[locale]/settings/providers/_components/forms/provider-form -name "index.tsx" -o -name "index.ts"Repository: ding113/claude-code-hub
Length of output: 144
🏁 Script executed:
#!/bin/bash
# Search for ProviderFormProps definition in the types file
cat -n src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts | sed -n '156,172p'Repository: ding113/claude-code-hub
Length of output: 645
🏁 Script executed:
#!/bin/bash
# Search for all ProviderFormProps definitions in the codebase
rg -A 15 "export interface ProviderFormProps" src/app/[locale]/settings/providers/_components/forms/provider-form/Repository: ding113/claude-code-hub
Length of output: 3698
🏁 Script executed:
#!/bin/bash
# Check the exact content of index.tsx lines 31-47
cat -n src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx | sed -n '31,47p'Repository: ding113/claude-code-hub
Length of output: 653
🏁 Script executed:
#!/bin/bash
# Search for FormMode definition
rg -B 2 "type FormMode|interface FormMode" src/app/[locale]/settings/providers/_components/forms/provider-form/Repository: ding113/claude-code-hub
Length of output: 395
🏁 Script executed:
#!/bin/bash
# Check the imports and exports in both files
echo "=== provider-form-types.ts imports/exports ===" && head -50 src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.tsRepository: ding113/claude-code-hub
Length of output: 1499
🏁 Script executed:
#!/bin/bash
# Check how index.tsx uses and exports ProviderFormProps
head -80 src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx | tail -60Repository: ding113/claude-code-hub
Length of output: 2208
移除重复的类型定义
ProviderFormProps 在此文件 (lines 156-172) 和 index.tsx (lines 31-47) 中均有定义,造成类型定义重复。这会导致两个定义可能不同步的风险。
建议从 provider-form-types.ts 中导入 ProviderFormProps,而不是在 index.tsx 中重新定义。由于 index.tsx 已经从该文件导入 TabId,可以扩展导入以包含 ProviderFormProps:
import type { ProviderFormProps, TabId } from "./provider-form-types";然后删除 index.tsx 中的重复接口定义。
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
around lines 156 - 172, The ProviderFormProps interface is duplicated between
provider-form-types.ts and index.tsx; remove the duplicate in index.tsx and
import the shared type from provider-form-types.ts instead. Update the index.tsx
import line to include ProviderFormProps alongside TabId (e.g., import type {
ProviderFormProps, TabId } from "./provider-form-types"), delete the local
ProviderFormProps declaration in index.tsx, and ensure any usages in index.tsx
reference the imported ProviderFormProps type.
| <button | ||
| type="button" | ||
| onClick={() => setShowKey(!showKey)} | ||
| className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" | ||
| > | ||
| {showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | ||
| </button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
密钥可见性切换按钮缺少可访问名称。 图标按钮无 aria-label/aria-pressed,屏幕阅读器难以识别;建议补充并用 i18n 文案。As per coding guidelines, ...
建议修改
<button
type="button"
onClick={() => setShowKey(!showKey)}
+ aria-label={showKey ? t("key.hide") : t("key.show")}
+ aria-pressed={showKey}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| type="button" | |
| onClick={() => setShowKey(!showKey)} | |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" | |
| > | |
| {showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setShowKey(!showKey)} | |
| aria-label={showKey ? t("key.hide") : t("key.show")} | |
| aria-pressed={showKey} | |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" | |
| > | |
| {showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | |
| </button> |
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx
around lines 143 - 149, The visibility toggle button for the key (the button
using showKey, setShowKey and rendering <Eye>/<EyeOff>) lacks accessible naming
and state; update that button to include an i18n-driven aria-label (e.g., "show
key" / "hide key") and an aria-pressed attribute bound to showKey so screen
readers get the label and state, and use the same i18n strings for any visually
hidden text if needed.
| <LimitCard | ||
| label={t("sections.rateLimit.limit5h.label")} | ||
| value={state.rateLimit.limit5hUsd} | ||
| unit="USD" | ||
| icon={Clock} | ||
| color="bg-blue-500/10 text-blue-500" | ||
| id={isEdit ? "edit-limit-5h" : "limit-5h"} | ||
| placeholder={t("sections.rateLimit.limit5h.placeholder")} | ||
| onChange={(value) => dispatch({ type: "SET_LIMIT_5H_USD", payload: value })} | ||
| disabled={state.ui.isPending} | ||
| /> | ||
| <LimitCard | ||
| label={t("sections.rateLimit.limitDaily.label")} | ||
| value={state.rateLimit.limitDailyUsd} | ||
| unit="USD" | ||
| icon={DollarSign} | ||
| color="bg-green-500/10 text-green-500" | ||
| id={isEdit ? "edit-limit-daily" : "limit-daily"} | ||
| placeholder={t("sections.rateLimit.limitDaily.placeholder")} | ||
| onChange={(value) => dispatch({ type: "SET_LIMIT_DAILY_USD", payload: value })} | ||
| disabled={state.ui.isPending} | ||
| /> | ||
| <LimitCard | ||
| label={t("sections.rateLimit.limitWeekly.label")} | ||
| value={state.rateLimit.limitWeeklyUsd} | ||
| unit="USD" | ||
| icon={DollarSign} | ||
| color="bg-purple-500/10 text-purple-500" | ||
| id={isEdit ? "edit-limit-weekly" : "limit-weekly"} | ||
| placeholder={t("sections.rateLimit.limitWeekly.placeholder")} | ||
| onChange={(value) => dispatch({ type: "SET_LIMIT_WEEKLY_USD", payload: value })} | ||
| disabled={state.ui.isPending} | ||
| /> | ||
| <LimitCard | ||
| label={t("sections.rateLimit.limitMonthly.label")} | ||
| value={state.rateLimit.limitMonthlyUsd} | ||
| unit="USD" | ||
| icon={DollarSign} | ||
| color="bg-orange-500/10 text-orange-500" | ||
| id={isEdit ? "edit-limit-monthly" : "limit-monthly"} | ||
| placeholder={t("sections.rateLimit.limitMonthly.placeholder")} | ||
| onChange={(value) => dispatch({ type: "SET_LIMIT_MONTHLY_USD", payload: value })} | ||
| disabled={state.ui.isPending} | ||
| /> | ||
| </div> | ||
| </FieldGroup> | ||
|
|
||
| {/* Daily Reset Settings */} | ||
| <FieldGroup label={t("sections.limits.dailyReset")}> | ||
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | ||
| <SmartInputWrapper | ||
| label={t("sections.rateLimit.dailyResetMode.label")} | ||
| description={ | ||
| state.rateLimit.dailyResetMode === "fixed" | ||
| ? t("sections.rateLimit.dailyResetMode.desc.fixed") | ||
| : t("sections.rateLimit.dailyResetMode.desc.rolling") | ||
| } | ||
| > | ||
| <Select | ||
| value={state.rateLimit.dailyResetMode} | ||
| onValueChange={(value: "fixed" | "rolling") => | ||
| dispatch({ type: "SET_DAILY_RESET_MODE", payload: value }) | ||
| } | ||
| disabled={state.ui.isPending} | ||
| > | ||
| <SelectTrigger id={isEdit ? "edit-daily-reset-mode" : "daily-reset-mode"}> | ||
| <SelectValue /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value="fixed"> | ||
| {t("sections.rateLimit.dailyResetMode.options.fixed")} | ||
| </SelectItem> | ||
| <SelectItem value="rolling"> | ||
| {t("sections.rateLimit.dailyResetMode.options.rolling")} | ||
| </SelectItem> | ||
| </SelectContent> | ||
| </Select> | ||
| </SmartInputWrapper> | ||
|
|
||
| {state.rateLimit.dailyResetMode === "fixed" && ( | ||
| <SmartInputWrapper label={t("sections.rateLimit.dailyResetTime.label")}> | ||
| <Input | ||
| id={isEdit ? "edit-daily-reset" : "daily-reset"} | ||
| type="time" | ||
| value={state.rateLimit.dailyResetTime} | ||
| onChange={(e) => | ||
| dispatch({ type: "SET_DAILY_RESET_TIME", payload: e.target.value || "00:00" }) | ||
| } | ||
| placeholder="00:00" | ||
| disabled={state.ui.isPending} | ||
| step="60" | ||
| /> | ||
| </SmartInputWrapper> | ||
| )} | ||
| </div> | ||
| </FieldGroup> | ||
|
|
||
| {/* Total and Concurrent Limits */} | ||
| <FieldGroup label={t("sections.limits.otherLimits")}> | ||
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> | ||
| <LimitCard | ||
| label={t("sections.rateLimit.limitTotal.label")} | ||
| value={state.rateLimit.limitTotalUsd} | ||
| unit="USD" | ||
| icon={Gauge} | ||
| color="bg-red-500/10 text-red-500" | ||
| id={isEdit ? "edit-limit-total" : "limit-total"} | ||
| placeholder={t("sections.rateLimit.limitTotal.placeholder")} | ||
| onChange={(value) => dispatch({ type: "SET_LIMIT_TOTAL_USD", payload: value })} | ||
| disabled={state.ui.isPending} | ||
| /> | ||
| <LimitCard | ||
| label={t("sections.rateLimit.limitConcurrent.label")} | ||
| value={state.rateLimit.limitConcurrentSessions} | ||
| unit="" | ||
| icon={Users} | ||
| color="bg-cyan-500/10 text-cyan-500" | ||
| id={isEdit ? "edit-limit-concurrent" : "limit-concurrent"} | ||
| placeholder={t("sections.rateLimit.limitConcurrent.placeholder")} | ||
| onChange={(value) => | ||
| dispatch({ type: "SET_LIMIT_CONCURRENT_SESSIONS", payload: value }) | ||
| } | ||
| disabled={state.ui.isPending} | ||
| step="1" | ||
| isDecimal={false} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
单位与占位符存在硬编码,需改用 i18n
USD、min 和 00:00 是用户可见文本,应通过翻译键输出并补齐多语言条目。根据编码规范,所有用户可见字符串需走 i18n。
建议修改
export function LimitsSection() {
const t = useTranslations("settings.providers.form");
const { state, dispatch, mode } = useProviderForm();
const isEdit = mode === "edit";
+ const unitUsd = t("sections.common.units.usd");
+ const unitMinutes = t("sections.common.units.minutes");
+ const timePlaceholder = t("sections.common.placeholders.time");
@@
- unit="USD"
+ unit={unitUsd}
@@
- placeholder="00:00"
+ placeholder={timePlaceholder}
@@
- <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">
- min
- </span>
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">
+ {unitMinutes}
+ </span>Also applies to: 231-240, 351-353
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx
around lines 150 - 275, Replace hard-coded user-visible strings (USD, min,
00:00) with i18n translation keys in the UI components: update LimitCard props
(unit for LimitCard instances using "USD" and the min unit passed to any
LimitCard/props elsewhere), the Input placeholder and default value for daily
reset time in the Input used by daily reset (currently "00:00"), and any
hardcoded step/unit labels passed to components in this file; use t(...)
translation keys (e.g., t("units.usd"), t("units.min"),
t("placeholders.dailyResetTime")) instead of literals so translations cover
those strings, and ensure the reducer/initial state still supplies the same
logical defaults but UI strings come from t(...) when rendering (refer to
LimitCard, Input, and the Select/SmartInputWrapper usages to find all
instances).
| <Input | ||
| id={isEdit ? "edit-failure-threshold" : "failure-threshold"} | ||
| type="number" | ||
| value={state.circuitBreaker.failureThreshold ?? ""} | ||
| onChange={(e) => { | ||
| const val = e.target.value; | ||
| dispatch({ | ||
| type: "SET_FAILURE_THRESHOLD", | ||
| payload: val === "" ? undefined : parseInt(val, 10), | ||
| }); | ||
| }} | ||
| placeholder={t("sections.circuitBreaker.failureThreshold.placeholder")} | ||
| disabled={state.ui.isPending} | ||
| min="0" | ||
| step="1" | ||
| className={cn(state.circuitBreaker.failureThreshold === 0 && "border-yellow-500")} | ||
| /> | ||
| <AlertTriangle | ||
| className={cn( | ||
| "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4", | ||
| state.circuitBreaker.failureThreshold === 0 | ||
| ? "text-yellow-500" | ||
| : "text-muted-foreground/30" | ||
| )} | ||
| /> | ||
| </div> | ||
| {state.circuitBreaker.failureThreshold === 0 && ( | ||
| <p className="text-xs text-yellow-600"> | ||
| {t("sections.circuitBreaker.failureThreshold.warning")} | ||
| </p> | ||
| )} | ||
| </SmartInputWrapper> | ||
|
|
||
| <SmartInputWrapper | ||
| label={t("sections.circuitBreaker.openDuration.label")} | ||
| description={t("sections.circuitBreaker.openDuration.desc")} | ||
| > | ||
| <div className="relative"> | ||
| <Input | ||
| id={isEdit ? "edit-open-duration" : "open-duration"} | ||
| type="number" | ||
| value={state.circuitBreaker.openDurationMinutes ?? ""} | ||
| onChange={(e) => { | ||
| const val = e.target.value; | ||
| dispatch({ | ||
| type: "SET_OPEN_DURATION_MINUTES", | ||
| payload: val === "" ? undefined : parseInt(val, 10), | ||
| }); | ||
| }} | ||
| placeholder={t("sections.circuitBreaker.openDuration.placeholder")} | ||
| disabled={state.ui.isPending} | ||
| min="1" | ||
| max="1440" | ||
| step="1" | ||
| className="pr-12" | ||
| /> | ||
| <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> | ||
| min | ||
| </span> | ||
| </div> | ||
| </SmartInputWrapper> | ||
|
|
||
| <SmartInputWrapper | ||
| label={t("sections.circuitBreaker.successThreshold.label")} | ||
| description={t("sections.circuitBreaker.successThreshold.desc")} | ||
| > | ||
| <Input | ||
| id={isEdit ? "edit-success-threshold" : "success-threshold"} | ||
| type="number" | ||
| value={state.circuitBreaker.halfOpenSuccessThreshold ?? ""} | ||
| onChange={(e) => { | ||
| const val = e.target.value; | ||
| dispatch({ | ||
| type: "SET_HALF_OPEN_SUCCESS_THRESHOLD", | ||
| payload: val === "" ? undefined : parseInt(val, 10), | ||
| }); | ||
| }} | ||
| placeholder={t("sections.circuitBreaker.successThreshold.placeholder")} | ||
| disabled={state.ui.isPending} | ||
| min="1" | ||
| max="10" | ||
| step="1" | ||
| /> | ||
| </SmartInputWrapper> | ||
|
|
||
| <SmartInputWrapper | ||
| label={t("sections.circuitBreaker.maxRetryAttempts.label")} | ||
| description={t("sections.circuitBreaker.maxRetryAttempts.desc")} | ||
| > | ||
| <div className="relative"> | ||
| <Input | ||
| id={isEdit ? "edit-max-retry-attempts" : "max-retry-attempts"} | ||
| type="number" | ||
| value={state.circuitBreaker.maxRetryAttempts ?? ""} | ||
| onChange={(e) => { | ||
| const val = e.target.value; | ||
| dispatch({ | ||
| type: "SET_MAX_RETRY_ATTEMPTS", | ||
| payload: val === "" ? null : parseInt(val, 10), | ||
| }); | ||
| }} | ||
| placeholder={t("sections.circuitBreaker.maxRetryAttempts.placeholder")} | ||
| disabled={state.ui.isPending} | ||
| min="1" | ||
| max="10" | ||
| step="1" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
断路器数值输入未阻止负数/非整数
当前使用 parseInt 直接入库,负数或小数可写入状态,容易产生无效配置。建议在 dispatch 前做非负/整数校验,并与 min 约束对齐,其它断路器字段同样处理。
建议修改(示例:failureThreshold)
- onChange={(e) => {
- const val = e.target.value;
- dispatch({
- type: "SET_FAILURE_THRESHOLD",
- payload: val === "" ? undefined : parseInt(val, 10),
- });
- }}
+ onChange={(e) => {
+ const val = e.target.value;
+ const parsed = Number(val);
+ dispatch({
+ type: "SET_FAILURE_THRESHOLD",
+ payload:
+ val === "" || !Number.isInteger(parsed) || parsed < 0
+ ? undefined
+ : parsed,
+ });
+ }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Input | |
| id={isEdit ? "edit-failure-threshold" : "failure-threshold"} | |
| type="number" | |
| value={state.circuitBreaker.failureThreshold ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_FAILURE_THRESHOLD", | |
| payload: val === "" ? undefined : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.failureThreshold.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="0" | |
| step="1" | |
| className={cn(state.circuitBreaker.failureThreshold === 0 && "border-yellow-500")} | |
| /> | |
| <AlertTriangle | |
| className={cn( | |
| "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4", | |
| state.circuitBreaker.failureThreshold === 0 | |
| ? "text-yellow-500" | |
| : "text-muted-foreground/30" | |
| )} | |
| /> | |
| </div> | |
| {state.circuitBreaker.failureThreshold === 0 && ( | |
| <p className="text-xs text-yellow-600"> | |
| {t("sections.circuitBreaker.failureThreshold.warning")} | |
| </p> | |
| )} | |
| </SmartInputWrapper> | |
| <SmartInputWrapper | |
| label={t("sections.circuitBreaker.openDuration.label")} | |
| description={t("sections.circuitBreaker.openDuration.desc")} | |
| > | |
| <div className="relative"> | |
| <Input | |
| id={isEdit ? "edit-open-duration" : "open-duration"} | |
| type="number" | |
| value={state.circuitBreaker.openDurationMinutes ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_OPEN_DURATION_MINUTES", | |
| payload: val === "" ? undefined : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.openDuration.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="1" | |
| max="1440" | |
| step="1" | |
| className="pr-12" | |
| /> | |
| <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> | |
| min | |
| </span> | |
| </div> | |
| </SmartInputWrapper> | |
| <SmartInputWrapper | |
| label={t("sections.circuitBreaker.successThreshold.label")} | |
| description={t("sections.circuitBreaker.successThreshold.desc")} | |
| > | |
| <Input | |
| id={isEdit ? "edit-success-threshold" : "success-threshold"} | |
| type="number" | |
| value={state.circuitBreaker.halfOpenSuccessThreshold ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_HALF_OPEN_SUCCESS_THRESHOLD", | |
| payload: val === "" ? undefined : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.successThreshold.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="1" | |
| max="10" | |
| step="1" | |
| /> | |
| </SmartInputWrapper> | |
| <SmartInputWrapper | |
| label={t("sections.circuitBreaker.maxRetryAttempts.label")} | |
| description={t("sections.circuitBreaker.maxRetryAttempts.desc")} | |
| > | |
| <div className="relative"> | |
| <Input | |
| id={isEdit ? "edit-max-retry-attempts" : "max-retry-attempts"} | |
| type="number" | |
| value={state.circuitBreaker.maxRetryAttempts ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_MAX_RETRY_ATTEMPTS", | |
| payload: val === "" ? null : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.maxRetryAttempts.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="1" | |
| max="10" | |
| step="1" | |
| /> | |
| <Input | |
| id={isEdit ? "edit-failure-threshold" : "failure-threshold"} | |
| type="number" | |
| value={state.circuitBreaker.failureThreshold ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| const parsed = Number(val); | |
| dispatch({ | |
| type: "SET_FAILURE_THRESHOLD", | |
| payload: | |
| val === "" || !Number.isInteger(parsed) || parsed < 0 | |
| ? undefined | |
| : parsed, | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.failureThreshold.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="0" | |
| step="1" | |
| className={cn(state.circuitBreaker.failureThreshold === 0 && "border-yellow-500")} | |
| /> | |
| <AlertTriangle | |
| className={cn( | |
| "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4", | |
| state.circuitBreaker.failureThreshold === 0 | |
| ? "text-yellow-500" | |
| : "text-muted-foreground/30" | |
| )} | |
| /> | |
| </div> | |
| {state.circuitBreaker.failureThreshold === 0 && ( | |
| <p className="text-xs text-yellow-600"> | |
| {t("sections.circuitBreaker.failureThreshold.warning")} | |
| </p> | |
| )} | |
| </SmartInputWrapper> | |
| <SmartInputWrapper | |
| label={t("sections.circuitBreaker.openDuration.label")} | |
| description={t("sections.circuitBreaker.openDuration.desc")} | |
| > | |
| <div className="relative"> | |
| <Input | |
| id={isEdit ? "edit-open-duration" : "open-duration"} | |
| type="number" | |
| value={state.circuitBreaker.openDurationMinutes ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_OPEN_DURATION_MINUTES", | |
| payload: val === "" ? undefined : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.openDuration.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="1" | |
| max="1440" | |
| step="1" | |
| className="pr-12" | |
| /> | |
| <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> | |
| min | |
| </span> | |
| </div> | |
| </SmartInputWrapper> | |
| <SmartInputWrapper | |
| label={t("sections.circuitBreaker.successThreshold.label")} | |
| description={t("sections.circuitBreaker.successThreshold.desc")} | |
| > | |
| <Input | |
| id={isEdit ? "edit-success-threshold" : "success-threshold"} | |
| type="number" | |
| value={state.circuitBreaker.halfOpenSuccessThreshold ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_HALF_OPEN_SUCCESS_THRESHOLD", | |
| payload: val === "" ? undefined : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.successThreshold.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="1" | |
| max="10" | |
| step="1" | |
| /> | |
| </SmartInputWrapper> | |
| <SmartInputWrapper | |
| label={t("sections.circuitBreaker.maxRetryAttempts.label")} | |
| description={t("sections.circuitBreaker.maxRetryAttempts.desc")} | |
| > | |
| <div className="relative"> | |
| <Input | |
| id={isEdit ? "edit-max-retry-attempts" : "max-retry-attempts"} | |
| type="number" | |
| value={state.circuitBreaker.maxRetryAttempts ?? ""} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| dispatch({ | |
| type: "SET_MAX_RETRY_ATTEMPTS", | |
| payload: val === "" ? null : parseInt(val, 10), | |
| }); | |
| }} | |
| placeholder={t("sections.circuitBreaker.maxRetryAttempts.placeholder")} | |
| disabled={state.ui.isPending} | |
| min="1" | |
| max="10" | |
| step="1" | |
| /> |
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx
around lines 295 - 401, The number inputs for circuit breaker fields
(state.circuitBreaker.failureThreshold, openDurationMinutes,
halfOpenSuccessThreshold, maxRetryAttempts) directly parse strings with parseInt
and allow negative/decimal values; before dispatching the actions
(SET_FAILURE_THRESHOLD, SET_OPEN_DURATION_MINUTES,
SET_HALF_OPEN_SUCCESS_THRESHOLD, SET_MAX_RETRY_ATTEMPTS) validate and normalize
the input: reject or coerce non-integers by taking Math.floor or using an
integer parse check, enforce min/max bounds (e.g., >= 0 for failureThreshold,
>=1 for openDurationMinutes/halfOpenSuccessThreshold/maxRetryAttempts) and
convert empty string to undefined/null as currently intended, then dispatch only
the sanitized integer value so the UI min/max attributes and state stay
consistent.
| onChange={(e) => { | ||
| const val = e.target.value; | ||
| onChange(val === "" ? undefined : parseInt(val, 10)); | ||
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
避免 NaN 写入超时状态。 number 输入在编辑中会出现中间态(如 "-"),parseInt 会得到 NaN 并写入 state,可能在 UI 中显示 NaN 并影响汇总。建议在写入前做 NaN 保护。
建议修改
- const val = e.target.value;
- onChange(val === "" ? undefined : parseInt(val, 10));
+ const val = e.target.value;
+ const next = val === "" ? undefined : Number.parseInt(val, 10);
+ onChange(Number.isNaN(next) ? undefined : next);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onChange={(e) => { | |
| const val = e.target.value; | |
| onChange(val === "" ? undefined : parseInt(val, 10)); | |
| }} | |
| onChange={(e) => { | |
| const val = e.target.value; | |
| const next = val === "" ? undefined : Number.parseInt(val, 10); | |
| onChange(Number.isNaN(next) ? undefined : next); | |
| }} |
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx
around lines 81 - 84, The onChange handler currently uses parseInt directly and
can write NaN into state when the input is in an intermediate form (e.g. "-");
update the onChange in the input so you compute const parsed = parseInt(val, 10)
(or Number(val)) then check for Number.isNaN(parsed) and call
onChange(undefined) for empty/invalid input, otherwise call onChange(parsed);
keep the existing behavior of treating "" as undefined and ensure you don't
write NaN into state (refer to the onChange handler, val variable, and parseInt
usage).
| <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> | ||
| s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
超时单位不应硬编码。 这里的 “s” 是用户可见文本,请改为翻译键并补齐多语言资源。As per coding guidelines, ...
建议修改
- s
+ {t("common.secondsShort")}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> | |
| s | |
| <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> | |
| {t("common.secondsShort")} |
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx
around lines 95 - 96, The visible "s" timeout unit is hardcoded; replace the
literal in the span inside the NetworkSection component with a call to the i18n
translation function (e.g. t('settings.network.timeout_unit_seconds')) and
update the component to import/use the translation hook if it doesn't already,
then add the corresponding translation keys/values for all supported locales
(e.g. en, zh, etc.) in the locale resource files so the unit is localized and
has a sensible fallback.
| <SelectTrigger className="w-full"> | ||
| <SelectValue placeholder="inherit" /> | ||
| </SelectTrigger> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Select 占位符 “inherit” 需本地化
多处 SelectValue 占位符直接写死为 "inherit",会绕过多语言。建议使用翻译键(如 t("common.inherit"))统一输出。根据编码规范,所有用户可见字符串需走 i18n。
建议修改(示例:Cache TTL)
- <SelectTrigger className="w-full">
- <SelectValue placeholder="inherit" />
- </SelectTrigger>
+ <SelectTrigger className="w-full">
+ <SelectValue placeholder={t("common.inherit")} />
+ </SelectTrigger>Also applies to: 370-372, 412-414, 451-453, 475-477, 501-503
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
around lines 341 - 343, Replace hardcoded SelectValue placeholder "inherit" with
a localized string via the component's translation function (e.g.,
t("common.inherit")); import and call the i18n hook used in this component
(e.g., const t = useTranslations(...) or const { t } = useTranslation()) at the
top of the RoutingSection component, then change each SelectValue placeholder to
use t("common.inherit") (applies to the SelectValue instances currently using
"inherit" such as those inside SelectTrigger blocks). Ensure the translation key
matches your locale files and keep the rest of the Select markup unchanged.
| const defaultProviderType: ProviderType = props.providers[0]?.providerType ?? "claude"; | ||
| const vendorAllowedTypes: ProviderType[] = [ | ||
| "claude", | ||
| "claude-auth", | ||
| "codex", | ||
| "gemini", | ||
| "gemini-cli", | ||
| ]; | ||
| const vendorAllowedTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"]; | ||
| const statistics = props.statistics ?? {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
确保默认类型落在允许列表内。
如果历史数据仍存在已移除的类型,当前默认值可能不在 vendorAllowedTypes 中,创建表单可能出现不一致或校验失败。建议做一次兜底。
建议修改
- const defaultProviderType: ProviderType = props.providers[0]?.providerType ?? "claude";
- const vendorAllowedTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];
+ const vendorAllowedTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];
+ const defaultProviderType: ProviderType = vendorAllowedTypes.includes(
+ props.providers[0]?.providerType ?? "claude"
+ )
+ ? (props.providers[0]?.providerType ?? "claude")
+ : vendorAllowedTypes[0];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const defaultProviderType: ProviderType = props.providers[0]?.providerType ?? "claude"; | |
| const vendorAllowedTypes: ProviderType[] = [ | |
| "claude", | |
| "claude-auth", | |
| "codex", | |
| "gemini", | |
| "gemini-cli", | |
| ]; | |
| const vendorAllowedTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"]; | |
| const statistics = props.statistics ?? {}; | |
| const vendorAllowedTypes: ProviderType[] = [ | |
| "claude", | |
| "codex", | |
| "gemini", | |
| "openai-compatible", | |
| ]; | |
| const defaultProviderType: ProviderType = vendorAllowedTypes.includes( | |
| props.providers[0]?.providerType ?? "claude" | |
| ) | |
| ? (props.providers[0]?.providerType ?? "claude") | |
| : vendorAllowedTypes[0]; | |
| const statistics = props.statistics ?? {}; |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 79 - 81, The computed defaultProviderType may be a
historical/removed type not present in vendorAllowedTypes; change the logic that
computes defaultProviderType (currently using props.providers[0]?.providerType
?? "claude") to validate it against vendorAllowedTypes and if it's not included,
fall back to a safe value (e.g., vendorAllowedTypes[0] or "claude"). Update the
code that reads props.providers and sets defaultProviderType so it first checks
vendorAllowedTypes.includes(candidate) before using it, ensuring form
initialization and validation use an allowed provider type.
| name: state.basic.name.trim(), | ||
| url: state.basic.url.trim(), | ||
| key: state.basic.key.trim() || (isEdit ? "" : ""), | ||
| website_url: state.basic.websiteUrl?.trim() || null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[CRITICAL] [LOGIC-BUG] Edit mode always sends an empty key, breaking updates
File: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:183
Why this is a problem: In edit mode, leaving the key blank is expected (the UI suggests it), but the payload always includes:
key: state.basic.key.trim() || (isEdit ? "" : ""),On the server, UpdateProviderSchema requires key to be non-empty when present:
// src/lib/validation/schemas.ts
key: z.string().min(1).max(1024).optional(),So key: "" causes validation to fail and blocks provider updates.
Suggested fix:
const trimmedKey = state.basic.key.trim();
const formData = {
// ...other fields
...(trimmedKey ? { key: trimmedKey } : {}),
};
// Or, explicitly only include `key` on edit when provided.| // Handle URL resolver for preset provider types | ||
| useEffect(() => { | ||
| if (urlResolver && preset?.providerType && !preset?.url) { | ||
| setAutoUrlPending(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[CRITICAL] [LOGIC-BUG] hideUrl + urlResolver never writes the resolved URL into form state
File: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:513
Why this is a problem: The resolver effect calls urlResolver(...) but discards the result:
urlResolver(preset.providerType)
.then((resolvedUrl) => {
if (resolvedUrl) {
// URL will be set through preset in createInitialState
}
})No code actually updates state.basic.url (or preset.url). When hideUrl is true, the user cannot fix this in the UI, yet submit still sends:
url: state.basic.url.trim(),This breaks vendor-key creation flows that depend on endpoint resolution and can submit an empty URL.
Suggested fix:
- Resolve against the current provider type (
state.routing.providerType) and dispatchSET_URLwith the resolved value. - Add
.catch(...)+ cancellation guard to avoid unhandled rejections / setting state after unmount.
Example (move into ProviderFormContent where dispatch + state are available, and plumb setAutoUrlPending or store pending state in context):
useEffect(() => {
if (isEdit) return;
if (!hideUrl || !urlResolver) return;
let cancelled = false;
setAutoUrlPending(true);
urlResolver(state.routing.providerType)
.then((resolved) => {
if (cancelled) return;
dispatch({
type: "SET_URL",
payload: resolved?.trim() ? resolved.trim() : "",
});
})
.catch((e) => {
if (cancelled) return;
console.error("Failed to resolve provider URL:", e);
dispatch({ type: "SET_URL", payload: "" });
})
.finally(() => {
if (!cancelled) setAutoUrlPending(false);
});
return () => {
cancelled = true;
};
}, [dispatch, hideUrl, isEdit, state.routing.providerType, urlResolver]);| return; | ||
| } | ||
| toast.success(t("success.created")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[HIGH] [LOGIC-BUG] Successful create does not reset form state (stale values on consecutive adds)
File: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:240
Why this is a problem: In create mode, after addProvider succeeds the code only shows a toast and calls onSuccess?.():
toast.success(t("success.created"));
// no RESET_FORM
onSuccess?.();If onSuccess is not provided (or the dialog stays open), the next provider creation reuses the previous state (notably limit_total_usd, but also other limits / routing fields). This is a regression from the legacy form behavior and is already covered by an existing unit test expectation (tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx).
Suggested fix:
- Use the existing reducer action and reset after successful create (optionally only when
onSuccessis not closing the dialog).
if (!isEdit) {
const res = await addProvider(formData);
if (!res.ok) {
toast.error(res.error || t("errors.createFailed"));
return;
}
toast.success(t("success.created"));
dispatch({ type: "RESET_FORM" });
onSuccess?.();
return;
}If presets/clones should persist across resets, consider making RESET_FORM derive from the current initial props instead of defaultInitialState.
| <div className="space-y-4"> | ||
| <SmartInputWrapper | ||
| label={isEdit ? `${t("key.label")}${t("key.leaveEmpty")}` : t("key.label")} | ||
| description={ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MEDIUM] [LOGIC-BUG] Missing whitespace + fragile i18n concatenation in edit-mode key label
File: src/app/[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx:127
Why this is a problem: The label concatenates two translations without spacing:
label={isEdit ? `${t("key.label")}${t("key.leaveEmpty")}` : t("key.label")}For English this becomes API Key(Leave empty to keep unchanged) because messages/en/settings/providers/form/key.json defines:
{ "label": "API Key", "leaveEmpty": "(Leave empty to keep unchanged)" }Also, concatenating separately translated strings prevents languages from reordering or formatting the phrase naturally.
Suggested fix:
Prefer a single translation key for the combined label, added to all locales:
label={isEdit ? t("key.labelEdit") : t("key.label")}Example messages/en/settings/providers/form/key.json addition:
{ "labelEdit": "API Key (Leave empty to keep unchanged)" }There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Summary
This is an XL PR that mixes a large provider-form UI refactor with i18n updates and provider-type list changes. The new modular ProviderForm introduces multiple behavior regressions that will break common workflows (edit/update and vendor key creation) by submitting invalid payloads.
PR Size: XL
- Lines changed: 3282
- Files changed: 40
- Split suggestion (required for XL): separate into (1) provider-form refactor, (2) i18n message updates, (3) dependency addition (
framer-motion), (4) provider-type list changes.
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 2 | 1 | 1 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Critical Issues (Must Fix)
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:183- Edit mode always sendskey: "", which fails server validation (UpdateProviderSchema) and blocks updates.src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:513-urlResolver(...)result is discarded; withhideUrlthe form can submit an emptyurl, breaking vendor key creation.
High Priority Issues (Should Fix)
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:240- Successful create does not reset form state whenonSuccessis absent; stale limit fields can leak into subsequent creates.src/app/[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx:127- Key label concatenation is missing whitespace and is fragile for i18n.
Review Coverage
- Logic and correctness
- Security (OWASP Top 10)
- Error handling
- Type safety
- Documentation accuracy
- Test coverage
- Code clarity
Automated review by Codex AI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied PR label size/XL and submitted a review on PR #637.
Inline review comments posted (diff-right):
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:183Critical: edit mode always sendskey: ""(will fail server validation and block updates).src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:513Critical:urlResolver(...)result is discarded; withhideUrlthe form can submit an emptyurl(breaks vendor key creation flow).src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx:240High: successful create doesn’t reset state whenonSuccessis absent (stale limits/fields leak into subsequent creates).src/app/[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx:127Medium: missing whitespace + fragile i18n concatenation in the edit-mode key label.
- Add BentoGrid and BentoCard components with subtle glass morphism styling - Add BentoMetricCard with icon, trend badge, and comparison data support - Add LeaderboardCard with progress bars for user/provider/model rankings - Add LiveSessionsPanel for real-time session monitoring - Add StatisticsChartCard with time range selector and user filtering - Support light/dark mode with appropriate contrast - Reduce shadow intensity for cleaner visual design Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add yesterday same-period comparison data for dashboard metric cards: - Concurrent sessions shows real-time RPM (requests per minute) - Today requests/cost/response time compare vs yesterday same period - Support user-scoped data for non-admin users when global view disabled - Add i18n translations for rpm and vsYesterday labels (5 languages) - Include dashboard page and metric card updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add new translation keys for notification and client version stats - Implement page transition animations and responsive layout improvements - Update form components with consistent styling and accessibility - Add icon support to settings sections and navigation items - Improve table skeletons and loading states - Refactor notification cards and webhook targets section - Optimize provider form layout and validation
replace dark mode specific background and border colors with theme-aware muted variants to improve consistency and maintainability across the application
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
119 files reviewed, 3 comments
| providerType?: | ||
| | "claude" | ||
| | "claude-auth" | ||
| | "codex" | ||
| | "gemini" | ||
| | "gemini-cli" | ||
| | "openai-compatible"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntax: Remove deprecated provider types from preset definition. According to the PR description, claude-auth and gemini-cli were removed. This type definition should only include valid types: "claude" | "codex" | "gemini" | "openai-compatible".
| providerType?: | |
| | "claude" | |
| | "claude-auth" | |
| | "codex" | |
| | "gemini" | |
| | "gemini-cli" | |
| | "openai-compatible"; | |
| providerType?: | |
| | "claude" | |
| | "codex" | |
| | "gemini" | |
| | "openai-compatible"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Line: 22:28
Comment:
**syntax:** Remove deprecated provider types from preset definition. According to the PR description, `claude-auth` and `gemini-cli` were removed. This type definition should only include valid types: `"claude" | "codex" | "gemini" | "openai-compatible"`.
```suggestion
providerType?:
| "claude"
| "codex"
| "gemini"
| "openai-compatible";
```
How can I resolve this? If you propose a fix, please make it concise.| providerType?: | ||
| | "claude" | ||
| | "claude-auth" | ||
| | "codex" | ||
| | "gemini" | ||
| | "gemini-cli" | ||
| | "openai-compatible"; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntax: Remove deprecated provider types from preset parameter. Same issue as line 22-28 - should only include valid types.
| providerType?: | |
| | "claude" | |
| | "claude-auth" | |
| | "codex" | |
| | "gemini" | |
| | "gemini-cli" | |
| | "openai-compatible"; | |
| }; | |
| providerType?: | |
| | "claude" | |
| | "codex" | |
| | "gemini" | |
| | "openai-compatible"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Line: 299:306
Comment:
**syntax:** Remove deprecated provider types from preset parameter. Same issue as line 22-28 - should only include valid types.
```suggestion
providerType?:
| "claude"
| "codex"
| "gemini"
| "openai-compatible";
```
How can I resolve this? If you propose a fix, please make it concise.| name?: string; | ||
| url?: string; | ||
| websiteUrl?: string; | ||
| providerType?: ProviderType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntax: Also update this type reference to match the corrected provider types
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 44:44
Comment:
**syntax:** Also update this type reference to match the corrected provider types
How can I resolve this? If you propose a fix, please make it concise.- Add new labelEdit translations for API key field in edit mode - Update icon size in section card component - Refactor provider form to handle URL resolution and key updates more cleanly - Reset form state after successful submission
|
u are sure about this? |
…d imports Clean up form data construction logic by separating base fields from conditional ones Remove unused Label import and urlResolver prop Improve tab navigation mapping by removing unused index parameter
Remove unused imports from provider-form-types.ts Rename unused displayValue to _displayValue in network-section Remove unused TAB_CONFIG import and rename unused scrollTop to _scrollTop in index.tsx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/app/[locale]/settings/_lib/nav-items.ts (1)
98-104: 所有翻译键已在 5 种语言中完整定义,但存在硬编码标签的 i18n 违规。验证结果确认
SETTINGS_NAV_ITEMS中所有labelKey值(nav.config、nav.prices、nav.providers、nav.sensitiveWords、nav.requestFilters、nav.clientVersions、nav.data、nav.logs、nav.notifications、nav.apiDocs、nav.errorRules、nav.feedback、nav.docs)在 5 种语言的 messages/{locale}/settings/nav.json 文件中都已完整定义,无缺失。但是,代码中存在硬编码的英文标签(如 "Configuration"、"Prices" 等)作为回退值,这违反了"所有用户界面字符串必须使用 i18n,禁止硬编码显示文本"的编码指南。建议移除这些硬编码标签,或确保它们也通过 i18n 系统处理。
src/app/[locale]/settings/config/_components/auto-cleanup-form.tsx (2)
86-103: 开关缺少可访问性标签
当前开关与文本未建立语义关联,建议为Switch添加aria-label或用Label绑定htmlFor指向id="enableAutoCleanup",方便读屏器识别。
105-185: 关闭开关后隐藏字段仍可能阻塞保存
如果用户清空数字字段后再关闭开关,隐藏字段可能仍携带无效值,提交会被校验拦截。建议在关闭时重置这些字段为安全默认值或在关闭状态下跳过校验。src/app/[locale]/settings/error-rules/_components/regex-tester.tsx (1)
38-44: 错误消息 "Invalid regex pattern" 硬编码需改用 i18n
catch块中的默认错误消息是英文硬编码,违反了编码规范中"所有用户可见字符串需走 i18n"的要求。建议修复
} catch (error) { setMatchResult({ isValid: false, matches: false, - error: error instanceof Error ? error.message : "Invalid regex pattern", + error: error instanceof Error ? error.message : t("errorRules.dialog.invalidPatternDefault"), }); }需要在对应的 5 个语言文件中添加翻译键
errorRules.dialog.invalidPatternDefault。src/app/[locale]/settings/notifications/_components/binding-selector.tsx (1)
69-75: 模板覆盖校验错误需走 i18n这里抛出的错误会直接展示给用户,目前是硬编码英文。建议改为 i18n key,或在 catch 中映射错误码后再翻译。As per coding guidelines, ...
Proposed fix
-function parseJsonObjectOrNull(value: string | null | undefined): Record<string, unknown> | null { +function parseJsonObjectOrNull( + value: string | null | undefined, + t: (key: string) => string +): Record<string, unknown> | null { const trimmed = value?.trim(); if (!trimmed) return null; const parsed = JSON.parse(trimmed) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error("Template override must be a JSON object"); + throw new Error(t("notifications.bindings.templateOverrideInvalid")); } return parsed as Record<string, unknown>; } @@ - templateOverride: parseJsonObjectOrNull(r.templateOverrideJson), + templateOverride: parseJsonObjectOrNull(r.templateOverrideJson, t),Also applies to: 140-150
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx:
- Around line 151-174: The date formatting functions formatDate and
formatTooltipDate currently hardcode "zh-CN"; change them to read the current
locale (e.g., from next-intl or useLocale) and pass that locale string into
toLocaleDateString/toLocaleTimeString/toLocaleString instead of "zh-CN"; keep
the same options shapes (hour/minute/month/day/year) but use the dynamic locale
value so the app supports zh-CN, zh-TW, en, ja, ru appropriately.
In `@src/app/`[locale]/dashboard/page.bento.tsx:
- Around line 1-26: 当前 page.bento.tsx 文件是与 page.tsx 完全相同的冗余死代码(Next.js 不认
.bento.tsx 为路由),请删除 page.bento.tsx 或在文件顶部添加明确用途注释(例如用于 A/B 测试/渐进迁移)并更新路由配置以确保
Next.js 能识别该变体;定位符可参考导出函数名 DashboardPage 以及文件名 page.bento.tsx 和 page.tsx
来做出删除或改动决定。
In `@src/app/`[locale]/settings/_components/settings-nav.tsx:
- Around line 221-261: The mobile nav is hard-limited by internalItems.slice(0,
5), blocking access to later items; to fix, remove the slice and render all
internalItems (change internalItems.slice(0, 5).map(...) to
internalItems.map(...)) and make the bar horizontally scrollable by updating the
container div wrapper to allow overflow-x (e.g., add classes like
overflow-x-auto whitespace-nowrap/ flex-nowrap on the immediate container and
ensure each item (the motion.div / Link) is inline-flex or flex-shrink-0 so
items don’t wrap), keeping existing getIcon, getIsActive, motion
layoutId="settingsActiveTabIndicatorMobile" and Link usage intact.
- Line 222: The CSS utility class safe-area-bottom is used in the components
SettingsNav (settings-nav.tsx) and FormTabNav (form-tab-nav.tsx) but is not
defined; add a small utility in the global stylesheet (src/app/globals.css)
inside `@layer` utilities that defines .safe-area-bottom to apply padding-bottom:
env(safe-area-inset-bottom) so iOS safe areas are honored by those components.
In `@src/app/`[locale]/settings/_components/ui/settings-ui.tsx:
- Around line 473-474: The SettingsFormActions component currently hardcodes
default button labels ("Save Changes", "Cancel") which violates i18n rules;
update SettingsFormActions to import and call useTranslations (e.g., const t =
useTranslations("common")) and resolve labels as resolvedSaveLabel = saveLabel
?? t("actions.save") and resolvedCancelLabel = cancelLabel ??
t("actions.cancel"), removing the English literal defaults so callers may
override or receive localized fallbacks; ensure you update the
SettingsFormActionsProps to make saveLabel and cancelLabel optional and remove
the hardcoded default values.
In `@src/app/`[locale]/settings/client-versions/page.tsx:
- Line 8: 当前页面引入并使用了 SettingsSection,但项目中其他 9 个设置页面一致使用 Section;请将 import
和使用替换为统一的 Section(替换 SettingsSection → Section,修改 import 来源为
"@/components/section"),并调整该页面中使用该组件的 props 和 heading 层级以匹配其他设置页面的用法(检查并对齐
title/description/children 等 prop 名称和行为),确保渲染结果与其余设置页面一致。
In `@src/app/`[locale]/settings/data/_components/database-export.tsx:
- Around line 57-61: The Button in database-export.tsx uses a hardcoded brand
color class "bg-[`#E25706`]" (and similar occurrences across the repo); add a CSS
custom property for the brand color inside the existing `@theme` inline block in
globals.css (e.g., --color-brand: `#E25706`;) and expose it via your Tailwind
theme as utility names (e.g., bg-brand, text-brand, ring-brand), then replace
className uses like "bg-[`#E25706`]" and "hover:bg-[`#E25706`]/90" in the Button
component (and all other instances) with the new utilities; update any related
focus/ring/text/icon classes to use the new brand utilities so the color is
centralized and modifiable from globals.css.
In `@src/app/`[locale]/settings/data/_components/log-cleanup-panel.tsx:
- Around line 131-137: The current runtime string parsing of
t("descriptionWarning") is fragile for i18n; instead add two distinct
translation keys (e.g. "descriptionText" and "descriptionWarningText") in all
locale files and update the component to stop using
t("descriptionWarning").split(...) and directly render t("descriptionText") for
the main text and t("descriptionWarningText") inside the <strong> element
(replace the split logic in the component that currently references
t("descriptionWarning")) so translations no longer depend on a hard-coded
"Note:" delimiter.
In
`@src/app/`[locale]/settings/notifications/_components/webhook-targets-section.tsx:
- Line 8: 移除未使用的 cn 导入:在组件(在该文件顶部有 import { cn } from
"@/lib/utils";)中删除这条未被引用的导入语句(或如果后续需要样式合并改为实际使用 cn),确保没有其他对 cn 的引用(例如在
WebhookTargetsSection 或同文件导出的任何函数/组件中),然后保存以通过 CI 的 unused import 检查。
In `@src/app/`[locale]/settings/sensitive-words/_components/word-list-table.tsx:
- Around line 124-126: The date formatting in the WordListTable component is
hardcoded to "zh-CN"; update it to use the current locale from the useLocale
hook instead: import and call useLocale inside the component (e.g., in the
WordListTable or the parent component rendering the table), store the returned
locale value, and pass that locale to new
Date(word.createdAt).toLocaleString(locale) (or equivalent) where createdAt is
formatted; ensure the change references the component/function name
WordListTable and the createdAt rendering cell so the locale is applied
consistently.
♻️ Duplicate comments (15)
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx (2)
81-84: 避免将 NaN 写入超时状态。数字输入在编辑过程中可能出现中间态(如 "-"),
parseInt会返回NaN并写入 state。建议在写入前添加 NaN 保护。建议修改
onChange={(e) => { const val = e.target.value; - onChange(val === "" ? undefined : parseInt(val, 10)); + const parsed = parseInt(val, 10); + onChange(val === "" ? undefined : Number.isNaN(parsed) ? undefined : parsed); }}
95-97: 超时单位 "s" 应使用 i18n。硬编码的 "s" 是用户可见文本,应替换为翻译键以支持多语言。
建议修改
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> - s + {t("common.secondsShort")} </span>src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx (3)
310-310: SectionCard 标题应使用通用名称当前 SectionCard 标题使用了第一个字段的标签 "Forward client IP",但该区块包含多个设置项(Cache TTL、1M Context Window)。建议使用更通用的标题如 "Advanced Settings"。
341-343: SelectValue 占位符 "inherit" 需本地化多处
SelectValue的placeholder直接使用硬编码的"inherit",应改用翻译键如t("common.inherit")。此问题同样出现在 lines 370-372, 412-414, 451-453, 475-477, 501-503。
9-9: 移除未使用的导入LabelPipeline 检测到
Label导入未被使用,需要移除以通过 lint 检查。建议修复
import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Select,src/app/[locale]/settings/providers/_components/forms/provider-form/sections/limits-section.tsx (4)
150-194: 单位 "USD" 硬编码需改用 i18n多处
LimitCard组件的unit属性直接使用硬编码的"USD",违反了编码规范中"所有用户可见字符串需走 i18n"的要求。
229-243: 占位符 "00:00" 硬编码需本地化
placeholder="00:00"是用户可见文本,应通过翻译键输出。
351-353: 单位 "min" 硬编码需改用 i18n时间单位
min应使用翻译键,如t("sections.common.units.minutes")。
295-326: 断路器数值输入缺少非负/整数校验当前使用
parseInt直接入库,负数或小数可写入状态。建议在 dispatch 前做非负/整数校验,与min约束保持一致。其他断路器字段(openDurationMinutes、halfOpenSuccessThreshold、maxRetryAttempts)同样存在此问题。src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx (2)
18-28: 预设 providerType 仍包含已弃用类型UI 已移除
claude-auth/gemini-cli,这里仍允许会导致类型不一致。建议使用ProviderType或排除弃用值的ActiveProviderType。Proposed fix
-import type { ProviderDisplay } from "@/types/provider"; +import type { ProviderDisplay, ProviderType } from "@/types/provider"; +type ActiveProviderType = Exclude<ProviderType, "claude-auth" | "gemini-cli">; @@ - providerType?: - | "claude" - | "claude-auth" - | "codex" - | "gemini" - | "gemini-cli" - | "openai-compatible"; + providerType?: ActiveProviderType; @@ - providerType?: - | "claude" - | "claude-auth" - | "codex" - | "gemini" - | "gemini-cli" - | "openai-compatible"; + providerType?: ActiveProviderType;Also applies to: 295-305
3-5: 移除未使用的导入
PROVIDER_TIMEOUT_DEFAULTS未被使用,且 CI 已报 unused import。Proposed fix
-import { PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (4)
180-185: 编辑模式下不应提交空 key当前会发送
key: "",后端验证会失败。应仅在有值时附带 key。Proposed fix
- const formData = { + const trimmedKey = state.basic.key.trim(); + const formData = { name: state.basic.name.trim(), url: state.basic.url.trim(), - key: state.basic.key.trim() || (isEdit ? "" : ""), + ...(trimmedKey ? { key: trimmedKey } : {}),
234-242: 创建成功后建议重置表单否则连续新增会复用上一轮的路由/限额配置。
Proposed fix
toast.success(t("success.created")); + dispatch({ type: "RESET_FORM" }); } onSuccess?.();
51-58: urlResolver 结果未落地且参数未使用当前 effect 丢弃 resolved URL,
hideUrl为 true 时用户无法修正;同时ProviderFormContent的urlResolver参数未使用,lint 会失败。建议将 resolved URL 写回 preset(或移动到内容层并 dispatch 设置 URL),并补错误处理。Proposed fix
-function ProviderFormContent({ - onSuccess, - urlResolver, - autoUrlPending, -}: { - onSuccess?: () => void; - urlResolver?: (providerType: ProviderType) => Promise<string | null>; - autoUrlPending: boolean; -}) { +function ProviderFormContent({ + onSuccess, + autoUrlPending, +}: { + onSuccess?: () => void; + autoUrlPending: boolean; +}) { @@ export function ProviderForm({ @@ }: ProviderFormProps) { const [groupSuggestions, setGroupSuggestions] = useState<string[]>([]); const [autoUrlPending, setAutoUrlPending] = useState(false); + const [resolvedPreset, setResolvedPreset] = useState(preset); + + useEffect(() => { + setResolvedPreset(preset); + }, [preset]); @@ useEffect(() => { if (urlResolver && preset?.providerType && !preset?.url) { setAutoUrlPending(true); urlResolver(preset.providerType) .then((resolvedUrl) => { if (resolvedUrl) { - // URL will be set through preset in createInitialState + setResolvedPreset((prev) => ({ + ...(prev ?? preset), + url: resolvedUrl, + })); } }) + .catch((e) => { + console.error("Failed to resolve provider URL:", e); + }) .finally(() => { setAutoUrlPending(false); }); } }, [urlResolver, preset?.providerType, preset?.url]); @@ <ProviderFormProvider @@ - preset={preset} + preset={resolvedPreset} groupSuggestions={groupSuggestions} > <ProviderFormContent onSuccess={onSuccess} - urlResolver={urlResolver} autoUrlPending={autoUrlPending} /> </ProviderFormProvider>Also applies to: 493-542, 511-523
19-24: 清理未使用的导入以通过 lint
PROVIDER_DEFAULTS、TAB_CONFIG、createInitialState未被使用。Proposed fix
-import { PROVIDER_DEFAULTS } from "@/lib/constants/provider.constants"; @@ -import { FormTabNav, TAB_CONFIG } from "./components/form-tab-nav"; -import { createInitialState, ProviderFormProvider, useProviderForm } from "./provider-form-context"; +import { FormTabNav } from "./components/form-tab-nav"; +import { ProviderFormProvider, useProviderForm } from "./provider-form-context";
🟡 Minor comments (13)
src/app/[locale]/settings/data/_components/database-status.tsx-85-87 (1)
85-87: 刷新按钮缺少无障碍标签。按钮仅包含图标,屏幕阅读器用户无法理解其用途。建议添加
aria-label或使用sr-only文本。🔧 建议修复
- <Button variant="ghost" size="sm" onClick={fetchStatus} className="h-8"> + <Button variant="ghost" size="sm" onClick={fetchStatus} className="h-8" aria-label={t("refresh")}> <RefreshCw className="h-3.5 w-3.5" /> </Button>需要在 i18n 文件中添加
refresh翻译键。src/app/[locale]/settings/config/_components/system-settings-form.tsx-213-223 (1)
213-223: 无障碍性问题: Switch 缺少正确的 label 关联标题使用
<p>标签而非<Label>组件,导致屏幕阅读器无法将标题与 Switch 控件正确关联。建议用<Label htmlFor="allow-global-usage">包裹标题文本,或将整个文本区域包裹在可点击的 Label 内以提升可访问性和用户体验(点击标题可切换开关)。此问题在所有卡片式 toggle 块中重复出现(Lines 213-223, 232-244, 253-263, 272-286, 295-309, 318-332, 342-354 等)。
♿ 建议修复
- <div> - <p className="text-sm font-medium text-foreground">{t("allowGlobalView")}</p> - <p className="text-xs text-muted-foreground mt-0.5">{t("allowGlobalViewDesc")}</p> - </div> + <Label htmlFor="allow-global-usage" className="cursor-pointer"> + <p className="text-sm font-medium text-foreground">{t("allowGlobalView")}</p> + <p className="text-xs text-muted-foreground mt-0.5">{t("allowGlobalViewDesc")}</p> + </Label>src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx-448-454 (1)
448-454: 优先级输入缺少边界校验。
Number(e.target.value)在某些边缘情况下可能产生NaN。建议添加基础校验:🛡️ 建议的防御性改进
<DarkInput id="filter-priority" type="number" value={priority} - onChange={(e) => setPriority(Number(e.target.value))} + onChange={(e) => { + const val = Number(e.target.value); + setPriority(Number.isNaN(val) ? 0 : val); + }} className="w-24" />src/app/[locale]/settings/logs/_components/log-level-form.tsx-74-94 (1)
74-94: 选项文本的分隔符应可本地化。当前使用固定的
" - "拼接label和description,这属于用户可见文本,且不同语言可能需要不同的顺序或分隔方式。建议用 i18n 模板键统一处理,并在五种语言中补充该键。建议修改
- <option key={level.value} value={level.value}> - {level.label} - {level.description} - </option> + <option key={level.value} value={level.value}> + {t("form.levelOption", { label: level.label, description: level.description })} + </option>As per coding guidelines, ...
src/app/[locale]/dashboard/_components/bento/metric-card.tsx-128-159 (1)
128-159: 动画 Effect 缺少清理函数,可能导致内存泄漏当组件在动画进行中卸载时,
requestAnimationFrame的回调会继续执行,尝试调用setDisplayValue和setIsAnimating,可能导致 "Can't perform a React state update on an unmounted component" 警告或内存泄漏。建议添加清理逻辑
useEffect(() => { if (typeof value === "number" && typeof prevValueRef.current === "number") { if (value !== prevValueRef.current) { setIsAnimating(true); const duration = 400; const startValue = prevValueRef.current; const diff = value - startValue; const startTime = Date.now(); + let cancelled = false; const animate = () => { + if (cancelled) return; const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = 1 - (1 - progress) ** 3; const currentValue = startValue + diff * easeProgress; setDisplayValue(currentValue); if (progress < 1) { requestAnimationFrame(animate); } else { setIsAnimating(false); prevValueRef.current = value; } }; requestAnimationFrame(animate); + return () => { + cancelled = true; + }; } } else { setDisplayValue(value); prevValueRef.current = value; } }, [value]);src/app/[locale]/dashboard/_components/bento/live-sessions-panel.tsx-19-37 (1)
19-37: completed 状态未映射到 done,可能导致状态显示偏差
ActiveSessionInfo.status允许 "completed",但当前逻辑不会返回 "done",完成态会落到 idle/init。建议显式处理 completed。修改示例
if (session.status === "error" || (session as { status?: string }).status === "error") { return "error"; } + if (session.status === "completed") { + return "done"; + }src/app/[locale]/settings/_components/page-transition.tsx-14-27 (1)
14-27:exit动画定义了但不会生效,因为缺少AnimatePresence包装器。组件在第 19 行定义了
exit属性,但在settings/layout.tsx中使用PageTransition时没有被AnimatePresence包装。Framer Motion 的退出动画只有在组件被AnimatePresence包裹时才会触发,因此这个exit属性目前是不生效的。建议要么删除不必要的exit属性,要么在父布局中添加AnimatePresence包装。src/app/[locale]/settings/notifications/_components/global-settings-card.tsx-3-3 (1)
3-3: 移除未使用的Power导入。Pipeline 失败日志显示
Power图标已导入但未使用。仅保留Bell导入即可。建议修复
-import { Bell, Power } from "lucide-react"; +import { Bell } from "lucide-react";src/app/[locale]/settings/notifications/_components/notification-type-card.tsx-204-206 (1)
204-206: 数字输入缺少边界验证。
Number(e.target.value)在输入为空或无效时会返回NaN。虽然 HTMLmin/max属性提供了浏览器端约束,但建议在onChange中添加 JavaScript 验证以确保数据完整性。🛡️ 建议添加输入验证
onChange={(e) => - onUpdateSettings({ dailyLeaderboardTopN: Number(e.target.value) }) + { + const val = Number(e.target.value); + if (!Number.isNaN(val) && val >= 1 && val <= 20) { + onUpdateSettings({ dailyLeaderboardTopN: val }); + } + } }src/app/[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx-45-46 (1)
45-46: 移除未使用的变量。
displayValue已定义但从未使用,导致 lint 失败。请移除或使用该变量。建议修改
- const displayValue = value ?? defaultValue; const isCustom = value !== undefined;src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx-143-144 (1)
143-144: 日期格式化硬编码了中文区域设置。
toLocaleString("zh-CN")违反了 i18n 指南。应使用用户当前的 locale 来格式化日期。建议修改
+import { useLocale } from "next-intl"; + export function RuleListTable({ rules }: RuleListTableProps) { const t = useTranslations("settings"); + const locale = useLocale(); // ... - <p className="text-[10px] text-muted-foreground/60 mt-1"> - {new Date(rule.createdAt).toLocaleString("zh-CN")} - </p> + <p className="text-[10px] text-muted-foreground/60 mt-1"> + {new Date(rule.createdAt).toLocaleString(locale)} + </p>src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx-32-48 (1)
32-48: unused prop 建议处理
allowedProviderTypes在参数解构中未使用会触发 lint。可暂时从解构中移除,或在内部逻辑中使用。Also applies to: 480-491
src/app/[locale]/settings/notifications/_components/binding-selector.tsx-243-291 (1)
243-291: 时区占位符需要 i18nplaceholder "Asia/Shanghai" 属于用户可见文本,建议改为翻译 key 并补齐多语言。As per coding guidelines, ...
Proposed fix
- placeholder="Asia/Shanghai" + placeholder={t("notifications.bindings.scheduleTimezonePlaceholder")}
🧹 Nitpick comments (23)
src/app/[locale]/settings/sensitive-words/_components/edit-word-dialog.tsx (1)
97-97: 建议将硬编码颜色提取为主题变量。在三个组件中使用了相同的硬编码颜色
#E25706(第 97、105、127 行)。建议将此品牌色定义为 CSS 变量或 Tailwind 主题颜色,以便:
- 支持深色/浅色主题适配
- 便于全局维护和统一调整
♻️ 建议的重构方案
在
tailwind.config.ts中定义主题颜色后,可以这样使用:- className="bg-muted/50 border border-border rounded-lg focus:border-[`#E25706`]/50 focus:ring-[`#E25706`]/20" + className="bg-muted/50 border border-border rounded-lg focus:border-primary/50 focus:ring-primary/20"或者如果该颜色用于特定品牌标识,可在 CSS 变量中定义:
:root { --brand-accent: `#E25706`; }然后使用:
- focus:border-[`#E25706`]/50 focus:ring-[`#E25706`]/20 + focus:border-(--brand-accent)/50 focus:ring-(--brand-accent)/20Also applies to: 105-105, 127-127
src/app/[locale]/settings/prices/_components/model-price-drawer.tsx (1)
332-332: 建议将重复的样式类提取为共享常量或 CSS 变量。多个 Input 和 Select 组件使用相同的样式模式
bg-white/[0.02] border-white/10 focus:border-[#E25706]/50。考虑提取为共享常量以提高可维护性。♻️ 可选重构方案
// 在文件顶部定义共享样式 const INPUT_BASE_CLASSES = "bg-white/[0.02] border-white/10 focus:border-[`#E25706`]/50"; // 使用示例 <Input className={`pl-9 ${INPUT_BASE_CLASSES}`} />Also applies to: 405-405, 417-417, 429-429, 447-447
src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx (1)
176-184: 考虑使用更稳定的 key。当前使用数组索引作为 key,对于测试结果的静态展示是可接受的。但如果
warnings数组将来可能动态变化或重新排序,建议考虑使用更稳定的标识符(如 warning 内容的 hash 或组合索引)以避免 React 重渲染问题。鉴于当前场景是一次性展示测试结果,此问题优先级较低。
src/app/[locale]/settings/sensitive-words/_components/word-list-table.tsx (1)
66-66: 硬编码的颜色值假定了深色主题,可能在浅色模式下显示异常。以下样式使用了硬编码的黑/白透明度值,这些样式假定了深色背景:
bg-black/10(第 66 行)bg-white/[0.03](第 77 行)hover:bg-white/[0.02](第 102 行)bg-black/30(第 105 行)hover:bg-white/10(第 133、141 行)建议使用主题感知的颜色变量(如
bg-muted、hover:bg-muted)以确保在浅色和深色模式下都能正常显示。Also applies to: 77-77, 102-102, 105-105, 133-133, 141-141
src/app/[locale]/settings/data/_components/database-status.tsx (1)
93-106: 玻璃态样式在浅色主题下可能不可见。
bg-white/[0.02]和border-white/5的透明度极低,在浅色模式下卡片可能几乎看不见。如果需要支持浅色主题,建议使用主题感知的背景色。♻️ 可选的主题感知方案
- <div className="p-4 rounded-xl bg-white/[0.02] border border-white/5"> + <div className="p-4 rounded-xl bg-muted/50 border border-border/50">或者使用
dark:前缀区分主题:- <div className="p-4 rounded-xl bg-white/[0.02] border border-white/5"> + <div className="p-4 rounded-xl bg-black/[0.02] dark:bg-white/[0.02] border border-black/5 dark:border-white/5">src/app/[locale]/settings/config/_components/system-settings-form.tsx (1)
208-224: 建议: 卡片样式存在主题兼容性隐患硬编码的
bg-white/[0.02]和border-white/5针对深色主题优化,在浅色主题下可能几乎不可见。建议使用 Tailwind 的语义化颜色类(如bg-muted/50或bg-card)以确保跨主题兼容性。此外,重复的卡片 UI 模式(图标 + 标题 + 描述 + Switch)可考虑提取为可复用组件以减少代码重复。
♻️ 建议提取的 SettingToggleCard 组件
interface SettingToggleCardProps { id: string; icon: React.ReactNode; iconBgClass: string; title: string; description: string; checked: boolean; onCheckedChange: (checked: boolean) => void; disabled?: boolean; } function SettingToggleCard({ id, icon, iconBgClass, title, description, checked, onCheckedChange, disabled, }: SettingToggleCardProps) { return ( <div className="p-4 rounded-xl bg-muted/30 border border-border/50 flex items-center justify-between hover:bg-muted/50 transition-colors"> <Label htmlFor={id} className="flex items-start gap-3 cursor-pointer flex-1"> <div className={`w-8 h-8 flex items-center justify-center rounded-lg ${iconBgClass} shrink-0`}> {icon} </div> <div> <p className="text-sm font-medium text-foreground">{title}</p> <p className="text-xs text-muted-foreground mt-0.5">{description}</p> </div> </Label> <Switch id={id} checked={checked} onCheckedChange={onCheckedChange} disabled={disabled} /> </div> ); }Also applies to: 227-245, 248-264, 267-287, 290-310, 313-333
src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx (1)
47-114: 建议将DarkInput和DarkTextarea提取为共享组件。根据 PR 摘要,这些暗色主题的输入组件在 provider form 等多处使用。建议:
- 提取到共享位置:如
@/components/ui/dark-input.tsx,避免代码重复- 扩展 props 支持:当前缺少
disabled、name、autoComplete、maxLength等常用属性♻️ 建议的改进方案
// `@/components/ui/dark-input.tsx` import { cn } from "@/lib/utils"; import { forwardRef, type InputHTMLAttributes } from "react"; export const DarkInput = forwardRef< HTMLInputElement, InputHTMLAttributes<HTMLInputElement> >(({ className, ...props }, ref) => { return ( <input ref={ref} className={cn( "w-full bg-muted/50 border border-border rounded-lg py-2 px-3 text-sm text-foreground", "placeholder:text-muted-foreground/50", "focus:border-[`#E25706`] focus:ring-1 focus:ring-[`#E25706`] outline-none transition-all", "disabled:cursor-not-allowed disabled:opacity-50", className )} {...props} /> ); }); DarkInput.displayName = "DarkInput";src/app/[locale]/settings/_components/settings-nav.tsx (1)
65-68:getIcon函数中的 fallback 逻辑可简化。由于
ICON_MAP覆盖了所有SettingsNavIconName值,当iconName有效时ICON_MAP[iconName]必然返回有效组件,|| Settings仅在类型扩展后未同步更新 map 时才会触发。当前实现作为防御性编码是可接受的。♻️ 可选:移除冗余 fallback
const getIcon = (iconName?: SettingsNavIconName): LucideIcon => { if (!iconName) return Settings; - return ICON_MAP[iconName] || Settings; + return ICON_MAP[iconName]; };src/app/[locale]/settings/logs/_components/log-level-form.tsx (1)
3-10: 复用共享 LogLevel 类型以避免漂移。项目里已有
src/lib/logger.ts的LogLevel定义,本地重复声明容易与日志系统的真实取值范围不同步。建议直接复用共享类型。建议修改
-import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils"; +import type { LogLevel } from "@/lib/logger"; -type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";src/app/[locale]/dashboard/_components/bento/bento-grid.tsx (1)
56-99: BentoCard 作为 button 时缺少无障碍属性当
onClick存在时,组件渲染为<button>,但缺少type="button"属性。在表单上下文中,未指定 type 的 button 默认为submit,可能导致意外的表单提交行为。建议修复
return ( <Component onClick={onClick} + {...(onClick ? { type: "button" } : {})} className={cn(src/app/[locale]/dashboard/_components/bento/metric-card.tsx (1)
173-197: 与 BentoCard 相同的无障碍问题当
onClick存在时渲染为<button>,建议添加type="button"防止意外表单提交。建议修复
return ( <Component onClick={onClick} + {...(onClick ? { type: "button" } : {})} className={cn(src/app/[locale]/dashboard/_components/bento/leaderboard-card.tsx (1)
10-16: 考虑复用 repository 中的类型定义
LeaderboardEntry接口与src/repository/leaderboard.ts中的定义相似,但id类型不同(string | numbervsnumber)。如果这是有意为之以支持更灵活的 ID 类型,建议添加注释说明;否则考虑从 repository 导入并扩展。src/app/[locale]/dashboard/_components/dashboard-bento-sections.tsx (1)
13-27: 考虑为 getSystemSettings 添加错误处理当前实现中,如果
getCachedSystemSettings()抛出异常,整个页面会报错。虽然getUserStatistics的结果通过.ok检查进行了处理,但系统设置获取失败时没有降级策略。建议添加 try-catch 或提供默认值
export async function DashboardBentoSection({ isAdmin }: DashboardBentoSectionProps) { - const [systemSettings, statistics] = await Promise.all([ - getCachedSystemSettings(), - getUserStatistics(DEFAULT_TIME_RANGE), - ]); + const [systemSettings, statistics] = await Promise.all([ + getCachedSystemSettings().catch(() => ({ + currencyDisplay: "USD" as const, + allowGlobalUsageView: false, + })), + getUserStatistics(DEFAULT_TIME_RANGE), + ]); return ( <DashboardBento或者,如果系统设置是关键依赖,保持当前行为让错误冒泡到 error boundary 也是合理的选择。
src/app/[locale]/settings/notifications/_components/webhook-target-card.tsx (2)
82-86: 考虑使用主题感知的颜色 token。硬编码的
white/[0.02]、white/5、white/10等颜色值在浅色主题下可能无法正常显示。建议使用bg-card/50、border-border/50等主题感知的 token,与src/components/section.tsx中的样式保持一致。建议修复
<div className={cn( - "p-4 rounded-xl bg-white/[0.02] border border-white/5", - "hover:bg-white/[0.04] hover:border-white/10 transition-colors" + "p-4 rounded-xl bg-card/50 border border-border/50", + "hover:bg-card/60 hover:border-border transition-colors" )} >
153-155: 同样的主题兼容性问题。
border-white/5在浅色主题下可能不可见,建议改用border-border/50。src/app/[locale]/settings/_components/settings-page-header.tsx (2)
77-85: 图标属性 API 不一致。
SettingsPageHeader使用字符串名称 (PageHeaderIconName),而SettingsSectionHeader直接接受LucideIcon组件。这种不一致可能导致使用时的困惑。建议统一采用字符串名称模式以保持与Section组件的一致性。
104-108: 使用主题感知的背景色。
bg-white/5在浅色主题下可能不可见。建议使用bg-muted/50或类似的主题感知 token。建议修复
{Icon && ( - <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-white/5 shrink-0 mt-0.5"> + <div className="flex items-center justify-center w-8 h-8 rounded-lg bg-muted/50 shrink-0 mt-0.5"> <Icon className={cn("h-4 w-4", iconColor)} /> </div> )}src/app/[locale]/settings/request-filters/page.tsx (1)
22-22: 硬编码的颜色值。
text-[#E25706]是硬编码的十六进制颜色。如果这是品牌色或特定设计 token,建议在 Tailwind 配置中定义为语义化变量(如text-filter-icon),以便统一管理。src/app/[locale]/settings/sensitive-words/page.tsx (1)
24-29: 图标属性集成正确。图标和颜色属性的添加符合
Section组件的 API 规范。不过variant="default"是冗余的,因为"default"已是默认值,可以省略。♻️ 可选:移除冗余的 variant 属性
<Section title={t("sensitiveWords.section.title")} description={t("sensitiveWords.section.description")} icon="shield-alert" iconColor="text-primary" - variant="default" actions={src/app/[locale]/settings/notifications/_components/notification-type-card.tsx (1)
159-159: 避免使用as any类型断言。此处的
as any绕过了 TypeScript 类型检查。建议使用更精确的类型或调整onUpdateSettings的类型定义以正确推断动态键。♻️ 建议的改进方案
- onCheckedChange={(checked) => onUpdateSettings({ [meta.enabledKey]: checked } as any)} + onCheckedChange={(checked) => onUpdateSettings({ [meta.enabledKey]: checked } as Partial<NotificationSettingsState>)}src/app/[locale]/settings/client-versions/page.tsx (1)
42-43: 考虑使用主题变量替代硬编码颜色。
text-[#E25706]是硬编码的十六进制颜色。如果这是品牌色,建议将其添加到 Tailwind 主题配置中或使用 CSS 变量,以便于全局维护和主题切换。src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx (1)
153-171: 建议改进键盘用户的可访问性。操作按钮通过 hover 显示,但键盘用户在 tab 聚焦时无法看到它们。建议添加
group-focus-within:opacity-100以改善键盘导航体验。建议修改
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> + <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (1)
547-547: 建议移除默认导出,保留具名导出当前同时存在具名与默认导出,和代码规范偏好不一致。As per coding guidelines, ...
Proposed fix
-export default ProviderForm;
| const formatDate = (dateStr: string) => { | ||
| const date = new Date(dateStr); | ||
| if (data.resolution === "hour") { | ||
| return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); | ||
| } | ||
| return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); | ||
| }; | ||
|
|
||
| const formatTooltipDate = (dateStr: string) => { | ||
| const date = new Date(dateStr); | ||
| if (data.resolution === "hour") { | ||
| return date.toLocaleString("zh-CN", { | ||
| month: "long", | ||
| day: "numeric", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| }); | ||
| } | ||
| return date.toLocaleDateString("zh-CN", { | ||
| year: "numeric", | ||
| month: "long", | ||
| day: "numeric", | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
日期格式化使用了硬编码的 "zh-CN" locale
formatDate 和 formatTooltipDate 函数中硬编码了 "zh-CN" locale,违反了 i18n 要求。应用支持 5 种语言(zh-CN, zh-TW, en, ja, ru),日期格式应根据用户当前 locale 动态确定。
建议使用 next-intl 的 locale 或 useLocale hook
+import { useLocale } from "next-intl";
export function StatisticsChartCard({
// ...
}: StatisticsChartCardProps) {
const t = useTranslations("dashboard.statistics");
+ const locale = useLocale();
// ...
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
if (data.resolution === "hour") {
- return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
+ return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
}
- return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" });
+ return date.toLocaleDateString(locale, { month: "numeric", day: "numeric" });
};
const formatTooltipDate = (dateStr: string) => {
const date = new Date(dateStr);
if (data.resolution === "hour") {
- return date.toLocaleString("zh-CN", {
+ return date.toLocaleString(locale, {
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
- return date.toLocaleDateString("zh-CN", {
+ return date.toLocaleDateString(locale, {
year: "numeric",
month: "long",
day: "numeric",
});
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const formatDate = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| if (data.resolution === "hour") { | |
| return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); | |
| } | |
| return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); | |
| }; | |
| const formatTooltipDate = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| if (data.resolution === "hour") { | |
| return date.toLocaleString("zh-CN", { | |
| month: "long", | |
| day: "numeric", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| }); | |
| } | |
| return date.toLocaleDateString("zh-CN", { | |
| year: "numeric", | |
| month: "long", | |
| day: "numeric", | |
| }); | |
| }; | |
| import { useLocale } from "next-intl"; | |
| export function StatisticsChartCard({ | |
| // ... | |
| }: StatisticsChartCardProps) { | |
| const t = useTranslations("dashboard.statistics"); | |
| const locale = useLocale(); | |
| // ... | |
| const formatDate = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| if (data.resolution === "hour") { | |
| return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }); | |
| } | |
| return date.toLocaleDateString(locale, { month: "numeric", day: "numeric" }); | |
| }; | |
| const formatTooltipDate = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| if (data.resolution === "hour") { | |
| return date.toLocaleString(locale, { | |
| month: "long", | |
| day: "numeric", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| }); | |
| } | |
| return date.toLocaleDateString(locale, { | |
| year: "numeric", | |
| month: "long", | |
| day: "numeric", | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx
around lines 151 - 174, The date formatting functions formatDate and
formatTooltipDate currently hardcode "zh-CN"; change them to read the current
locale (e.g., from next-intl or useLocale) and pass that locale string into
toLocaleDateString/toLocaleTimeString/toLocaleString instead of "zh-CN"; keep
the same options shapes (hour/minute/month/day/year) but use the dynamic locale
value so the app supports zh-CN, zh-TW, en, ja, ru appropriately.
| import { Suspense } from "react"; | ||
| import { hasPriceTable } from "@/actions/model-prices"; | ||
| import { redirect } from "@/i18n/routing"; | ||
| import { getSession } from "@/lib/auth"; | ||
| import { DashboardBentoSection } from "./_components/dashboard-bento-sections"; | ||
| import { DashboardOverviewSkeleton } from "./_components/dashboard-skeletons"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| export default async function DashboardPage({ params }: { params: Promise<{ locale: string }> }) { | ||
| const { locale } = await params; | ||
|
|
||
| const hasPrices = await hasPriceTable(); | ||
| if (!hasPrices) { | ||
| return redirect({ href: "/settings/prices?required=true", locale }); | ||
| } | ||
|
|
||
| const session = await getSession(); | ||
| const isAdmin = session?.user?.role === "admin"; | ||
|
|
||
| return ( | ||
| <Suspense fallback={<DashboardOverviewSkeleton />}> | ||
| <DashboardBentoSection isAdmin={isAdmin} /> | ||
| </Suspense> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 检查 dashboard 目录下的所有文件
find src/app -type d -name dashboard | head -5Repository: ding113/claude-code-hub
Length of output: 190
🏁 Script executed:
#!/bin/bash
# 列出 dashboard 目录中的所有 page* 文件
find src/app -path "*dashboard*" -name "page*" -type f | head -20Repository: ding113/claude-code-hub
Length of output: 190
🏁 Script executed:
#!/bin/bash
# 检查是否存在 .bento.tsx 模式的其他文件
fd -t f '\.bento\.tsx$' src/ || echo "No .bento.tsx files found beyond the one mentioned"Repository: ding113/claude-code-hub
Length of output: 108
🏁 Script executed:
#!/bin/bash
# 查看 dashboard 目录的完整结构
ls -la src/app/*/dashboard/ 2>/dev/null || ls -la src/app/[locale]/dashboard/ 2>/dev/null || find src/app -type d -name dashboard -exec ls -la {} \;Repository: ding113/claude-code-hub
Length of output: 1022
🏁 Script executed:
#!/bin/bash
# 比较两个文件的内容
echo "=== page.tsx 内容 ===" && cat -n src/app/[locale]/dashboard/page.tsx
echo ""
echo "=== page.bento.tsx 内容 ===" && cat -n src/app/[locale]/dashboard/page.bento.tsxRepository: ding113/claude-code-hub
Length of output: 2239
🏁 Script executed:
#!/bin/bash
# 确认两个文件是否完全相同
if diff src/app/[locale]/dashboard/page.tsx src/app/[locale]/dashboard/page.bento.tsx > /dev/null; then
echo "文件内容完全相同"
else
echo "文件内容有区别:"
diff src/app/[locale]/dashboard/page.tsx src/app/[locale]/dashboard/page.bento.tsx
fiRepository: ding113/claude-code-hub
Length of output: 75
🏁 Script executed:
#!/bin/bash
# 检查项目中是否有 .bento 相关的配置或说明
grep -r "bento" --include="*.ts" --include="*.tsx" --include="*.md" --include="*.json" src/ 2>/dev/null | head -20 || echo "未找到 bento 相关配置"Repository: ding113/claude-code-hub
Length of output: 1338
删除重复的 page.bento.tsx 文件或澄清其用途
文件 page.bento.tsx 与 page.tsx 内容完全相同,但 Next.js 只会识别 page.tsx 作为路由文件,不识别 .bento.tsx 后缀。page.bento.tsx 是无效的、不会被执行的死代码。
应选择以下之一:
- 删除
page.bento.tsx文件 - 如果有特殊用途(如 A/B 测试或渐进迁移),在文件顶部添加明确的注释说明,并调整 Next.js 路由配置使其生效
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/page.bento.tsx around lines 1 - 26, 当前
page.bento.tsx 文件是与 page.tsx 完全相同的冗余死代码(Next.js 不认 .bento.tsx 为路由),请删除
page.bento.tsx 或在文件顶部添加明确用途注释(例如用于 A/B 测试/渐进迁移)并更新路由配置以确保 Next.js
能识别该变体;定位符可参考导出函数名 DashboardPage 以及文件名 page.bento.tsx 和 page.tsx 来做出删除或改动决定。
| {/* Mobile: Bottom Navigation */} | ||
| <nav className="flex md:hidden fixed bottom-0 left-0 right-0 z-50 border-t border-border/50 bg-card/95 backdrop-blur-md safe-area-bottom"> | ||
| <div className="flex items-center justify-around w-full px-2 py-1"> | ||
| {internalItems.slice(0, 5).map((item) => { | ||
| const Icon = getIcon(item.iconName); | ||
| const isActive = getIsActive(item.href); | ||
|
|
||
| return ( | ||
| <motion.div | ||
| key={item.href} | ||
| whileTap={{ scale: 0.9 }} | ||
| transition={{ type: "spring", stiffness: 400, damping: 25 }} | ||
| > | ||
| <Link | ||
| href={item.href} | ||
| className={cn( | ||
| "relative flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg transition-colors duration-200", | ||
| "hover:bg-accent/50 focus-visible:outline-none", | ||
| isActive ? "text-primary" : "text-muted-foreground" | ||
| )} | ||
| > | ||
| <motion.div | ||
| animate={{ scale: isActive ? 1.1 : 1 }} | ||
| transition={{ type: "spring", stiffness: 400, damping: 25 }} | ||
| > | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| strokeWidth={2} | ||
| d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" | ||
| <Icon className={cn("h-5 w-5", isActive && "text-primary")} /> | ||
| </motion.div> | ||
| <span className="text-[10px] font-medium truncate max-w-[48px]"> | ||
| {item.label} | ||
| </span> | ||
| {isActive && ( | ||
| <motion.div | ||
| layoutId="settingsActiveTabIndicatorMobile" | ||
| className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-4 h-1 bg-primary rounded-full" | ||
| transition={{ type: "spring", stiffness: 500, damping: 30 }} | ||
| /> | ||
| </svg> | ||
| </a> | ||
| ) : ( | ||
| // Internal link: use i18n Link | ||
| <Link href={item.href} className={linkClassName}> | ||
| {item.label} | ||
| )} | ||
| </Link> | ||
| )} | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> | ||
| <div className="mt-3 rounded-lg border border-dashed border-border/80 bg-muted/40 p-3"> | ||
| <div className="flex items-center justify-between gap-3"> | ||
| <div className="space-y-1"> | ||
| <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> | ||
| {t("appearance")} | ||
| </p> | ||
| <p className="text-sm text-foreground/90">{t("theme")}</p> | ||
| </div> | ||
| <ThemeSwitcher /> | ||
| </motion.div> | ||
| ); | ||
| })} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
移动端导航仅显示前 5 个菜单项,其余项不可访问。
internalItems.slice(0, 5) 限制了移动端只能访问前 5 个内部导航项(Config、Prices、Providers、Sensitive Words、Error Rules)。用户无法在移动端访问 Request Filters、Client Versions、Data、Logs、Notifications 等页面,也无法看到外部链接(API Docs、Documentation、Feedback)。
建议考虑以下方案之一:
- 添加 "更多" 菜单展开其余选项
- 使用可滚动的底部导航栏
- 在移动端页面内提供补充导航入口
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/_components/settings-nav.tsx around lines 221 -
261, The mobile nav is hard-limited by internalItems.slice(0, 5), blocking
access to later items; to fix, remove the slice and render all internalItems
(change internalItems.slice(0, 5).map(...) to internalItems.map(...)) and make
the bar horizontally scrollable by updating the container div wrapper to allow
overflow-x (e.g., add classes like overflow-x-auto whitespace-nowrap/
flex-nowrap on the immediate container and ensure each item (the motion.div /
Link) is inline-flex or flex-shrink-0 so items don’t wrap), keeping existing
getIcon, getIsActive, motion layoutId="settingsActiveTabIndicatorMobile" and
Link usage intact.
| </nav> | ||
|
|
||
| {/* Mobile: Bottom Navigation */} | ||
| <nav className="flex md:hidden fixed bottom-0 left-0 right-0 z-50 border-t border-border/50 bg-card/95 backdrop-blur-md safe-area-bottom"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== 搜索 safe-area-bottom 样式定义 ==="
echo ""
# 在整个仓库中搜索 safe-area-bottom
echo "在仓库中搜索 safe-area-bottom:"
rg -n "safe-area-bottom" -i 2>/dev/null | head -20
echo ""
echo "=== 查找 Tailwind 配置文件 ==="
fd -t f "tailwind\|next\.config" | head -10
echo ""
echo "=== 查找全局样式文件 ==="
fd -t f -e css -e scss | grep -E "(global|style)" | head -10
echo ""
echo "=== 查找样式定义位置 ==="
# 搜索包含 safe-area 的文件
rg -l "safe-area" 2>/dev/nullRepository: ding113/claude-code-hub
Length of output: 807
🏁 Script executed:
#!/bin/bash
echo "=== 查看 src/app/globals.css ==="
cat -n src/app/globals.css
echo ""
echo "=== 搜索 CSS class 定义语法 ==="
rg -n "\.safe-area|safe-area-bottom\s*{" -A 3 2>/dev/null
echo ""
echo "=== 检查是否在 Tailwind 主题中定义 ==="
find . -name "tailwind.config.*" -o -name "next.config.*" 2>/dev/null | head -5
echo ""
echo "=== 检查 src 目录下的配置文件 ==="
ls -la src/ | grep -i config
</parameter>Repository: ding113/claude-code-hub
Length of output: 6549
🏁 Script executed:
#!/bin/bash
echo "=== 查看 next.config.ts ==="
cat -n next.config.ts | head -50
echo ""
echo "=== 查找所有 tailwind.config 文件 ==="
find . -name "tailwind.config.*" -o -name "tailwind.config.js" 2>/dev/null
echo ""
echo "=== 查找所有 CSS 文件 ==="
fd -t f -e css -e scss
echo ""
echo "=== 在所有 CSS 文件中搜索 safe-area ==="
rg -n "safe-area" --type css --type scss 2>/dev/null || echo "未在 CSS 文件中找到 safe-area"
echo ""
echo "=== 在 src 目录中查找 CSS 文件 ==="
find src -name "*.css" -o -name "*.scss" 2>/dev/null | xargs ls -la 2>/dev/nullRepository: ding113/claude-code-hub
Length of output: 1857
safe-area-bottom 样式类未定义,需要添加。
safe-area-bottom 在两处使用(settings-nav.tsx 第 222 行、form-tab-nav.tsx 第 151 行),但在全局样式和 Tailwind 配置中均未找到定义。请在 src/app/globals.css 中添加此实用程序类以正确处理 iOS 安全区域:
建议的修复
`@layer` utilities {
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
}🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/_components/settings-nav.tsx at line 222, The CSS
utility class safe-area-bottom is used in the components SettingsNav
(settings-nav.tsx) and FormTabNav (form-tab-nav.tsx) but is not defined; add a
small utility in the global stylesheet (src/app/globals.css) inside `@layer`
utilities that defines .safe-area-bottom to apply padding-bottom:
env(safe-area-inset-bottom) so iOS safe areas are honored by those components.
| saveLabel = "Save Changes", | ||
| cancelLabel = "Cancel", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
默认按钮标签违反 i18n 规范。
根据编码规范,所有用户可见的字符串必须使用 i18n。默认值 "Save Changes" 和 "Cancel" 是硬编码的英文文本。
由于这是一个通用 UI 组件,建议要求调用方必须提供这些标签,或在组件内部使用翻译函数。
🌐 建议的解决方案
方案 1:移除默认值,要求调用方提供
interface SettingsFormActionsProps {
onCancel?: () => void;
onSave?: () => void;
- saveLabel?: string;
- cancelLabel?: string;
+ saveLabel: string;
+ cancelLabel: string;
loading?: boolean;
disabled?: boolean;
dangerAction?: {
label: string;
onClick: () => void;
};
}方案 2:组件内部使用 i18n
import { useTranslations } from "next-intl";
export function SettingsFormActions({ ... }: SettingsFormActionsProps) {
const t = useTranslations("common");
const resolvedSaveLabel = saveLabel ?? t("actions.save");
const resolvedCancelLabel = cancelLabel ?? t("actions.cancel");
// ...
}🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/_components/ui/settings-ui.tsx around lines 473 -
474, The SettingsFormActions component currently hardcodes default button labels
("Save Changes", "Cancel") which violates i18n rules; update SettingsFormActions
to import and call useTranslations (e.g., const t = useTranslations("common"))
and resolve labels as resolvedSaveLabel = saveLabel ?? t("actions.save") and
resolvedCancelLabel = cancelLabel ?? t("actions.cancel"), removing the English
literal defaults so callers may override or receive localized fallbacks; ensure
you update the SettingsFormActionsProps to make saveLabel and cancelLabel
optional and remove the hardcoded default values.
| import { redirect } from "@/i18n/routing"; | ||
| import { getSession } from "@/lib/auth"; | ||
| import { SettingsPageHeader } from "../_components/settings-page-header"; | ||
| import { SettingsSection } from "../_components/ui/settings-ui"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Section and SettingsSection usage in settings pages
echo "=== Section usage in settings ==="
rg "from \"@/components/section\"" src/app/ --type ts --type tsx
echo ""
echo "=== SettingsSection usage in settings ==="
rg "SettingsSection" src/app/ --type ts --type tsx
echo ""
echo "=== List all settings pages ==="
fd "page.tsx" src/app/*/settings/Repository: ding113/claude-code-hub
Length of output: 742
🏁 Script executed:
#!/bin/bash
# Check imports in the specific settings page files
echo "=== client-versions/page.tsx imports ==="
head -20 src/app/[locale]/settings/client-versions/page.tsx
echo ""
echo "=== error-rules/page.tsx imports ==="
head -20 src/app/[locale]/settings/error-rules/page.tsx
echo ""
echo "=== sensitive-words/page.tsx imports ==="
head -20 src/app/[locale]/settings/sensitive-words/page.tsx
echo ""
echo "=== Check for Section imports across settings ==="
grep -r "from.*section" src/app/[locale]/settings/ || echo "No Section imports found"
echo ""
echo "=== Check for SettingsSection imports across settings ==="
grep -r "SettingsSection" src/app/[locale]/settings/ || echo "No SettingsSection imports found"Repository: ding113/claude-code-hub
Length of output: 7036
🏁 Script executed:
#!/bin/bash
# Check the SettingsSection component definition
echo "=== SettingsSection component ==="
cat src/app/[locale]/settings/_components/ui/settings-ui.tsx
echo ""
echo "=== Section component ==="
cat src/app/[locale]/components/section.tsxRepository: ding113/claude-code-hub
Length of output: 15415
🏁 Script executed:
#!/bin/bash
# Find Section component definition
echo "=== Finding Section component ==="
find src -name "section.tsx" -o -name "section.ts"
echo ""
echo "=== Check what `@/components` maps to ==="
grep -r "@/components" src/app/[locale]/settings/ | head -5Repository: ding113/claude-code-hub
Length of output: 838
🏁 Script executed:
#!/bin/bash
# Read the Section component
echo "=== Section component ==="
cat src/components/section.tsxRepository: ding113/claude-code-hub
Length of output: 4812
组件选择不一致。
此页面使用 SettingsSection(来自 _components/ui/settings-ui.tsx),而其他 9 个设置页面(error-rules、sensitive-words、config、data、logs、prices、providers、request-filters)使用 Section(来自 @/components/section.tsx)。虽然两个组件功能相似,但它们的样式、heading 标签(h3 vs h2)和可用 props 存在差异。
建议统一整个设置模块使用单个组件以保持一致性。
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/client-versions/page.tsx at line 8, 当前页面引入并使用了
SettingsSection,但项目中其他 9 个设置页面一致使用 Section;请将 import 和使用替换为统一的 Section(替换
SettingsSection → Section,修改 import 来源为 "@/components/section"),并调整该页面中使用该组件的
props 和 heading 层级以匹配其他设置页面的用法(检查并对齐 title/description/children 等 prop
名称和行为),确保渲染结果与其余设置页面一致。
| <Button | ||
| onClick={handleExport} | ||
| disabled={isExporting} | ||
| className="w-full sm:w-auto bg-[#E25706] hover:bg-[#E25706]/90 text-white" | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Search for `#E25706` color usage across the codebase
rg -n "#E25706" --type-add 'web:*.{tsx,ts,jsx,js,css}' --type webRepository: ding113/claude-code-hub
Length of output: 8455
🏁 Script executed:
# Find and examine tailwind configuration
fd -e 'config' -e 'conf.ts' -e 'conf.js' -e '.config.ts' -e '.config.js' | head -20Repository: ding113/claude-code-hub
Length of output: 478
🏁 Script executed:
# Check the structure of the project and look for color/theme definitions
find . -maxdepth 3 -type f \( -name "tailwind.config.*" -o -name "theme*" -o -name "*colors*" \) 2>/dev/null | head -20Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Read the database-export.tsx file to understand full context
cat -n src/app/[locale]/settings/data/_components/database-export.tsxRepository: ding113/claude-code-hub
Length of output: 2593
🏁 Script executed:
# Search for tailwind config with any extension
find . -name "tailwind.config*" -o -name "globals.*" -o -name "colors.*" 2>/dev/null | grep -E "\.(ts|js|css|tsx|jsx)$"Repository: ding113/claude-code-hub
Length of output: 88
🏁 Script executed:
# Check src directory structure for theme/color definitions
ls -la src/ 2>/dev/null | head -30Repository: ding113/claude-code-hub
Length of output: 834
🏁 Script executed:
# Look for CSS files with color definitions
find . -name "*.css" -type f | head -10 | xargs grep -l "E25706\|--.*color\|@layer" 2>/dev/nullRepository: ding113/claude-code-hub
Length of output: 88
🏁 Script executed:
# Read the globals.css file to see if there are color definitions
cat -n src/app/globals.cssRepository: ding113/claude-code-hub
Length of output: 6294
🏁 Script executed:
# Also check if there's a tailwind.config at the project root
find . -maxdepth 2 -name "tailwind*" -o -name "next.config*" 2>/dev/nullRepository: ding113/claude-code-hub
Length of output: 83
应将品牌色 #E25706 定义为 Tailwind 主题变量,而非在每处使用硬编码
该颜色在整个项目中出现 63 次,包括:
- 按钮的背景色和悬停状态
- 表单焦点边框和环形效果
- 图标和徽章的文字颜色
硬编码方式存在的问题:
- 品牌色调整时需要修改 63 处位置
- 项目已有完整的 Tailwind 主题系统(见
src/app/globals.css),但该颜色未被纳入 - 违反 DRY 原则,增加维护成本
建议在 globals.css 的 @theme inline 块中添加品牌色定义,例如:
--color-brand: `#E25706`;
然后将所有 bg-[#E25706] 替换为 bg-brand,text-[#E25706] 替换为 text-brand,依此类推。这样可以:
- 集中管理品牌色
- 后续调整时只需修改一处
- 保持代码库的一致性
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/data/_components/database-export.tsx around lines
57 - 61, The Button in database-export.tsx uses a hardcoded brand color class
"bg-[`#E25706`]" (and similar occurrences across the repo); add a CSS custom
property for the brand color inside the existing `@theme` inline block in
globals.css (e.g., --color-brand: `#E25706`;) and expose it via your Tailwind
theme as utility names (e.g., bg-brand, text-brand, ring-brand), then replace
className uses like "bg-[`#E25706`]" and "hover:bg-[`#E25706`]/90" in the Button
component (and all other instances) with the new utilities; update any related
focus/ring/text/icon classes to use the new brand utilities so the color is
centralized and modifiable from globals.css.
| {t("descriptionWarning").split("Note:")[0]} | ||
| <strong> | ||
| {t("descriptionWarning").includes("注意:") | ||
| ? t("descriptionWarning").split("注意:")[1] | ||
| : ""} | ||
| {t("descriptionWarning").includes("Note:") | ||
| ? t("descriptionWarning").split("Note:")[1] | ||
| : t("descriptionWarning").includes(":") | ||
| ? t("descriptionWarning").split(":")[1] | ||
| : ""} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
字符串解析逻辑脆弱,可能导致 i18n 问题。
当前逻辑依赖 "Note:" 作为分隔符,但不同语言的翻译格式可能不同(如中文使用 "注意:",日文使用 "注意:")。这种基于硬编码分隔符的解析方式在多语言环境下容易出错。
建议将翻译拆分为两个独立的 i18n key,而非依赖运行时字符串解析:
建议的重构方案
方案:拆分翻译键
在各语言文件中添加:
{
"descriptionText": "...",
"descriptionWarningText": "..."
}然后在组件中直接使用:
- {t("descriptionWarning").split("Note:")[0]}
- <strong>
- {t("descriptionWarning").includes("Note:")
- ? t("descriptionWarning").split("Note:")[1]
- : t("descriptionWarning").includes(":")
- ? t("descriptionWarning").split(":")[1]
- : ""}
- </strong>
+ {t("descriptionText")}
+ <strong>{t("descriptionWarningText")}</strong>🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/data/_components/log-cleanup-panel.tsx around
lines 131 - 137, The current runtime string parsing of t("descriptionWarning")
is fragile for i18n; instead add two distinct translation keys (e.g.
"descriptionText" and "descriptionWarningText") in all locale files and update
the component to stop using t("descriptionWarning").split(...) and directly
render t("descriptionText") for the main text and t("descriptionWarningText")
inside the <strong> element (replace the split logic in the component that
currently references t("descriptionWarning")) so translations no longer depend
on a hard-coded "Note:" delimiter.
| import { toast } from "sonner"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; | ||
| import { cn } from "@/lib/utils"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
移除未使用的 cn 导入
CI 已报告 unused import,当前 cn 未被使用,会阻断合并。
Proposed fix
-import { cn } from "@/lib/utils";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { cn } from "@/lib/utils"; |
🧰 Tools
🪛 GitHub Actions: 🧪 Test Suite
[error] 8-8: lint/correctness/noUnusedImports: Unused import(s) detected.
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/notifications/_components/webhook-targets-section.tsx
at line 8, 移除未使用的 cn 导入:在组件(在该文件顶部有 import { cn } from
"@/lib/utils";)中删除这条未被引用的导入语句(或如果后续需要样式合并改为实际使用 cn),确保没有其他对 cn 的引用(例如在
WebhookTargetsSection 或同文件导出的任何函数/组件中),然后保存以通过 CI 的 unused import 检查。
| <td className="py-3 px-4 text-sm text-muted-foreground"> | ||
| {new Date(word.createdAt).toLocaleString("zh-CN")} | ||
| </td> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
日期格式化硬编码了 zh-CN 语言环境,违反了 i18n 规范。
根据编码规范,应用支持 5 种语言(zh-CN、zh-TW、en、ja、ru),用户可见的内容必须使用 i18n。日期格式化应该使用当前用户的语言环境,而不是硬编码 "zh-CN"。
建议修复方案
可以通过 useLocale hook 获取当前语言环境:
import { useTranslations } from "next-intl";
+import { useLocale } from "next-intl";
import { useState } from "react";在组件中使用:
export function WordListTable({ words }: WordListTableProps) {
const t = useTranslations("settings");
+ const locale = useLocale();然后在日期格式化时使用:
- {new Date(word.createdAt).toLocaleString("zh-CN")}
+ {new Date(word.createdAt).toLocaleString(locale)}🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/sensitive-words/_components/word-list-table.tsx
around lines 124 - 126, The date formatting in the WordListTable component is
hardcoded to "zh-CN"; update it to use the current locale from the useLocale
hook instead: import and call useLocale inside the component (e.g., in the
WordListTable or the parent component rendering the table), store the returned
locale value, and pass that locale to new
Date(word.createdAt).toLocaleString(locale) (or equivalent) where createdAt is
formatted; ensure the change references the component/function name
WordListTable and the createdAt rendering cell so the locale is applied
consistently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Comments (1)
124 files reviewed, 2 comments
| const scrollToSection = useCallback((tab: TabId) => { | ||
| const section = sectionRefs.current[tab]; | ||
| if (section && contentRef.current) { | ||
| isScrollingToSection.current = true; | ||
| const containerTop = contentRef.current.getBoundingClientRect().top; | ||
| const sectionTop = section.getBoundingClientRect().top; | ||
| const offset = sectionTop - containerTop + contentRef.current.scrollTop; | ||
| contentRef.current.scrollTo({ top: offset, behavior: "smooth" }); | ||
| setTimeout(() => { | ||
| isScrollingToSection.current = false; | ||
| }, 500); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using requestAnimationFrame for smoother scroll detection. The current setTimeout with fixed 500ms may cause tab highlighting issues if user manually scrolls before the timeout expires.
| const scrollToSection = useCallback((tab: TabId) => { | |
| const section = sectionRefs.current[tab]; | |
| if (section && contentRef.current) { | |
| isScrollingToSection.current = true; | |
| const containerTop = contentRef.current.getBoundingClientRect().top; | |
| const sectionTop = section.getBoundingClientRect().top; | |
| const offset = sectionTop - containerTop + contentRef.current.scrollTop; | |
| contentRef.current.scrollTo({ top: offset, behavior: "smooth" }); | |
| setTimeout(() => { | |
| isScrollingToSection.current = false; | |
| }, 500); | |
| } | |
| }, []); | |
| const scrollToSection = useCallback((tab: TabId) => { | |
| const section = sectionRefs.current[tab]; | |
| if (section && contentRef.current) { | |
| isScrollingToSection.current = true; | |
| const containerTop = contentRef.current.getBoundingClientRect().top; | |
| const sectionTop = section.getBoundingClientRect().top; | |
| const offset = sectionTop - containerTop + contentRef.current.scrollTop; | |
| contentRef.current.scrollTo({ top: offset, behavior: "smooth" }); | |
| // Use requestAnimationFrame to detect scroll completion | |
| const checkScrollEnd = () => { | |
| if (Math.abs(contentRef.current!.scrollTop - offset) < 1) { | |
| isScrollingToSection.current = false; | |
| } else { | |
| requestAnimationFrame(checkScrollEnd); | |
| } | |
| }; | |
| requestAnimationFrame(checkScrollEnd); | |
| } | |
| }, []); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 86:98
Comment:
**style:** Consider using `requestAnimationFrame` for smoother scroll detection. The current `setTimeout` with fixed 500ms may cause tab highlighting issues if user manually scrolls before the timeout expires.
```suggestion
const scrollToSection = useCallback((tab: TabId) => {
const section = sectionRefs.current[tab];
if (section && contentRef.current) {
isScrollingToSection.current = true;
const containerTop = contentRef.current.getBoundingClientRect().top;
const sectionTop = section.getBoundingClientRect().top;
const offset = sectionTop - containerTop + contentRef.current.scrollTop;
contentRef.current.scrollTo({ top: offset, behavior: "smooth" });
// Use requestAnimationFrame to detect scroll completion
const checkScrollEnd = () => {
if (Math.abs(contentRef.current!.scrollTop - offset) < 1) {
isScrollingToSection.current = false;
} else {
requestAnimationFrame(checkScrollEnd);
}
};
requestAnimationFrame(checkScrollEnd);
}
}, []);
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.- Add dynamic height calculation for live sessions panel - Make BentoCard accept refs with forwardRef - Add select/deselect all buttons to statistics chart - Allow non-percentage values in metric card comparisons
…arkline Add interactive tooltip showing latency values on hover and display average latency for the last 5 minutes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx:
- Around line 38-40: Normalize legacy providerType values when initializing the
routing object in provider-form-context.tsx: create a small helper (e.g.,
normalizeProviderType(value)) that maps deprecated values like "claude-auth" ->
"claude", "gemini-cli" -> "gemini" (and any other legacy tokens) and use it when
selecting the initial routing.providerType instead of using
sourceProvider?.providerType or preset?.providerType directly; update the
routing initialization (the routing: { providerType: ... } assignment) to call
normalizeProviderType(sourceProvider?.providerType) ??
normalizeProviderType(preset?.providerType) ?? "claude" so the RoutingSection
selector only sees supported types while leaving groupTag unchanged.
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts:
- Around line 173-176: The interface ProviderFormContextValue uses
React.Dispatch<ProviderFormAction> but React types are not imported; fix by
importing the Dispatch type from 'react' and replacing React.Dispatch with
Dispatch so the interface (ProviderFormContextValue) compiles correctly
alongside ProviderFormState, ProviderFormAction and FormMode referenced in this
file.
♻️ Duplicate comments (6)
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx (2)
340-342: Select 占位符需使用 i18n
SelectValue的placeholder="inherit"是硬编码文案,应使用翻译键统一输出。可改为t("common.inherit")或对应的公共文案键。修改示例
- <SelectValue placeholder="inherit" /> + <SelectValue placeholder={t("common.inherit")} />As per coding guidelines, 所有用户可见文案必须走 i18n。
Also applies to: 370-372, 412-414, 451-453, 475-477, 501-503
308-310: 高级设置卡片标题过于具体该卡片包含多项高级设置,但标题使用了“转发客户端 IP”的字段文案,语义不够贴合。建议使用更泛化的标题并补充对应翻译键。
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx (2)
81-84: 避免将 NaN 写入超时状态
parseInt在输入中间态(如 "-")会得到 NaN,目前会被写入状态,导致 UI 可能展示 NaN。建议在写入前做 NaN 保护。修改建议
onChange={(e) => { const val = e.target.value; - onChange(val === "" ? undefined : parseInt(val, 10)); + const next = val === "" ? undefined : Number.parseInt(val, 10); + onChange(Number.isNaN(next) ? undefined : next); }}
95-96: 超时单位需走 i18n这里的单位文本
s属于用户可见内容,应改为翻译键输出。修改建议
- <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> - s - </span> + <span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"> + {t("common.secondsShort")} + </span>As per coding guidelines, 所有用户可见文案必须走 i18n。
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx (1)
143-149: 密钥可见性按钮缺少可访问名称图标按钮缺少
aria-label与aria-pressed,屏幕阅读器难以识别状态。建议补充并使用 i18n 文案。修改建议
<button type="button" onClick={() => setShowKey(!showKey)} + aria-label={showKey ? t("key.hide") : t("key.show")} + aria-pressed={showKey} className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" >As per coding guidelines, 所有用户可见文案必须走 i18n。
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (1)
527-543: 缺少取消机制,可能导致竞态条件或内存泄漏。当
preset快速变化或组件卸载时,异步操作可能在完成后尝试更新已卸载组件的状态。建议添加取消标志:建议的修复方案
useEffect(() => { if (urlResolver && preset?.providerType && !preset?.url) { + let cancelled = false; setAutoUrlPending(true); urlResolver(preset.providerType) .then((url) => { - if (url) { + if (url && !cancelled) { setResolvedUrl(url); } }) .catch((e) => { + if (cancelled) return; console.error("Failed to resolve provider URL:", e); }) .finally(() => { - setAutoUrlPending(false); + if (!cancelled) { + setAutoUrlPending(false); + } }); + return () => { + cancelled = true; + }; } }, [urlResolver, preset?.providerType, preset?.url]);
🧹 Nitpick comments (4)
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts (1)
51-60: 限额字段建议区分 null 与 undefined 的语义当前限额字段仅使用
number | null,若null表示“无限制”,则无法表达“继承默认/未设置”的undefined语义。建议补充undefined并明确含义,避免语义丢失。修改建议
export interface RateLimitState { - limit5hUsd: number | null; - limitDailyUsd: number | null; + limit5hUsd: number | null | undefined; + limitDailyUsd: number | null | undefined; dailyResetMode: "fixed" | "rolling"; dailyResetTime: string; - limitWeeklyUsd: number | null; - limitMonthlyUsd: number | null; - limitTotalUsd: number | null; - limitConcurrentSessions: number | null; + limitWeeklyUsd: number | null | undefined; + limitMonthlyUsd: number | null | undefined; + limitTotalUsd: number | null | undefined; + limitConcurrentSessions: number | null | undefined; }Based on learnings, 请确认 null/undefined 的业务语义在这里保持一致。
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx (3)
100-128: 建议对滚动处理函数添加节流以提升性能。
handleScroll在每次滚动事件时都会执行,包括遍历所有 section 并调用getBoundingClientRect()。在快速滚动时可能造成性能问题。建议使用
lodash-es的throttle或自定义节流逻辑:建议的优化方案
+import { throttle } from "lodash-es"; // ... - const handleScroll = useCallback(() => { + const handleScroll = useCallback( + throttle(() => { if (isScrollingToSection.current || !contentRef.current) return; // ... rest of the logic - }, [dispatch, state.ui.activeTab]); + }, 100), + [dispatch, state.ui.activeTab] + );
300-340: 可选优化:考虑使用useMemo缓存 tab 状态计算。
getTabStatus在每次渲染时都会重新计算。虽然计算本身不复杂,但如果希望优化渲染性能,可以使用useMemo:可选的优化方案
- const getTabStatus = (): Partial<Record<TabId, "default" | "warning" | "configured">> => { - const status: Partial<Record<TabId, "default" | "warning" | "configured">> = {}; - // ... calculation logic - return status; - }; + const tabStatus = useMemo((): Partial<Record<TabId, "default" | "warning" | "configured">> => { + const status: Partial<Record<TabId, "default" | "warning" | "configured">> = {}; + // ... calculation logic + return status; + }, [ + state.basic.name, + state.basic.url, + hideUrl, + state.routing.allowedModels, + state.routing.modelRedirects, + state.rateLimit, + state.network.proxyUrl, + state.mcp.mcpPassthroughType, + ]);然后将
tabStatus={getTabStatus()}改为tabStatus={tabStatus}。
511-524: 建议为异步获取添加清理函数以避免竞态条件。虽然
fetchGroups仅在挂载时执行一次,但添加AbortController可以确保组件卸载时取消进行中的请求,是更健壮的做法:建议的改进
useEffect(() => { + let cancelled = false; const fetchGroups = async () => { try { const res = await getDistinctProviderGroupsAction(); - if (res.ok && res.data) { + if (res.ok && res.data && !cancelled) { setGroupSuggestions(res.data); } } catch (e) { + if (cancelled) return; console.error("Failed to fetch group suggestions:", e); } }; fetchGroups(); + return () => { + cancelled = true; + }; }, []);
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Show resolved
Hide resolved
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/_components/bento/dashboard-bento.tsx:
- Around line 193-199: The frontend is mapping lastActivityAt to startTime in
sessionsWithActivity which causes sessions to appear idle; update the backend
getActiveSessions to include the same lastRequestAt field that getAllSessions
exposes, and then change the front-end mapping in sessionsWithActivity to use
s.lastRequestAt (fallback to s.startTime if missing); reference the
getActiveSessions function on the backend and the sessionsWithActivity constant
in dashboard-bento.tsx when making these changes.
- Around line 177-180: The hardcoded units in formatResponseTime should be
replaced with locale-aware Intl.NumberFormat formatters: import useLocale,
useMemo (and useCallback if desired), get locale from useLocale(), create
msFormatter and secFormatter using useMemo (style: "unit", unit:
"millisecond"/"second", unitDisplay: "narrow", appropriate
maximumFractionDigits), then update formatResponseTime to use
msFormatter.format(ms) for ms < 1000 and secFormatter.format(ms / 1000)
otherwise (optionally wrap formatResponseTime in useCallback and depend on the
two formatters); reference formatResponseTime, msFormatter, secFormatter,
useLocale, useMemo (and useCallback) when making the change.
In `@src/app/`[locale]/dashboard/_components/bento/live-sessions-panel.tsx:
- Around line 164-169: The dynamic height calculation in calculateMaxItems
currently overwrites any explicit maxItems passed in; modify calculateMaxItems
to compute calculatedItems as before (using containerRef, HEADER_HEIGHT,
FOOTER_HEIGHT, SESSION_ITEM_HEIGHT) then take the minimum between
calculatedItems and the explicit maxItems prop (if maxItems is defined) before
calling setDynamicMaxItems, ensuring you reference calculateMaxItems,
containerRef, setDynamicMaxItems and the
SESSION_ITEM_HEIGHT/HEADER_HEIGHT/FOOTER_HEIGHT constants when making the
change.
- Around line 20-38: The getSessionStatus function never returns the 'done'
SessionStatus because activity-based checks run before mapping session.status;
update getSessionStatus to first inspect and map session.status (e.g., if
session.status === 'done' or 'completed' return 'done', if 'error' return
'error', etc.), then fall back to the existing activity-based logic using
lastActivityAt/startTime and idle thresholds; reference function
getSessionStatus, type SessionStatus and ActiveSessionInfo, and the
session.status property when implementing the change.
- Around line 41-75: The statusConfig object currently contains hardcoded
user-facing labels ("RUNNING","INIT","IDLE","ERROR","DONE"); replace these with
next-intl translation lookups (e.g., call the translation function used in this
component such as t = useTranslations(...) or intl.formatMessage and set label:
t('dashboard.sessions.status.running') etc.), add corresponding translation keys
for these five statuses plus the "unknown" and "Loading..." strings referenced
elsewhere in this file, and ensure a sensible fallback (e.g., t('...') ??
'RUNNING') where appropriate; update any other occurrences of "unknown" and
"Loading..." in this component to use the same translation keys so all
user-visible text here is i18n-enabled.
In `@src/app/`[locale]/dashboard/_components/bento/metric-card.tsx:
- Around line 175-199: The clickable card uses Component = onClick ? "button" :
"div" which means a rendered button will default to type="submit" inside forms;
update the render so when Component is a "button" it explicitly sets
type="button" (e.g., pass a type prop only when onClick is truthy) — modify the
JSX where Component is returned (the element using onClick, className etc.) to
include an explicit type attribute for the button case to prevent accidental
form submissions.
In
`@src/app/`[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx:
- Line 5: Tests are failing because the recharts mock does not export Tooltip;
update your recharts mock (e.g., __mocks__/recharts.ts or your test setup mock)
to export Tooltip so components importing Tooltip (from import { Tooltip } in
endpoint-latency-sparkline.tsx) resolve in tests; either add a simple
passthrough functional export named Tooltip that returns its children, or
re-export the real Tooltip using vi.importActual / jest.requireActual to
preserve actual behavior.
♻️ Duplicate comments (1)
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx (1)
151-174: 日期格式仍硬编码 zh-CN。
应使用当前 locale(例如 next-intl 的 useLocale)以支持多语言。依据编码规范。
🧹 Nitpick comments (2)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)
67-75: 平均延迟计算逻辑正确,有一处小冗余。在 filter 中已确保
p.latencyMs !== null,因此 reduce 中的p.latencyMs ?? 0是冗余的。此外,建议确认 5 分钟时间窗口与数据采样频率是否匹配,如果探测间隔较长,可能导致平均值频繁为 null。♻️ 可选优化
const avgLatency = useMemo(() => { const fiveMinutesAgo = Date.now() - 5 * 60 * 1000; const recentPoints = points.filter( (p) => p.latencyMs !== null && p.timestamp && p.timestamp >= fiveMinutesAgo ); if (recentPoints.length === 0) return null; - const sum = recentPoints.reduce((acc, p) => acc + (p.latencyMs ?? 0), 0); + const sum = recentPoints.reduce((acc, p) => acc + p.latencyMs!, 0); return sum / recentPoints.length; }, [points]);src/app/[locale]/dashboard/_components/bento/metric-card.tsx (1)
130-161: 补充 rAF 清理避免卸载后仍更新状态。
当前动画未取消时,组件卸载或 value 频繁变化可能导致并发 rAF 与卸载后 setState。建议在 effect 中记录并取消 rAF。Proposed fix
useEffect(() => { - if (typeof value === "number" && typeof prevValueRef.current === "number") { + let rafId: number | null = null; + let cancelled = false; + if (typeof value === "number" && typeof prevValueRef.current === "number") { if (value !== prevValueRef.current) { setIsAnimating(true); const duration = 400; const startValue = prevValueRef.current; const diff = value - startValue; const startTime = Date.now(); const animate = () => { + if (cancelled) return; const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = 1 - (1 - progress) ** 3; const currentValue = startValue + diff * easeProgress; setDisplayValue(currentValue); if (progress < 1) { - requestAnimationFrame(animate); + rafId = requestAnimationFrame(animate); } else { setIsAnimating(false); prevValueRef.current = value; } }; - requestAnimationFrame(animate); + rafId = requestAnimationFrame(animate); } } else { setDisplayValue(value); prevValueRef.current = value; } + return () => { + cancelled = true; + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + }; }, [value]);
| const formatResponseTime = (ms: number) => { | ||
| if (ms < 1000) return `${ms}ms`; | ||
| return `${(ms / 1000).toFixed(1)}s`; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx | sed -n '170,200p'Repository: ding113/claude-code-hub
Length of output: 1169
🏁 Script executed:
# Search for i18n patterns in the codebase for metrics/response time
rg "metrics\." --type ts --type tsx -A 2 -B 2 | head -50Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Check if response time related i18n keys exist
rg "responseTime" --type json -A 2 -B 2 | head -30Repository: ding113/claude-code-hub
Length of output: 2043
🏁 Script executed:
# Look for how other formatting functions handle i18n in this codebase
rg "const format" --type ts --type tsx -A 3 | head -60Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Check the i18n setup in the project
fd "i18n\|i18next\|translation" --type ts --type tsx | head -20Repository: ding113/claude-code-hub
Length of output: 236
🏁 Script executed:
# Search for response time formatting in the codebase
rg "formatResponseTime|responseTime" -A 3 -B 2 | head -80Repository: ding113/claude-code-hub
Length of output: 4207
🏁 Script executed:
# Check for i18n usage patterns in tsx files
rg "useTranslation|t\(" src/ -A 2 | head -60Repository: ding113/claude-code-hub
Length of output: 3038
🏁 Script executed:
# Look for metrics i18n keys
fd "messages" -type f -name "*.json" | xargs rg "ms|second" | head -30Repository: ding113/claude-code-hub
Length of output: 2892
🏁 Script executed:
# Check the dashboard-bento file for i18n imports and usage
cat src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx | head -50Repository: ding113/claude-code-hub
Length of output: 1810
🏁 Script executed:
# Search for how numbers with units are formatted in i18n
rg "toFixed|format.*number" src/ -A 2 -B 2 | head -60Repository: ding113/claude-code-hub
Length of output: 3925
🏁 Script executed:
# Check for metrics-related i18n keys in all language files
fd "messages" -type f -name "*.json" -exec rg "metrics" {} \;Repository: ding113/claude-code-hub
Length of output: 237
🏁 Script executed:
# Check for dashboard-related i18n keys
fd "messages" -type f -name "*.json" -path "*/dashboard*"Repository: ding113/claude-code-hub
Length of output: 237
🏁 Script executed:
# Look at the full dashboard-bento component to see how it uses translations
cat src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx | head -150Repository: ding113/claude-code-hub
Length of output: 5078
🏁 Script executed:
# Search for all uses of formatResponseTime in the codebase
rg "formatResponseTime" -B 5 -A 5Repository: ding113/claude-code-hub
Length of output: 6157
🏁 Script executed:
# Check if there are any existing i18n keys for time units
fd "messages" -type f -name "*.json" -exec rg "\\bms\\b|\\bs\\b|millisecond|second" {} +Repository: ding113/claude-code-hub
Length of output: 237
🏁 Script executed:
# Look at how usage-logs-view-virtualized.tsx implements formatResponseTime with i18n
cat -n src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx | sed -n '1,100p'Repository: ding113/claude-code-hub
Length of output: 4366
🏁 Script executed:
# Search for msFormatter and secFormatter definitions
rg "msFormatter|secFormatter" -B 5 -A 2Repository: ding113/claude-code-hub
Length of output: 2688
🏁 Script executed:
# Check dashboard customs i18n keys
cat messages/zh-CN/customs.jsonRepository: ding113/claude-code-hub
Length of output: 1023
🏁 Script executed:
# Check if metrics.responseTimeMs or similar keys exist
rg "responseTime" messages/ -A 1 -B 1Repository: ding113/claude-code-hub
Length of output: 2464
🏁 Script executed:
# Get the full secFormatter setup from logs component
cat -n src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx | sed -n '93,115p'Repository: ding113/claude-code-hub
Length of output: 715
🏁 Script executed:
# Check if there are other formatResponseTime implementations in the codebase
rg "formatResponseTime" -B 2 -A 8Repository: ding113/claude-code-hub
Length of output: 6358
🏁 Script executed:
# Check how overview-panel.tsx uses this formatter
cat -n src/components/customs/overview-panel.tsx | sed -n '1,60p'Repository: ding113/claude-code-hub
Length of output: 2189
响应时间单位需使用国际化格式化,应对齐现有代码模式。
硬编码的 ms 和 s 违反 i18n 规范。同项目中 usage-logs-view-virtualized.tsx 已展示标准做法:使用 Intl.NumberFormat 配合 useLocale() 实现locale感知的单位格式化。
推荐修复方式:
- 导入
useLocale和useMemo - 创建
msFormatter和secFormatter(参考同文件usage-logs-view-virtualized.tsx第93-113行) - 更新
formatResponseTime使用这两个格式化器
此方案对齐现有代码模式,自动处理多语言单位显示,无需额外翻译键。
参考实现(来自usage-logs-view-virtualized.tsx)
const msFormatter = useMemo(
() =>
new Intl.NumberFormat(locale, {
style: "unit",
unit: "millisecond",
unitDisplay: "narrow",
maximumFractionDigits: 0,
}),
[locale]
);
const secFormatter = useMemo(
() =>
new Intl.NumberFormat(locale, {
style: "unit",
unit: "second",
unitDisplay: "narrow",
maximumFractionDigits: 1,
}),
[locale]
);
const formatResponseTime = useCallback(
(ms: number) => {
if (ms < 1000) return msFormatter.format(ms);
return secFormatter.format(ms / 1000);
},
[msFormatter, secFormatter]
);🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/bento/dashboard-bento.tsx around
lines 177 - 180, The hardcoded units in formatResponseTime should be replaced
with locale-aware Intl.NumberFormat formatters: import useLocale, useMemo (and
useCallback if desired), get locale from useLocale(), create msFormatter and
secFormatter using useMemo (style: "unit", unit: "millisecond"/"second",
unitDisplay: "narrow", appropriate maximumFractionDigits), then update
formatResponseTime to use msFormatter.format(ms) for ms < 1000 and
secFormatter.format(ms / 1000) otherwise (optionally wrap formatResponseTime in
useCallback and depend on the two formatters); reference formatResponseTime,
msFormatter, secFormatter, useLocale, useMemo (and useCallback) when making the
change.
| // Sessions with lastActivityAt for LiveSessionsPanel | ||
| const sessionsWithActivity = useMemo(() => { | ||
| return sessions.map((s) => ({ | ||
| ...s, | ||
| lastActivityAt: s.startTime, | ||
| })); | ||
| }, [sessions]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "dashboard-bento.tsx" -type fRepository: ding113/claude-code-hub
Length of output: 133
🏁 Script executed:
wc -l ./src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsxRepository: ding113/claude-code-hub
Length of output: 137
🏁 Script executed:
cat -n ./src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsxRepository: ding113/claude-code-hub
Length of output: 12231
🏁 Script executed:
find . -path "*/types/session*" -type fRepository: ding113/claude-code-hub
Length of output: 89
🏁 Script executed:
cat -n ./src/types/session.tsRepository: ding113/claude-code-hub
Length of output: 2071
🏁 Script executed:
find . -name "live-sessions-panel*" -type fRepository: ding113/claude-code-hub
Length of output: 137
🏁 Script executed:
cat -n ./src/app/[locale]/dashboard/_components/bento/live-sessions-panel.tsxRepository: ding113/claude-code-hub
Length of output: 10512
🏁 Script executed:
find . -path "*/actions/active-sessions*" -type fRepository: ding113/claude-code-hub
Length of output: 200
🏁 Script executed:
cat -n ./src/actions/active-sessions.tsRepository: ding113/claude-code-hub
Length of output: 38232
后端需要在 getActiveSessions 返回值中暴露 lastRequestAt 字段,以正确反映会话的实际活跃状态。
当前 lastActivityAt 硬编码为 startTime 会导致所有会话在 60 秒后错误地进入 idle 状态。后端的 getAllSessions 已追踪 lastRequestAt,建议复用该逻辑,在 getActiveSessions 中也返回此字段,供前端用于准确的状态判断。
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/bento/dashboard-bento.tsx around
lines 193 - 199, The frontend is mapping lastActivityAt to startTime in
sessionsWithActivity which causes sessions to appear idle; update the backend
getActiveSessions to include the same lastRequestAt field that getAllSessions
exposes, and then change the front-end mapping in sessionsWithActivity to use
s.lastRequestAt (fallback to s.startTime if missing); reference the
getActiveSessions function on the backend and the sessionsWithActivity constant
in dashboard-bento.tsx when making these changes.
| function getSessionStatus(session: ActiveSessionInfo & { lastActivityAt?: number }): SessionStatus { | ||
| // Determine status based on session activity and startTime | ||
| const now = Date.now(); | ||
| const lastActivity = session.lastActivityAt ?? session.startTime; | ||
| const idleThreshold = 60 * 1000; // 1 minute | ||
|
|
||
| if (session.status === "error" || (session as { status?: string }).status === "error") { | ||
| return "error"; | ||
| } | ||
|
|
||
| if (now - lastActivity < 5000) { | ||
| return "running"; | ||
| } | ||
|
|
||
| if (now - lastActivity < idleThreshold) { | ||
| return "init"; | ||
| } | ||
|
|
||
| return "idle"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
completed 状态不会进入 DONE 分支。
SessionStatus 包含 done,但当前逻辑从不返回,完成态会话会被误判为 idle/init。建议优先映射 session.status。
Proposed fix
- if (session.status === "error" || (session as { status?: string }).status === "error") {
- return "error";
- }
+ if (session.status === "error") {
+ return "error";
+ }
+ if (session.status === "completed") {
+ return "done";
+ }🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/bento/live-sessions-panel.tsx around
lines 20 - 38, The getSessionStatus function never returns the 'done'
SessionStatus because activity-based checks run before mapping session.status;
update getSessionStatus to first inspect and map session.status (e.g., if
session.status === 'done' or 'completed' return 'done', if 'error' return
'error', etc.), then fall back to the existing activity-based logic using
lastActivityAt/startTime and idle thresholds; reference function
getSessionStatus, type SessionStatus and ActiveSessionInfo, and the
session.status property when implementing the change.
| import { useQuery } from "@tanstack/react-query"; | ||
| import { Line, LineChart, ResponsiveContainer, YAxis } from "recharts"; | ||
| import { useMemo } from "react"; | ||
| import { Line, LineChart, ResponsiveContainer, Tooltip, YAxis } from "recharts"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
测试失败:需要更新 recharts mock 以导出 Tooltip。
根据流水线错误,Tooltip 组件在 recharts mock 中未被导出,导致测试失败。需要在测试 mock 文件中添加 Tooltip 的导出。
🔧 建议修复
在 recharts mock 文件中(通常位于 __mocks__/recharts.ts 或测试 setup 文件)添加:
export const Tooltip = ({ children }: { children?: React.ReactNode }) => <>{children}</>;或者使用 vi.importActual / jest.requireActual 保留真实行为:
const actual = await vi.importActual("recharts");
export const Tooltip = actual.Tooltip;🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
at line 5, Tests are failing because the recharts mock does not export Tooltip;
update your recharts mock (e.g., __mocks__/recharts.ts or your test setup mock)
to export Tooltip so components importing Tooltip (from import { Tooltip } in
endpoint-latency-sparkline.tsx) resolve in tests; either add a simple
passthrough functional export named Tooltip that returns its children, or
re-export the real Tooltip using vi.importActual / jest.requireActual to
preserve actual behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/_components/bento/bento-grid.tsx:
- Around line 56-94: BentoCard renders a clickable div when onClick is provided
but lacks keyboard accessibility; update the BentoCard component to add
role="button" and tabIndex={0} (only when onClick exists or interactive is true)
and implement an onKeyDown handler that calls the same onClick callback when
Enter or Space is pressed; ensure you wire these props on the outer div
(alongside existing onClick) and keep behavior identical otherwise (no changes
when onClick is undefined).
In `@src/app/`[locale]/dashboard/_components/bento/live-sessions-panel.tsx:
- Around line 41-75: statusConfig currently hardcodes English labels ("RUNNING",
"INIT", etc.), violating i18n; change it into a function (e.g., getStatusConfig)
that accepts the i18n translator (t) and returns the same shape but with label:
t('...') keys, then update the component to call getStatusConfig(t) (or compute
inside SessionItem) and use those localized labels; reference statusConfig →
replace with getStatusConfig(t) and ensure SessionItem imports/receives t from
the i18n hook and uses the returned config.
- Around line 83-84: Replace the hard-coded English fallback "unknown" used when
deriving userName with a call to the app's i18n translation function and add the
corresponding key to the translation files; specifically, change the fallback
assignment for the userName variable (currently: userName = session.userName ||
"unknown") to use the translator (e.g., t('...') or getTranslator(...)) so the
default text is localized, and add that key to all locale JSON/translation
resources.
- Around line 225-231: The JSX in live-sessions-panel.tsx currently renders a
hard-coded "Loading..." when isLoading && sessions.length === 0; replace this
literal with the i18n translation used elsewhere (like
statistics-chart-card.tsx) by importing and using the existing translation
hook/function (e.g., useTranslations or t) in this component and rendering
t('loading') or a semantically named key such as t('liveSessions.loading'); add
the translation key to the appropriate locale files and ensure the import/usage
is consistent with other components' patterns.
In `@src/app/`[locale]/dashboard/_components/bento/metric-card.tsx:
- Around line 130-161: The effect in metric-card.tsx animates value with
requestAnimationFrame but lacks cleanup; modify the useEffect (the effect that
references prevValueRef, setDisplayValue, setIsAnimating and launches
requestAnimationFrame(animate)) to track the RAF id and a mounted flag, assign
the RAF id each time you call requestAnimationFrame, and return a cleanup
function that calls cancelAnimationFrame(rafId) and flips the mounted flag (or
otherwise prevents setState) so animate stops and won't call
setDisplayValue/setIsAnimating after unmount or when value changes; ensure
prevValueRef.current is still updated only when the animation completes or in
the cleanup as appropriate.
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx:
- Around line 225-231: In StatisticsChartCard
(src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx) replace
the hard-coded "Loading..." text with a call to the app's i18n translation
function (e.g. t or useTranslations) — e.g. t('dashboard.statistics.loading') —
and ensure the component imports/uses the project translation hook
(useTranslation/useTranslations) if not already present; then add the
dashboard.statistics.loading key to the translation files for zh-CN, zh-TW, en,
ja and ru with appropriate localized strings.
In
`@src/app/`[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx:
- Around line 16-20: In formatLatency(ms: number | null) replace hardcoded
user-facing strings ("-", "ms", "s") with your i18n resources by calling the
project's translation/formatter (e.g., use the existing t/formatMessage
function) — return the translated placeholder when ms is null and append the
localized unit string when formatting values (use localized short forms for
milliseconds and seconds and keep numeric formatting logic unchanged); update
references inside formatLatency so all visible text flows through the i18n
utility.
♻️ Duplicate comments (1)
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx (1)
151-174: 日期格式化使用了硬编码的 "zh-CN" locale
formatDate和formatTooltipDate函数中硬编码了"zh-CN"locale,应使用next-intl的useLocalehook 获取当前用户 locale。
🧹 Nitpick comments (2)
src/app/[locale]/dashboard/_components/bento/live-sessions-panel.tsx (1)
26-28: 冗余的类型断言检查
(session as { status?: string }).status === "error"与前面的session.status === "error"检查重复,类型断言是多余的。建议简化
- if (session.status === "error" || (session as { status?: string }).status === "error") { + if (session.status === "error") { return "error"; }src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx (1)
62-95: 数据获取模式不一致
fetchLeaderboard使用直接fetchAPI 调用/api/leaderboard路由,而fetchOverviewData、fetchActiveSessions、fetchStatistics使用 Server Actions。保持一致性有助于代码可维护性。不过,现有 API 路由方案提供了缓存控制优化(
s-maxage=60)。如果考虑统一使用 Server Actions,建议结合对所有 Server Actions 错误消息处理的综合规划来执行,而不是逐个 PR 转换。
src/app/[locale]/dashboard/_components/bento/live-sessions-panel.tsx
Outdated
Show resolved
Hide resolved
| useEffect(() => { | ||
| if (typeof value === "number" && typeof prevValueRef.current === "number") { | ||
| if (value !== prevValueRef.current) { | ||
| setIsAnimating(true); | ||
| const duration = 400; | ||
| const startValue = prevValueRef.current; | ||
| const diff = value - startValue; | ||
| const startTime = Date.now(); | ||
|
|
||
| const animate = () => { | ||
| const elapsed = Date.now() - startTime; | ||
| const progress = Math.min(elapsed / duration, 1); | ||
| const easeProgress = 1 - (1 - progress) ** 3; | ||
| const currentValue = startValue + diff * easeProgress; | ||
|
|
||
| setDisplayValue(currentValue); | ||
|
|
||
| if (progress < 1) { | ||
| requestAnimationFrame(animate); | ||
| } else { | ||
| setIsAnimating(false); | ||
| prevValueRef.current = value; | ||
| } | ||
| }; | ||
|
|
||
| requestAnimationFrame(animate); | ||
| } | ||
| } else { | ||
| setDisplayValue(value); | ||
| prevValueRef.current = value; | ||
| } | ||
| }, [value]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
动画效果缺少清理逻辑
当组件在动画进行中卸载时,requestAnimationFrame 回调仍会尝试调用 setDisplayValue 和 setIsAnimating,可能导致内存泄漏或在已卸载组件上调用 setState 的警告。
建议添加清理逻辑
useEffect(() => {
if (typeof value === "number" && typeof prevValueRef.current === "number") {
if (value !== prevValueRef.current) {
setIsAnimating(true);
const duration = 400;
const startValue = prevValueRef.current;
const diff = value - startValue;
const startTime = Date.now();
+ let cancelled = false;
const animate = () => {
+ if (cancelled) return;
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - (1 - progress) ** 3;
const currentValue = startValue + diff * easeProgress;
setDisplayValue(currentValue);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
setIsAnimating(false);
prevValueRef.current = value;
}
};
requestAnimationFrame(animate);
+ return () => {
+ cancelled = true;
+ };
}
} else {
setDisplayValue(value);
prevValueRef.current = value;
}
}, [value]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (typeof value === "number" && typeof prevValueRef.current === "number") { | |
| if (value !== prevValueRef.current) { | |
| setIsAnimating(true); | |
| const duration = 400; | |
| const startValue = prevValueRef.current; | |
| const diff = value - startValue; | |
| const startTime = Date.now(); | |
| const animate = () => { | |
| const elapsed = Date.now() - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| const easeProgress = 1 - (1 - progress) ** 3; | |
| const currentValue = startValue + diff * easeProgress; | |
| setDisplayValue(currentValue); | |
| if (progress < 1) { | |
| requestAnimationFrame(animate); | |
| } else { | |
| setIsAnimating(false); | |
| prevValueRef.current = value; | |
| } | |
| }; | |
| requestAnimationFrame(animate); | |
| } | |
| } else { | |
| setDisplayValue(value); | |
| prevValueRef.current = value; | |
| } | |
| }, [value]); | |
| useEffect(() => { | |
| if (typeof value === "number" && typeof prevValueRef.current === "number") { | |
| if (value !== prevValueRef.current) { | |
| setIsAnimating(true); | |
| const duration = 400; | |
| const startValue = prevValueRef.current; | |
| const diff = value - startValue; | |
| const startTime = Date.now(); | |
| let cancelled = false; | |
| const animate = () => { | |
| if (cancelled) return; | |
| const elapsed = Date.now() - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| const easeProgress = 1 - (1 - progress) ** 3; | |
| const currentValue = startValue + diff * easeProgress; | |
| setDisplayValue(currentValue); | |
| if (progress < 1) { | |
| requestAnimationFrame(animate); | |
| } else { | |
| setIsAnimating(false); | |
| prevValueRef.current = value; | |
| } | |
| }; | |
| requestAnimationFrame(animate); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| } | |
| } else { | |
| setDisplayValue(value); | |
| prevValueRef.current = value; | |
| } | |
| }, [value]); |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/bento/metric-card.tsx around lines
130 - 161, The effect in metric-card.tsx animates value with
requestAnimationFrame but lacks cleanup; modify the useEffect (the effect that
references prevValueRef, setDisplayValue, setIsAnimating and launches
requestAnimationFrame(animate)) to track the RAF id and a mounted flag, assign
the RAF id each time you call requestAnimationFrame, and return a cleanup
function that calls cancelAnimationFrame(rafId) and flips the mounted flag (or
otherwise prevents setState) so animate stops and won't call
setDisplayValue/setIsAnimating after unmount or when value changes; ensure
prevValueRef.current is still updated only when the animation completes or in
the cleanup as appropriate.
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Metric Tabs */} | ||
| <div className="flex border-b border-border/50 dark:border-white/[0.06]"> | ||
| <button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
硬编码的 "Loading..." 文本违反 i18n 要求
根据编码规范,所有用户可见的字符串必须使用 i18n(支持 zh-CN, zh-TW, en, ja, ru 5 种语言)。此处的 "Loading..." 应使用翻译函数。
建议的修复方案
{isLoading && sessions.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-primary animate-pulse" />
- <span>Loading...</span>
+ <span>{t("loading")}</span>
</div>
</div>需要在相关的翻译文件中添加 dashboard.statistics.loading 键。
根据编码规范,所有用户可见文本必须使用 i18n。
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/_components/bento/statistics-chart-card.tsx
around lines 225 - 231, In StatisticsChartCard
(src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx) replace
the hard-coded "Loading..." text with a call to the app's i18n translation
function (e.g. t or useTranslations) — e.g. t('dashboard.statistics.loading') —
and ensure the component imports/uses the project translation hook
(useTranslation/useTranslations) if not already present; then add the
dashboard.statistics.loading key to the translation files for zh-CN, zh-TW, en,
ja and ru with appropriate localized strings.
| function formatLatency(ms: number | null): string { | ||
| if (ms === null) return "-"; | ||
| if (ms < 1000) return `${Math.round(ms)}ms`; | ||
| return `${(ms / 1000).toFixed(1)}s`; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
将延迟单位与占位符改为 i18n 资源
Line 16-20 的 "-", "ms", "s" 都是用户可见文案,需走多语言资源而非硬编码。建议用现有 i18n 工具(如翻译函数或格式化器)输出占位符与单位。As per coding guidelines, ...
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
around lines 16 - 20, In formatLatency(ms: number | null) replace hardcoded
user-facing strings ("-", "ms", "s") with your i18n resources by calling the
project's translation/formatter (e.g., use the existing t/formatMessage
function) — return the translated placeholder when ms is null and append the
localized unit string when formatting values (use localized short forms for
milliseconds and seconds and keep numeric formatting logic unchanged); update
references inside formatLatency so all visible text flows through the i18n
utility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Comments (1)
-
src/types/provider.ts, line 6-12 (link)syntax: Deprecated provider types
claude-authandgemini-clistill present in type definition. According to the PR description, these types were removed. The refactored forms use only["claude", "codex", "gemini", "openai-compatible"], but the base type definition still includes the deprecated types.
125 files reviewed, 1 comment
- Implement batch edit functionality for providers including selection, edit, delete and circuit reset - Add quick paste feature to extract provider info from clipboard text - Introduce multi-select mode with toolbar and action buttons - Create batch edit dialog with field toggles and confirmation - Add clipboard utils and provider text parser for quick paste - Include translations for new features in multiple languages - Add unit tests for provider text parsing logic
add quick paste translations for multiple languages implement comprehensive unit tests for provider batch operations
…tates - Add loading and unknownUser translations for active sessions panel - Add status translations (running, init, idle, error, done) for all supported languages - Update LiveSessionsPanel to use translated status labels - Add keyboard navigation support to BentoCard components - Fix animation cleanup in MetricCard component - Add Tooltip mock to sparkline test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
150 files reviewed, 2 comments
| setTimeout(() => { | ||
| isScrollingToSection.current = false; | ||
| }, 500); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: The fixed 500ms timeout may cause race conditions if scrolling completes faster than 500ms. The tab indicator could update incorrectly if the user manually scrolls before the timeout expires.
| setTimeout(() => { | |
| isScrollingToSection.current = false; | |
| }, 500); | |
| contentRef.current.scrollTo({ top: offset, behavior: "smooth" }); | |
| // Clear flag after animation - use transitionend listener or longer timeout | |
| setTimeout(() => { | |
| isScrollingToSection.current = false; | |
| }, 800); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 94:96
Comment:
**style:** The fixed 500ms timeout may cause race conditions if scrolling completes faster than 500ms. The tab indicator could update incorrectly if the user manually scrolls before the timeout expires.
```suggestion
contentRef.current.scrollTo({ top: offset, behavior: "smooth" });
// Clear flag after animation - use transitionend listener or longer timeout
setTimeout(() => {
isScrollingToSection.current = false;
}, 800);
```
How can I resolve this? If you propose a fix, please make it concise.| } | ||
|
|
||
| export function extractUrl(text: string): string | null { | ||
| const urlPattern = /https?:\/\/[^\s"'<>\])+,]+/gi; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: URL regex doesn't handle trailing brackets correctly. https://api.example.com/v1] would match including the ].
| const urlPattern = /https?:\/\/[^\s"'<>\])+,]+/gi; | |
| const urlPattern = /https?:\/\/[^\s"'<>\[\]]+/gi; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/provider-text-parser.ts
Line: 53:53
Comment:
**logic:** URL regex doesn't handle trailing brackets correctly. `https://api.example.com/v1]` would match including the `]`.
```suggestion
const urlPattern = /https?:\/\/[^\s"'<>\[\]]+/gi;
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
provider-form.tsx(2215 lines) into modular architecture with separate sectionsclaude-auth,gemini-cliopenai-compatiblepermanently available (no longer behind feature flag)framer-motionfor smooth animationsRelated PRs
limits-section.tsx. If this PR merges first, feat: 支持按供应商配置 Session TTL(Fixes #603) #609 will need to add the session TTL field to the refactored structure.Changes
New Architecture
Core Changes
Refactoring:
provider-form.legacy.tsxfor referenceUX Improvements:
framer-motionProvider Types:
claude-auth,gemini-cliopenai-compatiblepermanently visible (no longer behind feature flag)routing-section.tsx,provider-vendor-view.tsx,vendor-keys-compact-list.tsxi18n Cleanup:
Breaking Changes
None. This is a pure refactoring with identical functionality.
Testing
Automated Tests
Manual Testing
Checklist
Description enhanced by Claude AI
Greptile Summary
Successfully refactored a monolithic 2215-line provider form into a clean modular architecture with 8 focused files using React Context + useReducer for state management. Replaced tab content switching with vertical scroll navigation featuring smooth
framer-motionanimations and responsive layouts (desktop sidebar, tablet horizontal tabs, mobile bottom nav). Correctly removed deprecated provider types (claude-auth,gemini-cli) and madeopenai-compatiblepermanently visible across all relevant components. Added Quick Paste feature for auto-parsing provider credentials from clipboard. Updated i18n across all 5 languages removing asterisks from labels as SmartInputWrapper handles required markers.Key improvements:
Minor issues found:
Confidence Score: 4/5
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx(scroll timing) andsrc/lib/utils/provider-text-parser.ts(URL regex pattern)Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant ProviderForm participant Context participant Reducer participant FormTabNav participant Sections participant Actions User->>ProviderForm: Open form (create/edit/clone) ProviderForm->>Context: Initialize with createInitialState() Context->>Reducer: Setup useReducer with initial state Context-->>ProviderForm: Provide state & dispatch User->>FormTabNav: Click tab FormTabNav->>Context: dispatch(SET_ACTIVE_TAB) Context->>Reducer: Update activeTab in state FormTabNav->>ProviderForm: scrollToSection(tabId) ProviderForm->>ProviderForm: Smooth scroll to section User->>ProviderForm: Scroll manually ProviderForm->>ProviderForm: handleScroll() detects position ProviderForm->>Context: dispatch(SET_ACTIVE_TAB) Context->>FormTabNav: Update active tab indicator User->>Sections: Edit field (name/url/key/etc) Sections->>Context: dispatch(SET_NAME/SET_URL/etc) Context->>Reducer: Update specific field in state Reducer-->>Context: Return new state Context-->>Sections: Re-render with new values User->>ProviderForm: Submit form ProviderForm->>ProviderForm: validateForm() alt Validation fails ProviderForm->>User: Show error toast else Validation passes alt Circuit breaker needs confirmation ProviderForm->>User: Show confirmation dialog User->>ProviderForm: Confirm end ProviderForm->>Actions: addProvider() or editProvider() Actions->>Actions: Transform state to API format Actions-->>ProviderForm: Success/Error response alt Success ProviderForm->>Context: dispatch(RESET_FORM) if create ProviderForm->>User: Show success toast ProviderForm->>User: Call onSuccess callback else Error ProviderForm->>User: Show error toast end end