Die monolithische ui.js (1534 Zeilen) zusammen mit chat.js (612) und history.js (626) in fokussierte Module aufgeteilt: Neue Dateien aus ui.js: - ui-helpers.js (126) - DOM-Utilities, $, esc, truncate, scrollDown, Toast - markdown.js (143) - renderMarkdown, wrapKreaGalleries, LaTeX/Code-Schutz - cards.js (90) - buildSessionCard, buildAgentCard - msg-actions.js (208) - Delete, Mark, Share, Reply-Button, Timestamp - tool-ui.js (332) - Tool-Boxes, Diffs, Todo-Liste, Visibility - modals.js (190) - System-Prompt, Command-Output, Krea-Image Modals - stream-ui.js (124) - Stream-Bubble, Review-Streaming - bubbles.js (312) - User/Assistant/Error/Warning/Choice/Review Bubbles Neue Dateien aus chat.js: - reply.js (54) - Reply/Quote-Handling mit pendingReply State - files.js (110) - File-Upload, Workspace-Refs mit pendingFiles/Refs State Neue Datei aus history.js: - archive.js (173) - Archiv-Loading mit loadedArchiveCount State Verbleibend: - chat.js (448) - Stream-Events, sendMessage, handleChoiceClick, Plan-Mode - history.js (464) - History laden, Polling, Message-Rendering Kein Code verändert, nur verschoben. Alle 94 Funktionen verifiziert. Globale Variablen in die Module verschoben, wo sie primär genutzt werden.
144 lines
5.7 KiB
JavaScript
144 lines
5.7 KiB
JavaScript
// ── Markdown Rendering ──
|
|
|
|
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(/(?<!\$)\$(?!\$)(.+?)(?<!\$)\$(?!\$)/g, function(m, latex) {
|
|
mathBlocks.push({ latex: latex, displayMode: false });
|
|
return '%%MATH' + (mathBlocks.length - 1) + '%%';
|
|
});
|
|
|
|
// 2) Markdown parsen
|
|
var html = marked.parse(src);
|
|
|
|
// 3) Code-Blöcke schützen (damit Emojis/Krea dort nicht greifen)
|
|
var codeBlocks = [];
|
|
html = html.replace(/<pre[\s>][\s\S]*?<\/pre>|<code[\s>][\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 '<span class="review-inline-header"><i class="fa fa-check-circle-o"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
|
|
});
|
|
|
|
// 4a2) [Finalizer: AgentName] prefix → Finalizer-Header
|
|
html = html.replace(/\[Finalizer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
|
|
return '<span class="review-inline-header finalizer-inline-header"><i class="fa fa-flag-checkered"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
|
|
});
|
|
|
|
// 4a3) [Preparer: AgentName] prefix → Preparer-Header
|
|
html = html.replace(/\[Preparer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
|
|
return '<span class="review-inline-header preparer-inline-header"><i class="fa fa-filter"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
|
|
});
|
|
|
|
// 4a4) [Vorbereitet von AgentName]: → Preparer-Forwarded-Header
|
|
html = html.replace(/\[Vorbereitet von\s+([^\]]+)\]:\s*/g, function(match, agentName) {
|
|
return '<div class="preparer-forwarded-header"><i class="fa fa-forward"></i> <strong>Vorbereitet von ' + esc(agentName.trim()) + '</strong></div>';
|
|
});
|
|
|
|
// 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 '<div class="review-inline-feedback"><i class="fa fa-repeat"></i> <strong>Feedback von ' + esc(agentName.trim()) + '</strong> <span class="review-round">Runde ' + esc(round) + '/' + esc(maxRounds) + '</span></div>';
|
|
});
|
|
|
|
// 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 '<span class="krea-ref" data-filename="' + safeFn + '" data-fullurl="' + fullUrl + '" title="Klicken für Vollbild">'
|
|
+ '<img class="krea-thumb" src="' + thumbUrl + '" alt="' + safeFn + '" loading="lazy">'
|
|
+ '</span>';
|
|
});
|
|
|
|
// 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 '<code class="katex-error">' + esc(block.latex) + '</code>';
|
|
}
|
|
});
|
|
|
|
return html;
|
|
}
|
|
|
|
function wrapKreaGalleries(container) {
|
|
// Findet <p>-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 <p> nur krea-refs enthält (ggf. mit Whitespace)
|
|
if (el.tagName === 'P' && el.querySelector('.krea-ref') && el.textContent.trim() === '') {
|
|
// Sammle aufeinanderfolgende krea-only <p>-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++;
|
|
}
|
|
}
|
|
}
|