Files
gott/wwwroot/js/ui.js
T

212 lines
6.0 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 || '').trim();
chatEl().appendChild(div);
scrollDown();
}
function addAssistantBubble(text) {
const div = document.createElement('div');
div.className = 'msg assistant';
div.textContent = (text || '').trim();
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 || '').trim();
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, namesJson) {
const chat = chatEl();
const lastMsg = chat.lastElementChild;
if (!lastMsg) return;
let names = [];
try { if (namesJson) names = JSON.parse(namesJson); } catch {}
const indicator = document.createElement('div');
indicator.className = 'attachment-indicator';
const nameList = names.filter(n => n).join(', ');
indicator.textContent = `📎 ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${nameList}` : '');
indicator.style.cursor = 'pointer';
indicator.title = 'Klicken zum Anzeigen';
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);
} 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();
}