153 lines
3.9 KiB
JavaScript
153 lines
3.9 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) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg system-info';
|
|
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());
|
|
}
|
|
|
|
// ── 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();
|
|
}
|