Files
gott/wwwroot/js/ui.js
T
2026-02-27 23:39:06 +01:00

396 lines
11 KiB
JavaScript

// ── DOM Helpers ──
const $ = (sel) => document.querySelector(sel);
const chatEl = () => $('#chat');
const metaEl = () => $('#meta');
// ── Markdown ──
marked.setOptions({ breaks: true, gfm: true });
function renderMarkdown(text) {
if (!text) return '';
return marked.parse(text.trim());
}
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 markdown';
div.innerHTML = renderMarkdown(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 = 'var(--dangerous)';
div.textContent = (text || '').trim();
chatEl().appendChild(div);
scrollDown();
}
function addWarningBubble(text, streamed = false) {
const div = document.createElement('div');
div.className = 'msg system-info';
if (streamed) div.classList.add('streamed');
div.style.color = 'var(--warning)';
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 markdown streamed';
_streamDiv.innerHTML = '<span class="cursor"></span>';
_streamText = '';
chatEl().appendChild(_streamDiv);
scrollDown();
}
function appendToStream(text) {
if (!_streamDiv) startStreamBubble();
_streamText += text;
_streamDiv.innerHTML = renderMarkdown(_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();
else _streamDiv.innerHTML = renderMarkdown(_streamText);
}
_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 renderToolBoxContent(div, name, inputJson, result, isError) {
let parsedInput = null;
try { parsedInput = JSON.parse(inputJson); } catch {}
const header = document.createElement('div');
header.className = 'tool-box-header';
const nameSpan = document.createElement('span');
nameSpan.className = 'tool-name';
nameSpan.textContent = name;
header.appendChild(nameSpan);
if (isError) {
const errSpan = document.createElement('span');
errSpan.className = 'tool-error-badge';
errSpan.textContent = 'error';
header.appendChild(errSpan);
}
div.innerHTML = '';
div.appendChild(header);
let foldTarget = null;
if (name === 'edit_file' && parsedInput && parsedInput.path) {
const pathSpan = document.createElement('span');
pathSpan.className = 'tool-input';
pathSpan.textContent = parsedInput.path;
header.appendChild(pathSpan);
const diff = buildEditDiff(parsedInput.old_text || '', parsedInput.new_text || '');
diff.hidden = true;
div.appendChild(diff);
foldTarget = diff;
} else if (name === 'write_file' && parsedInput && parsedInput.path) {
const pathSpan = document.createElement('span');
pathSpan.className = 'tool-input';
pathSpan.textContent = parsedInput.path;
header.appendChild(pathSpan);
const content = parsedInput.content || '';
const lines = content.split('\n');
const lineCount = lines.length;
const charCount = content.length;
const meta = document.createElement('span');
meta.className = 'tool-write-meta';
meta.textContent = lineCount + ' Zeilen, ' + charCount + ' Zeichen';
header.appendChild(meta);
const preview = buildWritePreview(content);
preview.hidden = true;
div.appendChild(preview);
foldTarget = preview;
} else {
// Standard: path/command als input anzeigen
let inputLabel = '';
if (parsedInput) {
inputLabel = parsedInput.path || parsedInput.command || JSON.stringify(parsedInput).substring(0, 120);
} else {
inputLabel = truncate(inputJson, 120);
}
if (inputLabel) {
const inputSpan = document.createElement('span');
inputSpan.className = 'tool-input';
inputSpan.textContent = inputLabel;
header.appendChild(inputSpan);
}
const resultDiv = document.createElement('div');
resultDiv.className = 'tool-result' + (isError ? ' tool-error' : '');
resultDiv.textContent = result ? truncate(result, 300) : '';
resultDiv.hidden = true;
div.appendChild(resultDiv);
foldTarget = resultDiv;
}
if (foldTarget) {
header.style.cursor = 'pointer';
header.addEventListener('click', () => {
foldTarget.hidden = !foldTarget.hidden;
});
}
}
function buildEditDiff(oldText, newText) {
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
const container = document.createElement('div');
container.className = 'tool-diff';
// Entfernte Zeilen (-)
for (const line of oldLines) {
const row = document.createElement('div');
row.className = 'diff-line diff-removed';
const marker = document.createElement('span');
marker.className = 'diff-marker';
marker.textContent = '-';
const content = document.createElement('span');
content.className = 'diff-content';
content.textContent = line;
row.appendChild(marker);
row.appendChild(content);
container.appendChild(row);
}
// Hinzugefügte Zeilen (+)
for (const line of newLines) {
const row = document.createElement('div');
row.className = 'diff-line diff-added';
const marker = document.createElement('span');
marker.className = 'diff-marker';
marker.textContent = '+';
const content = document.createElement('span');
content.className = 'diff-content';
content.textContent = line;
row.appendChild(marker);
row.appendChild(content);
container.appendChild(row);
}
return container;
}
function buildWritePreview(content) {
const lines = content.split('\n');
const container = document.createElement('div');
container.className = 'tool-diff';
for (const line of lines) {
const row = document.createElement('div');
row.className = 'diff-line diff-added';
const marker = document.createElement('span');
marker.className = 'diff-marker';
marker.textContent = '+';
const contentSpan = document.createElement('span');
contentSpan.className = 'diff-content';
contentSpan.textContent = line;
row.appendChild(marker);
row.appendChild(contentSpan);
container.appendChild(row);
}
return container;
}
function finishToolBox(div, data) {
if (!div) return;
renderToolBoxContent(div, data.name, data.input, data.result, data.isError);
scrollDown();
}
function addToolBoxStatic(name, input, result, isError) {
const div = document.createElement('div');
div.className = 'tool-box';
renderToolBoxContent(div, name, input, result, isError);
chatEl().appendChild(div);
scrollDown();
}
function appendToolResult(result, isError) {
// Letzten tool-box im Chat finden und Result in die bereits vorhandene .tool-result-Div schreiben
const boxes = chatEl().querySelectorAll('.tool-box');
const lastBox = boxes[boxes.length - 1];
if (!lastBox) return;
// Wenn Diff vorhanden (edit_file/write_file) → kein Result anhängen
if (lastBox.querySelector('.tool-diff')) return;
const existing = lastBox.querySelector('.tool-result');
if (existing) {
existing.textContent = truncate(result, 300);
if (isError) existing.classList.add('tool-error');
} else {
const resultDiv = document.createElement('div');
resultDiv.className = 'tool-result' + (isError ? ' tool-error' : '');
resultDiv.textContent = truncate(result, 300);
resultDiv.hidden = true;
lastBox.appendChild(resultDiv);
}
}
// ── 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();
}