import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import toast from 'react-hot-toast'; import { getTokenSupportedModels } from '../api'; import { useSite } from '../context/SiteContext'; const TOOLS = [ { id: 'claudecode', name: 'Claude Code', path: '~/.claude/settings.json' }, { id: 'openclaw', name: 'OpenClaw', path: '~/.openclaw/openclaw.json' }, { id: 'opencode', name: 'OpenCode', path: '~/.config/opencode/opencode.json' }, { id: 'cursor', name: 'Cursor', path: 'Settings -> Models -> OpenAI API Key' }, { id: 'curl', name: 'cURL', path: 'Terminal' }, { id: 'python', name: 'Python SDK', path: 'main.py' }, { id: 'anthropic', name: 'Anthropic SDK', path: 'main.py' }, ]; const CCSWITCH_APPS = [ { id: 'codex', name: 'Codex', endpointType: 'openai' }, { id: 'claude', name: 'Claude Code', endpointType: 'anthropic' }, { id: 'opencode', name: 'OpenCode', endpointType: 'openai' }, { id: 'openclaw', name: 'OpenClaw', endpointType: 'openclaw' }, ]; const CCSWITCH_RELEASE_URL = 'https://github.com/farion1231/cc-switch/releases/latest'; const CCSWITCH_REPO_URL = 'https://github.com/farion1231/cc-switch'; function ThemedSelect({ value, onChange, options, placeholder, disabled = false, renderValue, renderOption, emptyLabel, }) { const [open, setOpen] = useState(false); const rootRef = useRef(null); useEffect(() => { if (disabled) { setOpen(false); } }, [disabled]); useEffect(() => { if (!open) return undefined; const handlePointerDown = (event) => { if (!rootRef.current?.contains(event.target)) { setOpen(false); } }; const handleKeyDown = (event) => { if (event.key === 'Escape') { setOpen(false); } }; document.addEventListener('mousedown', handlePointerDown); document.addEventListener('touchstart', handlePointerDown); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handlePointerDown); document.removeEventListener('touchstart', handlePointerDown); document.removeEventListener('keydown', handleKeyDown); }; }, [open]); const selectedOption = options.find((option) => option.value === value); const displayLabel = selectedOption ? (renderValue ? renderValue(selectedOption) : selectedOption.label) : placeholder; return (
{open && (
{options.length > 0 ? ( options.map((option) => { const selected = option.value === value; return ( ); }) ) : (
{emptyLabel}
)}
)}
); } const ConfigExporter = ({ tokens = [] }) => { const { t } = useTranslation(); const { site } = useSite(); const [selectedTokenId, setSelectedTokenId] = useState(null); const [availableModels, setAvailableModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [selectedTool, setSelectedTool] = useState('claudecode'); const [selectedCCSwitchApp, setSelectedCCSwitchApp] = useState('codex'); const [loadingModels, setLoadingModels] = useState(false); const [modelsError, setModelsError] = useState(false); const [copied, setCopied] = useState(false); const [launchingCCSwitch, setLaunchingCCSwitch] = useState(false); const [showCCSwitchDownload, setShowCCSwitchDownload] = useState(false); const serverAddress = window.location.origin; const selectedToken = useMemo( () => tokens.find((token) => token.id === selectedTokenId) || null, [tokens, selectedTokenId], ); const selectedToolMeta = useMemo( () => TOOLS.find((tool) => tool.id === selectedTool) || TOOLS[0], [selectedTool], ); const tokenOptions = useMemo( () => tokens.map((token) => ({ value: token.id, label: `${token.name} (sk-${token.key.substring(0, 16)}...)`, token, })), [tokens], ); const modelOptions = useMemo( () => availableModels.map((model) => ({ value: model, label: model, })), [availableModels], ); useEffect(() => { if (tokens.length === 0) { setSelectedTokenId(null); setAvailableModels([]); setSelectedModel(''); return; } const stillExists = tokens.some((token) => token.id === selectedTokenId); if (stillExists) return; const preferred = tokens.find((token) => token.status === 1) || tokens[0]; setSelectedTokenId(preferred.id); }, [tokens, selectedTokenId]); useEffect(() => { if (!selectedToken?.id) { setAvailableModels([]); setSelectedModel(''); return; } let cancelled = false; const loadModels = async () => { setLoadingModels(true); setModelsError(false); try { const res = await getTokenSupportedModels(selectedToken.id); if (cancelled) return; if (res.data.success) { const models = res.data.data?.models || []; setAvailableModels(models); setSelectedModel((prev) => prev && models.includes(prev) ? prev : models[0] || '', ); } else { setAvailableModels([]); setSelectedModel(''); setModelsError(true); } } catch (e) { if (cancelled) return; setAvailableModels([]); setSelectedModel(''); setModelsError(true); } if (!cancelled) { setLoadingModels(false); } }; loadModels(); return () => { cancelled = true; }; }, [selectedToken?.id]); const getProviderInfo = (modelName) => { const lower = modelName.toLowerCase(); if (lower.includes('claude')) { return { provider: 'anthropic', api: 'anthropic-messages' }; } if ( lower.includes('gpt') || lower.includes('o1') || lower.includes('o3') || lower.includes('o4') ) { return { provider: 'openai', api: 'openai-chat' }; } if (lower.includes('gemini')) { return { provider: 'google', api: 'google-chat' }; } return { provider: 'openai', api: 'openai-chat' }; }; const getCCSwitchEndpoint = () => { const app = CCSWITCH_APPS.find( (item) => item.id === selectedCCSwitchApp, ); if (app?.endpointType === 'anthropic' || app?.endpointType === 'openclaw') { return `${serverAddress}/`; } return `${serverAddress}/v1`; }; const generateCCSwitchLink = () => { if (!selectedToken || !selectedModel) return ''; const params = new URLSearchParams({ resource: 'provider', app: selectedCCSwitchApp, name: site?.name || window.location.hostname, homepage: serverAddress, endpoint: getCCSwitchEndpoint(), apiKey: `sk-${selectedToken.key}`, model: selectedModel, enabled: 'true', notes: `${site?.name || window.location.hostname} ยท ${selectedModel}`, }); return `ccswitch://v1/import?${params.toString()}`; }; const generateConfig = () => { if (!selectedToken || !selectedModel) return ''; const apiKey = `sk-${selectedToken.key}`; switch (selectedTool) { case 'claudecode': return `{ "env": { "ANTHROPIC_API_KEY": "${apiKey}", "ANTHROPIC_BASE_URL": "${serverAddress}/", "ANTHROPIC_MODEL": "${selectedModel}" } }`; case 'ccswitch': return generateCCSwitchLink(); case 'openclaw': { const info = getProviderInfo(selectedModel); return `{ "provider": "${info.provider}", "base_url": "${serverAddress}/", "api": "${info.api}", "api_key": "${apiKey}", "model": { "id": "${selectedModel}", "name": "${selectedModel}" } }`; } case 'opencode': return `{ "provider": { "openai": { "options": { "baseURL": "${serverAddress}/v1", "apiKey": "${apiKey}" }, "models": { "${selectedModel}": { "name": "${selectedModel}", "options": { "store": false } } } } }, "$schema": "https://opencode.ai/config.json" }`; case 'cursor': return `API Key: ${apiKey} Base URL: ${serverAddress}/v1 Model: ${selectedModel}`; case 'curl': return `curl ${serverAddress}/v1/chat/completions \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey}" \\ -d '{ "model": "${selectedModel}", "messages": [ {"role": "user", "content": "Hello!"} ] }'`; case 'python': return `from openai import OpenAI client = OpenAI( api_key="${apiKey}", base_url="${serverAddress}/v1" ) response = client.chat.completions.create( model="${selectedModel}", messages=[ {"role": "user", "content": "Hello!"} ] ) print(response.choices[0].message.content)`; case 'anthropic': return `import anthropic client = anthropic.Anthropic( api_key="${apiKey}", base_url="${serverAddress}/" ) message = client.messages.create( model="${selectedModel}", max_tokens=1024, messages=[ {"role": "user", "content": "Hello!"} ] ) print(message.content[0].text)`; default: return ''; } }; const getFilename = () => { switch (selectedTool) { case 'curl': return 'api-call.sh'; case 'python': case 'anthropic': return 'main.py'; case 'cursor': return 'cursor-config.txt'; default: return ( selectedToolMeta.path.split('/').pop() || `${selectedToolMeta.id}.json` ); } }; const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); } catch (e) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } }; const getSelectedToolBaseUrl = () => { if ( selectedTool === 'claudecode' || selectedTool === 'anthropic' || selectedTool === 'openclaw' ) { return `${serverAddress}/`; } return `${serverAddress}/v1`; }; const handleCopyValue = async (text, successKey = 'config.copied') => { if (!text) return; await copyToClipboard(text); toast.success(t(successKey)); }; const handleCopy = async () => { const config = generateConfig(); if (!config) return; await copyToClipboard(config); setCopied(true); toast.success(t('config.copied')); window.setTimeout(() => setCopied(false), 2000); }; const handleCopyCCSwitchLink = async () => { const deeplink = generateCCSwitchLink(); if (!deeplink) return; await copyToClipboard(deeplink); toast.success(t('config.importLinkCopied')); }; const handleDownload = () => { const config = generateConfig(); if (!config) return; const blob = new Blob([config], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = getFilename(); a.click(); URL.revokeObjectURL(url); toast.success(t('config.downloaded')); }; const handleImportCCSwitch = () => { const deeplink = generateCCSwitchLink(); if (!deeplink) return; setShowCCSwitchDownload(false); setLaunchingCCSwitch(true); let dismissed = false; let timerId = null; const cleanup = () => { window.removeEventListener('blur', handleBlur); document.removeEventListener('visibilitychange', handleVisibilityChange); if (timerId) { window.clearTimeout(timerId); } }; const handleSuccess = () => { if (dismissed) return; dismissed = true; cleanup(); setLaunchingCCSwitch(false); }; const handleBlur = () => { handleSuccess(); }; const handleVisibilityChange = () => { if (document.hidden) { handleSuccess(); } }; window.addEventListener('blur', handleBlur); document.addEventListener('visibilitychange', handleVisibilityChange); timerId = window.setTimeout(() => { if (dismissed) return; cleanup(); setLaunchingCCSwitch(false); setShowCCSwitchDownload(true); }, 1500); window.location.href = deeplink; }; const config = generateConfig(); const ccSwitchLink = generateCCSwitchLink(); if (tokens.length === 0) { return (

{t('config.noKeyPrompt')}

{t('config.noKeyDesc')}

); } return ( <>

{t('config.title')}

{t('config.subtitle')}

{t('config.apiUrlTitle')}

{t('config.apiUrlHint')}

{t('config.currentToolApiUrl')} {getSelectedToolBaseUrl()}
{TOOLS.map((tool) => ( ))}

{t('config.ccswitchTitle')}

{t('config.ccswitchHint')}

CC Switch

{t('config.selectCCSwitchApp')}

{CCSWITCH_APPS.map((app) => ( ))}
{ccSwitchLink}
{selectedToolMeta.path}
            {config || t('tokens.noSupportedModels')}
          
{showCCSwitchDownload && (
setShowCCSwitchDownload(false)} >
e.stopPropagation()} >

{t('config.ccswitchNotInstalledTitle')}

{t('config.ccswitchNotInstalledDesc')}

{t('config.downloadCCSwitch')} {t('config.openCCSwitchRepo')}
)} ); }; export default ConfigExporter;