/** * CLIProxy Variant Dialog Component * Phase 03: REST API Routes & CRUD * Phase 05: Dashboard UI full CRUD for composite variants * Phase 06: Multi-Account Support */ import { useForm, useWatch } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy'; import { usePrivacy } from '@/contexts/privacy-context'; import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config'; import { isDeniedAgyModelId } from '@/lib/utils'; const singleProviderSchema = z.object({ name: z .string() .min(1, 'Name is required') .regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'), provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }), model: z.string().optional(), account: z.string().optional(), target: z.enum(['claude', 'droid']), }); const compositeSchema = z.object({ name: z .string() .min(1, 'Name is required') .regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'), default_tier: z.enum(['opus', 'sonnet', 'haiku'], { message: 'Default tier is required' }), target: z.enum(['claude', 'droid']), tiers: z.object({ opus: z.object({ provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }), model: z.string().trim().min(1, 'Model is required'), account: z.string().optional(), }), sonnet: z.object({ provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }), model: z.string().trim().min(1, 'Model is required'), account: z.string().optional(), }), haiku: z.object({ provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }), model: z.string().trim().min(1, 'Model is required'), account: z.string().optional(), }), }), }); type SingleProviderFormData = z.infer; type CompositeFormData = z.infer; interface CliproxyDialogProps { open: boolean; onClose: () => void; } const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({ value: id, label: getProviderDisplayName(id), })); const AGY_DENYLIST_MESSAGE = 'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'; function isDeniedAgyModelForProvider(provider: string, modelId: string | undefined): boolean { return provider === 'agy' && typeof modelId === 'string' && isDeniedAgyModelId(modelId); } export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { const { t } = useTranslation(); const createMutation = useCreateVariant(); const { data: authData } = useCliproxyAuth(); const { privacyMode } = usePrivacy(); const [mode, setMode] = useState<'single' | 'composite'>('single'); const singleForm = useForm({ resolver: zodResolver(singleProviderSchema), defaultValues: { target: 'claude' }, }); const compositeForm = useForm({ resolver: zodResolver(compositeSchema), defaultValues: { default_tier: 'opus', target: 'claude', tiers: { opus: { provider: 'gemini', model: '' }, sonnet: { provider: 'gemini', model: '' }, haiku: { provider: 'gemini', model: '' }, }, }, }); const selectedProvider = useWatch({ control: singleForm.control, name: 'provider' }); const providerAuth = authData?.authStatus.find((s) => s.provider === selectedProvider); const providerAccounts = providerAuth?.accounts || []; const onSubmitSingle = async (data: SingleProviderFormData) => { if (isDeniedAgyModelForProvider(data.provider, data.model)) { singleForm.setError('model', { message: AGY_DENYLIST_MESSAGE }); toast.error(AGY_DENYLIST_MESSAGE); return; } try { await createMutation.mutateAsync(data); singleForm.reset(); onClose(); } catch (error) { console.error('Failed to create variant:', error); } }; const onSubmitComposite = async (data: CompositeFormData) => { for (const tier of ['opus', 'sonnet', 'haiku'] as const) { const tierConfig = data.tiers[tier]; if (!isDeniedAgyModelForProvider(tierConfig.provider, tierConfig.model)) continue; compositeForm.setError(`tiers.${tier}.model`, { message: AGY_DENYLIST_MESSAGE }); toast.error(AGY_DENYLIST_MESSAGE); return; } try { await createMutation.mutateAsync({ name: data.name, provider: data.tiers[data.default_tier].provider, target: data.target, type: 'composite', default_tier: data.default_tier, tiers: data.tiers, }); compositeForm.reset(); onClose(); } catch (error) { console.error('Failed to create composite variant:', error); } }; return ( Create CLIProxy Variant
{mode === 'single' ? (
{singleForm.formState.errors.name && ( {singleForm.formState.errors.name.message} )}
{singleForm.formState.errors.provider && ( {singleForm.formState.errors.provider.message} )}
{selectedProvider && providerAccounts.length > 0 && (
)}
) : (
{compositeForm.formState.errors.name && ( {compositeForm.formState.errors.name.message} )}
{t('cliproxyDialog.opus')} {t('cliproxyDialog.sonnet')} {t('cliproxyDialog.haiku')} {(['opus', 'sonnet', 'haiku'] as const).map((tier) => (
{compositeForm.formState.errors.tiers?.[tier]?.model && ( {compositeForm.formState.errors.tiers[tier]?.model?.message} )}
))}
)}
); }