dsh-comfyui 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +4 -6
- package/README.md +4 -6
- package/client/client.js +329 -174
- package/client/client.js.map +1 -1
- package/lib/comfyui.d.ts +14 -3
- package/lib/comfyui.js +31 -10
- package/lib/skill.js +27 -3
- package/lib/tools.d.ts +21 -4
- package/lib/tools.js +260 -153
- package/package.json +7 -5
- package/lib/templates.d.ts +0 -36
- package/lib/templates.js +0 -84
package/client/client.js
CHANGED
|
@@ -52,6 +52,11 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
cardWorkflow: "工作流",
|
|
53
53
|
cardMode: "模式",
|
|
54
54
|
cardListed: "已列出 {runs} 个运行工作流,{graphs} 个图工作流",
|
|
55
|
+
cardHandWritten: "手写工作流",
|
|
56
|
+
cardWaiting: "等待 ComfyUI…",
|
|
57
|
+
cardQueued: "排队中(前面还有 {n} 个)",
|
|
58
|
+
cardStep: "步数 {value}/{max}",
|
|
59
|
+
cardInReply: "结果图见对话回复,文件:",
|
|
55
60
|
panelTitle: "ComfyUI 面板",
|
|
56
61
|
panelReset: "复位位置与大小",
|
|
57
62
|
close: "关闭",
|
|
@@ -317,6 +322,11 @@ window.__ModuleLoader__.load({
|
|
|
317
322
|
cardWorkflow: "Workflow",
|
|
318
323
|
cardMode: "Mode",
|
|
319
324
|
cardListed: "Listed {runs} runnable workflows, {graphs} graph workflows",
|
|
325
|
+
cardHandWritten: "Hand-written workflow",
|
|
326
|
+
cardWaiting: "Waiting for ComfyUI…",
|
|
327
|
+
cardQueued: "Queued ({n} ahead)",
|
|
328
|
+
cardStep: "Step {value}/{max}",
|
|
329
|
+
cardInReply: "Results are shown in the reply. Files:",
|
|
320
330
|
panelTitle: "ComfyUI Panel",
|
|
321
331
|
panelReset: "Reset position & size",
|
|
322
332
|
close: "Close",
|
|
@@ -572,90 +582,15 @@ window.__ModuleLoader__.load({
|
|
|
572
582
|
};
|
|
573
583
|
}
|
|
574
584
|
//#endregion
|
|
575
|
-
//#region src/client/lightbox.tsx
|
|
576
|
-
/**
|
|
577
|
-
* Shared lightbox: opens a generated image full-screen with prev/next
|
|
578
|
-
* navigation, keyboard support, and a download pill. Used by both the panel
|
|
579
|
-
* (assets/queue previews) and the tool card (result images).
|
|
580
|
-
*/
|
|
581
|
-
/** Full-screen media overlay with prev/next navigation. */
|
|
582
|
-
function Lightbox({ t, images, kinds, index, onClose, onIndex }) {
|
|
583
|
-
const count = images.length;
|
|
584
|
-
(0, react.useEffect)(() => {
|
|
585
|
-
const onKey = (event) => {
|
|
586
|
-
if (event.key === "Escape") onClose();
|
|
587
|
-
else if (event.key === "ArrowLeft") onIndex((index - 1 + count) % count);
|
|
588
|
-
else if (event.key === "ArrowRight") onIndex((index + 1) % count);
|
|
589
|
-
};
|
|
590
|
-
document.addEventListener("keydown", onKey);
|
|
591
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
592
|
-
}, [
|
|
593
|
-
index,
|
|
594
|
-
count,
|
|
595
|
-
onClose,
|
|
596
|
-
onIndex
|
|
597
|
-
]);
|
|
598
|
-
const src = images[index];
|
|
599
|
-
if (src === void 0) return null;
|
|
600
|
-
const kind = kinds?.[index] ?? "image";
|
|
601
|
-
const media = kind === "video" ? (0, react.createElement)("video", {
|
|
602
|
-
className: "dsc-lightbox-media",
|
|
603
|
-
src,
|
|
604
|
-
controls: true,
|
|
605
|
-
autoPlay: true
|
|
606
|
-
}) : kind === "audio" ? (0, react.createElement)("audio", {
|
|
607
|
-
className: "dsc-lightbox-media",
|
|
608
|
-
src,
|
|
609
|
-
controls: true,
|
|
610
|
-
autoPlay: true
|
|
611
|
-
}) : kind === "image" ? (0, react.createElement)("img", {
|
|
612
|
-
className: "dsc-lightbox-img",
|
|
613
|
-
src,
|
|
614
|
-
alt: ""
|
|
615
|
-
}) : (0, react.createElement)("div", { className: "dsc-lightbox-media" }, src);
|
|
616
|
-
return (0, react.createElement)("div", {
|
|
617
|
-
className: "dsc-lightbox",
|
|
618
|
-
onClick: onClose
|
|
619
|
-
}, (0, react.createElement)("div", {
|
|
620
|
-
className: "dsc-lightbox-body",
|
|
621
|
-
onClick: (event) => event.stopPropagation()
|
|
622
|
-
}, (0, react.createElement)("button", {
|
|
623
|
-
className: "dsc-lightbox-close",
|
|
624
|
-
"aria-label": t("close"),
|
|
625
|
-
onClick: (event) => {
|
|
626
|
-
event.stopPropagation();
|
|
627
|
-
onClose();
|
|
628
|
-
}
|
|
629
|
-
}, "✕"), count > 1 ? (0, react.createElement)("button", {
|
|
630
|
-
className: "dsc-lightbox-nav dsc-lightbox-nav--prev",
|
|
631
|
-
"aria-label": t("lbPrev"),
|
|
632
|
-
onClick: (event) => {
|
|
633
|
-
event.stopPropagation();
|
|
634
|
-
onIndex((index - 1 + count) % count);
|
|
635
|
-
}
|
|
636
|
-
}, "‹") : null, media, (0, react.createElement)("div", { className: "dsc-lightbox-meta" }, (0, react.createElement)("span", null, `${index + 1} / ${count}`), (0, react.createElement)("a", {
|
|
637
|
-
className: "dsc-lightbox-download",
|
|
638
|
-
href: src,
|
|
639
|
-
download: "",
|
|
640
|
-
target: "_blank",
|
|
641
|
-
rel: "noreferrer"
|
|
642
|
-
}, t("cardDownload"))), count > 1 ? (0, react.createElement)("button", {
|
|
643
|
-
className: "dsc-lightbox-nav dsc-lightbox-nav--next",
|
|
644
|
-
"aria-label": t("lbNext"),
|
|
645
|
-
onClick: (event) => {
|
|
646
|
-
event.stopPropagation();
|
|
647
|
-
onIndex((index + 1) % count);
|
|
648
|
-
}
|
|
649
|
-
}, "›") : null));
|
|
650
|
-
}
|
|
651
|
-
//#endregion
|
|
652
585
|
//#region src/client/card.tsx
|
|
653
586
|
/**
|
|
654
587
|
* Tool cards for comfyui_run and comfyui_workflow (tool.call.toolview, keys
|
|
655
|
-
* 'comfyui_run' / 'comfyui_workflow').
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
588
|
+
* 'comfyui_run' / 'comfyui_workflow'). Running calls show the workflow name
|
|
589
|
+
* and live progress (polled from /comfyui/queue); settled calls show status
|
|
590
|
+
* and the output files as links. The media itself is NOT rendered here: the
|
|
591
|
+
* model pastes it into its reply (see CLAUDE.md contract 18) — dsh 0.2
|
|
592
|
+
* collapses finished tool rows and PTC mode shows no card at all, so an image
|
|
593
|
+
* wall here was either invisible or a duplicate of the reply.
|
|
659
594
|
*/
|
|
660
595
|
/** Discriminate the settled result node from a running call. */
|
|
661
596
|
function isResultNode(block) {
|
|
@@ -669,73 +604,96 @@ window.__ModuleLoader__.load({
|
|
|
669
604
|
return {};
|
|
670
605
|
}
|
|
671
606
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
function aspectLabel(width, height) {
|
|
680
|
-
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
|
|
681
|
-
const g = gcd(width, height);
|
|
682
|
-
const rw = width / g;
|
|
683
|
-
const rh = height / g;
|
|
684
|
-
return rw <= 32 && rh <= 32 ? `${rw}:${rh}` : "";
|
|
685
|
-
}
|
|
686
|
-
function MediaItem({ item, t, onOpen }) {
|
|
687
|
-
const [size, setSize] = (0, react.useState)(null);
|
|
688
|
-
const [failed, setFailed] = (0, react.useState)(false);
|
|
689
|
-
const media = item.kind === "video" ? (0, react.createElement)("video", {
|
|
690
|
-
src: item.url,
|
|
691
|
-
controls: true,
|
|
692
|
-
preload: "metadata"
|
|
693
|
-
}) : item.kind === "audio" ? (0, react.createElement)("audio", {
|
|
694
|
-
src: item.url,
|
|
695
|
-
controls: true,
|
|
696
|
-
preload: "metadata"
|
|
697
|
-
}) : item.kind === "image" ? failed ? (0, react.createElement)("span", { className: "dsc-media-other" }, `${item.filename}(${t("cardLoadFailed")})`) : (0, react.createElement)("img", {
|
|
698
|
-
src: item.url,
|
|
699
|
-
alt: item.filename,
|
|
700
|
-
loading: "lazy",
|
|
701
|
-
className: "dsc-media-img dsc-media-img--clickable",
|
|
702
|
-
onClick: onOpen,
|
|
703
|
-
onLoad: (event) => {
|
|
704
|
-
const width = event.target.naturalWidth;
|
|
705
|
-
const height = event.target.naturalHeight;
|
|
706
|
-
if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) setSize({
|
|
707
|
-
width,
|
|
708
|
-
height
|
|
709
|
-
});
|
|
710
|
-
},
|
|
711
|
-
onError: () => setFailed(true)
|
|
712
|
-
}) : (0, react.createElement)("span", { className: "dsc-media-other" }, item.filename);
|
|
713
|
-
const ratio = size === null ? null : aspectLabel(size.width, size.height);
|
|
714
|
-
return (0, react.createElement)("div", { className: "dsc-media" }, media, (0, react.createElement)("div", { className: "dsc-media-meta" }, size !== null ? (0, react.createElement)("span", { className: "dsc-media-size" }, `${size.width}×${size.height}${ratio !== "" ? ` · ${ratio}` : ""}`) : null, (0, react.createElement)("a", {
|
|
607
|
+
/** The output files as links (the images themselves are in the model's reply). */
|
|
608
|
+
function MediaLinks({ media, t }) {
|
|
609
|
+
if (media.length === 0) return (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty"));
|
|
610
|
+
return (0, react.createElement)("div", { className: "dsc-files" }, (0, react.createElement)("span", { className: "dsc-meta" }, t("cardInReply")), media.map((item) => (0, react.createElement)("a", {
|
|
611
|
+
key: `${item.node}-${item.index}-${item.filename}`,
|
|
612
|
+
className: "dsc-file",
|
|
715
613
|
href: item.url,
|
|
716
|
-
download: item.filename,
|
|
717
614
|
target: "_blank",
|
|
718
|
-
rel: "noreferrer"
|
|
719
|
-
|
|
615
|
+
rel: "noreferrer",
|
|
616
|
+
title: item.filename
|
|
617
|
+
}, item.filename)));
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Poll /comfyui/queue (every 2 s while `active`) for one prompt's queue
|
|
621
|
+
* position and sampler progress. A running sync call does not know its
|
|
622
|
+
* prompt id yet, so without one this follows the first prompt of ours that
|
|
623
|
+
* is running (else pending) — sync calls block, so that is the call's own.
|
|
624
|
+
*/
|
|
625
|
+
function useQueueState(promptId, active) {
|
|
626
|
+
const [state, setState] = (0, react.useState)(null);
|
|
627
|
+
(0, react.useEffect)(() => {
|
|
628
|
+
if (!active) return void 0;
|
|
629
|
+
let stopped = false;
|
|
630
|
+
let timer;
|
|
631
|
+
const poll = async () => {
|
|
632
|
+
try {
|
|
633
|
+
const data = await (await fetch("/comfyui/queue", { headers: { accept: "application/json" } })).json();
|
|
634
|
+
if (stopped) return;
|
|
635
|
+
const running = data.running ?? [];
|
|
636
|
+
const pending = data.pending ?? [];
|
|
637
|
+
const match = (entry) => promptId !== void 0 ? entry.promptId === promptId : entry.ours;
|
|
638
|
+
const run = running.find(match);
|
|
639
|
+
const waitIndex = pending.findIndex(match);
|
|
640
|
+
if (run !== void 0) setState({
|
|
641
|
+
phase: "running",
|
|
642
|
+
value: run.progress?.value ?? 0,
|
|
643
|
+
max: run.progress?.max ?? 0
|
|
644
|
+
});
|
|
645
|
+
else if (waitIndex >= 0) setState({
|
|
646
|
+
phase: "pending",
|
|
647
|
+
ahead: running.length + waitIndex
|
|
648
|
+
});
|
|
649
|
+
else setState(null);
|
|
650
|
+
} catch {}
|
|
651
|
+
if (!stopped) timer = window.setTimeout(() => {
|
|
652
|
+
poll();
|
|
653
|
+
}, 2e3);
|
|
654
|
+
};
|
|
655
|
+
poll();
|
|
656
|
+
return () => {
|
|
657
|
+
stopped = true;
|
|
658
|
+
if (timer !== void 0) window.clearTimeout(timer);
|
|
659
|
+
};
|
|
660
|
+
}, [promptId, active]);
|
|
661
|
+
return state;
|
|
662
|
+
}
|
|
663
|
+
function ProgressLine({ state, t }) {
|
|
664
|
+
if (state === null) return (0, react.createElement)("div", { className: "dsc-meta" }, t("cardWaiting"));
|
|
665
|
+
if (state.phase === "pending") return (0, react.createElement)("div", { className: "dsc-meta" }, t("cardQueued", { n: state.ahead }));
|
|
666
|
+
const percent = state.max > 0 ? Math.min(100, Math.round(state.value / state.max * 100)) : 0;
|
|
667
|
+
return (0, react.createElement)("div", { className: "dsc-progress" }, (0, react.createElement)("div", { className: "dsc-progress-track" }, (0, react.createElement)("div", {
|
|
668
|
+
className: "dsc-progress-fill",
|
|
669
|
+
style: { width: `${percent}%` }
|
|
670
|
+
})), (0, react.createElement)("span", { className: "dsc-meta" }, state.max > 0 ? t("cardStep", {
|
|
671
|
+
value: state.value,
|
|
672
|
+
max: state.max
|
|
673
|
+
}) : t("cardWaiting")));
|
|
674
|
+
}
|
|
675
|
+
/** A saved workflow's name by id, for the running card of comfyui_workflow run. */
|
|
676
|
+
function useWorkflowName(id) {
|
|
677
|
+
const [name, setName] = (0, react.useState)(void 0);
|
|
678
|
+
(0, react.useEffect)(() => {
|
|
679
|
+
if (id === void 0) return void 0;
|
|
680
|
+
let stopped = false;
|
|
681
|
+
fetch("/comfyui/workflows", { headers: { accept: "application/json" } }).then(async (response) => await response.json()).then((data) => {
|
|
682
|
+
const found = data.workflows?.find((workflow) => workflow.id === id);
|
|
683
|
+
if (!stopped && found !== void 0) setName(found.name);
|
|
684
|
+
}).catch(() => void 0);
|
|
685
|
+
return () => {
|
|
686
|
+
stopped = true;
|
|
687
|
+
};
|
|
688
|
+
}, [id]);
|
|
689
|
+
return name;
|
|
720
690
|
}
|
|
721
691
|
function ResultCard({ title, result, t }) {
|
|
722
692
|
const failed = result.status === "interrupted";
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
key: `${item.node}-${item.index}`,
|
|
728
|
-
item,
|
|
729
|
-
t,
|
|
730
|
-
onOpen: () => setLightbox(index)
|
|
731
|
-
}))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty")), lightbox !== null ? (0, react.createElement)(Lightbox, {
|
|
732
|
-
t,
|
|
733
|
-
images: urls,
|
|
734
|
-
kinds,
|
|
735
|
-
index: lightbox,
|
|
736
|
-
onClose: () => setLightbox(null),
|
|
737
|
-
onIndex: setLightbox
|
|
738
|
-
}) : null);
|
|
693
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, title !== "" ? (0, react.createElement)("span", { className: "dsc-badge" }, title) : null, (0, react.createElement)("span", { className: failed ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, failed ? t("cardInterrupted") : result.status), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardPrompt")} ${result.promptId}`), (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardElapsed")} ${result.elapsedMs} ms`)), (0, react.createElement)("div", { className: "dsc-meta" }, result.summary), (0, react.createElement)(MediaLinks, {
|
|
694
|
+
media: result.media,
|
|
695
|
+
t
|
|
696
|
+
}));
|
|
739
697
|
}
|
|
740
698
|
/** Transient-failure retry budget for the jobs/media poll (backoff steps). */
|
|
741
699
|
const POLL_MAX_FAILURES = 20;
|
|
@@ -745,7 +703,7 @@ window.__ModuleLoader__.load({
|
|
|
745
703
|
const POLL_UNKNOWN_GRACE = 100;
|
|
746
704
|
function BackgroundCard({ label, promptId, t }) {
|
|
747
705
|
const [result, setResult] = (0, react.useState)(null);
|
|
748
|
-
const
|
|
706
|
+
const queue = useQueueState(promptId, result === null);
|
|
749
707
|
(0, react.useEffect)(() => {
|
|
750
708
|
let stopped = false;
|
|
751
709
|
let timer;
|
|
@@ -833,23 +791,15 @@ window.__ModuleLoader__.load({
|
|
|
833
791
|
}, [promptId, t]);
|
|
834
792
|
if (result !== null && (result.status === "completed" || result.status === "failed")) {
|
|
835
793
|
const media = result.media ?? [];
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
item,
|
|
841
|
-
t,
|
|
842
|
-
onOpen: () => setLightbox(index)
|
|
843
|
-
}))) : (0, react.createElement)("div", { className: "dsc-meta" }, t("cardEmpty")), lightbox !== null ? (0, react.createElement)(Lightbox, {
|
|
844
|
-
t,
|
|
845
|
-
images: urls,
|
|
846
|
-
kinds,
|
|
847
|
-
index: lightbox,
|
|
848
|
-
onClose: () => setLightbox(null),
|
|
849
|
-
onIndex: setLightbox
|
|
850
|
-
}) : null);
|
|
794
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: result.status === "failed" ? "dsc-badge dsc-badge--err" : "dsc-badge dsc-badge--ok" }, result.status === "failed" ? t("cardFailed") : t("cardBackgroundDone")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), result.status !== "failed" && result.recovered === true ? (0, react.createElement)("div", { className: "dsc-meta" }, t("cardRecovered")) : null, result.status === "failed" ? (0, react.createElement)("div", { className: "dsc-meta dsc-job-error" }, result.error ?? t("cardFailed")) : (0, react.createElement)(MediaLinks, {
|
|
795
|
+
media,
|
|
796
|
+
t
|
|
797
|
+
}));
|
|
851
798
|
}
|
|
852
|
-
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardBackground")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), (0, react.createElement)(
|
|
799
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardBackground")), (0, react.createElement)("span", { className: "dsc-meta" }, `${label} · ${promptId}`)), (0, react.createElement)(ProgressLine, {
|
|
800
|
+
state: queue,
|
|
801
|
+
t
|
|
802
|
+
}));
|
|
853
803
|
}
|
|
854
804
|
function SettledCard({ block, t }) {
|
|
855
805
|
if (block.isError === true) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge dsc-badge--err" }, t("cardFailed"))), (0, react.createElement)("div", { className: "dsc-meta" }, block.error?.name ?? "error"));
|
|
@@ -890,14 +840,30 @@ window.__ModuleLoader__.load({
|
|
|
890
840
|
t
|
|
891
841
|
});
|
|
892
842
|
}
|
|
843
|
+
/** A call still running: what is generating, and how far along it is. */
|
|
844
|
+
function RunningCard({ block, t }) {
|
|
845
|
+
const args = parseArgs(block.argsRaw);
|
|
846
|
+
const isRun = block.name === "comfyui_run" || args.action === "run";
|
|
847
|
+
const workflowId = block.name === "comfyui_workflow" && typeof args.id === "string" ? args.id : void 0;
|
|
848
|
+
const savedName = useWorkflowName(isRun ? workflowId : void 0);
|
|
849
|
+
const queue = useQueueState(void 0, isRun && args.mode !== "async");
|
|
850
|
+
if (!isRun) return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, block.name), typeof args.action === "string" ? (0, react.createElement)("span", { className: "dsc-meta" }, args.action) : null));
|
|
851
|
+
const title = block.name === "comfyui_run" ? t("cardHandWritten") : savedName ?? workflowId ?? "";
|
|
852
|
+
return (0, react.createElement)("div", { className: "dsc-card" }, (0, react.createElement)("div", { className: "dsc-card-head" }, (0, react.createElement)("span", { className: "dsc-badge" }, t("cardGenerating")), title !== "" ? (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardWorkflow")} ${title}`) : null, (0, react.createElement)("span", { className: "dsc-meta" }, `${t("cardMode")} ${String(args.mode ?? "sync")}`)), args.mode === "async" ? null : (0, react.createElement)(ProgressLine, {
|
|
853
|
+
state: queue,
|
|
854
|
+
t
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
893
857
|
/** The card component: picks running vs settled rendering from the block. */
|
|
894
858
|
function ComfyUICard({ t, block }) {
|
|
895
859
|
if (isResultNode(block)) return (0, react.createElement)(SettledCard, {
|
|
896
860
|
block,
|
|
897
861
|
t
|
|
898
862
|
});
|
|
899
|
-
|
|
900
|
-
|
|
863
|
+
return (0, react.createElement)(RunningCard, {
|
|
864
|
+
block,
|
|
865
|
+
t
|
|
866
|
+
});
|
|
901
867
|
}
|
|
902
868
|
//#endregion
|
|
903
869
|
//#region src/client/api.ts
|
|
@@ -1354,6 +1320,83 @@ window.__ModuleLoader__.load({
|
|
|
1354
1320
|
}, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle"))), (0, react.createElement)(ConnectionToast, { t }));
|
|
1355
1321
|
}
|
|
1356
1322
|
//#endregion
|
|
1323
|
+
//#region src/client/lightbox.tsx
|
|
1324
|
+
/**
|
|
1325
|
+
* Shared lightbox: opens a generated image full-screen with prev/next
|
|
1326
|
+
* navigation, keyboard support, and a download pill. Used by both the panel
|
|
1327
|
+
* (assets/queue previews) and the tool card (result images).
|
|
1328
|
+
*/
|
|
1329
|
+
/** Full-screen media overlay with prev/next navigation. */
|
|
1330
|
+
function Lightbox({ t, images, kinds, index, onClose, onIndex }) {
|
|
1331
|
+
const count = images.length;
|
|
1332
|
+
(0, react.useEffect)(() => {
|
|
1333
|
+
const onKey = (event) => {
|
|
1334
|
+
if (event.key === "Escape") onClose();
|
|
1335
|
+
else if (event.key === "ArrowLeft") onIndex((index - 1 + count) % count);
|
|
1336
|
+
else if (event.key === "ArrowRight") onIndex((index + 1) % count);
|
|
1337
|
+
};
|
|
1338
|
+
document.addEventListener("keydown", onKey);
|
|
1339
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1340
|
+
}, [
|
|
1341
|
+
index,
|
|
1342
|
+
count,
|
|
1343
|
+
onClose,
|
|
1344
|
+
onIndex
|
|
1345
|
+
]);
|
|
1346
|
+
const src = images[index];
|
|
1347
|
+
if (src === void 0) return null;
|
|
1348
|
+
const kind = kinds?.[index] ?? "image";
|
|
1349
|
+
const media = kind === "video" ? (0, react.createElement)("video", {
|
|
1350
|
+
className: "dsc-lightbox-media",
|
|
1351
|
+
src,
|
|
1352
|
+
controls: true,
|
|
1353
|
+
autoPlay: true
|
|
1354
|
+
}) : kind === "audio" ? (0, react.createElement)("audio", {
|
|
1355
|
+
className: "dsc-lightbox-media",
|
|
1356
|
+
src,
|
|
1357
|
+
controls: true,
|
|
1358
|
+
autoPlay: true
|
|
1359
|
+
}) : kind === "image" ? (0, react.createElement)("img", {
|
|
1360
|
+
className: "dsc-lightbox-img",
|
|
1361
|
+
src,
|
|
1362
|
+
alt: ""
|
|
1363
|
+
}) : (0, react.createElement)("div", { className: "dsc-lightbox-media" }, src);
|
|
1364
|
+
return (0, react.createElement)("div", {
|
|
1365
|
+
className: "dsc-lightbox",
|
|
1366
|
+
onClick: onClose
|
|
1367
|
+
}, (0, react.createElement)("div", {
|
|
1368
|
+
className: "dsc-lightbox-body",
|
|
1369
|
+
onClick: (event) => event.stopPropagation()
|
|
1370
|
+
}, (0, react.createElement)("button", {
|
|
1371
|
+
className: "dsc-lightbox-close",
|
|
1372
|
+
"aria-label": t("close"),
|
|
1373
|
+
onClick: (event) => {
|
|
1374
|
+
event.stopPropagation();
|
|
1375
|
+
onClose();
|
|
1376
|
+
}
|
|
1377
|
+
}, "✕"), count > 1 ? (0, react.createElement)("button", {
|
|
1378
|
+
className: "dsc-lightbox-nav dsc-lightbox-nav--prev",
|
|
1379
|
+
"aria-label": t("lbPrev"),
|
|
1380
|
+
onClick: (event) => {
|
|
1381
|
+
event.stopPropagation();
|
|
1382
|
+
onIndex((index - 1 + count) % count);
|
|
1383
|
+
}
|
|
1384
|
+
}, "‹") : null, media, (0, react.createElement)("div", { className: "dsc-lightbox-meta" }, (0, react.createElement)("span", null, `${index + 1} / ${count}`), (0, react.createElement)("a", {
|
|
1385
|
+
className: "dsc-lightbox-download",
|
|
1386
|
+
href: src,
|
|
1387
|
+
download: "",
|
|
1388
|
+
target: "_blank",
|
|
1389
|
+
rel: "noreferrer"
|
|
1390
|
+
}, t("cardDownload"))), count > 1 ? (0, react.createElement)("button", {
|
|
1391
|
+
className: "dsc-lightbox-nav dsc-lightbox-nav--next",
|
|
1392
|
+
"aria-label": t("lbNext"),
|
|
1393
|
+
onClick: (event) => {
|
|
1394
|
+
event.stopPropagation();
|
|
1395
|
+
onIndex((index + 1) % count);
|
|
1396
|
+
}
|
|
1397
|
+
}, "›") : null));
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
1357
1400
|
//#region src/client/panel.tsx
|
|
1358
1401
|
/**
|
|
1359
1402
|
* The ComfyUI panel (shell.overlay, id 'comfyui.panel'): a floating panel
|
|
@@ -4275,7 +4318,10 @@ window.__ModuleLoader__.load({
|
|
|
4275
4318
|
if (first === void 0 || broken) return (0, react.createElement)("div", {
|
|
4276
4319
|
className: "dsc-asset dsc-asset--empty",
|
|
4277
4320
|
onClick: props.onClick
|
|
4278
|
-
}, (0, react.createElement)("div", null, props.t("assetGone")), (0, react.createElement)("div", { className: "dsc-meta" }, props.asset.workflowName ?? "—"),
|
|
4321
|
+
}, (0, react.createElement)("div", null, props.t("assetGone")), (0, react.createElement)("div", { className: "dsc-meta" }, props.asset.workflowName ?? "—"), first !== void 0 ? (0, react.createElement)("div", {
|
|
4322
|
+
className: "dsc-meta dsc-asset-file",
|
|
4323
|
+
title: first.filename
|
|
4324
|
+
}, first.filename) : null, trash);
|
|
4279
4325
|
return (0, react.createElement)("div", {
|
|
4280
4326
|
className: "dsc-asset",
|
|
4281
4327
|
onClick: props.onClick
|
|
@@ -4405,7 +4451,10 @@ window.__ModuleLoader__.load({
|
|
|
4405
4451
|
alt: first.filename,
|
|
4406
4452
|
loading: "lazy",
|
|
4407
4453
|
onError: () => setBroken(true)
|
|
4408
|
-
}), (0, react.createElement)("div", { className: "dsc-asset-meta" }, props.asset.workflowName ?? "—"),
|
|
4454
|
+
}), (0, react.createElement)("div", { className: "dsc-asset-meta" }, props.asset.workflowName ?? "—"), (0, react.createElement)("div", {
|
|
4455
|
+
className: "dsc-asset-meta dsc-asset-file",
|
|
4456
|
+
title: first.filename
|
|
4457
|
+
}, first.filename), trash);
|
|
4409
4458
|
}
|
|
4410
4459
|
/**
|
|
4411
4460
|
* Tab 3: the task center — active tasks (pending / generating) shown as
|
|
@@ -4618,16 +4667,13 @@ window.__ModuleLoader__.load({
|
|
|
4618
4667
|
}
|
|
4619
4668
|
.dsc-badge--ok { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 16%, transparent); }
|
|
4620
4669
|
.dsc-badge--err { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent); }
|
|
4621
|
-
|
|
4622
|
-
.dsc-
|
|
4623
|
-
.dsc-
|
|
4624
|
-
.dsc-
|
|
4625
|
-
.dsc-
|
|
4626
|
-
.dsc-
|
|
4627
|
-
.dsc-
|
|
4628
|
-
.dsc-media-size { font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
|
4629
|
-
.dsc-media-meta a { display: inline; padding: 0; font-size: 11px; color: var(--dsw-alias-label-secondary); text-decoration: none; }
|
|
4630
|
-
.dsc-media-meta a:hover { color: var(--dsw-alias-label-primary); }
|
|
4670
|
+
/* Inline players inserted after video/audio links in chat replies (inline-media.ts). */
|
|
4671
|
+
.dsc-inline-player { display: block; margin: 6px 0; }
|
|
4672
|
+
.dsc-inline-player video { display: block; max-width: 100%; max-height: 360px; border-radius: 8px; background: transparent; }
|
|
4673
|
+
.dsc-inline-player audio { display: block; width: min(100%, 480px); height: 40px; }
|
|
4674
|
+
.dsc-files { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin-top: 4px; }
|
|
4675
|
+
.dsc-file { font-size: 12px; color: var(--dsw-alias-label-secondary); text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
4676
|
+
.dsc-file:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }
|
|
4631
4677
|
.dsc-meta { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
4632
4678
|
.dsc-err { color: var(--dsw-alias-state-error-primary); font-size: 12px; margin: 4px 0; }
|
|
4633
4679
|
.dsc-ok { color: var(--dsw-alias-state-success-primary); font-size: 12px; margin: 4px 0; }
|
|
@@ -4836,6 +4882,9 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
4836
4882
|
font-size: 11px; color: var(--dsw-alias-label-secondary); text-align: center;
|
|
4837
4883
|
}
|
|
4838
4884
|
.dsc-asset-meta { padding: 3px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
4885
|
+
/* Second meta line: the output filename, under the workflow name. */
|
|
4886
|
+
.dsc-asset-file { padding-top: 0; font-size: 10px; opacity: 0.8; }
|
|
4887
|
+
.dsc-asset--empty .dsc-asset-file { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
4839
4888
|
/* confirmation modal (asset deletion) */
|
|
4840
4889
|
.dsc-confirm { width: min(360px, 88vw); display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.32); }
|
|
4841
4890
|
.dsc-confirm-title { font-weight: 600; font-size: 14px; }
|
|
@@ -5032,6 +5081,111 @@ video.dsc-picker-player { max-height: 180px; }
|
|
|
5032
5081
|
document.head.append(style);
|
|
5033
5082
|
}
|
|
5034
5083
|
//#endregion
|
|
5084
|
+
//#region src/client/inline-media.ts
|
|
5085
|
+
/**
|
|
5086
|
+
* Inline players for generated video/audio in chat replies.
|
|
5087
|
+
*
|
|
5088
|
+
* The model shows results by pasting Markdown into its reply (CLAUDE.md
|
|
5089
|
+
* contract 18). Images embed natively, but dsh's chat Markdown has no video or
|
|
5090
|
+
* audio syntax and renders raw HTML as literal text, so a video/audio result
|
|
5091
|
+
* can only be a link there. This module watches the page and, after every
|
|
5092
|
+
* link to this plugin's own `/comfyui/media` route whose file is a video or
|
|
5093
|
+
* audio file, inserts a native <video>/<audio> player.
|
|
5094
|
+
*
|
|
5095
|
+
* It only ever ADDS sibling nodes next to dsh-rendered links — it never moves
|
|
5096
|
+
* or removes dsh's own nodes — so React's reconciliation of the message keeps
|
|
5097
|
+
* working. Streaming re-renders and dsh updates simply trigger a re-scan: a
|
|
5098
|
+
* player is added only when the link is not already followed by its player,
|
|
5099
|
+
* and players whose link has gone are removed. The plugin's own UI (card file
|
|
5100
|
+
* links, panel) is skipped. Everything is undone when the plugin unloads.
|
|
5101
|
+
*/
|
|
5102
|
+
const VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5103
|
+
"mp4",
|
|
5104
|
+
"webm",
|
|
5105
|
+
"mov",
|
|
5106
|
+
"m4v",
|
|
5107
|
+
"mkv"
|
|
5108
|
+
]);
|
|
5109
|
+
const AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5110
|
+
"mp3",
|
|
5111
|
+
"wav",
|
|
5112
|
+
"flac",
|
|
5113
|
+
"ogg",
|
|
5114
|
+
"oga",
|
|
5115
|
+
"opus",
|
|
5116
|
+
"m4a",
|
|
5117
|
+
"aac"
|
|
5118
|
+
]);
|
|
5119
|
+
const PLAYER_CLASS = "dsc-inline-player";
|
|
5120
|
+
/** The player kind for a /comfyui/media link, or undefined for anything else. */
|
|
5121
|
+
function mediaKind(href) {
|
|
5122
|
+
let url;
|
|
5123
|
+
try {
|
|
5124
|
+
url = new URL(href, window.location.href);
|
|
5125
|
+
} catch {
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
if (!url.pathname.endsWith("/comfyui/media")) return void 0;
|
|
5129
|
+
const file = url.searchParams.get("file") ?? "";
|
|
5130
|
+
const dot = file.lastIndexOf(".");
|
|
5131
|
+
if (dot < 0) return void 0;
|
|
5132
|
+
const ext = file.slice(dot + 1).toLowerCase();
|
|
5133
|
+
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
5134
|
+
if (AUDIO_EXTENSIONS.has(ext)) return "audio";
|
|
5135
|
+
}
|
|
5136
|
+
function isPlayerFor(node, href) {
|
|
5137
|
+
return node !== null && node.classList.contains(PLAYER_CLASS) && node.getAttribute("data-src") === href;
|
|
5138
|
+
}
|
|
5139
|
+
function makePlayer(kind, href) {
|
|
5140
|
+
const wrap = document.createElement("span");
|
|
5141
|
+
wrap.className = `${PLAYER_CLASS} ${PLAYER_CLASS}--${kind}`;
|
|
5142
|
+
wrap.setAttribute("data-src", href);
|
|
5143
|
+
const player = document.createElement(kind);
|
|
5144
|
+
player.src = href;
|
|
5145
|
+
player.controls = true;
|
|
5146
|
+
player.preload = "metadata";
|
|
5147
|
+
wrap.appendChild(player);
|
|
5148
|
+
return wrap;
|
|
5149
|
+
}
|
|
5150
|
+
function scan() {
|
|
5151
|
+
for (const player of Array.from(document.querySelectorAll(`.${PLAYER_CLASS}`))) {
|
|
5152
|
+
const prev = player.previousElementSibling;
|
|
5153
|
+
const src = player.getAttribute("data-src");
|
|
5154
|
+
if (!(prev instanceof HTMLAnchorElement) || src === null || prev.href !== src) player.remove();
|
|
5155
|
+
}
|
|
5156
|
+
for (const link of Array.from(document.querySelectorAll("a[href*=\"/comfyui/media\"]"))) {
|
|
5157
|
+
if (link.closest("[class^=\"dsc-\"], [class*=\" dsc-\"]") !== null) continue;
|
|
5158
|
+
const kind = mediaKind(link.href);
|
|
5159
|
+
if (kind === void 0) continue;
|
|
5160
|
+
if (isPlayerFor(link.nextElementSibling, link.href)) continue;
|
|
5161
|
+
link.after(makePlayer(kind, link.href));
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
/** Start watching the page; returns the disposer that stops and cleans up. */
|
|
5165
|
+
function installInlineMedia() {
|
|
5166
|
+
let frame;
|
|
5167
|
+
const schedule = () => {
|
|
5168
|
+
if (frame !== void 0) return;
|
|
5169
|
+
frame = window.requestAnimationFrame(() => {
|
|
5170
|
+
frame = void 0;
|
|
5171
|
+
scan();
|
|
5172
|
+
});
|
|
5173
|
+
};
|
|
5174
|
+
const observer = new MutationObserver((mutations) => {
|
|
5175
|
+
if (mutations.some((mutation) => [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)].some((node) => !(node instanceof HTMLElement && node.classList.contains(PLAYER_CLASS))))) schedule();
|
|
5176
|
+
});
|
|
5177
|
+
observer.observe(document.body, {
|
|
5178
|
+
childList: true,
|
|
5179
|
+
subtree: true
|
|
5180
|
+
});
|
|
5181
|
+
schedule();
|
|
5182
|
+
return () => {
|
|
5183
|
+
observer.disconnect();
|
|
5184
|
+
if (frame !== void 0) window.cancelAnimationFrame(frame);
|
|
5185
|
+
for (const player of Array.from(document.querySelectorAll(`.${PLAYER_CLASS}`))) player.remove();
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
//#endregion
|
|
5035
5189
|
//#region src/client/index.ts
|
|
5036
5190
|
/**
|
|
5037
5191
|
* dsh-comfyui client half: registers the comfyui_run tool card and the
|
|
@@ -5043,6 +5197,7 @@ video.dsc-picker-player { max-height: 180px; }
|
|
|
5043
5197
|
function apply(ctx) {
|
|
5044
5198
|
const t = makeT(getLang());
|
|
5045
5199
|
ctx.effect(() => injectStyles(), "dsh-comfyui: styles");
|
|
5200
|
+
ctx.effect(() => installInlineMedia(), "dsh-comfyui: inline video/audio players");
|
|
5046
5201
|
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
5047
5202
|
name: "tool.call.toolview",
|
|
5048
5203
|
key: "comfyui_run"
|