☰

Prompt Gen

Prompt builder overlay. Manual copy only: it never reads from or writes to the site's prompt editor.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Prompt Gen
// @namespace    promptgen
// @version      1.0.0
// @description  Prompt builder overlay. Manual copy only: it never reads from or writes to the site's prompt editor.
// @match        https://pixai.art/*
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_setClipboard
// @license GNU GPLv3
// ==/UserScript==

(function () {
  // ---------- styles (tool CSS lives inside a closed shadow root) ----------
  const TOOL_CSS = `
:host{--bg:#14141a;--card:#1d1d26;--line:#2e2e3b;--tx:#e6e6ee;--mut:#8a8a9c;--acc:#b08cff;color-scheme:dark}
@media (prefers-color-scheme: light){:host{--bg:#f4f4f8;--card:#fff;--line:#d8d8e2;--tx:#1b1b24;--mut:#6a6a7c;--acc:#6d3fe0;color-scheme:light}}
*{box-sizing:border-box}
.back{position:fixed;inset:0;background:rgba(0,0,0,.6);overflow:auto;overscroll-behavior:contain;padding:16px;display:flex;align-items:flex-start;justify-content:center}
.panel{position:relative;background:var(--bg);color:var(--tx);font:14px/1.4 system-ui,sans-serif;border:1px solid var(--line);border-radius:12px;padding:44px 16px 16px;width:100%;max-width:1300px}
.x{position:absolute;top:8px;right:8px;width:32px;height:32px;padding:0;font-size:16px;line-height:1}
.layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,520px);gap:14px;max-width:1300px;margin:auto;align-items:start}
@media (max-width:900px){.layout{grid-template-columns:1fr}.outp{order:-1;position:static!important}}
.mods{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px}
.mod{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px}
.mod h2{font-size:13px;margin:0 0 6px;color:var(--acc);text-transform:uppercase;letter-spacing:.05em}
.sub{font-size:11px;color:var(--mut);margin:8px 0 2px;text-transform:uppercase}
label.o{display:flex;gap:7px;align-items:flex-start;padding:2px 0;cursor:pointer}
label.o input{margin-top:3px}
label.o.none{color:var(--mut)}
.dis{opacity:.4;pointer-events:none}
.gate{margin-top:6px;padding-top:6px;border-top:1px dashed var(--line)}
.gate .inner{margin-left:20px}
.subbox{margin:2px 0 4px 22px;padding-left:8px;border-left:2px solid var(--line)}
.subbox.off{opacity:.55}
.side{display:flex;gap:16px}
.hint{font-size:11px;color:var(--mut);margin-left:22px}
.outp{position:sticky;top:12px;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px;max-height:calc(100vh - 90px);display:flex;flex-direction:column}
#outs{overflow:auto;display:flex;flex-direction:column;gap:8px;margin-top:8px}
.res{border:1px solid var(--line);border-radius:6px;padding:6px 8px}
.res .top{display:flex;gap:8px;align-items:center;color:var(--mut);font-size:11px;margin-bottom:4px}
.res .top span{flex:1}
.res pre{margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,monospace}
button{font:inherit;color:var(--tx);background:var(--card);border:1px solid var(--line);border-radius:6px;padding:5px 11px;cursor:pointer}
button.pri{background:var(--acc);border-color:var(--acc);color:#fff}
button.sm{padding:1px 8px;font-size:11px}
.row{display:flex;gap:8px;align-items:center}.row small{color:var(--mut);margin-left:auto}
`;
  const BTN_CSS = `
:host{display:block;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1}
button{font:500 13px/1 system-ui,sans-serif;height:36px;padding:0 14px;border-radius:9999px;white-space:nowrap;cursor:pointer;
  color:inherit;background:transparent;border:1px solid color-mix(in srgb,currentColor 22%,transparent)}
button:hover{background:color-mix(in srgb,currentColor 8%,transparent)}`;

  const h = (tag, props = {}, ...kids) => {
    const e = document.createElement(tag);
    for (const k in props) e.setAttribute(k, props[k]);
    e.append(...kids);
    return e;
  };

  // ---------- the tool (same logic as index.html) ----------
  function initTool($) {

// ===== always-on, not shown in UI =====
const PREFIX = "uncensored, 1girl, cute";
const SUFFIX = "very aesthetic, absurdres, detailed background, newest, intricate details, scenery, blurry background, masterpiece, best quality, detailed illustration, shiny skin, (detailed_hands:1.5), detailed eyes, detailed eyelashes, extremely aesthetic, amazing quality";

// ===== modules =====
// radio = pick one (unless noNone) | check = any | gated = checkbox enabling an inner radio
// option = "tags" or {l:label, t:tags, always:true?}. A multi-tag selection (e.g. group sex) = one option with all tags in t.
// ===== Acts (custom module: one radio across everything, sub modules are radio pairs) =====
// sub modules: timing (side by side) x target (stacked) -> tag bundle tags[timing][target]
const ACTS = [
  {k:"none"},
  {k:"solo", t:"solo focus"},
  {k:"pen", l:"Penetration", sub:true},
  {k:"titjob", l:"titjob", t:"paizuri"},
  {k:"handjob", t:"handjob"},
  {k:"mast", l:"masturbation", t:"solo focus, masturbation, female masturbation"},
  {k:"dil", l:"Dildo play", sub:true},
  {k:"tentacle", t:"tentacle sex, tentacles"},
  {k:"double", t:"double penetration"},
  {k:"yuri", t:"yuri"},
];
const SUBS = {
  pen: {timing:["imminent","sex"], target:["mouth","pussy","ass"], tags:[
    ["imminent fellatio","imminent penetration, imminent vaginal","imminent penetration, imminent anal"],
    ["fellatio","hetero, sex, vaginal","hetero, sex, anal"]]},
  dil: {timing:["imminent","insertion"], target:["pussy","ass"], tags:[
    ["solo focus, imminent object insertion, dildo, imminent vaginal object insertion, sex toy",
     "solo focus, imminent object insertion, dildo, imminent anal object insertion, sex toy"],
    ["solo focus, object insertion, dildo, vaginal object insertion, sex toy",
     "solo focus, object insertion, dildo, anal object insertion, sex toy"]]},
};
const AUTO_BOY = ["pen","titjob","handjob"]; // selecting these ticks "Faceless nude male"

const MODULES = [
 {name:"Characters", groups:[
  {id:"girl_state", sub:"girl", type:"radio", noNone:true, def:0, opts:[
    {l:"Fully clothed", t:""},
    {l:"Partly exposed", t:"breasts, nipples, nipples, breasts out, open clothes, clothes pull"},
    {l:"Nude", t:"completely nude, breasts, nipples"}]},
  {id:"pants", type:"pants"},
  {id:"girl_detail", type:"check", opts:[
    {l:"Genital detail", t:"clitoris, pussy, pussy juice, detailed pussy, visible labia clitoris urethra"}]},
  {id:"boy", sub:"boy", type:"check", opts:[
    {l:"Faceless nude male", t:"1boy, faceless male, nude male, penis"}]},
 ]},
 {name:"Acts", custom:"acts"},
 {name:"Positions", groups:[
  {id:"pos", type:"radio", opts:[
   "standing","sitting","kneeling","lying, on back","straddling","lying, on side","doggystyle","reverse carry congression"]},
  {id:"legs", type:"check", opts:["spread legs"]}]},
 {name:"Arms", extra:{key:"cycle", label:"cycle", hint:"one prompt per arm state"},
  groups:[{id:"arms", type:"radio", disIf:"cycle", opts:[
   "spread pussy","arms behind back","arms behind head","reaching towards viewer"]}]},
 {name:"Sensation", extra:{key:"mix", label:"random mix", hint:"4 prompts: one with everything ticked, rest random subsets"}, groups:[
  {id:"sens", type:"check", def:[0], opts:[
   {t:"aroused, blush", always:true},"moaning","horny","happy","in heat","surprised","wink","excited","tears","satisfied","afterglow","saliva"]},
 ]},
 {name:"Cum & Defloration", groups:[
  {id:"defl", type:"check", opts:["defloration"]},
  {id:"cum", type:"gated", gate:"cum", opts:["precum","cum, cum overflow, ejaculation"]},
  {id:"cumx", sub:"extras", type:"check", opts:["cum on body","cum on face","cum on hair","show cum in mouth"]},
 ]},
 {name:"Camera", hint:"none ticked = auto set of 8 camera prompts", groups:[{id:"cam", type:"check", opts:[
   "pov","dutch angle","close-up","straight-on","from below","from behind","from above","looking at viewer","internal","pantyshot"]}]},
 {name:"Location", groups:[
  {id:"loc", type:"radio", noNone:true, def:0, opts:[
   {l:"Bedroom", t:"indoors, bedroom, bed, day, light rays, window"},
   {l:"Church (gold)", t:"church, gold theme, (intricate:1.1), (gold tone:1.1), light rays, feathers"},
   {l:"Above clouds (blue/white)", t:"(arch background:0.9), above clouds, (bright light:1.1), (intricate:1.1), (white tone:1.1), (blue tone:0.9), blue and white theme"}]},
  {id:"surf", type:"gated", gate:"surface", opts:["on bed","on floor"]}]},
];

// auto camera set used when no camera is ticked
const CAM_BASE = [["pov"],["pov","dutch angle"],["close-up"],["straight-on"]];
const CAM_AUTO = [false,true].flatMap(int => CAM_BASE.map(b => [...b, "looking at viewer", ...(int?["internal"]:[])]));

// ===== state =====
const KEY = "promptgen.v4";
const GROUPS = MODULES.flatMap(m => m.groups || []), GM = Object.fromEntries(GROUPS.map(g=>[g.id,g]));
let S = {}, RESULTS = [];
const txt = o => typeof o === "string" ? o : o.t;
const lab = o => typeof o === "string" ? o : (o.l || o.t);
function defaults(){
  const d = {cycle:false, mix:false, acts:{kind:"none", pen:{t:0,x:0}, dil:{t:0,x:0}}};
  GROUPS.forEach(g => d[g.id] = g.type==="radio" ? (g.def ?? (g.noNone?0:-1)) : g.type==="check" ? [...(g.def||[])] : g.type==="pants" ? {mode:0,color:0,aside:false} : {on:false,i:0});
  return d;
}
function load(){
  S = defaults();
  try{ if(typeof GM_getValue==="function") Object.assign(S, JSON.parse(GM_getValue(KEY, "{}") || "{}")); }catch(e){}
}
function save(){ try{ if(typeof GM_setValue==="function") GM_setValue(KEY, JSON.stringify(S)); }catch(e){} }

// ===== generation =====
const fmt = s => s.split(",").map(x=>x.trim()).filter(Boolean).map(x=>x+",").join(" ");
const rnd = a => a[Math.floor(Math.random()*a.length)];
const radioText = id => S[id] >= 0 ? txt(GM[id].opts[S[id]]) : "";
const sel = id => GM[id].opts.filter((_,i)=>S[id].includes(i));

const AFFINITY = [["surprised","wink",2/3]]; // [tagA, tagB, chance the other follows]
function randomSubset(pool){ // 1..k-1 seeds (all if k<=1), original order kept
  const k = pool.length; if(k<=1) return pool.slice();
  const size = 1 + Math.floor(Math.random()*(k-1));
  const order = pool.map(t=>[Math.random(),t]).sort((x,y)=>x[0]-y[0]).map(x=>x[1]);
  const pairOf = t => { for(const [x,y,p] of AFFINITY){ if(!pool.includes(x)||!pool.includes(y)) continue; if(t===x) return [y,p]; if(t===y) return [x,p]; } return null; };
  const out = new Set(), blocked = new Set();
  for(const t of order){
    if(out.size >= size) break;
    if(out.has(t) || blocked.has(t)) continue;
    out.add(t);
    // seeding rule: when a pair member is seeded its partner follows with chance p, otherwise it is skipped for this prompt
    const pr = pairOf(t);
    if(pr && !out.has(pr[0])){ if(Math.random() < pr[1]) out.add(pr[0]); else blocked.add(pr[0]); }
  }
  return pool.filter(t=>out.has(t));
}
// strict: only distinct results (may return fewer than count); loose: pad with repeats
function subsetVariants(pool, count, includeFull, strict){
  const out=[], seen=new Set(), key=s=>s.join("\u0000");
  if(includeFull){ out.push(pool.slice()); seen.add(key(pool)); }
  for(let t=0; out.length<count && t<300; t++){
    const s = randomSubset(pool); if(seen.has(key(s))) continue;
    seen.add(key(s)); out.push(s);
  }
  if(!strict) while(out.length<count) out.push(randomSubset(pool));
  return out;
}

const PANTS_COLORS = ["white","black"];
function pantsTags(){
  const p = S.pants;
  if(p.mode === 1) return ["no panties, nopan"];      // nopan wipes every pantsu tag, aside included
  const t = ["panties, lace_panties, " + PANTS_COLORS[p.color] + " panties"];
  if(p.aside) t.push("panties aside");
  return t;
}
function actText(){
  const a = S.acts, k = a.kind;
  if(SUBS[k]) return SUBS[k].tags[a[k].t][a[k].x];
  const o = ACTS.find(x=>x.k===k); return o && o.t ? o.t : "";
}
function setAct(k){
  const prev = S.acts.kind; S.acts.kind = k;
  if(prev !== k && AUTO_BOY.includes(k) && !S.boy.includes(0)) S.boy = [0];
}

function generate(){
  const girl = [radioText("girl_state"), ...pantsTags(), ...sel("girl_detail").map(txt)];
  const boy = sel("boy").map(txt), pos = radioText("pos"), legs = sel("legs").map(txt), act = actText(), loc = radioText("loc"), surf = S.surf.on ? txt(GM.surf.opts[S.surf.i]) : "";
  const armsAll = GM.arms.opts.map(txt), armFixed = radioText("arms");
  const sensSel = sel("sens"), always = sensSel.find(o=>o.always);
  const pool = sensSel.filter(o=>!o.always).map(txt);
  const alwaysT = always ? [txt(always)] : [];
  const cumTags = [...sel("defl").map(txt), ...(S.cum.on ? [txt(GM.cum.opts[S.cum.i])] : []), ...sel("cumx").map(txt)];
  const cams = sel("cam").map(txt), auto = cams.length === 0;

  const specs = []; // {cam, arms, sub, tag}
  if(auto){
    const subs = S.mix ? subsetVariants(pool, 8, false, false) : null;
    CAM_AUTO.forEach((cam,i)=> specs.push({cam, arms: S.cycle ? rnd(armsAll) : armFixed, sub: subs?subs[i]:pool, mixed:!!subs, armPick:S.cycle}));
  } else if(S.cycle){
    const subs = S.mix ? subsetVariants(pool, 4, true, false) : null;
    armsAll.forEach((a,i)=> specs.push({cam:cams, arms:a, sub: subs?subs[i]:pool, mixed:!!subs, armPick:true}));
  } else if(S.mix){
    subsetVariants(pool, 4, true, true).forEach(s=> specs.push({cam:cams, arms:armFixed, sub:s, mixed:true}));
  } else specs.push({cam:cams, arms:armFixed, sub:pool});

  RESULTS = specs.map((p,i)=>{
    const lines = [
      fmt([PREFIX, ...girl].join(", ")),
      fmt([pos, ...legs, act, p.arms].filter(Boolean).join(", ")),
      fmt([...alwaysT, ...p.sub, ...cumTags].join(", ")),
      fmt(p.cam.join(", ")),
      fmt(boy.join(", ")),
      fmt([loc, surf].filter(Boolean).join(", ")),
      fmt(SUFFIX),
    ].filter(Boolean);
    const meta = [`#${i+1}`];
    if(auto) meta.push(p.cam.join(", "));
    if(p.armPick) meta.push("arms: " + p.arms);
    if(p.mixed) meta.push(`sens ${p.sub.length}/${pool.length}`);
    return {text: lines.join("\n"), meta: meta.join(" · ")};
  });
  renderOut();
}

// ===== output ui =====
function copy(t, btn){
  const done = ()=>{ const o = btn.textContent; btn.textContent = "copied"; setTimeout(()=>btn.textContent = o, 800); };
  try{ if(typeof GM_setClipboard==="function"){ GM_setClipboard(t, "text"); return done(); } }catch(e){}
  if(navigator.clipboard) navigator.clipboard.writeText(t).then(done);
}
function renderOut(){
  const box = $("outs"); box.replaceChildren();
  RESULTS.forEach(r=>{
    const d = document.createElement("div"); d.className = "res";
    const top = document.createElement("div"); top.className = "top";
    const m = document.createElement("span"); m.textContent = r.meta;
    const b = document.createElement("button"); b.className = "sm"; b.textContent = "copy"; b.onclick = ()=>copy(r.text, b);
    top.append(m, b);
    const pre = document.createElement("pre"); pre.textContent = r.text;
    d.append(top, pre); box.append(d);
  });
  $("cnt").textContent = RESULTS.length + (RESULTS.length===1?" prompt":" prompts");
}

// ===== module ui =====
function mkOpt(type, name, o, checked, onchange, cls){
  const l = document.createElement("label"); l.className = "o " + (cls||"");
  const i = document.createElement("input"); i.type = type; i.name = name; i.checked = checked; i.onchange = onchange;
  const s = document.createElement("span"); s.textContent = lab(o);
  if(typeof o !== "string" && o.l) l.title = o.t;
  l.append(i, s); return l;
}
function note(t){ const h = document.createElement("div"); h.className = "hint"; h.textContent = t; return h; }
function subBox(id, active){
  const sp = SUBS[id], st = S.acts[id];
  const box = document.createElement("div"); box.className = "subbox" + (active ? "" : " off");
  const set = (key,i)=>{ st[key] = i; setAct(id); save(); render(); generate(); };
  const side = document.createElement("div"); side.className = "side";
  const sub = (name, n, checked, fn) => { const l = mkOpt("radio", name, n, checked, null); l.querySelector("input").onclick = fn; return l; };
  sp.timing.forEach((n,i)=> side.append(sub(id+"_t", n, st.t===i, ()=>set("t",i))));
  box.append(side);
  sp.target.forEach((n,i)=> box.append(sub(id+"_x", n, st.x===i, ()=>set("x",i))));
  return box;
}
function renderPants(c){
  const P = S.pants;
  const done = () => { save(); render(); generate(); };
  // click (not change) so re-clicking an already-selected option still enables pantsu
  const radio = (name, label, checked, fn) => { const l = mkOpt("radio", name, label, checked, null); l.querySelector("input").onclick = () => { fn(); done(); }; return l; };
  const wrap = document.createElement("div");
  wrap.append(radio("pants_m", "pantsu", P.mode===0, ()=>{ P.mode = 0; }));
  const inner = document.createElement("div"); inner.className = "subbox" + (P.mode===0 ? "" : " off");
  PANTS_COLORS.forEach((n,i)=> inner.append(radio("pants_c", n, P.color===i, ()=>{ P.color = i; P.mode = 0; })));
  const aside = mkOpt("checkbox", "pants_aside", "aside", P.aside, null);
  aside.querySelector("input").onclick = e => { P.aside = e.target.checked; if(P.aside) P.mode = 0; done(); };
  inner.append(aside); wrap.append(inner);
  wrap.append(radio("pants_m", "nopan", P.mode===1, ()=>{ P.mode = 1; }));
  c.append(wrap);
}
function renderActs(c){
  ACTS.forEach(a=>{
    const o = a.k==="none" ? "none" : a.l ? {l:a.l, t:a.t||""} : a.t;
    c.append(mkOpt("radio", "act", o, S.acts.kind===a.k, ()=>{ setAct(a.k); save(); render(); generate(); }, a.k==="none"?"none":""));
    if(a.sub) c.append(subBox(a.k, S.acts.kind===a.k));
  });
}
function render(){
  const root = $("mods"); root.replaceChildren();
  MODULES.forEach(m=>{
    const c = document.createElement("div"); c.className = "mod";
    const h = document.createElement("h2"); h.textContent = m.name; c.append(h);
    if(m.custom==="acts"){ renderActs(c); root.append(c); return; }
    m.groups.forEach(g=>{
      if(g.sub){ const s = document.createElement("div"); s.className = "sub"; s.textContent = g.sub; c.append(s); }
      if(g.type==="pants"){ renderPants(c); return; }
      const v = S[g.id], dis = g.disIf && S[g.disIf] ? "dis" : "";
      if(g.type==="radio"){
        if(!g.noNone) c.append(mkOpt("radio", g.id, "none", v===-1, ()=>{S[g.id]=-1;save();generate()}, "none "+dis));
        g.opts.forEach((o,i)=> c.append(mkOpt("radio", g.id, o, v===i, ()=>{S[g.id]=i;save();generate()}, dis)));
      } else if(g.type==="check"){
        g.opts.forEach((o,i)=> c.append(mkOpt("checkbox", g.id+i, o, v.includes(i), e=>{
          const cur = S[g.id]; S[g.id] = e.target.checked ? [...cur.filter(x=>x!==i), i].sort((a,b)=>a-b) : cur.filter(x=>x!==i); save(); generate(); })));
      } else {
        const box = document.createElement("div"); box.className = "gate";
        box.append(mkOpt("checkbox", g.id+"_on", g.gate, v.on, e=>{ v.on = e.target.checked; save(); render(); generate(); }));
        const inner = document.createElement("div"); inner.className = "inner" + (v.on ? "" : " dis");
        g.opts.forEach((o,i)=> inner.append(mkOpt("radio", g.id, o, v.i===i, ()=>{ v.i = i; save(); generate(); })));
        box.append(inner); c.append(box);
      }
    });
    if(m.extra){
      const box = document.createElement("div"); box.className = "gate";
      box.append(mkOpt("checkbox", m.extra.key, m.extra.label, S[m.extra.key], e=>{ S[m.extra.key] = e.target.checked; save(); render(); generate(); }));
      box.append(note(m.extra.hint)); c.append(box);
    }
    if(m.hint) c.append(note(m.hint));
    root.append(c);
  });
}

$("copyall").onclick = function(){ copy(RESULTS.map(r=>r.text).join("\n\n"), this); };
$("reroll").onclick = generate;
$("reset").onclick = ()=>{ const loc = S.loc, surf = S.surf; S = defaults(); S.loc = loc; S.surf = surf; save(); render(); generate(); };
load(); render(); generate();

  }

  // ---------- overlay ----------
  let shell = null;
  const isOpen = () => shell && shell.style.display !== "none";
  function close() { if (shell) shell.style.display = "none"; }
  function open() {
    if (!shell) shell = buildOverlay();
    shell.style.display = "block";
  }
  function buildOverlay() {
    const host = document.createElement("div");
    host.style.cssText = "position:fixed;inset:0;z-index:2147483647;display:none";
    const sh = host.attachShadow({ mode: "closed" });
    const style = document.createElement("style"); style.textContent = TOOL_CSS;
    const x = h("button", { class: "x", "aria-label": "Close", type: "button" }, "\u2715");
    const layout = h("div", { class: "layout" },
      h("div", { class: "mods", id: "mods" }),
      h("div", { class: "outp" },
        h("div", { class: "row" },
          h("button", { class: "pri", id: "copyall", type: "button" }, "Copy all"),
          h("button", { id: "reroll", type: "button" }, "Re-roll"),
          h("button", { id: "reset", type: "button" }, "Reset"),
          h("small", { id: "cnt" })),
        h("div", { id: "outs" })));
    const panel = h("div", { class: "panel" }, x, layout);
    const back = h("div", { class: "back" }, panel);
    sh.append(style, back);
    x.onclick = close;
    back.addEventListener("mousedown", e => { if (e.target === back) close(); });
    host.addEventListener("keydown", e => e.stopPropagation());
    document.body.append(host);
    initTool(id => sh.getElementById(id));
    return host;
  }
  document.addEventListener("keydown", e => {
    if (isOpen() && e.key === "Escape") { e.stopPropagation(); e.preventDefault(); close(); }
  }, true);

  // ---------- launcher button inside the prompt box ----------
  // Anchors on stable attributes, not Tailwind classes: the bottom row is the closest
  // common ancestor of the "+" (add image) button and the generate button.
  function findRow() {
    for (const comp of document.querySelectorAll('section[data-share-guide="composer"]')) {
      if (!comp.getClientRects().length) continue;               // skip hidden copies
      const add = comp.querySelector('[data-role="add-image"]');
      const gen = comp.querySelector('[data-share-guide="generate"]');
      if (!add || !gen) continue;
      let el = add.parentElement;
      while (el && el !== comp && !el.contains(gen)) el = el.parentElement;
      if (el && el !== comp) return el;
    }
    return null;
  }
  let btnHost = null;
  function ensureButton() {
    const row = findRow();
    if (!row) return;
    if (!btnHost) {
      btnHost = document.createElement("div");
      const sh = btnHost.attachShadow({ mode: "closed" });
      const st = document.createElement("style"); st.textContent = BTN_CSS;
      const b = h("button", { type: "button" }, "Prompt Gen");
      b.onclick = open;
      sh.append(st, b);
    }
    if (btnHost.parentElement !== row) {
      if (getComputedStyle(row).position === "static") row.style.position = "relative";
      row.append(btnHost);
    }
  }
  let queued = false;
  new MutationObserver(() => {
    if (queued) return;
    queued = true;
    requestAnimationFrame(() => { queued = false; ensureButton(); });
  }).observe(document.body, { childList: true, subtree: true });
  ensureButton();
})();