// ── Session Card Helper ──
// Erzeugt ein .card-Element für eine Session (wiederverwendbar für Dashboard + Picker).
// session: Session-Objekt, agent: zugehöriges Agent-Objekt (oder null)
// Gibt ein DOM-Element zurück (.card mit .card-image + .card-body inkl. Badges + Meta).
function buildSessionCard(session, agent) {
const card = document.createElement('div');
card.className = 'card';
const scaleStyle = session.avatarScale && session.avatarScale !== 1 ? ` style="scale: ${session.avatarScale}"` : '';
const imageHtml = session.image ? `` : '';
// Badges
const badges = [];
if (agent) badges.push(`${esc(agent.name)}`);
if (session.context) badges.push(``);
if (session.telegramConfigured) badges.push(``);
// Meta
const metaParts = [];
if (session.messageCount) metaParts.push(``);
if (session.archiveMessageCount) metaParts.push(``);
if (session.workspace) metaParts.push(``);
card.innerHTML = `
][\s\S]*?<\/code>/gi, function(m) {
codeBlocks.push(m);
return '%%CODE' + (codeBlocks.length - 1) + '%%';
});
// 4) [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild
html = html.replace(/\[Krea:\s*([^\]]+)\]/g, function(match, filename) {
var fn = filename.trim();
var safeFn = esc(fn);
var thumbUrl = '/api/images/file/' + encodeURIComponent(fn) + '/thumb';
var fullUrl = '/api/images/file/' + encodeURIComponent(fn);
return ''
+ '
'
+ '';
});
// 5) Unicode-Emojis durch Fluent Emoji 3D ersetzen (außerhalb von Code-Blöcken)
html = replaceEmojisWithFluent(html);
// 6) Code-Blöcke wiederherstellen
html = html.replace(/%%CODE(\d+)%%/g, function(m, idx) {
return codeBlocks[parseInt(idx)];
});
// 7) LaTeX-Platzhalter durch KaTeX-gerenderten HTML ersetzen
html = html.replace(/%%MATH(\d+)%%/g, function(m, idx) {
var block = mathBlocks[parseInt(idx)];
try {
return katex.renderToString(block.latex.trim(), {
displayMode: block.displayMode,
throwOnError: false,
output: 'html'
});
} catch (e) {
// Fallback: Originaltext anzeigen
return '' + esc(block.latex) + '';
}
});
return html;
}
function wrapKreaGalleries(container) {
// Findet -Tags die NUR krea-ref(s) enthalten und gruppiert aufeinanderfolgende zu einer Gallery
var children = Array.from(container.children);
var i = 0;
while (i < children.length) {
var el = children[i];
// Prüfe ob
nur krea-refs enthält (ggf. mit Whitespace)
if (el.tagName === 'P' && el.querySelector('.krea-ref') && el.textContent.trim() === '') {
// Sammle aufeinanderfolgende krea-only
-Tags
var group = [el];
var j = i + 1;
while (j < children.length) {
var next = children[j];
if (next.tagName === 'P' && next.querySelector('.krea-ref') && next.textContent.trim() === '') {
group.push(next);
j++;
} else {
break;
}
}
// Gallery erstellen wenn mindestens 1 Bild
if (group.length >= 1) {
var gallery = document.createElement('div');
gallery.className = 'krea-gallery';
el.parentNode.insertBefore(gallery, group[0]);
for (var k = 0; k < group.length; k++) {
var refs = group[k].querySelectorAll('.krea-ref');
for (var r = 0; r < refs.length; r++) {
gallery.appendChild(refs[r]);
}
group[k].remove();
}
// children-Array aktualisieren
children = Array.from(container.children);
// i bleibt gleich, da gallery jetzt an der Position steht
i++;
} else {
i = j;
}
} else {
i++;
}
}
}
function scrollDown() {
const chat = chatEl();
if (!chat) return;
chat.scrollTop = chat.scrollHeight;
// Nach Font-Swap kann sich die Höhe ändern → nochmal scrollen
document.fonts.ready.then(() => {
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 || '');
}
// ── Delete Button ──
function _addDeleteButton(div, messageId, deletedOn) {
if (!messageId) return;
const isDeleted = !!deletedOn;
const btn = document.createElement('button');
btn.className = 'msg-delete-btn icon-btn' + (isDeleted ? ' deleted' : '');
btn.innerHTML = '';
btn.title = isDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
btn.addEventListener('click', async (e) => {
e.stopPropagation();
try {
const result = await apiToggleMessageDelete(messageId);
const nowDeleted = !!result.deletedOn;
btn.classList.toggle('deleted', nowDeleted);
btn.title = nowDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
if (nowDeleted && !isDeletedVisible()) {
// Nachricht ausblenden
div.classList.add('msg-deleted-hidden');
} else {
div.classList.remove('msg-deleted-hidden');
div.classList.toggle('msg-deleted', nowDeleted);
}
} catch (err) {
console.error('Delete toggle failed:', err);
}
});
// Nach dem Reply-Button einfügen
const replyBtn = div.querySelector('.msg-reply-btn');
if (replyBtn) {
replyBtn.after(btn);
} else {
div.prepend(btn);
}
}
function isDeletedVisible() {
return loadSettings().showDeletedMessages === true;
}
// ── Mark Button ──
function _addMarkButton(div, messageId, isMarked) {
if (!messageId) return; // kein DB-Eintrag → kein Button
const btn = document.createElement('button');
btn.className = 'msg-mark-btn icon-btn' + (isMarked ? ' marked' : '');
btn.innerHTML = isMarked ? '' : '';
btn.title = 'Als wichtig markieren';
btn.addEventListener('click', async (e) => {
e.stopPropagation();
try {
const result = await apiToggleMessageMark(messageId);
btn.classList.toggle('marked', result.isMarked);
btn.innerHTML = result.isMarked ? '' : '';
div.classList.toggle('marked', result.isMarked);
} catch (err) {
console.error('Mark toggle failed:', err);
}
});
// Nach dem Delete-Button einfügen (oder nach Reply-Button, oder am Anfang)
const deleteBtn = div.querySelector('.msg-delete-btn');
if (deleteBtn) {
deleteBtn.after(btn);
} else {
const replyBtn = div.querySelector('.msg-reply-btn');
if (replyBtn) {
replyBtn.after(btn);
} else {
div.prepend(btn);
}
}
}
// ── Share Button ──
function _addShareButton(div, messageId) {
if (!messageId) return;
const btn = document.createElement('button');
btn.className = 'msg-share-btn icon-btn';
btn.innerHTML = '';
btn.title = 'Nachricht teilen';
btn.addEventListener('click', async (e) => {
e.stopPropagation();
try {
// Kontextmenü: Einzelnachricht oder mit Kontext?
const mode = await _showShareMenu(btn);
if (!mode) return; // abgebrochen
const result = await apiShareMessage(messageId, mode === 'context');
const url = location.origin + '/#shared/' + result.token;
await navigator.clipboard.writeText(url);
showToast('Link kopiert!', 'success');
} catch (err) {
showToast('Fehler: ' + err.message, 'error');
}
});
// Nach dem Mark-Button einfügen
const markBtn = div.querySelector('.msg-mark-btn');
if (markBtn) {
markBtn.after(btn);
} else {
const deleteBtn = div.querySelector('.msg-delete-btn');
if (deleteBtn) {
deleteBtn.after(btn);
} else {
div.prepend(btn);
}
}
}
function _showShareMenu(anchorBtn) {
return new Promise((resolve) => {
// Wenn schon ein Menü offen, schließen
const existing = document.querySelector('.share-menu');
if (existing) { existing.remove(); resolve(null); return; }
const menu = document.createElement('div');
menu.className = 'share-menu';
menu.innerHTML = `
`;
// Positionierung
const rect = anchorBtn.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = (rect.bottom + 4) + 'px';
menu.style.left = rect.left + 'px';
menu.style.zIndex = '10000';
document.body.appendChild(menu);
function cleanup() {
menu.remove();
document.removeEventListener('click', outsideClick);
}
function outsideClick(e) {
if (!menu.contains(e.target)) {
cleanup();
resolve(null);
}
}
menu.querySelectorAll('.share-menu-item').forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation();
const mode = item.dataset.mode;
cleanup();
resolve(mode);
});
});
// Beim nächsten Klick außerhalb schließen
setTimeout(() => document.addEventListener('click', outsideClick), 10);
});
}
// ── Timestamp ──
function _formatTimestamp(isoString) {
if (!isoString) return '';
var d = new Date(isoString);
if (isNaN(d.getTime())) return '';
var dd = String(d.getDate()).padStart(2, '0');
var mm = String(d.getMonth() + 1).padStart(2, '0');
var yyyy = d.getFullYear();
var hh = String(d.getHours()).padStart(2, '0');
var min = String(d.getMinutes()).padStart(2, '0');
var ss = String(d.getSeconds()).padStart(2, '0');
return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss;
}
function _addTimestamp(div, timestamp) {
if (!timestamp) return;
var formatted = _formatTimestamp(timestamp);
if (!formatted) return;
var span = document.createElement('span');
span.className = 'msg-timestamp';
span.textContent = formatted;
div.prepend(span);
}
// ── Reply Button ──
function _addReplyButton(div) {
const btn = document.createElement('button');
btn.className = 'msg-reply-btn icon-btn';
btn.innerHTML = '';
btn.title = 'Antworten';
btn.addEventListener('click', (e) => {
e.stopPropagation();
// Nur den eigentlichen Nachrichten-Text holen (ohne Reply-Button-Text)
const clone = div.cloneNode(true);
clone.querySelectorAll('.msg-reply-btn, .attachment-indicator, .reply-quote').forEach(el => el.remove());
const text = clone.textContent.trim();
setReply({
role: div.dataset.role,
timestamp: div.dataset.timestamp || '',
text
});
});
div.prepend(btn);
}
// ── Message Bubbles ──
function renderUserContent(container, text, inlineImages) {
const raw = (text || '').trim();
// Reply-Zitat erkennen und als visuellen Block rendern
// Format: > [Zitat: HH:MM, Role]:\n> text...\n\nrest
const quoteMatch = raw.match(/^> \[Zitat: ([^\]]+)\]:\n((?:> .*\n?)*)\n*([\s\S]*)$/);
if (quoteMatch) {
const quoteMeta = quoteMatch[1]; // z.B. "14:32, Assistant"
const quoteBody = quoteMatch[2].replace(/^> /gm, '').trim();
const restText = quoteMatch[3].trim();
const quoteEl = document.createElement('div');
quoteEl.className = 'reply-quote';
const metaEl = document.createElement('div');
metaEl.className = 'reply-quote-meta';
metaEl.innerHTML = ' ' + esc(quoteMeta);
quoteEl.appendChild(metaEl);
const bodyEl = document.createElement('div');
bodyEl.className = 'reply-quote-text';
bodyEl.textContent = truncate(quoteBody, 200);
quoteEl.appendChild(bodyEl);
container.appendChild(quoteEl);
// Rest als normalen Text weiterverarbeiten
if (restText) {
_renderUserTextContent(container, restText, inlineImages);
}
return;
}
_renderUserTextContent(container, raw, inlineImages);
}
function _renderUserTextContent(container, raw, inlineImages) {
// [Bild: name] durch Platzhalter ersetzen
const imagePattern = /\[Bild: ([^\]]+)\]/g;
const imageRefs = [];
let mdText = raw.replace(imagePattern, function(m, name) {
imageRefs.push(name);
return '%%IMGREF' + (imageRefs.length - 1) + '%%';
});
// [Datei: path] oder [Datei: path:1-10] durch Platzhalter ersetzen (vor Markdown-Parsing)
const pattern = /\[Datei: ([^\]]+)\]/g;
const fileRefs = [];
mdText = mdText.replace(pattern, function(m, ref) {
fileRefs.push(ref);
return '%%FILEREF' + (fileRefs.length - 1) + '%%';
});
// Markdown rendern
let html = renderMarkdown(mdText);
// Bild-Platzhalter durch Inline-Bilder ersetzen
html = html.replace(/%%IMGREF(\d+)%%/g, function(m, idx) {
const name = imageRefs[parseInt(idx)];
// Wenn Inline-Bild-Daten vorhanden (beim Senden), direkt anzeigen
const img = inlineImages && inlineImages.find(function(i) { return i.name === name; });
if (img) {
return '
';
}
// Sonst: Platzhalter mit data-Attribut für späteres Laden
return ''
+ ' ' + esc(name) + '';
});
// Datei-Platzhalter durch File-Ref-Chips ersetzen (als HTML)
html = html.replace(/%%FILEREF(\d+)%%/g, function(m, idx) {
const ref = fileRefs[parseInt(idx)];
const filePath = ref.replace(/:\d+(-\d+)?$/, '');
const fileName = filePath.includes('/') ? filePath.substring(filePath.lastIndexOf('/') + 1) : filePath;
const lineInfo = ref.replace(filePath, '').trim();
return ''
+ ' ' + esc(fileName + lineInfo) + '';
});
// In ein Wrapper-Div rendern (damit vorherige Elemente wie Reply-Quotes erhalten bleiben)
const wrap = document.createElement('div');
wrap.className = 'user-text-content';
wrap.innerHTML = html;
container.appendChild(wrap);
// Click-Handler für File-Ref-Chips nachträglich binden
wrap.querySelectorAll('.file-ref-chip[data-filepath]').forEach(function(chip) {
chip.addEventListener('click', function() {
openFileExplorer({ sessionId: getCurrentSessionId(), initialFile: chip.dataset.filepath });
});
});
// Click-Handler für Inline-Bilder (Vollbild)
wrap.querySelectorAll('.inline-user-image').forEach(function(img) {
img.addEventListener('click', function() {
showKreaImageModal(img.alt || 'Bild', img.src);
});
});
// Fluent Emoji DOM-Parsing
parseFluentEmojis(wrap);
}
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null, inlineImages = null) {
const div = document.createElement('div');
div.className = 'msg user markdown' + (isMarked ? ' marked' : '');
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'user';
div.dataset.timestamp = timestamp || new Date().toISOString();
if (messageId) div.dataset.messageId = messageId;
if (streamed) div.classList.add('streamed');
renderUserContent(div, text, inlineImages);
_addTimestamp(div, timestamp);
_addReplyButton(div);
_addDeleteButton(div, messageId, deletedOn);
_addMarkButton(div, messageId, isMarked);
_addShareButton(div, messageId);
chatEl().appendChild(div);
scrollDown();
}
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
const div = document.createElement('div');
div.className = 'msg assistant markdown' + (isMarked ? ' marked' : '');
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'assistant';
div.dataset.timestamp = timestamp || new Date().toISOString();
if (messageId) div.dataset.messageId = messageId;
div.innerHTML = renderMarkdown(text);
parseFluentEmojis(div);
wrapKreaGalleries(div);
_addTimestamp(div, timestamp);
_addReplyButton(div);
_addDeleteButton(div, messageId, deletedOn);
_addMarkButton(div, messageId, isMarked);
_addShareButton(div, messageId);
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.innerHTML = ' ' + esc((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.innerHTML = ' ' + esc((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 = '
';
_streamText = '';
chatEl().appendChild(_streamDiv);
scrollDown();
}
function appendToStream(text) {
if (!_streamDiv) startStreamBubble();
_streamText += text;
_streamDiv.innerHTML = renderMarkdown(_streamText);
// Cursor ins letzte Inline/Text-Element einfügen, damit er nicht auf eine neue Zeile rutscht
const cursor = document.createElement('span');
cursor.className = 'cursor';
const lastBlock = _streamDiv.lastElementChild;
if (lastBlock) {
lastBlock.appendChild(cursor);
} else {
_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);
parseFluentEmojis(_streamDiv);
wrapKreaGalleries(_streamDiv);
}
}
_streamDiv = null;
_streamText = '';
}
// ── Tool Call Visibility ──
const MODIFICATION_TOOLS = ['write_file', 'edit_file', 'delete_file', 'memory_write'];
const TOOL_DISPLAY_NAMES = {
write_file: 'Datei schreiben',
edit_file: 'Datei bearbeiten',
delete_file: 'Datei löschen',
memory_write: 'Erinnerungen festhalten',
web_search: 'Web-Suche'
};
function isToolVisible(toolName) {
const mode = loadSettings().toolCallDisplay;
if (mode === 'all') return true;
if (mode === 'hidden') return false;
// 'modifications' – nur modifizierende Tools anzeigen
return MODIFICATION_TOOLS.includes(toolName);
}
// ── Chain Meta Visibility ──
function isChainMetaVisible(chainData) {
const mode = loadSettings().chainMetaDisplay;
if (mode === 'always') return true;
if (mode === 'never') return false;
// 'sometimes' – nur bei auffälligen Chains anzeigen
if (!chainData) return false;
const inOut = (chainData.inputTokens || 0) + (chainData.outputTokens || 0);
if (inOut >= 10000) return true;
if ((chainData.iterations || 0) >= 25) return true;
if ((chainData.durationMs || 0) >= 90000) return true;
return false;
}
// ── Tool Boxes ──
function addToolBoxLoading(name) {
const div = document.createElement('div');
div.className = 'tool-box streamed';
div.dataset.toolName = name;
const displayName = TOOL_DISPLAY_NAMES[name] || name;
div.innerHTML = '' + esc(displayName) + ' ';
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 = TOOL_DISPLAY_NAMES[name] || 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 if (name === 'todo_write') {
// Todo-Liste: kein tool-input, standardmäßig sichtbar, custom parser
if (result) {
const todoList = buildTodoList(result, isError);
div.appendChild(todoList);
foldTarget = todoList;
}
} 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 || '';
resultDiv.hidden = true;
div.appendChild(resultDiv);
foldTarget = resultDiv;
}
if (foldTarget && name !== 'todo_write') {
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 buildTodoList(result, isError) {
const container = document.createElement('div');
container.className = 'todo-list' + (isError ? ' tool-error' : '');
const lines = result.split('\n').filter(l => l.trim());
for (const line of lines) {
// Format: ICON [id] [priority] content
const m = line.match(/^(\S+)\s+\[([^\]]+)\]\s+(?:\[([^\]]+)\]\s+)?(.+)$/);
const row = document.createElement('div');
row.className = 'todo-item';
if (m) {
const icon = m[1]; // ✅ / 🔄 / ⬜
const id = m[2];
const prio = m[3] || null;
const text = m[4];
const statusClass =
icon === '✅' ? 'todo-done' :
icon === '🔄' ? 'todo-progress' :
'todo-pending';
row.classList.add(statusClass);
const iconSpan = document.createElement('span');
iconSpan.className = 'todo-icon';
iconSpan.textContent = icon;
const idSpan = document.createElement('span');
idSpan.className = 'todo-id';
idSpan.textContent = id + '.';
const textSpan = document.createElement('span');
textSpan.className = 'todo-text';
textSpan.textContent = text;
row.appendChild(iconSpan);
row.appendChild(idSpan);
row.appendChild(textSpan);
if (prio) {
const prioSpan = document.createElement('span');
prioSpan.className = 'todo-prio todo-prio-' + prio.toLowerCase();
prioSpan.textContent = prio;
row.appendChild(prioSpan);
}
} else {
row.textContent = line;
}
container.appendChild(row);
}
parseFluentEmojis(container);
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';
div.dataset.toolName = name;
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 toolName = lastBox.dataset.toolName || '';
const isTodoWrite = toolName === 'todo_write';
if (isTodoWrite) {
// Vorhandene todo-list ersetzen oder neu erstellen
const existing = lastBox.querySelector('.todo-list');
if (existing) existing.remove();
if (result && !isError) {
const todoList = buildTodoList(result, isError);
lastBox.appendChild(todoList);
}
return;
}
const existing = lastBox.querySelector('.tool-result');
if (existing) {
existing.textContent = result || '';
if (isError) existing.classList.add('tool-error');
} else {
const resultDiv = document.createElement('div');
resultDiv.className = 'tool-result' + (isError ? ' tool-error' : '');
resultDiv.textContent = result || '';
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.innerHTML = ` ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${esc(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.innerHTML = ' 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.innerHTML = ` ${esc(att.fileName || 'Dokument')}`;
container.appendChild(link);
}
}
indicator.innerHTML = ` ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`;
indicator.parentElement.appendChild(container);
} catch (e) {
indicator.innerHTML = ' Fehler beim Laden';
indicator.dataset.loaded = '';
}
}
// ── System-Prompt Modal ──
function _renderSystemPromptBlocks(container, blocks) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (!block.content) continue;
var wrapper = document.createElement('div');
wrapper.className = 'sp-block';
var label = document.createElement('div');
label.className = 'sp-block-label';
label.innerHTML = ' ' + esc(block.label || 'Block');
wrapper.appendChild(label);
var content = document.createElement('div');
content.className = 'sp-block-content markdown';
content.innerHTML = renderMarkdown(block.content);
parseFluentEmojis(content);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
}
async function showSystemPromptModal(sessionId) {
// Overlay
const overlay = document.createElement('div');
overlay.className = 'sp-overlay';
// Modal
const modal = document.createElement('div');
modal.className = 'sp-modal';
// Header
const header = document.createElement('div');
header.className = 'sp-header';
const title = document.createElement('span');
title.textContent = 'System-Prompt';
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '';
closeBtn.className = 'sp-close icon-btn';
header.appendChild(title);
header.appendChild(closeBtn);
// Body
const body = document.createElement('div');
body.className = 'sp-body markdown';
body.innerHTML = 'Laden...';
modal.appendChild(header);
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Schließen
const close = () => overlay.remove();
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const onKey = (e) => {
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
};
document.addEventListener('keydown', onKey);
// Laden
try {
const blocks = await apiGetSystemPromptBlocks(sessionId);
body.innerHTML = '';
_renderSystemPromptBlocks(body, blocks);
} catch (e) {
body.innerHTML = 'Fehler beim Laden des System-Prompts';
}
}
// ── Command Output Modal ──
function showCommandOutputModal(name, command, result) {
const overlay = document.createElement('div');
overlay.className = 'sp-overlay';
const modal = document.createElement('div');
modal.className = 'sp-modal cmd-output-modal';
// Header
const header = document.createElement('div');
header.className = 'sp-header';
const title = document.createElement('span');
title.textContent = name;
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '';
closeBtn.className = 'sp-close icon-btn';
header.appendChild(title);
header.appendChild(closeBtn);
// Body
const body = document.createElement('div');
body.className = 'sp-body';
const exitCode = result.exitCode != null ? result.exitCode : '?';
const isError = exitCode !== 0;
const statusText = result.status === 'timeout' ? 'Timeout' : (isError ? `Exit ${exitCode}` : `Exit ${exitCode}`);
const meta = document.createElement('div');
meta.className = 'cmd-output-meta';
meta.innerHTML = `${esc(command)} ${esc(statusText)}`;
body.appendChild(meta);
const output = document.createElement('pre');
output.className = 'cmd-output-content';
output.textContent = result.output || '(kein Output)';
body.appendChild(output);
modal.appendChild(header);
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Schließen
const close = () => overlay.remove();
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const onKey = (e) => {
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
};
document.addEventListener('keydown', onKey);
}
// ── Meta Bar ──
function setMeta(text) {
metaEl().textContent = text;
}
function setMetaHtml(html) {
metaEl().innerHTML = html;
}
// ── Older Messages Button ──
function showOlderMessagesButton(remainingCount) {
removeOlderMessagesButton();
const btn = document.createElement('button');
btn.className = 'archive-load-btn older-messages-btn';
btn.innerHTML = ' ' + remainingCount + ' ältere Nachrichten laden';
btn.addEventListener('click', () => loadOlderMessages());
chatEl().prepend(btn);
}
function removeOlderMessagesButton() {
const btn = chatEl().querySelector('.older-messages-btn');
if (btn) btn.remove();
}
// ── Archive Button ──
function showArchiveLoadButton() {
const existing = chatEl().querySelector('.archive-load-btn:not(.older-messages-btn)');
if (existing) existing.remove();
const btn = document.createElement('button');
btn.className = 'archive-load-btn';
btn.innerHTML = ' Archivierte Nachrichten laden';
btn.addEventListener('click', loadLatestArchive);
// Immer ganz oben im Chat (aber nach dem older-messages-btn falls vorhanden)
const olderBtn = chatEl().querySelector('.older-messages-btn');
if (olderBtn) {
olderBtn.after(btn);
} else {
chatEl().prepend(btn);
}
}
function removeArchiveLoadButton() {
const btns = chatEl().querySelectorAll('.archive-load-btn:not(.older-messages-btn)');
btns.forEach(b => b.remove());
}
// ── Krea Image Preview Modal ──
function showKreaImageModal(title, fullUrl) {
var overlay = document.createElement('div');
overlay.className = 'sp-overlay krea-overlay';
var modal = document.createElement('div');
modal.className = 'sp-modal krea-modal';
// Header
var header = document.createElement('div');
header.className = 'sp-header';
var titleEl = document.createElement('span');
titleEl.textContent = title;
var closeBtn = document.createElement('button');
closeBtn.innerHTML = '';
closeBtn.className = 'sp-close icon-btn';
header.appendChild(titleEl);
header.appendChild(closeBtn);
// Body
var body = document.createElement('div');
body.className = 'sp-body krea-modal-body';
var img = document.createElement('img');
img.src = fullUrl;
img.className = 'krea-modal-image';
img.alt = title;
img.addEventListener('error', function() {
body.innerHTML = 'Bild konnte nicht geladen werden';
});
body.appendChild(img);
modal.appendChild(header);
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Schließen
var close = function() { overlay.remove(); };
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close();
});
var onKey = function(e) {
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
};
document.addEventListener('keydown', onKey);
}
// Event-Delegation für Krea-Thumbnails im Chat
document.addEventListener('click', function(e) {
var ref = e.target.closest('.krea-ref');
if (ref) {
var fullUrl = ref.dataset.fullurl;
var filename = ref.dataset.filename;
if (fullUrl) showKreaImageModal(filename || 'Bild', fullUrl);
}
});