diff --git a/ui/src/components/copilot/copilot-config-form.tsx b/ui/src/components/copilot/copilot-config-form.tsx index 1df9a38d..47603aa7 100644 --- a/ui/src/components/copilot/copilot-config-form.tsx +++ b/ui/src/components/copilot/copilot-config-form.tsx @@ -27,7 +27,7 @@ import { SelectGroup, SelectLabel, } from '@/components/ui/select'; -import { useCopilot } from '@/hooks/use-copilot'; +import { useCopilot, type CopilotModel, type CopilotPlanTier } from '@/hooks/use-copilot'; import { Loader2, Save, Code2, X, Info, RefreshCw, Sparkles, Zap, Check } from 'lucide-react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/confirm-dialog'; @@ -38,27 +38,74 @@ const CodeEditor = lazy(() => ); // Model presets for quick configuration -const MODEL_PRESETS = [ +// Grouped by tier: Free (available to all) and Paid (requires Pro+) +const FREE_PRESETS = [ + { + name: 'GPT-4.1 (Free)', + description: 'Free tier - no premium usage', + default: 'gpt-4.1', + opus: 'gpt-4.1', + sonnet: 'gpt-4.1', + haiku: 'gpt-4.1', + }, + { + name: 'GPT-5 Mini (Free)', + description: 'Free tier - lightweight model', + default: 'gpt-5-mini', + opus: 'gpt-5-mini', + sonnet: 'gpt-5-mini', + haiku: 'gpt-5-mini', + }, + { + name: 'Claude Haiku 4.5 (Free)', + description: 'Free tier - fast Anthropic model', + default: 'claude-haiku-4.5', + opus: 'claude-haiku-4.5', + sonnet: 'claude-haiku-4.5', + haiku: 'claude-haiku-4.5', + }, +]; + +const PAID_PRESETS = [ { name: 'Claude Opus 4.5', - default: 'claude-opus-4-5-20250514', - opus: 'claude-opus-4-5-20250514', - sonnet: 'claude-sonnet-4-20250514', - haiku: 'claude-haiku-3-5-20250514', + description: 'Pro+ (3x) - Most capable reasoning', + default: 'claude-opus-4.5', + opus: 'claude-opus-4.5', + sonnet: 'claude-sonnet-4.5', + haiku: 'claude-haiku-4.5', }, { - name: 'Claude Sonnet 4', - default: 'claude-sonnet-4-20250514', - opus: 'claude-sonnet-4-20250514', - sonnet: 'claude-sonnet-4-20250514', - haiku: 'claude-haiku-3-5-20250514', + name: 'Claude Sonnet 4.5', + description: 'Pro+ (1x) - Balanced performance', + default: 'claude-sonnet-4.5', + opus: 'claude-opus-4.5', + sonnet: 'claude-sonnet-4.5', + haiku: 'claude-haiku-4.5', }, { - name: 'GPT-4o', - default: 'gpt-4o', - opus: 'gpt-4o', - sonnet: 'gpt-4o', - haiku: 'gpt-4o-mini', + name: 'GPT-5.2', + description: 'Pro+ (1x) - Latest OpenAI (Preview)', + default: 'gpt-5.2', + opus: 'gpt-5.2', + sonnet: 'gpt-5.1', + haiku: 'gpt-5-mini', + }, + { + name: 'GPT-5.1 Codex Max', + description: 'Pro+ (1x) - Best for coding', + default: 'gpt-5.1-codex-max', + opus: 'gpt-5.1-codex-max', + sonnet: 'gpt-5.1-codex', + haiku: 'gpt-5.1-codex-mini', + }, + { + name: 'Gemini 2.5 Pro', + description: 'Pro+ (1x) - Google latest', + default: 'gemini-2.5-pro', + opus: 'gemini-2.5-pro', + sonnet: 'gemini-2.5-pro', + haiku: 'gemini-3-flash', }, ]; @@ -67,10 +114,37 @@ interface FlexibleModelSelectorProps { description?: string; value: string | undefined; onChange: (model: string) => void; - models: { id: string }[]; + models: CopilotModel[]; disabled?: boolean; } +/** Get badge style for plan tier */ +function getPlanBadgeStyle(plan?: CopilotPlanTier): string { + switch (plan) { + case 'free': + return 'bg-green-100 text-green-700 border-green-200'; + case 'pro': + return 'bg-blue-100 text-blue-700 border-blue-200'; + case 'pro+': + return 'bg-purple-100 text-purple-700 border-purple-200'; + case 'business': + return 'bg-orange-100 text-orange-700 border-orange-200'; + case 'enterprise': + return 'bg-red-100 text-red-700 border-red-200'; + default: + return 'bg-muted text-muted-foreground'; + } +} + +/** Get multiplier display */ +function getMultiplierDisplay(multiplier?: number): string | null { + if (multiplier === undefined || multiplier === null) return null; + if (multiplier === 0) return 'Free'; + if (multiplier < 1) return `${multiplier}x`; + if (multiplier === 1) return '1x'; + return `${multiplier}x`; +} + function FlexibleModelSelector({ label, description, @@ -79,6 +153,9 @@ function FlexibleModelSelector({ models, disabled, }: FlexibleModelSelectorProps) { + // Find current model for display + const currentModel = models.find((m) => m.id === value); + return (