// ── Chat State ── const messages = []; let isStreaming = false; function clearChat() { messages.length = 0; chatEl().innerHTML = ''; setMeta(''); } // ── Send Message ── async function sendMessage() { const input = $('#input'); const btn = $('#send'); const model = $('#model').value; const systemPrompt = $('#systemPrompt').value.trim(); const text = input.value.trim(); if (!text) return; messages.push({ role: 'user', content: text }); addUserBubble(text); input.value = ''; input.style.height = 'auto'; btn.disabled = true; isStreaming = true; setMetaHtml('⏳ Streaming...'); let fullText = ''; let currentToolBox = null; try { const resp = await apiChat(model, messages, systemPrompt); const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let eventType = null; 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) { if (line.startsWith('event: ')) { eventType = line.substring(7).trim(); } else if (line.startsWith('data: ') && eventType) { let data; try { data = JSON.parse(line.substring(6)); } catch { continue; } switch (eventType) { case 'text_start': endStreamBubble(); startStreamBubble(); break; case 'text_delta': appendToStream(data.text); fullText += data.text; break; case 'text_end': endStreamBubble(); break; case 'tool_start': endStreamBubble(); currentToolBox = addToolBoxLoading(data.name); break; case 'tool_end': finishToolBox(currentToolBox, data); currentToolBox = null; break; case 'error': endStreamBubble(); addErrorBubble(data.error); break; case 'done': endStreamBubble(); let meta = `Model: ${data.model || model} | In: ${data.inputTokens || 0} | Out: ${data.outputTokens || 0}`; if (data.iterations > 1) meta += ` | Iterations: ${data.iterations}`; if (data.durationMs) meta += ` | ${(data.durationMs / 1000).toFixed(1)}s`; setMeta(meta); break; } eventType = null; } } } if (fullText) { messages.push({ role: 'assistant', content: fullText }); } } catch (e) { endStreamBubble(); addErrorBubble('Netzwerk-Fehler: ' + e.message); } finally { btn.disabled = false; isStreaming = false; endStreamBubble(); } }