☰

Prompt Gen

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

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==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();
})();