From 844df69b7c2a698a2d63a56e37d8c044c1a4ab87 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sat, 20 Dec 2025 01:04:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BB=84=E4=BB=B6):=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E6=94=AF=E6=8C=81=E5=A4=9A=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ModelSelector 组件支持可选的 selectedModel - ModelCardSelector 新增 Claude 和 Codex 模型分组展示 - 添加模型类型切换提示,提醒用户不同模型的功能差异 - 优化模型选择交互体验 --- src/components/features/ModelSelector.tsx | 20 +- src/components/ui/ModelCardSelector.tsx | 278 +++++++++++++++++----- 2 files changed, 239 insertions(+), 59 deletions(-) diff --git a/src/components/features/ModelSelector.tsx b/src/components/features/ModelSelector.tsx index 60d2292..7e96a21 100644 --- a/src/components/features/ModelSelector.tsx +++ b/src/components/features/ModelSelector.tsx @@ -7,7 +7,7 @@ import type { Model } from '@/types'; interface ModelSelectorProps { models: Model[]; - selectedModel: Model; + selectedModel?: Model; onSelect: (model: Model) => void; } @@ -15,6 +15,9 @@ export function ModelSelector({ models, selectedModel, onSelect }: ModelSelector const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); + // 如果没有 selectedModel 或 models 为空,使用第一个模型或显示占位符 + const currentModel = selectedModel || models[0]; + // 点击外部关闭 useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -27,6 +30,15 @@ export function ModelSelector({ models, selectedModel, onSelect }: ModelSelector return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + // 如果没有任何模型数据,显示加载状态 + if (!currentModel || models.length === 0) { + return ( +
+ 加载中... +
+ ); + } + return (
{/* 触发按钮 */} @@ -34,7 +46,7 @@ export function ModelSelector({ models, selectedModel, onSelect }: ModelSelector onClick={() => setIsOpen(!isOpen)} className="flex items-center gap-1 px-2 py-1 text-sm font-medium text-[var(--color-text-secondary)] rounded-lg hover:bg-[var(--color-bg-hover)] transition-colors" > - {selectedModel.name} + {currentModel.name} {models.map((model) => { - const isSelected = model.id === selectedModel.id; + const isSelected = model.id === currentModel.id; return ( diff --git a/src/components/ui/ModelCardSelector.tsx b/src/components/ui/ModelCardSelector.tsx index a1c9bd2..29b62bb 100644 --- a/src/components/ui/ModelCardSelector.tsx +++ b/src/components/ui/ModelCardSelector.tsx @@ -1,9 +1,11 @@ 'use client'; import { cn } from '@/lib/utils'; +import { useState, useEffect } from 'react'; +import { AlertCircle } from 'lucide-react'; -// 模型卡片配置 -const MODEL_CARDS = [ +// Claude 模型卡片配置 +const CLAUDE_MODEL_CARDS = [ { id: 'haiku', name: 'Haiku', @@ -24,11 +26,36 @@ const MODEL_CARDS = [ }, ]; +// Codex 模型卡片配置 +const CODEX_MODEL_CARDS = [ + { + id: 'gpt-5.1-codex', + name: 'Codex', + description: 'Standard', + modelIdPattern: 'gpt-5.1-codex', + }, + { + id: 'gpt-5.1-codex-max', + name: 'Codex Max', + description: 'Enhanced', + modelIdPattern: 'gpt-5.1-codex-max', + }, + { + id: 'gpt-5.2-codex', + name: 'Codex Pro', + description: 'Latest', + modelIdPattern: 'gpt-5.2-codex', + }, +]; + +// 模型类型 +type ModelType = 'claude' | 'codex'; + interface ModelCardSelectorProps { value: string; onChange: (modelId: string) => void; disabled?: boolean; - models?: { modelId: string; displayName: string }[]; + models?: { modelId: string; displayName: string; modelType?: string }[]; } export function ModelCardSelector({ @@ -37,72 +64,213 @@ export function ModelCardSelector({ disabled = false, models = [], }: ModelCardSelectorProps) { - // 根据当前选中的模型ID判断选中的卡片 - const getSelectedCard = (modelId: string): string => { + // 当前选中的模型类型 + const [showModelChangeHint, setShowModelChangeHint] = useState(false); + const [previousModelType, setPreviousModelType] = useState(null); + + // 判断模型类型 + const getModelType = (modelId: string): ModelType => { + if (modelId.startsWith('gpt-') && modelId.includes('codex')) { + return 'codex'; + } + return 'claude'; + }; + + // 根据当前选中的模型ID判断选中的卡片(Claude) + const getSelectedClaudeCard = (modelId: string): string | null => { const lowerModelId = modelId.toLowerCase(); if (lowerModelId.includes('haiku')) return 'haiku'; if (lowerModelId.includes('opus')) return 'opus'; - return 'sonnet'; // 默认 sonnet + if (lowerModelId.includes('sonnet')) return 'sonnet'; + return null; }; - // 根据卡片类型找到对应的实际模型ID - const findModelIdByCard = (cardId: string): string => { + // 根据当前选中的模型ID判断选中的卡片(Codex) + const getSelectedCodexCard = (modelId: string): string | null => { + if (modelId === 'gpt-5.1-codex') return 'gpt-5.1-codex'; + if (modelId === 'gpt-5.1-codex-max') return 'gpt-5.1-codex-max'; + if (modelId === 'gpt-5.2-codex') return 'gpt-5.2-codex'; + return null; + }; + + // 根据卡片类型找到对应的实际模型ID(Claude) + const findClaudeModelIdByCard = (cardId: string): string => { const model = models.find((m) => m.modelId.toLowerCase().includes(cardId) ); return model?.modelId || value; }; - const selectedCard = getSelectedCard(value); + // 根据卡片类型找到对应的实际模型ID(Codex) + const findCodexModelIdByCard = (cardId: string): string => { + const model = models.find((m) => m.modelId === cardId); + return model?.modelId || cardId; + }; + + const currentModelType = getModelType(value); + const selectedClaudeCard = getSelectedClaudeCard(value); + const selectedCodexCard = getSelectedCodexCard(value); + + // 处理模型选择 + const handleModelSelect = (modelId: string, modelType: ModelType) => { + if (disabled) return; + + const newModelType = modelType; + + // 检测模型类型切换 + if (previousModelType && previousModelType !== newModelType) { + setShowModelChangeHint(true); + setTimeout(() => setShowModelChangeHint(false), 5000); + } + + setPreviousModelType(newModelType); + onChange(modelId); + }; + + // 初始化时设置 previousModelType + useEffect(() => { + if (value && !previousModelType) { + setPreviousModelType(getModelType(value)); + } + }, [value, previousModelType]); + + // 检查是否有 Codex 模型可用 + const hasCodexModels = models.some(m => m.modelType === 'codex' || (m.modelId.startsWith('gpt-') && m.modelId.includes('codex'))); return ( -
- {MODEL_CARDS.map((card) => { - const isSelected = selectedCard === card.id; - return ( - + ); + })} +
+
+ + {/* Codex 模型组 */} + {hasCodexModels && ( +
+
+ + Codex 模型 + + OpenAI - - {card.description} - - - ); - })} +
+
+ {CODEX_MODEL_CARDS.map((card) => { + const isSelected = selectedCodexCard === card.id; + // 检查该模型是否在可用模型列表中 + const isAvailable = models.some(m => m.modelId === card.id); + + if (!isAvailable) return null; + + return ( + + ); + })} +
+
+ )} ); }