207 lines
5.8 KiB
JavaScript
207 lines
5.8 KiB
JavaScript
// ── DOM Helpers ──
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
const chatEl = () => $('#chat');
|
|
const metaEl = () => $('#meta');
|
|
|
|
function scrollDown() {
|
|
const chat = chatEl();
|
|
chat.scrollTop = chat.scrollHeight;
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function truncate(s, max) {
|
|
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
|
|
}
|
|
|
|
// ── Message Bubbles ──
|
|
|
|
function addUserBubble(text, streamed = false) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg user';
|
|
if (streamed) div.classList.add('streamed');
|
|
div.textContent = text;
|
|
chatEl().appendChild(div);
|
|
scrollDown();
|
|
}
|
|
|
|
function addAssistantBubble(text) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg assistant';
|
|
div.textContent = text;
|
|
chatEl().appendChild(div);
|
|
scrollDown();
|
|
return div;
|
|
}
|
|
|
|
function addErrorBubble(text, streamed = false) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg system-info';
|
|
if (streamed) div.classList.add('streamed');
|
|
div.style.color = '#ff6b6b';
|
|
div.textContent = text;
|
|
chatEl().appendChild(div);
|
|
scrollDown();
|
|
}
|
|
|
|
// ── Streaming Bubble ──
|
|
|
|
let _streamDiv = null;
|
|
let _streamText = '';
|
|
|
|
function startStreamBubble() {
|
|
_streamDiv = document.createElement('div');
|
|
_streamDiv.className = 'msg assistant streamed';
|
|
_streamDiv.innerHTML = '<span class="cursor"></span>';
|
|
_streamText = '';
|
|
chatEl().appendChild(_streamDiv);
|
|
scrollDown();
|
|
}
|
|
|
|
function appendToStream(text) {
|
|
if (!_streamDiv) startStreamBubble();
|
|
_streamText += text;
|
|
_streamDiv.textContent = _streamText;
|
|
const cursor = document.createElement('span');
|
|
cursor.className = 'cursor';
|
|
_streamDiv.appendChild(cursor);
|
|
scrollDown();
|
|
}
|
|
|
|
function endStreamBubble() {
|
|
if (_streamDiv) {
|
|
const cursor = _streamDiv.querySelector('.cursor');
|
|
if (cursor) cursor.remove();
|
|
if (!_streamText.trim()) _streamDiv.remove();
|
|
}
|
|
_streamDiv = null;
|
|
_streamText = '';
|
|
}
|
|
|
|
// ── Tool Boxes ──
|
|
|
|
function addToolBoxLoading(name) {
|
|
const div = document.createElement('div');
|
|
div.className = 'tool-box streamed';
|
|
div.innerHTML = '<span class="tool-name">' + esc(name) + '</span> <span class="tool-spinner">⏳</span>';
|
|
chatEl().appendChild(div);
|
|
scrollDown();
|
|
return div;
|
|
}
|
|
|
|
function finishToolBox(div, data) {
|
|
if (!div) return;
|
|
div.innerHTML = '<span class="tool-name">' + esc(data.name) + '</span>'
|
|
+ '<span class="tool-input">' + esc(truncate(data.input, 120)) + '</span>'
|
|
+ '<div class="tool-result ' + (data.isError ? 'tool-error' : '') + '">'
|
|
+ esc(truncate(data.result, 300)) + '</div>';
|
|
scrollDown();
|
|
}
|
|
|
|
function addToolBoxStatic(name, input, result, isError) {
|
|
const div = document.createElement('div');
|
|
div.className = 'tool-box';
|
|
div.innerHTML = '<span class="tool-name">' + esc(name) + '</span>'
|
|
+ '<span class="tool-input">' + esc(truncate(input, 120)) + '</span>';
|
|
if (result) {
|
|
div.innerHTML += '<div class="tool-result ' + (isError ? 'tool-error' : '') + '">'
|
|
+ esc(truncate(result, 300)) + '</div>';
|
|
}
|
|
chatEl().appendChild(div);
|
|
scrollDown();
|
|
}
|
|
|
|
// ── Streamed Elements ──
|
|
|
|
function removeStreamedElements() {
|
|
chatEl().querySelectorAll('.streamed').forEach(el => el.remove());
|
|
}
|
|
|
|
// ── Attachment Indicator ──
|
|
|
|
function addAttachmentIndicator(messageId, count) {
|
|
const chat = chatEl();
|
|
const lastMsg = chat.lastElementChild;
|
|
if (!lastMsg) return;
|
|
|
|
const indicator = document.createElement('div');
|
|
indicator.className = 'attachment-indicator';
|
|
indicator.textContent = `📎 ${count} Datei${count > 1 ? 'en' : ''}`;
|
|
indicator.style.cursor = 'pointer';
|
|
indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator));
|
|
lastMsg.appendChild(indicator);
|
|
}
|
|
|
|
async function loadAndShowAttachments(messageId, indicator) {
|
|
if (indicator.dataset.loaded) return;
|
|
indicator.dataset.loaded = '1';
|
|
indicator.textContent = '📎 Laden...';
|
|
|
|
try {
|
|
const resp = await fetch(`/api/attachments/${messageId}`);
|
|
const attachments = await resp.json();
|
|
|
|
const container = document.createElement('div');
|
|
container.className = 'attachment-container';
|
|
|
|
for (const att of attachments) {
|
|
if (att.type === 'image') {
|
|
const img = document.createElement('img');
|
|
img.src = `data:${att.mediaType};base64,${att.data}`;
|
|
img.className = 'attachment-image';
|
|
img.title = att.fileName || 'Bild';
|
|
container.appendChild(img);
|
|
} else {
|
|
const link = document.createElement('a');
|
|
link.href = `data:${att.mediaType};base64,${att.data}`;
|
|
link.download = att.fileName || 'datei';
|
|
link.className = 'attachment-file';
|
|
link.textContent = `📄 ${att.fileName || 'Dokument'}`;
|
|
container.appendChild(link);
|
|
}
|
|
}
|
|
|
|
indicator.textContent = `📎 ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`;
|
|
indicator.parentElement.appendChild(container);
|
|
scrollDown();
|
|
} catch (e) {
|
|
indicator.textContent = '📎 Fehler beim Laden';
|
|
indicator.dataset.loaded = '';
|
|
}
|
|
}
|
|
|
|
// ── Meta Bar ──
|
|
|
|
function setMeta(text) {
|
|
metaEl().textContent = text;
|
|
}
|
|
|
|
function setMetaHtml(html) {
|
|
metaEl().innerHTML = html;
|
|
}
|
|
|
|
// ── Archive Button ──
|
|
|
|
function showArchiveLoadButton() {
|
|
const existing = chatEl().querySelector('.archive-load-btn');
|
|
if (existing) existing.remove();
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'archive-load-btn';
|
|
btn.textContent = '📦 Archivierte Nachrichten laden';
|
|
btn.addEventListener('click', loadLatestArchive);
|
|
|
|
// Immer ganz oben im Chat
|
|
chatEl().prepend(btn);
|
|
}
|
|
|
|
function removeArchiveLoadButton() {
|
|
const btn = chatEl().querySelector('.archive-load-btn');
|
|
if (btn) btn.remove();
|
|
}
|