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}
{config || t('tokens.noSupportedModels')}
{showCCSwitchDownload && (
setShowCCSwitchDownload(false)}
>
e.stopPropagation()}
>
{t('config.ccswitchNotInstalledTitle')}
{t('config.ccswitchNotInstalledDesc')}
)}
>
);
};
export default ConfigExporter;