/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useState, useEffect, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { getUserModels } from '@/lib/api' import { Button } from '@/components/ui/button' import { ComboboxInput } from '@/components/ui/combobox-input' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' const APP_CONFIGS = { claude: { label: 'Claude', defaultName: 'My Claude', modelFields: [ { key: 'model', labelKey: 'Primary Model', required: true }, { key: 'haikuModel', labelKey: 'Haiku Model', required: false }, { key: 'sonnetModel', labelKey: 'Sonnet Model', required: false }, { key: 'opusModel', labelKey: 'Opus Model', required: false }, ], }, codex: { label: 'Codex', defaultName: 'My Codex', modelFields: [{ key: 'model', labelKey: 'Primary Model', required: true }], }, gemini: { label: 'Gemini', defaultName: 'My Gemini', modelFields: [{ key: 'model', labelKey: 'Primary Model', required: true }], }, } as const type AppType = keyof typeof APP_CONFIGS function getServerAddress(): string { try { const raw = localStorage.getItem('status') if (raw) { const status = JSON.parse(raw) if (status.server_address) return status.server_address } } catch { /* empty */ } return window.location.origin } function buildCCSwitchURL( app: string, name: string, models: Record, apiKey: string ): string { const serverAddress = getServerAddress() const endpoint = app === 'codex' ? serverAddress + '/v1' : serverAddress const params = new URLSearchParams() params.set('resource', 'provider') params.set('app', app) params.set('name', name) params.set('endpoint', endpoint) params.set('apiKey', apiKey) for (const [k, v] of Object.entries(models)) { if (v) params.set(k, v) } params.set('homepage', serverAddress) params.set('enabled', 'true') return `ccswitch://v1/import?${params.toString()}` } interface Props { open: boolean onOpenChange: (open: boolean) => void tokenKey: string } export function CCSwitchDialog(props: Props) { const { t } = useTranslation() const [app, setApp] = useState('claude') const [name, setName] = useState(APP_CONFIGS.claude.defaultName) const [models, setModels] = useState>({}) const { data: modelsData } = useQuery({ queryKey: ['user-models-ccswitch'], queryFn: getUserModels, enabled: props.open, staleTime: 5 * 60 * 1000, }) const modelOptions = useMemo(() => { const items = modelsData?.data ?? [] return items.map((m) => ({ value: m, label: m })) }, [modelsData?.data]) useEffect(() => { if (props.open) { // eslint-disable-next-line react-hooks/set-state-in-effect setModels({}) setApp('claude') setName(APP_CONFIGS.claude.defaultName) } }, [props.open]) const currentConfig = APP_CONFIGS[app] const handleAppChange = (val: string) => { const appVal = val as AppType setApp(appVal) setName(APP_CONFIGS[appVal].defaultName) setModels({}) } const handleSubmit = () => { if (!models.model) { toast.warning(t('Please select a primary model')) return } const key = props.tokenKey.startsWith('sk-') ? props.tokenKey : `sk-${props.tokenKey}` const url = buildCCSwitchURL(app, name, models, key) window.open(url, '_blank') props.onOpenChange(false) } return ( {t('Import to CC Switch')}
{( Object.entries(APP_CONFIGS) as [ AppType, (typeof APP_CONFIGS)[AppType], ][] ).map(([key, cfg]) => (
))}
{currentConfig.modelFields.map((field) => (
setModels((prev) => ({ ...prev, [field.key]: v })) } placeholder={t('Select or enter model name')} emptyText={t('No models found')} />
))}
) }