import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { getTokens, getSiteModels } from '../api'; import toast from 'react-hot-toast'; const ConfigExporter = () => { const { t } = useTranslation(); const [tokens, setTokens] = useState([]); const [selectedToken, setSelectedToken] = useState(null); const [availableModels, setAvailableModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [selectedTool, setSelectedTool] = useState('claudecode'); const [loading, setLoading] = useState(true); const [copied, setCopied] = useState(false); const [modelSearch, setModelSearch] = useState(''); const [modelDropdownOpen, setModelDropdownOpen] = useState(false); const modelDropdownRef = useRef(null); const serverAddress = window.location.origin; 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' }, ]; useEffect(() => { loadData(); }, []); useEffect(() => { if (selectedToken) { loadModelsForToken(selectedToken); } }, [selectedToken]); // Close model dropdown on click outside useEffect(() => { const handleClickOutside = (e) => { if (modelDropdownRef.current && !modelDropdownRef.current.contains(e.target)) { setModelDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const loadData = async () => { setLoading(true); try { const res = await getTokens(); if (res.data.success) { const tokenList = res.data.data || []; setTokens(tokenList); if (tokenList.length > 0) { setSelectedToken(tokenList[0]); } } } catch (e) { /* interceptor */ } setLoading(false); }; const loadModelsForToken = async (token) => { try { // Try token-specific model limits first if (token.model_limits_enabled && token.model_limits) { const models = token.model_limits.split(',').filter(m => m.trim()); setAvailableModels(models); if (models.length > 0) setSelectedModel(models[0]); return; } // Fall back to site-wide model list const res = await getSiteModels(); if (res.data.success && res.data.data) { const list = Array.isArray(res.data.data) ? res.data.data : (res.data.data.items || []); const modelNames = list.map(m => m.model_name || m.id || m); setAvailableModels(modelNames); if (modelNames.length > 0) setSelectedModel(modelNames[0]); } } catch (e) { /* silent */ } }; 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')) return { provider: 'openai', api: 'openai-chat' }; if (lower.includes('gemini')) return { provider: 'google', api: 'google-chat' }; return { provider: 'openai', api: 'openai-chat' }; }; 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 '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': return 'main.py'; case 'anthropic': return 'main.py'; case 'cursor': return 'cursor-config.txt'; default: return tools.find(t => t.id === selectedTool)?.path.split('/').pop() || 'config.json'; } }; const handleCopy = async () => { const config = generateConfig(); try { await navigator.clipboard.writeText(config); } catch (e) { const ta = document.createElement('textarea'); ta.value = config; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } setCopied(true); toast.success(t('config.copied')); setTimeout(() => setCopied(false), 2000); }; const handleDownload = () => { const config = generateConfig(); const filename = getFilename(); const blob = new Blob([config], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); toast.success(t('config.downloaded')); }; if (loading) { return (
{t('config.loading')}
{t('config.loginPrompt')}
{t('config.loginDesc')}
{t('config.subtitle')}
{t('config.noModelMatch')}
)}
{config}