import { useState } from 'react' import { Send, Trash2, Loader2 } from 'lucide-react' const DEFAULT_MODEL = 'nvidia/nemotron-3-ultra-550b-a55b:free' const TIMEOUT_MS = 180000 /** 解析一筆 SSE 行:回傳 { delta } / { error } / { done } / null */ function parseSseLine(line) { const payload = line.trim() if (!payload.startsWith('data:')) return null const raw = payload.slice(5).trim() if (raw === '[DONE]') return { done: true } let data try { data = JSON.parse(raw) } catch { return null } if (data.error) return { error: data.error.message || 'API 回傳錯誤' } return { delta: data.choices?.[0]?.delta?.content } } /** 把內容接到最後一則 assistant 訊息(還沒有就新增一則) */ function withLastAssistant(prev, content, append) { const last = prev[prev.length - 1] if (last && last.role === 'assistant') { return [...prev.slice(0, -1), { role: 'assistant', content: append ? last.content + content : content }] } return [...prev, { role: 'assistant', content }] } async function readErrorMessage(res) { try { const data = await res.json() return data?.error?.message || data?.error?.data?.message || data?.message || '無錯誤訊息' } catch { try { return (await res.text()).slice(0, 300) || '無錯誤訊息' } catch { return '無錯誤訊息' } } } export default function ChatInterface({ apiConfig, onChange }) { const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const setField = (field, value) => onChange({ ...apiConfig, [field]: value }) const clearChat = () => { if (!loading) setMessages([]) } const sendMessage = async () => { const text = input.trim() if (!text || loading) return if (!apiConfig.apiKey?.trim()) return alert('請輸入 API Key') // 送完整對話歷史,不只最新一則 const next = [...messages, { role: 'user', content: text }, { role: 'assistant', content: '' }] setMessages(next) setInput('') setLoading(true) const controller = new AbortController() const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) try { const base = String(apiConfig.supplier).replace(/\/+$/, '') const res = await fetch(`${base}/chat/completions`, { method: 'POST', signal: controller.signal, headers: { Authorization: `Bearer ${apiConfig.apiKey.trim()}`, 'Content-Type': 'application/json', 'HTTP-Referer': window.location.origin, 'X-Title': 'Agent Studio', }, body: JSON.stringify({ model: apiConfig.model || DEFAULT_MODEL, messages: next.filter((m) => m.content !== ''), stream: true, }), }) if (!res.ok) { const detail = await readErrorMessage(res) setMessages((prev) => withLastAssistant(prev, `⚠️ API 錯誤 ${res.status}:${detail}`, false)) return } if (!res.body) { setMessages((prev) => withLastAssistant(prev, '⚠️ 回應不含可讀取的串流', false)) return } const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' let gotAny = false for (;;) { const { value, done } = 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 event = parseSseLine(line) if (!event) continue if (event.done) continue if (event.error) throw new Error(event.error) if (event.delta) { gotAny = true setMessages((prev) => withLastAssistant(prev, event.delta, true)) } } } if (!gotAny) { setMessages((prev) => withLastAssistant(prev, '⚠️ 模型沒有回傳任何內容', false)) } } catch (e) { const message = e.name === 'AbortError' ? `請求逾時(超過 ${TIMEOUT_MS / 1000} 秒)` : e.message setMessages((prev) => withLastAssistant(prev, `⚠️ ${message}`, false)) } finally { clearTimeout(timer) setLoading(false) } } const keyState = apiConfig.apiKey?.trim() ? Key 已設定 : 尚未設定 Key return (