Files
gott/wwwroot/js/chat.js
T

194 lines
5.5 KiB
JavaScript

// ── 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('<span style="color:#7c83ff">⏳ Streaming...</span>');
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 = `
<div class="file-preview-inner">
${isImage ? '<img src="data:' + pendingFile.type + ';base64,' + pendingFile.base64 + '" class="file-thumb">' : '<span class="file-icon">📄</span>'}
<span class="file-name">${esc(pendingFile.name)}</span>
<button class="file-remove" onclick="clearPendingFile()">✕</button>
</div>
`;
preview.hidden = false;
}
function clearPendingFile() {
pendingFile = null;
const preview = $('#filePreview');
if (preview) {
preview.innerHTML = '';
preview.hidden = true;
}
const fileInput = $('#fileInput');
if (fileInput) fileInput.value = '';
}