import { useEffect, useRef, useState } from 'react' import { Send, Square, MessageSquare, Cpu, Search as SearchIcon, Sparkles, Bot, User, Loader2, CheckCircle2, AlertCircle, ChevronDown, ShieldCheck, Terminal, Zap } 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 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 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 directTest = async () => { if (loading) return setLoading(true) setIsStreaming(true) setResponse(null) setStreamingContent('') setStreamingThinking('') abortControllerRef.current = new AbortController() try { const key = apiKey || (config.keys?.[0] || '') if (!key) { onMessage('error', t('apiTester.missingApiKey')) setLoading(false) setIsStreaming(false) return } const res = await fetch('/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}`, }, body: JSON.stringify({ model, messages: [{ role: 'user', content: message }], stream: true, }), signal: abortControllerRef.current.signal, }) if (!res.ok) { const data = await res.json() setResponse({ success: false, error: data.error?.message || t('apiTester.requestFailed') }) onMessage('error', data.error?.message || t('apiTester.requestFailed')) setLoading(false) setIsStreaming(false) return } 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) console.log('[ApiTester] Parsed JSON:', json) const choice = json.choices?.[0] if (choice?.delta) { const delta = choice.delta console.log('[ApiTester] Delta:', delta) if (delta.reasoning_content) { setStreamingThinking(prev => prev + delta.reasoning_content) } if (delta.content) { console.log('[ApiTester] Content:', delta.content) setStreamingContent(prev => prev + delta.content) } } } catch (e) { console.error('Invalid JSON hunk:', dataStr, e) } } } } 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 } } const sendTest = async () => { if (selectedAccount) { setLoading(true) setResponse(null) try { const res = await apiFetch('/admin/accounts/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: selectedAccount, model, message, }), }) const data = await res.json() setResponse({ success: data.success, status_code: res.status, response: data, account: selectedAccount, }) if (data.success) { onMessage('success', t('apiTester.testSuccess', { account: selectedAccount, time: data.response_time })) } else { onMessage('error', `${selectedAccount}: ${data.message}`) } } catch (e) { onMessage('error', t('apiTester.networkError', { error: e.message })) setResponse({ error: e.message }) } finally { setLoading(false) } return } directTest() } useEffect(() => { setMessage((prev) => (prev === defaultMessageRef.current ? defaultMessage : prev)) defaultMessageRef.current = defaultMessage }, [defaultMessage]) return (