Files
Nakano Kenji 87aff43e63 feat: add prompt tools and Anima workflow scheme
- Add Mustache vars/render + Text Input helpers
- Add JSON Parse + Prompt JSON Unpack (typed outputs + vars)
- Add Markdown Preview (marked + DOMPurify)
- Add Anima prompt/scheme docs + example workflow
- Add GitHub Wiki auto-publish workflow (docs/wiki -> wiki)
2026-02-01 22:50:16 +08:00

188 lines
5.4 KiB
JavaScript

import { app } from "../../scripts/app.js";
const EXT_NAME = "ComfyUI.SimpleChat.MarkdownPreview";
const STYLE_ID = "simplechat-markdown-preview-style";
function chainCallback(object, property, callback) {
if (!object) return;
if (property in object && object[property]) {
const callback_orig = object[property];
object[property] = function () {
const r = callback_orig.apply(this, arguments);
return callback.apply(this, arguments) ?? r;
};
} else {
object[property] = callback;
}
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.type = "text/css";
style.innerHTML = `
.simplechat-md-modal {
position: fixed;
right: 18px;
top: 18px;
width: min(720px, calc(100vw - 36px));
height: min(70vh, 760px);
background: var(--comfy-menu-bg);
color: var(--fg-color);
border: 1px solid var(--border-color);
border-radius: 12px;
z-index: 9999;
box-shadow: 0 16px 40px rgba(0,0,0,0.35);
overflow: hidden;
display: flex;
flex-direction: column;
}
.simplechat-md-modal header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
background: rgba(0,0,0,0.08);
font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.simplechat-md-modal header .title {
opacity: 0.9;
}
.simplechat-md-modal header .actions {
display: flex;
gap: 8px;
align-items: center;
}
.simplechat-md-modal header button {
border: 1px solid var(--border-color);
background: transparent;
color: var(--fg-color);
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
}
.simplechat-md-modal header button:hover {
background: rgba(255,255,255,0.06);
}
.simplechat-md-body {
padding: 12px;
overflow: auto;
}
.simplechat-md-body pre, .simplechat-md-body code {
background: rgba(0,0,0,0.16);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 8px 10px;
overflow: auto;
}
.simplechat-md-body a {
color: #ffd166;
}
.simplechat-md-body a:visited {
color: #f4a261;
}
`;
document.head.appendChild(style);
}
function loadScript(url) {
return new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${url}"]`);
if (existing) return resolve(true);
const el = document.createElement("script");
el.async = true;
el.type = "text/javascript";
el.src = url;
el.addEventListener("load", () => resolve(true));
el.addEventListener("error", () => reject(new Error(`Failed to load ${url}`)));
document.body.appendChild(el);
});
}
let _libsPromise = null;
async function ensureMarkdownLibs() {
if (window.marked && window.DOMPurify) return;
if (_libsPromise) return _libsPromise;
const markedUrl = new URL("./vendor/marked.min.js", import.meta.url).href;
const purifyUrl = new URL("./vendor/purify.min.js", import.meta.url).href;
_libsPromise = (async () => {
await loadScript(markedUrl);
await loadScript(purifyUrl);
})();
return _libsPromise;
}
function escapeHtml(text) {
return (text || "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
let modalEl = null;
function showMarkdown(rawText, title = "Markdown Preview") {
ensureStyles();
if (!modalEl) {
modalEl = document.createElement("div");
modalEl.className = "simplechat-md-modal";
modalEl.innerHTML = `
<header>
<div class="title"></div>
<div class="actions">
<button data-action="copy">Copy</button>
<button data-action="close">Close</button>
</div>
</header>
<div class="simplechat-md-body"></div>
`;
document.body.appendChild(modalEl);
modalEl.querySelector('button[data-action="close"]').addEventListener("click", () => {
modalEl.remove();
modalEl = null;
});
modalEl.querySelector('button[data-action="copy"]').addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(modalEl.__rawText || "");
} catch (e) {
console.warn("Clipboard copy failed", e);
}
});
}
modalEl.__rawText = rawText || "";
modalEl.querySelector(".title").textContent = title;
const body = modalEl.querySelector(".simplechat-md-body");
const hasLibs = window.marked && window.DOMPurify;
if (hasLibs) {
const html = window.DOMPurify.sanitize(window.marked.parse(rawText || ""));
body.innerHTML = html;
} else {
body.innerHTML = `<pre>${escapeHtml(rawText || "")}</pre>`;
}
}
app.registerExtension({
name: EXT_NAME,
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== "SimpleChatMarkdownPreview") return;
// Ensure libraries are loaded once; if it fails, we fall back to plain text
ensureMarkdownLibs().catch((e) => console.warn("[SimpleChat] markdown libs load failed:", e));
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
const text = (message?.text && message.text.length) ? message.text[0] : "";
showMarkdown(text, "SimpleChat Markdown Preview");
};
},
});