Files
olivv-cs-ComfyUI-FunPack/app/ui/features/ideas/index.js
T

95 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 💡 Shortcut ideas: a bulb beside the prompt box you are in. It never opens by itself; it glows while shortcut categories go unused,
// and its list mixes your own habits (what you paired before, what usually follows the previous shot) with browsing the library.
import { composer as c } from "../../composer/composer.js";
import { claimFor } from "../../composer/internals/zlayer.js";
import { genUnitId, isGenerative, unitRoot } from "../../shell/scenes.js";
import { analyze, byHabit, presentIn, shuffled } from "../../shell/habits.js";
const BOXES = new Set(["Story", "Prompt"]);
const first = (sc) => String((sc.triggers || [])[0] || "").trim();
export default {
id: "ideas",
mount: "menubar.menus",
needs: ["project", "api", "selection"],
setup({ app }) {
const p = app.project, on = () => p.pref("suggestions", true);
let box = null, node = null, layer = null, pop = null, hold = 0;
let frame = 0;
const closeBulb = () => { cancelAnimationFrame(frame); if (pop) pop.close(); if (node) { node.remove(); layer.release(); node = layer = box = null; } };
const prevText = () => { // the previous shot's prompt, for "your usual next shot" (scene prompt only)
const open = p.project; if (!open || box.getAttribute("aria-label") !== "Prompt") return "";
const roots = open.scenes.filter((s) => isGenerative(s) && unitRoot(open, genUnitId(s)) === s), at = roots.findIndex((s) => s.id === app.selection.focus);
return at > 0 ? roots[at - 1].text || "" : "";
};
const insert = (text) => {
const v = box.value, s = box.selectionStart ?? v.length, e = box.selectionEnd ?? s, before = v.slice(0, s), after = v.slice(e);
const ins = `${before && !/\s$/.test(before) ? " " : ""}${text}${/^(\s|[,;])/.test(after) ? "" : " "}`;
box.value = before + ins + after; box.setSelectionRange(s + ins.length, s + ins.length);
box.dispatchEvent(new Event("input", { bubbles: true })); box.focus();
};
async function openList() {
if (pop) return pop.close();
let library = [], stats = null;
try { library = (await app.api.shortcuts()).shortcuts || []; } catch (err) { return c.toast.warn({ text: err.message }); }
try { stats = await app.api.suggestionStats(); } catch { /* habits are optional */ }
const text = box.value, { used, missing } = analyze(library, text), here = presentIn(library, text), hereNames = new Set(here.map((s) => s.name));
const body = c.region.stack({ gap: "xs" });
const row = (sc, why) => c.button.sm({ label: `${first(sc)}${(sc.replacements || [])[0] ? ` — ${sc.replacements[0].slice(0, 40)}` : ""}${why ? ` (${why})` : ""}`, tone: "ghost", onClick: () => { insert(first(sc)); pop && pop.close(); } });
const draw = () => {
const parts = [];
if (stats && hereNames.size) {
const pairs = byHabit(library, stats.pairs || [], hereNames, hereNames, { both: true });
if (pairs.length) parts.push(c.label.section({ text: "You've paired these before" }), ...pairs.map((i) => row(i.sc, `with ${first(i.with)} ×${i.n}`)));
}
const prev = new Set(presentIn(library, prevText()).map((s) => s.name));
if (stats && prev.size) {
const next = byHabit(library, stats.follows || [], prev, hereNames);
if (next.length) parts.push(c.label.section({ text: "Your usual next shot" }), ...next.map((i) => row(i.sc, `after ${first(i.with)} ×${i.n}`)));
}
const more = used.map((g) => ({ cat: g.cat, items: g.items.filter((s) => !hereNames.has(s.name)) })).filter((g) => g.items.length);
if (more.length) parts.push(c.label.section({ text: "Continue the thread" }), ...shuffled(more).slice(0, 3).flatMap((g) => [c.hint.default({ text: `more ${g.cat}` }), ...shuffled(g.items).slice(0, 2).map((s) => row(s))]));
if (missing.length) parts.push(c.label.section({ text: "New directions" }), ...missing.sort((a, b) => a.cat.localeCompare(b.cat)).flatMap((g) => [c.hint.default({ text: `${g.cat} (${g.items.length})` }), ...shuffled(g.items).slice(0, 3).map((s) => row(s))]));
if (!parts.length) parts.push(c.hint.default({ text: "Every shortcut category already appears here — nothing left to browse." }));
body.set([c.toolbar.default({ items: [c.label.section({ text: "Ideas" })], trailing: [c.button.sm({ label: "⟳", tone: "ghost", title: "Shuffle the samples", onClick: draw })] }), ...parts]);
};
draw();
pop = c.popover.anchored({ anchor: node, body, side: "bottom", align: "end", onClose: () => { pop = null; settle(); } });
}
function show(target) {
if (box === target || !on()) return;
closeBulb();
box = target;
const button = c.button.sm({ label: "💡", tone: "ghost", title: "Shortcut ideas", onClick: openList });
node = button.node;
Object.assign(node.style, { position: "fixed" });
document.body.append(node); layer = claimFor("popover", node);
// Followed every frame while shown: the box moves with its window (dragged, resized, scrolled inside),
// which no page-level event reports.
const follow = () => { place(); if (node) frame = requestAnimationFrame(follow); };
follow();
}
function place() {
if (!box || !node) return;
const r = box.isConnected ? box.getBoundingClientRect() : null;
if (!r || !r.width) return closeBulb(); // gone, or hidden with its panel
// Inside the box's own bottom-right corner (left of the resize grip): on the box it belongs to, never over a neighbour's controls.
const b = node.getBoundingClientRect();
Object.assign(node.style, { left: `${Math.max(r.left + 4, r.right - b.width - 16)}px`, top: `${Math.max(r.top + 2, r.bottom - b.height - 4)}px` });
}
const onFocus = (e) => { const t = e.target; if (t instanceof HTMLTextAreaElement && BOXES.has(t.getAttribute("aria-label"))) { clearTimeout(hold); show(t); } };
const settle = () => { clearTimeout(hold); hold = setTimeout(() => { if (node && box !== document.activeElement && !node.contains(document.activeElement) && !pop) closeBulb(); }, 400); };
const onBlur = (e) => { if (e.target === box || (node && node.contains(e.target))) settle(); };
const onMove = () => place();
document.addEventListener("focusin", onFocus); document.addEventListener("focusout", onBlur);
window.addEventListener("scroll", onMove, true); window.addEventListener("resize", onMove);
app.editorSettings.push(() => c.region.stack({ gap: "sm", children: [
c.toggle.default({ label: "Shortcut ideas", hint: "Shows a 💡 beside the Story and Prompt boxes while you are in them. Click it for shortcuts to insert: pairings you use, what usually follows the previous shot, and categories this prompt does not use yet.",
checked: on(), onChange: (v) => { p.setPref("suggestions", v); if (!v) closeBulb(); } })] }));
return () => { closeBulb(); clearTimeout(hold); document.removeEventListener("focusin", onFocus); document.removeEventListener("focusout", onBlur); window.removeEventListener("scroll", onMove, true); window.removeEventListener("resize", onMove); };
},
};