Feat/provider: Refactor provider router and enhance UI components#109
Feat/provider: Refactor provider router and enhance UI components#109whhjdi merged 3 commits intoawsl-project:mainfrom
Conversation
📝 Walkthrough概述此PR引入了新的提供商创建和编辑工作流程,采用基于hooks和context的架构替代原有的prop驱动模式。同时添加了MarqueeBackground可复用UI组件,用于多个位置的流媒体背景动画。路由层面支持了多步骤创建流程,包括类型选择、token导入和自定义配置。 变更清单
时序图sequenceDiagram
actor User
participant Router as Router/Pages
participant FormContext as ProviderFormContext
participant FormHook as useProviderForm
participant NavHook as useProviderNavigation
participant Steps as Create Steps<br/>(Select/Config/Import)
participant API as Provider API
User->>Router: 点击"添加提供商"
Router->>Router: 导航至 /providers/create
Router->>FormContext: 初始化 ProviderFormProvider
FormContext->>FormHook: 提供表单状态和更新函数
User->>Steps: 选择提供商类型
Steps->>FormHook: updateFormData(type)
FormHook->>FormContext: 更新表单数据
Steps->>NavHook: goToAntigravity() 或 goToKiro()
NavHook->>Router: 导航至对应步骤
User->>Steps: 输入Token/配置
Steps->>FormHook: updateFormData(config)
FormHook->>FormContext: 合并配置数据
User->>Steps: 点击创建
Steps->>API: createProvider.mutateAsync(formData)
API-->>Steps: 成功响应
Steps->>NavHook: goToProviders()
NavHook->>Router: 导航至 /providers
Router-->>User: 显示提供商列表
预计审查工作量🎯 3 (中等复杂) | ⏱️ ~25 分钟 可能相关的PR
建议审查者
兔兔之歌
🚥 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
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/pages/providers/components/antigravity-token-import.tsx (1)
181-209: OAuth 创建失败不会向用户展示错误
handleOAuthCreate捕获异常只设置了error,但错误提示仅在oauthStatus === 'error'时渲染,导致创建失败时用户无反馈。建议在成功态也展示创建错误或设置独立状态。🛠️ 建议修复(示例)
- {error && oauthStatus === 'error' && ( + {error && (oauthStatus === 'error' || oauthStatus === 'success') && ( <div className="bg-error/5 border border-error/20 rounded-xl p-4 flex items-start gap-3 animate-in fade-in zoom-in-95"> <AlertCircle size={20} className="text-error shrink-0 mt-0.5" /> <div> - <p className="text-sm font-medium text-error">OAuth Failed</p> + <p className="text-sm font-medium text-error"> + {oauthStatus === 'error' ? 'OAuth Failed' : 'Provider Creation Failed'} + </p> <p className="text-xs text-error/80 mt-0.5">{error}</p> </div> </div> )}
🤖 Fix all issues with AI agents
In `@web/src/pages/providers/components/select-type-step.tsx`:
- Line 183: The "Empty template" button currently only navigates via
goToCustomConfig without clearing previously selected template/config; before
calling goToCustomConfig, reset the template-related form state (e.g.,
selectedTemplate, templateId, and any templateConfig values) by invoking the
form context's reset method (e.g., formContext.resetForm() or
form.resetFields()) or manually setting defaults from the form context defaults,
then call goToCustomConfig so the "从零开始" state is truly empty.
🧹 Nitpick comments (6)
web/src/pages/providers/edit.tsx (1)
12-12: ID 比较方式可以优化使用
p.id + '' === id + ''进行字符串比较虽然能正常工作,但可读性较差。建议使用更清晰的方式:♻️ 建议的改进
- const provider = providers?.find((p) => p.id + '' === id + ''); + const provider = providers?.find((p) => String(p.id) === id);web/src/pages/providers/context/provider-form-context.tsx (1)
22-28: 建议为validationResult和oauthState定义具体类型使用
any类型会失去 TypeScript 的类型安全优势。建议定义具体的接口类型以提高代码可维护性和类型安全性。♻️ 建议定义具体类型
+// 定义具体类型替代 any +interface ValidationResult { + isValid: boolean; + message?: string; + // 根据实际需求添加其他字段 +} + +interface OAuthState { + token?: string; + expiresAt?: number; + // 根据实际需求添加其他字段 +} + interface ProviderFormContextType { // ... - validationResult: any | null; - setValidationResult: (result: any | null) => void; + validationResult: ValidationResult | null; + setValidationResult: (result: ValidationResult | null) => void; - oauthState: any | null; - setOAuthState: (state: any | null) => void; + oauthState: OAuthState | null; + setOAuthState: (state: OAuthState | null) => void; }web/src/pages/client-routes/components/provider-row.tsx (1)
186-188: 未使用的isOverlay参数
isOverlay参数被解构但未使用,虽然已添加 eslint-disable 注释。如果此参数是为将来功能预留的,建议添加 TODO 注释说明用途;如果不再需要,可以考虑移除。web/src/pages/providers/create-layout.tsx (1)
11-16: 考虑添加未知路径的回退处理当前路由配置没有处理
/providers/create/invalid-path这类无效路径的情况。可以考虑添加一个通配符路由来重定向到选择类型页面。♻️ 可选的回退路由
<Routes> <Route index element={<SelectTypeStep />} /> <Route path="custom" element={<CustomConfigStep />} /> <Route path="antigravity" element={<AntigravityTokenImport />} /> <Route path="kiro" element={<KiroTokenImport />} /> + <Route path="*" element={<Navigate to="/providers/create" replace />} /> </Routes>需要从
react-router-dom导入Navigate。web/src/pages/providers/index.tsx (1)
1-1: 建议合并导入语句
useState可以与其他 React hooks 合并到同一行导入,保持导入语句的整洁。♻️ 合并导入
-import { useMemo, useRef } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { Plus, Layers, Download, Upload } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useProviders, useAllProviderStats } from '@/hooks/queries'; import { useStreamingRequests } from '@/hooks/use-streaming'; import type { Provider, ImportResult } from '@/lib/transport'; import { getTransport } from '@/lib/transport'; import { ProviderRow } from './components/provider-row'; import { useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { PageHeader } from '@/components/layout/page-header'; import { PROVIDER_TYPE_CONFIGS, type ProviderTypeKey } from './types'; -import { useState } from 'react';Also applies to: 14-14
web/src/pages/providers/components/custom-config-step.tsx (1)
45-48: setTimeout 导航建议加清理成功后延迟跳转若组件提前卸载,可能触发意外导航。可选地记录定时器并在卸载时清理。
♻️ 可选改进(示例)
+import { useEffect, useRef } from 'react'; import { Globe, ChevronLeft, Key, Check } from 'lucide-react'; ... export function CustomConfigStep() { + const redirectTimeoutRef = useRef<number | null>(null); ... - setTimeout(() => goToProviders(), 500); + redirectTimeoutRef.current = window.setTimeout(() => goToProviders(), 500); } catch (error) { console.error('Failed to create provider:', error); setSaveStatus('error'); } finally { setSaving(false); } }; + + useEffect(() => { + return () => { + if (redirectTimeoutRef.current !== null) { + clearTimeout(redirectTimeoutRef.current); + } + }; + }, []);
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
web/src/App.tsxweb/src/components/layout/app-sidebar/client-routes-items.tsxweb/src/components/layout/app-sidebar/requests-nav-item.tsxweb/src/components/ui/index.tsweb/src/components/ui/marquee-background.tsxweb/src/pages/client-routes/components/provider-row.tsxweb/src/pages/providers/components/antigravity-token-import.tsxweb/src/pages/providers/components/custom-config-step.tsxweb/src/pages/providers/components/kiro-token-import.tsxweb/src/pages/providers/components/provider-row.tsxweb/src/pages/providers/components/select-type-step.tsxweb/src/pages/providers/context/provider-form-context.tsxweb/src/pages/providers/create-layout.tsxweb/src/pages/providers/edit.tsxweb/src/pages/providers/hooks/use-provider-navigation.tsweb/src/pages/providers/index.tsx
🧰 Additional context used
🧬 Code graph analysis (10)
web/src/pages/providers/components/custom-config-step.tsx (2)
web/src/pages/providers/context/provider-form-context.tsx (1)
useProviderForm(95-101)web/src/pages/providers/hooks/use-provider-navigation.ts (1)
useProviderNavigation(3-14)
web/src/pages/providers/components/provider-row.tsx (2)
web/src/components/ui/index.ts (1)
MarqueeBackground(49-49)web/src/components/ui/marquee-background.tsx (1)
MarqueeBackground(24-39)
web/src/pages/providers/edit.tsx (1)
web/src/pages/providers/components/provider-edit-flow.tsx (1)
ProviderEditFlow(33-292)
web/src/components/ui/marquee-background.tsx (1)
web/src/components/ui/index.ts (1)
MarqueeBackground(49-49)
web/src/pages/providers/components/antigravity-token-import.tsx (3)
web/src/pages/providers/hooks/use-provider-navigation.ts (1)
useProviderNavigation(3-14)web/src/lib/transport/http-transport.ts (1)
createProvider(96-99)web/src/hooks/queries/use-providers.ts (1)
useCreateProvider(37-47)
web/src/components/layout/app-sidebar/client-routes-items.tsx (4)
web/src/components/ui/index.ts (1)
MarqueeBackground(49-49)web/src/components/ui/marquee-background.tsx (1)
MarqueeBackground(24-39)web/src/components/icons/client-icons.tsx (1)
ClientIcon(65-100)web/src/components/ui/streaming-badge.tsx (1)
StreamingBadge(27-81)
web/src/pages/client-routes/components/provider-row.tsx (2)
web/src/components/ui/index.ts (1)
MarqueeBackground(49-49)web/src/components/ui/marquee-background.tsx (1)
MarqueeBackground(24-39)
web/src/pages/providers/context/provider-form-context.tsx (1)
web/src/pages/providers/types.ts (3)
ProviderFormData(165-172)ClientConfig(150-155)defaultClients(157-162)
web/src/App.tsx (2)
web/src/pages/providers/create-layout.tsx (1)
ProviderCreateLayout(8-19)web/src/pages/providers/edit.tsx (1)
ProviderEditPage(6-31)
web/src/pages/providers/components/kiro-token-import.tsx (3)
web/src/pages/providers/hooks/use-provider-navigation.ts (1)
useProviderNavigation(3-14)web/src/lib/transport/http-transport.ts (1)
createProvider(96-99)web/src/hooks/queries/use-providers.ts (1)
useCreateProvider(37-47)
🔇 Additional comments (20)
web/src/pages/providers/edit.tsx (1)
6-30: 组件结构清晰,状态处理完善组件正确处理了加载状态、未找到状态和成功状态。使用
useProviders获取数据并传递给ProviderEditFlow的方式符合 React 最佳实践。web/src/pages/providers/hooks/use-provider-navigation.ts (1)
3-13: 导航 Hook 实现简洁明了这个 Hook 很好地封装了导航逻辑,提供了语义化的导航方法。这种模式使得组件代码更加清晰,也便于后续路由变更时统一修改。
web/src/components/ui/index.ts (1)
47-49: 导出新增正确
MarqueeBackground组件的导出遵循了现有的 barrel export 模式,保持了代码风格一致性。web/src/pages/providers/context/provider-form-context.tsx (2)
42-47: 状态管理实现良好Context Provider 正确初始化了所有必要的状态,使用
useState管理表单数据和各种状态标志。updateFormData和updateClient方法使用不可变更新模式,符合 React 最佳实践。
67-74: 此顾虑不适用 - 创建和编辑流程使用不同的组件
ProviderFormContext仅在创建流程中使用(create-layout.tsx),编辑流程使用独立的ProviderEditFlow组件(provider-edit-flow.tsx),两者有各自的验证逻辑。编辑流程中的isValid()不要求apiKey非空,并在保存时保留现有密钥(apiKey: formData.apiKey || provider.config?.custom?.apiKey || ''),与 UI 标签"留空保持当前"的设计一致。两个流程的验证差异是合理的,无需修改。Likely an incorrect or invalid review comment.
web/src/pages/client-routes/components/provider-row.tsx (1)
253-257: MarqueeBackground 组件使用正确使用新的
MarqueeBackground组件替代内联标记是很好的重构。show条件正确检查了streamingCount > 0 && enabled && !isInCooldown,与原有逻辑保持一致。web/src/components/ui/marquee-background.tsx (1)
24-38: 组件设计良好,CSS 动画类已正确定义
MarqueeBackground组件实现简洁优雅。使用pointer-events-none确保不会阻挡用户交互,absolute inset-0实现全覆盖定位。animate-marqueeCSS 类和@keyframes marquee动画已在全局样式web/src/index.css中正确定义,包括深色模式支持。组件通过transition-opacity提供平滑的淡入淡出效果,动画配置合理。web/src/pages/providers/components/provider-row.tsx (1)
4-4: 重构为可复用组件,实现良好!使用
MarqueeBackground组件替代内联的条纹背景实现是一个很好的抽象。这提高了代码的一致性和可维护性,与client-routes-items.tsx和requests-nav-item.tsx中的使用模式保持一致。Also applies to: 142-142
web/src/App.tsx (1)
50-51: 路由配置正确!新增的路由结构合理:
/providers/create/*使用通配符支持ProviderCreateLayout内部的嵌套路由(如/providers/create/custom、/providers/create/antigravity等)/providers/:id/edit用于编辑特定 provider路由放置在
/providers索引路由之后,React Router v7 的路由排名算法会正确处理匹配优先级。web/src/components/layout/app-sidebar/client-routes-items.tsx (1)
34-40: 重构一致且正确!
MarqueeBackground的show条件streamingCount > 0 && !isActive确保在激活状态下不显示动画背景,避免视觉冲突。SidebarMenuBadge始终渲染是合理的,因为StreamingBadge组件内部会在displayCount === 0时返回null,处理了自身的可见性逻辑。web/src/pages/providers/create-layout.tsx (1)
8-18: 布局结构清晰!
ProviderCreateLayout正确地将ProviderFormProvider上下文包裹在所有嵌套路由外层,确保表单状态在步骤切换时得以保持。嵌套路由结构符合 React Router v7 的模式。web/src/components/layout/app-sidebar/requests-nav-item.tsx (1)
28-34: 与其他侧边栏项保持一致的重构!实现与
client-routes-items.tsx完全一致:
MarqueeBackground使用相同的show条件模式SidebarMenuBadge始终渲染,StreamingBadge内部处理可见性这种一致性有助于代码维护。
web/src/pages/providers/index.tsx (2)
1-4: 导航驱动的流程重构良好!将状态驱动的 UI 流程改为路由导航是一个很好的架构改进:
- 更清晰的关注点分离
- 创建/编辑流程有独立的 URL,支持浏览器历史导航和深度链接
- 简化了
ProvidersPage的职责Also applies to: 14-14, 18-18
135-138: 导航路径正确!三处导航调用都使用了正确的路由路径:
/providers/create对应ProviderCreateLayout/providers/${provider.id}/edit对应ProviderEditPage与
App.tsx中定义的路由配置一致。Also applies to: 153-158, 186-186
web/src/pages/providers/components/kiro-token-import.tsx (2)
19-25: 内部导航接线清晰改为
useProviderNavigation并在返回按钮上统一路由,和新的创建流程契合。Also applies to: 106-106
88-89: 创建成功后跳转逻辑清楚
mutateAsync完成后再导航,流程一致且易读。web/src/pages/providers/components/antigravity-token-import.tsx (2)
24-33: 内部化导航与创建钩子替代 props 可读路由和创建逻辑收敛到组件内部,后续维护更一致。
Also applies to: 223-223
171-172: Token 创建后导航流程 OK创建成功即跳转列表,行为明确。
web/src/pages/providers/components/custom-config-step.tsx (1)
8-15: 表单上下文接线一致,状态驱动清楚通过
useProviderForm/updateFormData与导航钩子统一入口,UI 与状态同步明确。Also applies to: 60-77, 105-106, 121-122, 140-141
web/src/pages/providers/components/select-type-step.tsx (1)
11-48: 类型选择与模板应用逻辑清晰选择类型与应用模板的状态更新路径一致,阅读体验好。
Also applies to: 78-138, 216-216
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| {/* Empty Template Card */} | ||
| <Button | ||
| onClick={onSkipToConfig} | ||
| onClick={goToCustomConfig} |
There was a problem hiding this comment.
“空模板”未清空已选模板/配置
点击“空模板”仅跳转,若用户之前已选择模板,配置仍会保留,和“从零开始”语义不符。建议跳转前重置模板相关字段(最好复用表单上下文的默认值/重置方法)。
🛠️ 建议修复(示例)
- <Button
- onClick={goToCustomConfig}
+ <Button
+ onClick={() => {
+ updateFormData({
+ selectedTemplate: null,
+ name: '',
+ clients: formData.clients.map((client) => ({
+ ...client,
+ enabled: false,
+ urlOverride: '',
+ })),
+ });
+ goToCustomConfig();
+ }}
variant="ghost"
className="text-left group p-0 rounded-xl border border-dashed h-full w-full min-h-36 sm:min-h-40 transition-all duration-200 border-border bg-card hover:bg-muted hover:border-accent/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
>🤖 Prompt for AI Agents
In `@web/src/pages/providers/components/select-type-step.tsx` at line 183, The
"Empty template" button currently only navigates via goToCustomConfig without
clearing previously selected template/config; before calling goToCustomConfig,
reset the template-related form state (e.g., selectedTemplate, templateId, and
any templateConfig values) by invoking the form context's reset method (e.g.,
formContext.resetForm() or form.resetFields()) or manually setting defaults from
the form context defaults, then call goToCustomConfig so the "从零开始" state is
truly empty.
Summary by CodeRabbit
发布说明
新功能
重构
✏️ Tip: You can customize this high-level summary in your review settings.