- 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>
173 lines
7.0 KiB
JavaScript
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++;
|
|
}
|
|
}
|
|
}
|