import { useEffect, useRef, useState } from 'react' import { Send, Square, MessageSquare, Cpu, Search as SearchIcon, Sparkles, Bot, User, Loader2, CheckCircle2, AlertCircle, ChevronDown, ShieldCheck, Terminal, Zap, ToggleLeft, ToggleRight } from 'lucide-react' import clsx from 'clsx' import { useI18n } from '../i18n' export default function ApiTester({ config, onMessage, authFetch }) { const { t } = useI18n() const [model, setModel] = useState('deepseek-chat') const defaultMessage = t('apiTester.defaultMessage') const [message, setMessage] = useState(defaultMessage) const [apiKey, setApiKey] = useState('') const [selectedAccount, setSelectedAccount] = useState('') const [response, setResponse] = useState(null) const [loading, setLoading] = useState(false) const [streamingContent, setStreamingContent] = useState('') const [streamingThinking, setStreamingThinking] = useState('') const [isStreaming, setIsStreaming] = useState(false) const [streamingMode, setStreamingMode] = useState(true) const abortControllerRef = useRef(null) const defaultMessageRef = useRef(defaultMessage) const [sidebarOpen, setSidebarOpen] = useState(false) const [configExpanded, setConfigExpanded] = useState(false) const apiFetch = authFetch || fetch const accounts = config.accounts || [] const configuredKeys = config.keys || [] const trimmedApiKey = apiKey.trim() const defaultKey = configuredKeys[0] || '' const effectiveKey = trimmedApiKey || defaultKey const customKeyActive = trimmedApiKey !== '' const customKeyManaged = customKeyActive && configuredKeys.includes(trimmedApiKey) const models = [ { id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: t('apiTester.models.chat'), color: "text-amber-500" }, { id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: t('apiTester.models.reasoner'), color: "text-amber-600" }, { id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: t('apiTester.models.chatSearch'), color: "text-cyan-500" }, { id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: t('apiTester.models.reasonerSearch'), color: "text-cyan-600" }, ] const stopGeneration = () => { if (abortControllerRef.current) { abortControllerRef.current.abort() abortControllerRef.current = null } setLoading(false) setIsStreaming(false) } const extractErrorMessage = async (res) => { let raw = '' try { raw = await res.text() } catch { return t('apiTester.requestFailed') } if (!raw) { return t('apiTester.requestFailed') } try { const data = JSON.parse(raw) const fromErrorObject = data?.error?.message const fromErrorString = typeof data?.error === 'string' ? data.error : '' const detail = typeof data?.detail === 'string' ? data.detail : '' const message = typeof data?.message === 'string' ? data.message : '' return fromErrorObject || fromErrorString || detail || message || t('apiTester.requestFailed') } catch { return raw.length > 240 ? `${raw.slice(0, 240)}...` : raw } } const runTest = async () => { if (loading) return setLoading(true) setIsStreaming(true) setResponse(null) setStreamingContent('') setStreamingThinking('') abortControllerRef.current = new AbortController() try { if (!effectiveKey) { onMessage('error', t('apiTester.missingApiKey')) setLoading(false) setIsStreaming(false) return } const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${effectiveKey}`, } if (selectedAccount) { headers['X-Ds2-Target-Account'] = selectedAccount } const res = await fetch('/v1/chat/completions', { method: 'POST', headers, body: JSON.stringify({ model, messages: [{ role: 'user', content: message }], stream: streamingMode, }), signal: abortControllerRef.current.signal, }) if (!res.ok) { const errorMsg = await extractErrorMessage(res) setResponse({ success: false, error: errorMsg }) onMessage('error', errorMsg) setLoading(false) setIsStreaming(false) return } if (streamingMode) { setResponse({ success: true, status_code: res.status }) const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { const trimmed = line.trim() if (!trimmed || !trimmed.startsWith('data: ')) continue const dataStr = trimmed.slice(6) if (dataStr === '[DONE]') continue try { const json = JSON.parse(dataStr) const choice = json.choices?.[0] if (choice?.delta) { const delta = choice.delta if (delta.reasoning_content) { setStreamingThinking(prev => prev + delta.reasoning_content) } if (delta.content) { setStreamingContent(prev => prev + delta.content) } } } catch (e) { console.error('Invalid JSON hunk:', dataStr, e) } } } } else { const data = await res.json() setResponse({ success: true, status_code: res.status, ...data }) onMessage('success', t('apiTester.testSuccess', { account: selectedAccount || 'Auto', time: 'N/A' })) } } catch (e) { if (e.name === 'AbortError') { onMessage('info', t('messages.generationStopped')) } else { onMessage('error', t('apiTester.networkError', { error: e.message })) setResponse({ error: e.message, success: false }) } } finally { setLoading(false) setIsStreaming(false) abortControllerRef.current = null } } useEffect(() => { setMessage((prev) => (prev === defaultMessageRef.current ? defaultMessage : prev)) defaultMessageRef.current = defaultMessage }, [defaultMessage]) return (
{customKeyManaged ? t('apiTester.modeManaged') : t('apiTester.modeDirect')}
)}