Files

99 lines
4.1 KiB
JavaScript

// ComfyUI's log, in the app.
//
// Users do not open devtools. "It did not work" with nothing to read is where a
// bug report dies, and the one place the reason is written down is a terminal
// they may not even be able to see -- a rented box behind a tunnel has no
// terminal on this desk at all.
//
// It polls only while open, and it stops polling while you are selecting text:
// a panel that redraws under a drag makes copying a stack trace impossible,
// which is the single thing anybody opens a log to do.
import { composer } from "../../composer/composer.js";
const EVERY = 2000;
/** mount({ poll, setFooter, close }) -> { node, destroy }. See tempfiles.js's
* mount() for why setFooter/close are the only things asked for -- `destroy`
* additionally stops the poll, since this one runs a timer nothing else does. */
export function mount({ dom, poll = EVERY, setFooter = () => {}, close = () => {} }) {
let timer = null;
let follow = true; // stick to the end until the user scrolls off it
let last = "";
let previous = false; // the run before this one, from disk: what a restart took off the screen
let copyText = ""; // what Copy gives: the lines, never the colour bookkeeping
const view = composer.code.block({ text: "Reading…" });
const body = composer.region.stack({ gap: "sm", fill: true, children: [view] });
const selecting = () => dom.selectingIn(view.node);
// Following the end is the default and stops being it the moment someone
// scrolls up -- otherwise reading anything older than two seconds is a fight.
view.node.addEventListener("scroll", () => {
const room = view.node.scrollHeight - view.node.scrollTop - view.node.clientHeight;
follow = room < 40;
});
async function refresh() {
if (selecting()) return; // never redraw under a drag
try {
const res = await fetch(`/fun/api/log?limit=600${previous ? "&previous=1" : ""}`, { cache: "no-store" });
const payload = await res.json().catch(() => ({}));
const lines = payload.lines || [];
const text = lines.join("\n") || payload.detail || "Nothing in the log yet.";
const key = text + "\u0000" + (payload.tones || []).join(",");
if (key !== last) {
last = key;
copyText = text;
if (lines.length) view.setLines(lines, payload.tones || []);
else view.setText(text);
if (follow) view.node.scrollTop = view.node.scrollHeight;
}
setFooter({ note: payload.path || "", actions: footer() });
} catch (err) {
// The server being gone is the most interesting thing a log can say, so it
// is said -- and the lines already on screen are kept, because they are
// the ones from just before it went.
setFooter({ note: `Cannot reach ComfyUI: ${err.message}`, actions: footer() });
}
}
const footer = () => [
composer.button.md({
label: previous ? "This run" : "Previous run", tone: "ghost", title: "The server's console from before its last start, kept on disk",
onClick: () => { previous = !previous; last = ""; follow = true; refresh(); },
}),
composer.button.md({
label: "Copy", onClick: () => {
// Selecting a thousand lines by hand is the other reason this is hard to
// get out of a log panel.
if (navigator.clipboard) navigator.clipboard.writeText(copyText).catch(() => {});
},
}),
composer.button.md({ label: "Close", tone: "primary", onClick: () => close() }),
];
setFooter({ actions: footer() });
refresh();
timer = setInterval(refresh, poll);
return { node: body.node, destroy: () => { clearInterval(timer); timer = null; body.destroy(); } };
}
export function open({ dom, poll = EVERY }) {
let window_ = null;
const content = mount({
dom, poll,
setFooter: (f) => window_ && window_.setFooter(f),
close: () => window_ && window_.close("done"),
});
window_ = composer.modal.generic({
title: "ComfyUI log",
subtitle: "What the server printed. Newest at the bottom.",
size: "xl",
body: content,
onClose: () => { content.destroy(); window_ = null; },
});
return window_;
}