// ── Chat State ── const messages = []; let isStreaming = false; let pendingFile = null; // { name, type, base64 } async function clearChat() { const archive = await apiArchive(getCurrentSessionId()); if (!archive) return; messages.length = 0; chatEl().innerHTML = ''; lastKnownMessageId = 0; loadedArchiveCount = 0; setMeta(`Archiviert: ${archive.chainCount} Chains, ${archive.messageCount} Messages`); showArchiveLoadButton(); } // ── Send Message ── async function sendMessage() { const input = $('#input'); const btn = $('#send'); const text = input.value.trim(); if (!text && !pendingFile) return; // Content bauen: string oder array mit file-blocks let content; let bubbleText = text; if (pendingFile) { content = []; const blockType = pendingFile.type.startsWith('image/') ? 'image' : 'document'; content.push({ type: blockType, source: { type: 'base64', media_type: pendingFile.type, data: pendingFile.base64 }, _file_name: pendingFile.name }); if (text) content.push({ type: 'text', text }); bubbleText = (blockType === 'image' ? '📷 ' : '📄 ') + pendingFile.name + (text ? '\n' + text : ''); clearPendingFile(); } else { content = text; } messages.push({ role: 'user', content }); addUserBubble(bubbleText, true); input.value = ''; input.style.height = 'auto'; btn.disabled = true; isStreaming = true; setMetaHtml('⏳ Streaming...'); let fullText = ''; let currentToolBox = null; try { const resp = await apiChat(messages, getCurrentSessionId()); 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, true); // Fehlgeschlagene Message aus dem Array entfernen // damit das kaputte Bild nicht endlos mitgeschickt wird if (messages.length > 0 && messages[messages.length - 1].role === 'user') { messages.pop(); } break; case 'done': endStreamBubble(); let meta = `Model: ${data.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; } } } } catch (e) { endStreamBubble(); addErrorBubble('Netzwerk-Fehler: ' + e.message); } finally { btn.disabled = false; isStreaming = false; endStreamBubble(); // Erst neue aus DB laden, dann gestreamte Elemente ersetzen await pollHistory(); removeStreamedElements(); } } // ── File Handling ── function handleFileSelect(file) { if (!file) return; const maxSize = 20 * 1024 * 1024; // 20MB if (file.size > maxSize) { alert('Datei zu groß (max. 20MB)'); return; } const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf']; if (!allowed.includes(file.type)) { alert('Nicht unterstützter Dateityp. Erlaubt: JPEG, PNG, GIF, WebP, PDF'); return; } const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; pendingFile = { name: file.name, type: file.type, base64 }; showFilePreview(); }; reader.readAsDataURL(file); } function showFilePreview() { const preview = $('#filePreview'); if (!preview || !pendingFile) return; const isImage = pendingFile.type.startsWith('image/'); preview.innerHTML = `
${isImage ? '' : '📄'} ${esc(pendingFile.name)}
`; preview.hidden = false; } function clearPendingFile() { pendingFile = null; const preview = $('#filePreview'); if (preview) { preview.innerHTML = ''; preview.hidden = true; } const fileInput = $('#fileInput'); if (fileInput) fileInput.value = ''; }