diff --git a/wwwroot/index.html b/wwwroot/index.html
index 37d1993..3c72e1c 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -555,12 +555,22 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wwwroot/js/archive.js b/wwwroot/js/archive.js
new file mode 100644
index 0000000..efcb09a
--- /dev/null
+++ b/wwwroot/js/archive.js
@@ -0,0 +1,173 @@
+// ── Archive Loading ──
+
+let loadedArchiveCount = 0;
+
+function getOrCreateArchiveContainer() {
+ let container = chatEl().querySelector('.archive-container');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'archive-container';
+ chatEl().prepend(container);
+ }
+ return container;
+}
+
+function getOrCreateArchiveDivider() {
+ let divider = chatEl().querySelector('.archive-divider');
+ if (!divider) {
+ divider = document.createElement('div');
+ divider.className = 'msg system-info archive-divider';
+ divider.textContent = '── Aktuelle Konversation ──';
+ // Nach dem Container, vor den aktiven Messages
+ const container = getOrCreateArchiveContainer();
+ container.after(divider);
+ }
+ return divider;
+}
+
+async function loadLatestArchive() {
+ try {
+ const archives = await apiListArchives(getCurrentSessionId());
+ if (archives.length <= loadedArchiveCount) return;
+
+ const latest = archives[loadedArchiveCount];
+ loadedArchiveCount++;
+
+ removeArchiveLoadButton();
+
+ const data = await apiGetArchive(latest.archiveId);
+ const container = getOrCreateArchiveContainer();
+ getOrCreateArchiveDivider();
+
+ // Archiv-Block zusammenbauen
+ const fragment = document.createDocumentFragment();
+
+ const sep = document.createElement('div');
+ sep.className = 'msg system-info';
+ sep.innerHTML = ` Archiv vom ${new Date(latest.createdAt).toLocaleString('de-AT')} (${latest.chainCount} Chains, ${latest.messageCount} Messages)`;
+ fragment.appendChild(sep);
+
+ const archiveImageIds = [];
+ for (const entry of data) {
+ const mergedMsgs = _mergeConsecutiveAssistantTexts(entry.messages);
+ for (const msg of mergedMsgs) {
+ const div = document.createElement('div');
+ switch (msg.type) {
+ case 'text': {
+ const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback') || msg.content.startsWith('[Finalizer:') || msg.content.startsWith('[Preparer:'));
+ if (msg.role === 'user' && !isReviewMsg) {
+ div.className = 'msg user markdown archived' + (msg.isMarked ? ' marked' : '');
+ if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
+ div.dataset.role = 'user';
+ div.dataset.timestamp = msg.createdAt || '';
+ if (msg.id) div.dataset.messageId = msg.id;
+ renderUserContent(div, msg.content);
+ _addTimestamp(div, msg.createdAt);
+ _addReplyButton(div);
+ _addDeleteButton(div, msg.id, msg.deletedOn);
+ _addMarkButton(div, msg.id, msg.isMarked);
+ _addShareButton(div, msg.id);
+ } else if (isReviewMsg) {
+ div.className = 'msg review-bubble markdown archived' + (msg.isMarked ? ' marked' : '');
+ if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
+ div.dataset.role = msg.role;
+ div.dataset.timestamp = msg.createdAt || '';
+ if (msg.id) div.dataset.messageId = msg.id;
+ div.innerHTML = renderMarkdown(msg.content);
+ parseFluentEmojis(div);
+ _addTimestamp(div, msg.createdAt);
+ _addReplyButton(div);
+ _addDeleteButton(div, msg.id, msg.deletedOn);
+ _addMarkButton(div, msg.id, msg.isMarked);
+ _addShareButton(div, msg.id);
+ } else if (msg.role === 'assistant') {
+ div.className = 'msg assistant markdown archived' + (msg.isMarked ? ' marked' : '');
+ if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
+ div.dataset.role = 'assistant';
+ div.dataset.timestamp = msg.createdAt || '';
+ if (msg.id) div.dataset.messageId = msg.id;
+ div.innerHTML = renderMarkdown(msg.content);
+ parseFluentEmojis(div);
+ _addTimestamp(div, msg.createdAt);
+ _addReplyButton(div);
+ _addDeleteButton(div, msg.id, msg.deletedOn);
+ _addMarkButton(div, msg.id, msg.isMarked);
+ _addShareButton(div, msg.id);
+ }
+ if (msg.role === 'user' && msg.attachmentCount > 0) {
+ if (/\[Bild: [^\]]+\]/.test(msg.content)) archiveImageIds.push(msg.id);
+ let names = [];
+ try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {}
+ const nameList = names.filter(n => n).join(', ');
+ const ind = document.createElement('div');
+ ind.className = 'attachment-indicator';
+ ind.innerHTML = ` ${msg.attachmentCount} Datei${msg.attachmentCount > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
+ ind.style.cursor = 'pointer';
+ ind.title = 'Klicken zum Anzeigen';
+ ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind));
+ div.appendChild(ind);
+ }
+ break;
+ }
+ case 'tool_use':
+ if (!isToolVisible(msg.toolName)) continue;
+ div.className = 'tool-box archived';
+ renderToolBoxContent(div, msg.toolName, msg.content, null, false);
+ break;
+ case 'web_search_result': {
+ if (!isToolVisible('web_search')) continue;
+ var archSrc = [];
+ try { archSrc = JSON.parse(msg.content || '[]'); } catch(e) {}
+ if (archSrc.length === 0) continue; // Leere Suchergebnisse überspringen
+ div.className = 'tool-box archived';
+ div.dataset.toolName = 'web_search';
+ renderToolBoxContent(div, 'web_search', null, null, false, archSrc);
+ break;
+ }
+ case 'tool_result': {
+ // An vorherigen tool-box anhängen
+ const prevBoxes = fragment.querySelectorAll ? fragment.querySelectorAll('.tool-box') : [];
+ const lastToolBox = prevBoxes[prevBoxes.length - 1];
+ if (lastToolBox && !lastToolBox.querySelector('.tool-result')) {
+ const rd = document.createElement('div');
+ rd.className = 'tool-result' + (msg.isError ? ' tool-error' : '');
+ rd.textContent = truncate(msg.content || '', 300);
+ lastToolBox.appendChild(rd);
+ }
+ continue; // kein separates div
+ }
+ case 'error':
+ div.className = 'msg system-info archived';
+ div.style.color = 'var(--dangerous)';
+ div.textContent = (msg.content || '').trim();
+ break;
+ default:
+ continue;
+ }
+ fragment.appendChild(div);
+ }
+ // Chain-Meta-Badges nach jeder Chain einfügen
+ if (entry.chain && isChainMetaVisible(entry.chain)) {
+ const badges = buildMetaBadges(entry.chain);
+ const metaDiv = document.createElement('div');
+ metaDiv.className = 'chain-meta archived';
+ metaDiv.innerHTML = badges.map(b => '').join('');
+ fragment.appendChild(metaDiv);
+ }
+ }
+
+ // Älteres Archiv vor dem bisherigen Inhalt einfügen
+ container.prepend(fragment);
+
+ // Bild-Attachments automatisch laden
+ for (const mid of archiveImageIds) _autoLoadImageAttachments(mid);
+
+ // Button ganz oben, falls es noch ältere Archive gibt
+ if (archives.length > loadedArchiveCount) {
+ showArchiveLoadButton();
+ }
+
+ } catch (e) {
+ console.error('Archiv laden fehlgeschlagen:', e);
+ }
+}
diff --git a/wwwroot/js/bubbles.js b/wwwroot/js/bubbles.js
new file mode 100644
index 0000000..c7ab002
--- /dev/null
+++ b/wwwroot/js/bubbles.js
@@ -0,0 +1,312 @@
+// ── User Content Rendering ──
+
+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);
+}
+
+// ── Message Bubbles ──
+
+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();
+}
+
+// ── Choice Bubbles (ask_user_choice) ──
+
+// Wird live aus dem SSE-Stream aufgerufen
+function addChoiceBubble(toolCallId, question, choices, streamed = true) {
+ // Guard: choices muss ein Array sein
+ if (!Array.isArray(choices)) choices = [String(choices)];
+
+ const div = document.createElement('div');
+ div.className = 'msg choice-bubble';
+ if (streamed) div.classList.add('streamed');
+ div.dataset.toolCallId = toolCallId;
+
+ const questionDiv = document.createElement('div');
+ questionDiv.className = 'choice-question';
+ questionDiv.innerHTML = renderMarkdown(question);
+ parseFluentEmojis(questionDiv);
+ div.appendChild(questionDiv);
+
+ const buttonsDiv = document.createElement('div');
+ buttonsDiv.className = 'choice-buttons';
+ for (const choice of choices) {
+ const btn = document.createElement('button');
+ btn.className = 'choice-btn';
+ btn.textContent = choice;
+ btn.addEventListener('click', () => handleChoiceClick(toolCallId, choice, div));
+ buttonsDiv.appendChild(btn);
+ }
+ div.appendChild(buttonsDiv);
+
+ chatEl().appendChild(div);
+ scrollDown();
+ return div;
+}
+
+// Wird für History-Rendering aufgerufen (bereits beantwortete Choices)
+function addChoiceBubbleStatic(toolCallId, question, choices, selectedChoice, timestamp, messageId) {
+ // Guard: choices muss ein Array sein
+ if (!Array.isArray(choices)) choices = [String(choices)];
+
+ const div = document.createElement('div');
+ div.className = 'msg choice-bubble choice-answered';
+ div.dataset.toolCallId = toolCallId;
+ if (messageId) div.dataset.messageId = messageId;
+
+ const questionDiv = document.createElement('div');
+ questionDiv.className = 'choice-question';
+ questionDiv.innerHTML = renderMarkdown(question);
+ parseFluentEmojis(questionDiv);
+ div.appendChild(questionDiv);
+
+ const buttonsDiv = document.createElement('div');
+ buttonsDiv.className = 'choice-buttons';
+ for (const choice of choices) {
+ const btn = document.createElement('button');
+ btn.className = 'choice-btn';
+ btn.disabled = true;
+ btn.textContent = choice;
+ if (choice === selectedChoice) {
+ btn.classList.add('choice-selected');
+ } else {
+ btn.classList.add('choice-unselected');
+ }
+ buttonsDiv.appendChild(btn);
+ }
+ div.appendChild(buttonsDiv);
+
+ if (timestamp) _addTimestamp(div, timestamp);
+ chatEl().appendChild(div);
+ scrollDown();
+ return div;
+}
+
+// ── Review Bubbles (Static / History) ──
+
+function addReviewBubbleStatic(text, timestamp, messageId, isMarked, deletedOn) {
+ const div = document.createElement('div');
+ div.className = 'msg review-bubble markdown' + (isMarked ? ' marked' : '');
+ if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
+ div.dataset.role = 'assistant';
+ div.dataset.timestamp = timestamp || '';
+ if (messageId) div.dataset.messageId = messageId;
+ div.innerHTML = renderMarkdown(text);
+ parseFluentEmojis(div);
+ _addTimestamp(div, timestamp);
+ _addReplyButton(div);
+ _addDeleteButton(div, messageId, deletedOn);
+ _addMarkButton(div, messageId, isMarked);
+ _addShareButton(div, messageId);
+ chatEl().appendChild(div);
+ scrollDown();
+ return div;
+}
+
+// ── 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 = '';
+ }
+}
diff --git a/wwwroot/js/cards.js b/wwwroot/js/cards.js
new file mode 100644
index 0000000..b1ae60e
--- /dev/null
+++ b/wwwroot/js/cards.js
@@ -0,0 +1,90 @@
+// ── Session Card Helper ──
+// Erzeugt ein .card-Element für eine Session (wiederverwendbar für Dashboard + Picker).
+
+function buildSessionCard(session, agent) {
+ const card = document.createElement('div');
+ card.className = 'card';
+
+ const transformParts = [];
+ if (session.avatarScale && session.avatarScale !== 1) transformParts.push(`scale(${session.avatarScale})`);
+ if (session.avatarOffsetX || session.avatarOffsetY) transformParts.push(`translate(${session.avatarOffsetX || 0}%, ${session.avatarOffsetY || 0}%)`);
+ const transformStyle = transformParts.length ? ` style="transform: ${transformParts.join(' ')}"` : '';
+ const imageHtml = session.image ? `
` : '';
+
+ // Badges
+ const badges = [];
+ if (agent) badges.push(`${esc(agent.name)}`);
+ if (session.reviewAgentId) badges.push(``);
+ if (session.finalizerAgentId) badges.push(``);
+ 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) + '%%';
+ });
+
+ // 4a) [Review: AgentName] prefix → Review-Header
+ html = html.replace(/\[Review:\s*([^\]]+)\]\s*/g, function(match, agentName) {
+ return ' ' + esc(agentName.trim()) + ' ';
+ });
+
+ // 4a2) [Finalizer: AgentName] prefix → Finalizer-Header
+ html = html.replace(/\[Finalizer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
+ return ' ' + esc(agentName.trim()) + ' ';
+ });
+
+ // 4a3) [Preparer: AgentName] prefix → Preparer-Header
+ html = html.replace(/\[Preparer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
+ return ' ' + esc(agentName.trim()) + ' ';
+ });
+
+ // 4a4) [Vorbereitet von AgentName]: → Preparer-Forwarded-Header
+ html = html.replace(/\[Vorbereitet von\s+([^\]]+)\]:\s*/g, function(match, agentName) {
+ return ' Vorbereitet von ' + esc(agentName.trim()) + '';
+ });
+
+ // 4b) [Review-Feedback von AgentName (Runde X/Y)]: → Review-Feedback-Header
+ html = html.replace(/\[Review-Feedback von\s+([^\(]+)\(Runde\s+(\d+)\/(\d+)\)\]:\s*/g, function(match, agentName, round, maxRounds) {
+ return ' Feedback von ' + esc(agentName.trim()) + ' Runde ' + esc(round) + '/' + esc(maxRounds) + '';
+ });
+
+ // 4c) [APPROVED], [FORWARD] und [QUESTION] ausblenden
+ html = html.replace(/\[APPROVED\]/g, '');
+ html = html.replace(/\[FORWARD\]\s*/g, '');
+ html = html.replace(/\[QUESTION\]\s*/g, '');
+
+ // 4d) [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++;
+ }
+ }
+}
diff --git a/wwwroot/js/modals.js b/wwwroot/js/modals.js
new file mode 100644
index 0000000..569f83a
--- /dev/null
+++ b/wwwroot/js/modals.js
@@ -0,0 +1,190 @@
+// ── 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, typeof _planModeActive !== 'undefined' && _planModeActive);
+ 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);
+}
+
+// ── 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);
+ }
+});
diff --git a/wwwroot/js/msg-actions.js b/wwwroot/js/msg-actions.js
new file mode 100644
index 0000000..8bd825c
--- /dev/null
+++ b/wwwroot/js/msg-actions.js
@@ -0,0 +1,208 @@
+// ── 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);
+}
+
+// ── 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);
+ });
+}
+
+// ── 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);
+}
diff --git a/wwwroot/js/reply.js b/wwwroot/js/reply.js
new file mode 100644
index 0000000..b6d44dc
--- /dev/null
+++ b/wwwroot/js/reply.js
@@ -0,0 +1,54 @@
+// ── Reply / Quote ──
+
+let pendingReply = null; // { index, role, text } - Zitierte Nachricht
+
+function _formatReplyTimestamp(ts) {
+ if (!ts) return 'Jetzt';
+ try {
+ const d = new Date(ts);
+ const now = new Date();
+ const hh = d.getHours().toString().padStart(2, '0');
+ const mm = d.getMinutes().toString().padStart(2, '0');
+ // Gleiches Datum → nur Uhrzeit, sonst Datum + Uhrzeit
+ if (d.toDateString() === now.toDateString()) {
+ return hh + ':' + mm;
+ }
+ const dd = d.getDate().toString().padStart(2, '0');
+ const mo = (d.getMonth() + 1).toString().padStart(2, '0');
+ return dd + '.' + mo + '. ' + hh + ':' + mm;
+ } catch { return 'Jetzt'; }
+}
+
+function setReply(reply) {
+ pendingReply = reply;
+ renderReplyPreview();
+ document.getElementById('input')?.focus();
+}
+
+function clearReply() {
+ pendingReply = null;
+ renderReplyPreview();
+}
+
+function renderReplyPreview() {
+ const container = $('#replyPreview');
+ if (!container) return;
+
+ if (!pendingReply) {
+ container.innerHTML = '';
+ container.hidden = true;
+ return;
+ }
+
+ const roleLabel = pendingReply.role === 'user' ? 'User' : 'Assistant';
+ const timeLabel = _formatReplyTimestamp(pendingReply.timestamp);
+ const preview = truncate(pendingReply.text, 150);
+
+ container.innerHTML = ``;
+ container.hidden = false;
+}
diff --git a/wwwroot/js/stream-ui.js b/wwwroot/js/stream-ui.js
new file mode 100644
index 0000000..c8ba5e0
--- /dev/null
+++ b/wwwroot/js/stream-ui.js
@@ -0,0 +1,124 @@
+// ── 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 pauseStreamBubble() {
+ // Cursor entfernen, aber Bubble + Text behalten (z.B. bei Web-Search-Pause)
+ if (_streamDiv) {
+ const cursor = _streamDiv.querySelector('.cursor');
+ if (cursor) cursor.remove();
+ }
+}
+
+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 = '';
+}
+
+// ── Review Bubbles (Streaming) ──
+
+let _reviewBubble = null;
+let _reviewTextEl = null;
+let _reviewText = '';
+
+function addReviewBubble(type, data, agentType) {
+ _reviewBubble = document.createElement('div');
+ _reviewBubble.className = 'msg review-bubble streamed';
+ if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble');
+ if (agentType === 'preparer') _reviewBubble.classList.add('preparer-bubble');
+ const header = document.createElement('div');
+ header.className = 'review-header';
+ if (agentType === 'preparer') {
+ header.innerHTML = ' ' + esc(data.agentName || 'Vorbereiter-Agent') + '';
+ } else if (agentType === 'finalizer') {
+ header.innerHTML = ' ' + esc(data.agentName || 'Finalizer-Agent') + '';
+ } else {
+ header.innerHTML = ' ' + esc(data.agentName || 'Review-Agent') + ' Runde ' + data.round + '/' + data.maxRounds + '';
+ }
+ _reviewBubble.appendChild(header);
+ _reviewTextEl = document.createElement('div');
+ _reviewTextEl.className = 'review-content markdown';
+ _reviewBubble.appendChild(_reviewTextEl);
+ _reviewText = '';
+ chatEl().appendChild(_reviewBubble);
+ scrollDown();
+}
+
+function appendToReviewText(text) {
+ if (!_reviewBubble) return;
+ _reviewText += text;
+ _reviewTextEl.innerHTML = renderMarkdown(_reviewText);
+ scrollDown();
+}
+
+function addReviewToolUse(data) {
+ if (!_reviewBubble) return;
+ const toolDiv = document.createElement('div');
+ toolDiv.className = 'review-tool-use';
+ toolDiv.innerHTML = ' ' + esc(data.toolName) + '';
+ _reviewBubble.appendChild(toolDiv);
+ scrollDown();
+}
+
+function finalizeReviewBubble(type, data, agentType) {
+ if (!_reviewBubble) return;
+ const status = document.createElement('div');
+ status.className = 'review-status';
+ if (agentType === 'preparer' && type === 'forwarded') {
+ status.className += ' review-approved';
+ status.innerHTML = ' Weitergeleitet an Session-Agent';
+ } else if (agentType === 'preparer' && type === 'query') {
+ status.className += ' review-rejected';
+ status.innerHTML = ' Rückfrage an User';
+ } else if (agentType === 'finalizer' || type === 'done') {
+ status.className += ' review-approved';
+ status.innerHTML = ' Abgeschlossen';
+ } else if (type === 'approved') {
+ status.className += ' review-approved';
+ status.innerHTML = ' Approved';
+ } else {
+ status.className += ' review-rejected';
+ status.innerHTML = ' Feedback – Worker korrigiert...';
+ }
+ _reviewBubble.appendChild(status);
+ _reviewBubble = null;
+ _reviewTextEl = null;
+ _reviewText = '';
+ scrollDown();
+}
diff --git a/wwwroot/js/tool-ui.js b/wwwroot/js/tool-ui.js
new file mode 100644
index 0000000..5e0283b
--- /dev/null
+++ b/wwwroot/js/tool-ui.js
@@ -0,0 +1,332 @@
+// ── 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 + web_search anzeigen
+ if (toolName === 'web_search') return true;
+ 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, sources) {
+ 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;
+
+ // Web-Search Sources – immer sichtbar, kein Toggle
+ if (name === 'web_search' && sources && sources.length > 0) {
+ const sourcesList = document.createElement('div');
+ sourcesList.className = 'web-search-sources';
+ sources.forEach(function(s) {
+ const link = document.createElement('a');
+ link.href = s.url;
+ link.target = '_blank';
+ link.rel = 'noopener noreferrer';
+ link.className = 'web-search-source';
+ var domain = '';
+ try { domain = new URL(s.url).hostname.replace('www.', ''); } catch(e) { domain = s.url; }
+ link.innerHTML = ' ' + esc(domain) + '' +
+ (s.title ? '' + esc(s.title) + '' : '');
+ sourcesList.appendChild(link);
+ });
+ div.appendChild(sourcesList);
+ }
+
+ 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, data.sources);
+ scrollDown();
+}
+
+function addToolBoxStatic(name, input, result, isError, sources) {
+ const div = document.createElement('div');
+ div.className = 'tool-box';
+ div.dataset.toolName = name;
+ renderToolBoxContent(div, name, input, result, isError, sources);
+ 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);
+ }
+}
diff --git a/wwwroot/js/ui-helpers.js b/wwwroot/js/ui-helpers.js
new file mode 100644
index 0000000..ae8c6b4
--- /dev/null
+++ b/wwwroot/js/ui-helpers.js
@@ -0,0 +1,126 @@
+// ── DOM Helpers ──
+
+const $ = (sel) => document.querySelector(sel);
+const chatEl = () => $('#chat');
+const metaEl = () => $('#meta');
+
+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 || '');
+}
+
+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 prettyModelName(model) {
+ if (!model) return '?';
+ model = model.split('claude-').join('');
+ if (model == 'sonnet') return 'Sonnet';
+ if (model == 'sonnet-4-5') return 'Sonnet 4.5';
+ if (model == 'sonnet-4-6') return 'Sonnet 4.6';
+ if (model == 'opus') return 'Opus';
+ if (model == 'opus-4-5') return 'Opus 4.5';
+ if (model == 'opus-4-6') return 'Opus 4.6';
+ if (model == 'haiku') return 'Haiku';
+ if (model == 'haiku-4-5') return 'Haiku 4.5';
+ return 'Unbekannt (' + model + ')';
+}
+
+// ── Toast Notifications ──
+
+function showToast(message, type) {
+ type = type || 'info';
+ var container = document.getElementById('toastContainer');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'toastContainer';
+ document.body.appendChild(container);
+ }
+ var toast = document.createElement('div');
+ toast.className = 'toast toast-' + type;
+ var icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle';
+ toast.innerHTML = ' ' + (message || '');
+ container.appendChild(toast);
+ // Einblenden
+ requestAnimationFrame(function() { toast.classList.add('toast-visible'); });
+ // Automatisch ausblenden
+ var timeout = setTimeout(function() { removeToast(toast); }, 4000);
+ toast.addEventListener('click', function() {
+ clearTimeout(timeout);
+ removeToast(toast);
+ });
+}
+
+function removeToast(toast) {
+ toast.classList.remove('toast-visible');
+ toast.addEventListener('transitionend', function() { toast.remove(); });
+}
+
+// ── 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());
+}
+
+// ── Streamed Elements ──
+
+function removeStreamedElements() {
+ chatEl().querySelectorAll('.streamed').forEach(el => el.remove());
+}
diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js
deleted file mode 100644
index 2599ebc..0000000
--- a/wwwroot/js/ui.js
+++ /dev/null
@@ -1,1534 +0,0 @@
-// ── 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 transformParts = [];
- if (session.avatarScale && session.avatarScale !== 1) transformParts.push(`scale(${session.avatarScale})`);
- if (session.avatarOffsetX || session.avatarOffsetY) transformParts.push(`translate(${session.avatarOffsetX || 0}%, ${session.avatarOffsetY || 0}%)`);
- const transformStyle = transformParts.length ? ` style="transform: ${transformParts.join(' ')}"` : '';
- const imageHtml = session.image ? `
` : '';
-
- // Badges
- const badges = [];
- if (agent) badges.push(`${esc(agent.name)}`);
- if (session.reviewAgentId) badges.push(``);
- if (session.finalizerAgentId) badges.push(``);
- 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 = `
- ${imageHtml}
-
-
- ${esc(session.name || 'Unbenannt')}
- ${badges.join('')}
-
- ${metaParts.length ? `` : ''}
-
- `;
-
- return card;
-}
-
-
-// ── Agent Card Helper ──
-// Erzeugt ein .card-Element für einen Agent (wiederverwendbar für Dashboard + Picker).
-// agent: Agent-Objekt, skills: Array von Skills (optional)
-// Gibt ein DOM-Element zurück (.card mit .card-image + .card-body inkl. Badges).
-
-function buildAgentCard(agent, skills) {
- const card = document.createElement('div');
- card.className = 'card';
-
- const agentTransformParts = [];
- if (agent.avatarScale && agent.avatarScale !== 1) agentTransformParts.push(`scale(${agent.avatarScale})`);
- if (agent.avatarOffsetX || agent.avatarOffsetY) agentTransformParts.push(`translate(${agent.avatarOffsetX || 0}%, ${agent.avatarOffsetY || 0}%)`);
- const agentTransformStyle = agentTransformParts.length ? ` style="transform: ${agentTransformParts.join(' ')}"` : '';
- const imageHtml = agent.avatar ? `
` : '';
-
- // Badges
- const badges = [];
- if (agent.toolChoice === 'any') badges.push(' Erzwungen');
- if (agent.allowUserChoice) badges.push(' Rückfragen');
- if (agent.autoCommit === 'mention') badges.push(' Anmerken');
- if (agent.autoCommit === 'enforce') badges.push(' Erzwungen');
- if (agent.autoCommit === 'forbid') badges.push(' Verboten');
- if (agent.useSoul) badges.push(' Seele');
- if (agent.useIdentity) badges.push(' Ich');
- if (agent.useUserInfo) badges.push(' Du');
- if (agent.useMemory) badges.push(' Erinnerungen');
- if (agent.webSearchVersion) {
- const wsYear = agent.webSearchVersion.includes('2025') ? 'Version 2025' : 'v2026';
- badges.push(` Web-Suche`);
- }
-
- if (skills) {
- for (const skill of skills) {
- badges.push(` ${esc(skill.name)}`);
- }
- }
-
- card.innerHTML = `
- ${imageHtml}
-
-
- ${esc(agent.name)}
- ${prettyModelName(agent.model)}
-
- ${badges.length ? `${badges.join('')}` : ''}
-
- `;
-
- return card;
-}
-
-// ── Toast Notifications ──
-
-function showToast(message, type) {
- type = type || 'info';
- var container = document.getElementById('toastContainer');
- if (!container) {
- container = document.createElement('div');
- container.id = 'toastContainer';
- document.body.appendChild(container);
- }
- var toast = document.createElement('div');
- toast.className = 'toast toast-' + type;
- var icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle';
- toast.innerHTML = ' ' + (message || '');
- container.appendChild(toast);
- // Einblenden
- requestAnimationFrame(function() { toast.classList.add('toast-visible'); });
- // Automatisch ausblenden
- var timeout = setTimeout(function() { removeToast(toast); }, 4000);
- toast.addEventListener('click', function() {
- clearTimeout(timeout);
- removeToast(toast);
- });
-}
-
-function removeToast(toast) {
- toast.classList.remove('toast-visible');
- toast.addEventListener('transitionend', function() { toast.remove(); });
-}
-
-// ── DOM Helpers ──
-
-const $ = (sel) => document.querySelector(sel);
-const chatEl = () => $('#chat');
-const metaEl = () => $('#meta');
-
-function prettyModelName(model) {
- if (!model) return '?';
- model = model.split('claude-').join('');
- if (model == 'sonnet') return 'Sonnet';
- if (model == 'sonnet-4-5') return 'Sonnet 4.5';
- if (model == 'sonnet-4-6') return 'Sonnet 4.6';
- if (model == 'opus') return 'Opus';
- if (model == 'opus-4-5') return 'Opus 4.5';
- if (model == 'opus-4-6') return 'Opus 4.6';
- if (model == 'haiku') return 'Haiku';
- if (model == 'haiku-4-5') return 'Haiku 4.5';
- return 'Unbekannt (' + model + ')';
-}
-
-// ── Markdown ──
-
-marked.setOptions({ breaks: true, gfm: true });
-
-function renderMarkdown(text) {
- if (!text) return '';
- var src = text.trim();
-
- // 1) LaTeX-Ausdrücke VOR dem Markdown-Parsing schützen
- // Reihenfolge: zuerst Block ($$...$$), dann Inline ($...$)
- var mathBlocks = [];
- // Block-Math: $$...$$ (auch mehrzeilig)
- src = src.replace(/\$\$([\s\S]+?)\$\$/g, function(m, latex) {
- mathBlocks.push({ latex: latex, displayMode: true });
- return '%%MATH' + (mathBlocks.length - 1) + '%%';
- });
- // Inline-Math: $...$ (nicht-gierig, kein $ am Anfang/Ende)
- src = src.replace(/(?][\s\S]*?<\/pre>|][\s\S]*?<\/code>/gi, function(m) {
- codeBlocks.push(m);
- return '%%CODE' + (codeBlocks.length - 1) + '%%';
- });
-
- // 4a) [Review: AgentName] prefix → Review-Header
- html = html.replace(/\[Review:\s*([^\]]+)\]\s*/g, function(match, agentName) {
- return ' ' + esc(agentName.trim()) + ' ';
- });
-
- // 4a2) [Finalizer: AgentName] prefix → Finalizer-Header
- html = html.replace(/\[Finalizer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
- return ' ' + esc(agentName.trim()) + ' ';
- });
-
- // 4a3) [Preparer: AgentName] prefix → Preparer-Header
- html = html.replace(/\[Preparer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
- return ' ' + esc(agentName.trim()) + ' ';
- });
-
- // 4a4) [Vorbereitet von AgentName]: → Preparer-Forwarded-Header
- html = html.replace(/\[Vorbereitet von\s+([^\]]+)\]:\s*/g, function(match, agentName) {
- return ' Vorbereitet von ' + esc(agentName.trim()) + '';
- });
-
- // 4b) [Review-Feedback von AgentName (Runde X/Y)]: → Review-Feedback-Header
- html = html.replace(/\[Review-Feedback von\s+([^\(]+)\(Runde\s+(\d+)\/(\d+)\)\]:\s*/g, function(match, agentName, round, maxRounds) {
- return ' Feedback von ' + esc(agentName.trim()) + ' Runde ' + esc(round) + '/' + esc(maxRounds) + '';
- });
-
- // 4c) [APPROVED], [FORWARD] und [QUESTION] ausblenden
- html = html.replace(/\[APPROVED\]/g, '');
- html = html.replace(/\[FORWARD\]\s*/g, '');
- html = html.replace(/\[QUESTION\]\s*/g, '');
-
- // 4d) [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();
-}
-
-// ── Choice Bubbles (ask_user_choice) ──
-
-// Wird live aus dem SSE-Stream aufgerufen
-function addChoiceBubble(toolCallId, question, choices, streamed = true) {
- // Guard: choices muss ein Array sein
- if (!Array.isArray(choices)) choices = [String(choices)];
-
- const div = document.createElement('div');
- div.className = 'msg choice-bubble';
- if (streamed) div.classList.add('streamed');
- div.dataset.toolCallId = toolCallId;
-
- const questionDiv = document.createElement('div');
- questionDiv.className = 'choice-question';
- questionDiv.innerHTML = renderMarkdown(question);
- parseFluentEmojis(questionDiv);
- div.appendChild(questionDiv);
-
- const buttonsDiv = document.createElement('div');
- buttonsDiv.className = 'choice-buttons';
- for (const choice of choices) {
- const btn = document.createElement('button');
- btn.className = 'choice-btn';
- btn.textContent = choice;
- btn.addEventListener('click', () => handleChoiceClick(toolCallId, choice, div));
- buttonsDiv.appendChild(btn);
- }
- div.appendChild(buttonsDiv);
-
- chatEl().appendChild(div);
- scrollDown();
- return div;
-}
-
-// Wird für History-Rendering aufgerufen (bereits beantwortete Choices)
-function addChoiceBubbleStatic(toolCallId, question, choices, selectedChoice, timestamp, messageId) {
- // Guard: choices muss ein Array sein
- if (!Array.isArray(choices)) choices = [String(choices)];
-
- const div = document.createElement('div');
- div.className = 'msg choice-bubble choice-answered';
- div.dataset.toolCallId = toolCallId;
- if (messageId) div.dataset.messageId = messageId;
-
- const questionDiv = document.createElement('div');
- questionDiv.className = 'choice-question';
- questionDiv.innerHTML = renderMarkdown(question);
- parseFluentEmojis(questionDiv);
- div.appendChild(questionDiv);
-
- const buttonsDiv = document.createElement('div');
- buttonsDiv.className = 'choice-buttons';
- for (const choice of choices) {
- const btn = document.createElement('button');
- btn.className = 'choice-btn';
- btn.disabled = true;
- btn.textContent = choice;
- if (choice === selectedChoice) {
- btn.classList.add('choice-selected');
- } else {
- btn.classList.add('choice-unselected');
- }
- buttonsDiv.appendChild(btn);
- }
- div.appendChild(buttonsDiv);
-
- if (timestamp) _addTimestamp(div, timestamp);
- chatEl().appendChild(div);
- scrollDown();
- return div;
-}
-
-// handleChoiceClick ist in chat.js definiert (benötigt Stream-Infrastruktur)
-
-// ── Review Bubbles ──
-
-function addReviewBubbleStatic(text, timestamp, messageId, isMarked, deletedOn) {
- const div = document.createElement('div');
- div.className = 'msg review-bubble markdown' + (isMarked ? ' marked' : '');
- if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
- div.dataset.role = 'assistant';
- div.dataset.timestamp = timestamp || '';
- if (messageId) div.dataset.messageId = messageId;
- div.innerHTML = renderMarkdown(text);
- parseFluentEmojis(div);
- _addTimestamp(div, timestamp);
- _addReplyButton(div);
- _addDeleteButton(div, messageId, deletedOn);
- _addMarkButton(div, messageId, isMarked);
- _addShareButton(div, messageId);
- chatEl().appendChild(div);
- scrollDown();
- return div;
-}
-
-let _reviewBubble = null;
-let _reviewTextEl = null;
-let _reviewText = '';
-
-function addReviewBubble(type, data, agentType) {
- _reviewBubble = document.createElement('div');
- _reviewBubble.className = 'msg review-bubble streamed';
- if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble');
- if (agentType === 'preparer') _reviewBubble.classList.add('preparer-bubble');
- const header = document.createElement('div');
- header.className = 'review-header';
- if (agentType === 'preparer') {
- header.innerHTML = ' ' + esc(data.agentName || 'Vorbereiter-Agent') + '';
- } else if (agentType === 'finalizer') {
- header.innerHTML = ' ' + esc(data.agentName || 'Finalizer-Agent') + '';
- } else {
- header.innerHTML = ' ' + esc(data.agentName || 'Review-Agent') + ' Runde ' + data.round + '/' + data.maxRounds + '';
- }
- _reviewBubble.appendChild(header);
- _reviewTextEl = document.createElement('div');
- _reviewTextEl.className = 'review-content markdown';
- _reviewBubble.appendChild(_reviewTextEl);
- _reviewText = '';
- chatEl().appendChild(_reviewBubble);
- scrollDown();
-}
-
-function appendToReviewText(text) {
- if (!_reviewBubble) return;
- _reviewText += text;
- _reviewTextEl.innerHTML = renderMarkdown(_reviewText);
- scrollDown();
-}
-
-function addReviewToolUse(data) {
- if (!_reviewBubble) return;
- const toolDiv = document.createElement('div');
- toolDiv.className = 'review-tool-use';
- toolDiv.innerHTML = ' ' + esc(data.toolName) + '';
- _reviewBubble.appendChild(toolDiv);
- scrollDown();
-}
-
-function finalizeReviewBubble(type, data, agentType) {
- if (!_reviewBubble) return;
- const status = document.createElement('div');
- status.className = 'review-status';
- if (agentType === 'preparer' && type === 'forwarded') {
- status.className += ' review-approved';
- status.innerHTML = ' Weitergeleitet an Session-Agent';
- } else if (agentType === 'preparer' && type === 'query') {
- status.className += ' review-rejected';
- status.innerHTML = ' Rückfrage an User';
- } else if (agentType === 'finalizer' || type === 'done') {
- status.className += ' review-approved';
- status.innerHTML = ' Abgeschlossen';
- } else if (type === 'approved') {
- status.className += ' review-approved';
- status.innerHTML = ' Approved';
- } else {
- status.className += ' review-rejected';
- status.innerHTML = ' Feedback – Worker korrigiert...';
- }
- _reviewBubble.appendChild(status);
- _reviewBubble = null;
- _reviewTextEl = null;
- _reviewText = '';
- 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 pauseStreamBubble() {
- // Cursor entfernen, aber Bubble + Text behalten (z.B. bei Web-Search-Pause)
- if (_streamDiv) {
- const cursor = _streamDiv.querySelector('.cursor');
- if (cursor) cursor.remove();
- }
-}
-
-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 + web_search anzeigen
- if (toolName === 'web_search') return true;
- 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, sources) {
- 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;
-
- // Web-Search Sources – immer sichtbar, kein Toggle
- if (name === 'web_search' && sources && sources.length > 0) {
- const sourcesList = document.createElement('div');
- sourcesList.className = 'web-search-sources';
- sources.forEach(function(s) {
- const link = document.createElement('a');
- link.href = s.url;
- link.target = '_blank';
- link.rel = 'noopener noreferrer';
- link.className = 'web-search-source';
- var domain = '';
- try { domain = new URL(s.url).hostname.replace('www.', ''); } catch(e) { domain = s.url; }
- link.innerHTML = ' ' + esc(domain) + '' +
- (s.title ? '' + esc(s.title) + '' : '');
- sourcesList.appendChild(link);
- });
- div.appendChild(sourcesList);
- }
-
- 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, data.sources);
- scrollDown();
-}
-
-function addToolBoxStatic(name, input, result, isError, sources) {
- const div = document.createElement('div');
- div.className = 'tool-box';
- div.dataset.toolName = name;
- renderToolBoxContent(div, name, input, result, isError, sources);
- 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, typeof _planModeActive !== 'undefined' && _planModeActive);
- 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);
- }
-});