Files
gott/wwwroot/js/markdown.js
T
MelekiandClaude Opus 4.6 3b068a7866 feat: MCP-Server für remote_execute + Inline-Choice-UI
- Python MCP-Server (stdio, JSON-RPC 2.0) für remote_execute via Claude CLI
- Interner API-Endpoint /api/internal/remote-execute mit Token-Auth
- MCP-Config wird automatisch generiert wenn Remote-Verbindung vorhanden
- Auth-Middleware: /api/internal/ von Auth ausgenommen
- Frontend: :::ASK_USER_CHOICE::: Blöcke als interaktive Buttons rendern
- Streaming: Unvollständige Choice-Blöcke werden während des Streams versteckt
- tool-ui.js: MCP-Tool-Namen generisch normalisieren (mcp__server__tool → tool)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:00:50 +01:00

173 lines
7.0 KiB
JavaScript

// ── Markdown Rendering ──
marked.setOptions({ breaks: true, gfm: true });
function renderMarkdown(text) {
if (!text) return '';
var src = text.trim();
// 0) :::ASK_USER_CHOICE::: Blöcke erkennen und als Platzhalter speichern (vor Markdown-Parsing)
var choiceBlocks = [];
src = src.replace(/:::ASK_USER_CHOICE:::\s*\n?([\s\S]*?)\n?\s*:::END_ASK_USER_CHOICE:::/g, function(match, jsonStr) {
try {
var data = JSON.parse(jsonStr.trim());
choiceBlocks.push(data);
return '%%CHOICE' + (choiceBlocks.length - 1) + '%%';
} catch(e) {
return match; // JSON-Fehler → als Text belassen
}
});
// 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], [QUESTION] und [FEEDBACK] ausblenden
html = html.replace(/\[APPROVED\]/g, '');
html = html.replace(/\[FORWARD\]\s*/g, '');
html = html.replace(/\[QUESTION\]\s*/g, '');
html = html.replace(/\[FEEDBACK\]\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>';
}
});
// 8) Choice-Platzhalter durch interaktive Buttons ersetzen
html = html.replace(/%%CHOICE(\d+)%%/g, function(m, idx) {
var data = choiceBlocks[parseInt(idx)];
var q = data.question || '';
var choices = data.choices || [];
var jsonAttr = esc(JSON.stringify(data));
var out = '<div class="inline-choice-block" data-choice-json="' + jsonAttr + '">';
out += '<div class="choice-question">' + esc(q) + '</div>';
out += '<div class="choice-buttons">';
for (var i = 0; i < choices.length; i++) {
out += '<button class="choice-btn inline-choice-btn" data-choice="' + esc(choices[i]) + '">' + esc(choices[i]) + '</button>';
}
out += '</div></div>';
return out;
});
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++;
}
}
}