Sleazy Fork is available in English.
Prompt builder overlay. Manual copy only: it never reads from or writes to the site's prompt editor.
// ==UserScript==
// @name PixAI Prompt Generator
// @namespace promptgen
// @version 1.7.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}
.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-fit,minmax(230px,1fr));gap:10px;align-items:start}
.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}
.col{display:flex;flex-direction:column;gap:10px;min-width:0}
.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}
.gate .hint{margin-bottom:4px}
.outp{position:sticky;top:12px;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px;max-height:calc(100vh - 56px);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}
.res.locked{border-color:#4ac1ff;box-shadow:0 0 0 1px #4ac1ff}
.res.clip{border-color:#ffc765;box-shadow:0 0 0 1px #ffc765}
.star{background:none;border:none;padding:0 6px 0 0;font-size:16px;line-height:1;color:#8a8a9c;cursor:pointer}
.star.on{color:#4ac1ff}
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:6px;align-items:center;flex-wrap:wrap}.row small{color:var(--mut);margin-left:auto}
select{font:inherit;color:var(--tx);background:var(--card);border:1px solid var(--line);border-radius:6px;padding:4px 6px;min-width:0;flex:1}
.row select{min-width:120px}
.pset{display:flex;gap:6px;align-items:center}
.txt{width:100%;font:inherit;color:var(--tx);background:var(--card);border:1px solid var(--line);border-radius:6px;padding:5px 8px}
.clist{margin-top:6px}.crow{display:flex;align-items:center;gap:6px}
.crow label.o{flex:1;min-width:0;word-break:break-word}.crow .star{padding:0 2px}
.sym{padding:2px 9px;font-size:15px;line-height:1.3}
.sws{display:flex;gap:6px;margin:6px 0 6px 24px}
.sym.sw{font-size:17px;padding:2px 10px}
.sym.night{color:#8a8a9c}.sym.night.on{color:#fefdb2}
.sym.wx0{color:#ffe400}.sym.wx1{color:#4aa3ff}.sym.wx2{color:#fff;text-shadow:0 0 2px rgba(0,0,0,.65)}
.sym.soft{color:#8a8a9c}.sym.soft.on{color:#42ff00}
.sym.ld{color:#00ff00}.sym.sv{color:#ff0000}.sym.dl{color:#ff0096}.sym.rd{color:#ffc765}.sym.rr{color:#ffe400}
.back{position:fixed;inset:0;background:rgba(0,0,0,.6);overflow:auto;overscroll-behavior:contain;padding:8px;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:12px;width:100%;max-width:1360px}
.layout{grid-template-columns:minmax(0,1fr) minmax(0,520px) 32px}
.xcol{position:relative}
.x{position:sticky;top:8px;width:32px;height:32px;padding:0;font-size:16px;line-height:1}
@media (max-width:900px){.layout{grid-template-columns:1fr}.xcol{order:-2;display:flex;justify-content:flex-end}}
`;
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 = hands => "very aesthetic, absurdres, detailed background, newest, intricate details, scenery, blurry background, masterpiece, best quality, detailed illustration, shiny skin, " + hands + ", 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 GENITAL_TARGETS = ["pussy","ass"]; // choosing one of these auto-enables "genital detail"
const AUTO_BOY = ["pen","titjob","handjob"]; // selecting these ticks "Faceless nude male"
const MODULES = [
{name:"Presets", custom:"presets"},
{name:"Custom tags", custom:"custom"},
{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:"girl_extra", type:"check", opts:["undressing","wind lift"]},
{id:"boy", sub:"boy", type:"check", opts:[
{l:"faceless nude male", t:"1boy, faceless male, nude male, penis"}]},
]},
{name:"Quality", hint:"For users of Perfect Hands LoRA", groups:[
{id:"ph", type:"check", def:[0], opts:[{l:"perfect hands user", t:""}]}]},
{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","v-shaped eyebrows","tears","satisfied","afterglow","saliva",{l:"spoken heart", t:"spoken_heart"}]},
]},
{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","cum on ass","cum on back"]},
]},
{name:"Camera", hint:"none ticked = no camera tags", extras:[
{key:"mClose", label:"multi prompt close-up focus", hint:"4 prompts: pov / pov + dutch angle / close-up / straight-on (each + looking at viewer). Ignores the ticks above except internal."},
{key:"mAll", label:"multi prompt all", hint:"14 prompts: every angle, plain and with dutch angle. Ignores the ticks above except internal."}],
groups:[{id:"cam", type:"check", opts:[
"pov","dutch angle","close-up","straight-on","from side","from below","from behind","from above","looking at viewer","internal","pantyshot"]}]},
{name:"Location", groups:[
{id:"loc", type:"select", onKey:"locOn", def:0, opts:[
{l:"inn", t:"indoors, inn, bedroom, window, (intricate:1.1)"},
{l:"palace", t:"indoors, palace, (intricate:1.1)"},
{l:"church", t:"indoors, church, (intricate:1.1)"},
{l:"ruins", t:"indoors, ruins, mountains, above clouds, (intricate:1.1)"}]},
{id:"surf", type:"surface", opts:["on bed","on floor","on bench","on couch","on chair","on table"]}]},
];
// column layout: module names per column, top to bottom (edit to rearrange)
const LAYOUT = [
["Presets","Custom tags","Characters","Camera"],
["Location","Acts","Sensation"],
["Positions","Arms","Cum & Defloration","Quality"],
];
// multi-prompt camera sets ("internal" is added to each when it is ticked)
const CLOSE_SETS = [["pov","looking at viewer"],["pov","dutch angle","looking at viewer"],["close-up","looking at viewer"],["straight-on","looking at viewer"]];
const ALL_SETS = [
["pov","looking at viewer"],["close-up","looking at viewer"],["from below"],["from behind"],["from above"],["straight-on","looking at viewer"],["from side"],
["pov","dutch angle","looking at viewer"],["close-up","dutch angle","looking at viewer"],["from below","dutch angle"],["from behind","dutch angle"],["from above","dutch angle"],["straight-on","dutch angle","looking at viewer"],["from side","dutch angle"]];
// ===== state =====
const KEY = "promptgen.v4", PKEY = "promptgen.presets.v1", LKEY = "promptgen.locksaves.v1";
// KV (Tampermonkey storage)
const kv = {
get(k, d){ try{ if(typeof GM_getValue==="function"){ const v = GM_getValue(k, null); return v==null ? d : JSON.parse(v); } }catch(e){} return d; },
set(k, v){ try{ if(typeof GM_setValue==="function") GM_setValue(k, JSON.stringify(v)); }catch(e){} },
};
const GROUPS = MODULES.flatMap(m => m.groups || []), GM = Object.fromEntries(GROUPS.map(g=>[g.id,g]));
let S = {}, PINNED = [], FRESH = []; // PINNED: locked + clipboard prompts, FRESH: newly generated prompts
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, mClose:false, mAll:false, locOn:true, night:false, wx:0, soft:false, locMem:{}, locv:3, camv:2, sensv:2, custom:{text:"", items:[]}, acts:{kind:"none", pen:{t:0,x:0}, dil:{t:0,x:0}}};
GROUPS.forEach(g => d[g.id] = (g.type==="radio" || g.type==="select") ? (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;
}
let PRESETS = [], LSAVES = [], loadedPreset = null, loadedLock = null, presetSelId = "", lockSelId = "";
// saved states keep option positions, so shifts caused by inserted options are migrated here
function migrate(sv){
sv = JSON.parse(JSON.stringify(sv || {}));
if(!sv.camv && Array.isArray(sv.cam)) sv.cam = sv.cam.map(i => i>=4 ? i+1 : i); // "from side" inserted at 4
if(!sv.sensv && Array.isArray(sv.sens)) sv.sens = sv.sens.map(i => i>=8 ? i+1 : i); // "v-shaped eyebrows" inserted at 8
if(!sv.locv && typeof sv.loc === "number") sv.loc = sv.loc === 0 ? 0 : sv.loc + 1; // scenery list changed: bedroom -> inn, church/clouds shifted
if((sv.locv || 0) < 3 && "locDay" in sv){ sv.night = !sv.locDay; sv.soft = !sv.locDay; delete sv.locDay; } // old day/night toggle -> night + soft light
sv.camv = 2; sv.sensv = 2; sv.locv = 3; return sv;
}
const newest = list => list.length ? String(Math.max(...list.map(x=>x.id))) : "";
function load(){
S = Object.assign(defaults(), migrate(kv.get(KEY, {})));
if(S.locOn && !S.locMem[String(S.loc)]) saveLive(); // seed the scenery memory from the current settings
PRESETS = kv.get(PKEY, []); LSAVES = kv.get(LKEY, []);
presetSelId = newest(PRESETS); lockSelId = newest(LSAVES);
}
function save(){ kv.set(KEY, S); }
// ----- location memory: per scenery, one saved state for day and one for night (weather, soft light, surface) -----
// live settings = S.night / S.wx / S.soft / S.surf; while the scenery checkbox is on they are written through to S.locMem[scenery]
const newSub = night => ({wx: 0, soft: night, surf: {on: false, i: 0}}); // night starts with soft light on (not a lock)
const memoOf = idx => S.locMem[String(idx)] || (S.locMem[String(idx)] = {night: false, d: newSub(false), n: newSub(true)});
function saveLive(){
const m = memoOf(S.loc); m.night = S.night;
m[S.night ? "n" : "d"] = {wx: S.wx, soft: S.soft, surf: {on: S.surf.on, i: S.surf.i}};
}
function loadLive(idx){ // restore the full last-used state of a scenery
const m = memoOf(idx); S.night = m.night;
const sub = m[S.night ? "n" : "d"]; S.wx = sub.wx; S.soft = sub.soft; S.surf = {on: sub.surf.on, i: sub.surf.i};
}
function commitLoc(){ if(S.locOn) saveLive(); save(); render(); generate(); }
function toggleNight(){
if(S.locOn){
saveLive();
const m = memoOf(S.loc); m.night = !m.night; S.night = m.night;
const sub = m[S.night ? "n" : "d"]; S.wx = sub.wx; S.soft = sub.soft; S.surf = {on: sub.surf.on, i: sub.surf.i};
} else { S.night = !S.night; S.soft = S.night; } // no scenery: nothing is tracked, soft light just follows night
save(); render(); generate();
}
// ===== 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 shuffle = a => a.map(x=>[Math.random(),x]).sort((p,q)=>p[0]-q[0]).map(p=>p[1]);
const radioText = id => S[id] >= 0 ? txt(GM[id].opts[S[id]]) : "";
const sel = id => GM[id].opts.filter((_,i)=>S[id].includes(i));
// seeding groups: when one member is seeded, the rest of its group follows together with chance p;
// otherwise the rest is left out of that prompt (so the check can never be retriggered by a later reseed).
// A group fully contained in a larger active group is muted: with all three ticked only the 3-tag rule runs.
const AFFINITY = [
{tags:["surprised","wink"], p:2/3},
{tags:["surprised","wink","spoken_heart"], p:2/3},
];
function activeGroups(pool){
const act = AFFINITY.filter(g => g.tags.every(t => pool.includes(t)));
return act.filter(g => !act.some(h => h !== g && h.tags.length > g.tags.length && g.tags.every(t => h.tags.includes(t))));
}
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 = shuffle(pool), groups = activeGroups(pool);
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);
const g = groups.find(x => x.tags.includes(t));
if(g){
const rest = g.tags.filter(x => x !== t && !out.has(x));
if(rest.length){ if(Math.random() < g.p) rest.forEach(x => out.add(x)); else rest.forEach(x => blocked.add(x)); }
}
}
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");
const target = includeFull ? count-1 : count;
if(includeFull) seen.add(key(pool));
for(let t=0; out.length<target && t<300; t++){
const x = randomSubset(pool); if(seen.has(key(x))) continue;
seen.add(key(x)); out.push(x);
}
if(!strict) while(out.length<target) out.push(randomSubset(pool));
if(includeFull) out.splice(Math.floor(Math.random()*(out.length+1)), 0, pool.slice()); // the "everything" prompt can land anywhere
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];
if(prev !== k && SUBS[k] && GENITAL_TARGETS.includes(SUBS[k].target[S.acts[k].x]) && !S.girl_detail.includes(0)) S.girl_detail = [0];
}
function generate(){
const girl = [radioText("girl_state"), ...pantsTags(), ...sel("girl_detail").map(txt), ...sel("girl_extra").map(txt)];
const boy = sel("boy").map(txt), pos = radioText("pos"), legs = sel("legs").map(txt), act = actText(), loc = S.locOn ? radioText("loc") : "", surf = S.surf.on ? txt(GM.surf.opts[S.surf.i]) : "";
const dog = pos === "doggystyle", cycleOn = S.cycle && !dog; // doggystyle: arms forced to none, cycle ignored
const armsAll = GM.arms.opts.map(txt), armFixed = dog ? "" : 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 camOpts = GM.cam.opts.map(txt);
const tail = S.cam.includes(camOpts.indexOf("internal")) ? ["internal"] : []; // the only manual camera tick the multi modes honor
const multi = S.mClose || S.mAll;
const cams = multi ? [] : sel("cam").map(txt); // none ticked = no camera tags
const mixFor = (n, strict) => S.mix ? subsetVariants(pool, n, true, strict) : null;
const specs = [];
if(!multi){
if(cycleOn){
const subs = mixFor(4, false);
armsAll.forEach((a,i)=> specs.push({cam:cams, arms:a, sub: subs?subs[i]:pool, mixed:!!subs, armPick:true}));
} else if(S.mix){
mixFor(4, true).forEach(x=> specs.push({cam:cams, arms:armFixed, sub:x, mixed:true}));
} else specs.push({cam:cams, arms:armFixed, sub:pool});
} else {
const group = (label, sets) => {
const n = sets.length, subs = mixFor(n, false);
let arms = null;
if(cycleOn){ // as even as possible across the 4 states, randomly placed
const order = shuffle(armsAll); arms = [];
for(let i=0;i<n;i++) arms.push(order[i % order.length]);
arms = shuffle(arms);
}
sets.forEach((cam,i)=> specs.push({cam:[...cam, ...tail], arms: arms ? arms[i] : armFixed, sub: subs?subs[i]:pool, mixed:!!subs, armPick:cycleOn, group:label}));
};
if(S.mClose) group("close-up focus", CLOSE_SETS);
if(S.mAll) group("all", ALL_SETS);
}
FRESH = 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, S.night ? "night" : "day", ["light rays","rain","snow"][S.wx], S.soft ? "soft light" : "", surf].filter(Boolean).join(", ")),
fmt(S.custom.items.filter(x => x.on).map(x => x.tag).join(", ")),
fmt(SUFFIX(S.ph.includes(0) ? "(detailed_hands:1.5)" : "detailed_hands")),
].filter(Boolean);
const meta = [];
if(p.group) meta.push(p.group + ": " + (p.cam.join(", ") || "no camera"));
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);
}
const stamp = () => {
const d = new Date(), z = n => String(n).padStart(2, "0");
return d.getFullYear() + z(d.getMonth()+1) + z(d.getDate()) + " " + z(d.getHours()) + "-" + z(d.getMinutes()) + "-" + z(d.getSeconds());
};
const canon = v => JSON.stringify(v, (k, x) => (x && typeof x === "object" && !Array.isArray(x)) ? Object.keys(x).sort().reduce((o, k2) => (o[k2] = x[k2], o), {}) : x);
const uid = list => { let id = Date.now(); while(list.some(p => p.id === id)) id++; return id; };
function renderOut(){
normPinned();
const box = $("outs"); box.replaceChildren();
const card = (p, label, pinned, i) => {
const d = document.createElement("div");
d.className = "res" + (p.locked ? " locked" : p.src === "clip" ? " clip" : "");
const top = document.createElement("div"); top.className = "top";
const st = document.createElement("button"); st.type = "button"; st.className = "star" + (p.locked ? " on" : "");
st.textContent = "★"; st.title = p.locked ? "unlock" : "lock"; st.onclick = () => toggleLock(p, pinned, i);
const m = document.createElement("span"); m.textContent = label;
const b = document.createElement("button"); b.className = "sm"; b.textContent = "copy"; b.onclick = () => copy(p.text, b);
top.append(st, m, b);
const pre = document.createElement("pre"); pre.textContent = p.text;
d.append(top, pre); box.append(d);
};
PINNED.forEach((p,i) => card(p, (p.locked ? "locked" : "clipboard") + (p.meta ? " · " + p.meta : ""), true, i));
FRESH.forEach((p,i) => card(p, "#" + (i+1) + (p.meta ? " · " + p.meta : ""), false, i));
const n = PINNED.length + FRESH.length, c = $("cnt");
if(c) c.textContent = n + (n === 1 ? " prompt" : " prompts");
}
// the unlocked clipboard prompt always owns the very top slot; locked prompts follow in the order they were locked
function normPinned(){
const isClip = p => p.src === "clip" && !p.locked;
PINNED = [...PINNED.filter(isClip), ...PINNED.filter(p => !isClip(p))];
}
// lock: a locked prompt moves to the top (newest lock below older ones); the fresh set is then rolled again so its size stays the same
function toggleLock(p, pinned, i){
if(pinned){
PINNED.splice(i, 1);
if(p.locked){ p.locked = false; p.src = "gen"; FRESH.unshift(p); } // unlock: ordinary prompt again until the next roll
else { p.locked = true; PINNED.push(p); } // locking a clipboard prompt frees the clipboard slot
renderOut();
} else {
FRESH.splice(i, 1); p.locked = true; p.src = "gen"; PINNED.push(p);
generate();
}
}
// ----- selects / small helpers -----
function fillSel(sel, list, cur){
if(!sel) return;
sel.replaceChildren();
const add = (label, val, on, off) => { const o = document.createElement("option"); o.textContent = label; o.value = val; o.selected = !!on; o.disabled = !!off; sel.append(o); };
if(!list.length){ add("no saves yet", "", true, true); return; }
[...list].sort((a,b) => b.id - a.id).forEach(x => add(x.name, String(x.id), String(x.id) === String(cur)));
}
function refreshSelects(){ fillSel($("lockSel"), LSAVES, lockSelId); fillSel($("presetSel"), PRESETS, presetSelId); }
function sym(id, ch, cls, title, fn){
const b = document.createElement("button"); b.type = "button"; b.id = id; b.className = "sym " + cls; b.textContent = ch; b.title = title; b.onclick = fn; return b;
}
// ----- presets: the whole page = selections + generated prompts -----
function actLabel(){
const a = S.acts, k = a.kind;
if(k === "none") return "";
if(SUBS[k]) return (k === "dil" ? "dildo play " : "") + SUBS[k].timing[a[k].t] + " " + SUBS[k].target[a[k].x];
const o = ACTS.find(x => x.k === k); return o.l || o.t;
}
function presetName(){
const pos = S.pos >= 0 ? lab(GM.pos.opts[S.pos]) : "";
const tail = [pos, actLabel()].filter(Boolean).join(" ").replace(/,/g, "");
return stamp() + (tail ? " - " + tail : "");
}
const snapshot = () => ({state: JSON.parse(JSON.stringify(S)), prompts: FRESH.map(p => ({text: p.text, meta: p.meta}))});
const sameSnap = (a, b) => canon(a.state) === canon(b.state) && a.prompts.length === b.prompts.length && a.prompts.every((p,i) => p.text === b.prompts[i].text);
function savePreset(){
const snap = snapshot(), cur = PRESETS.find(p => p.id === loadedPreset);
if(cur && sameSnap(cur, snap)) return; // identical to the loaded preset: silently ignored
const p = Object.assign({id: uid(PRESETS), name: presetName()}, snap);
PRESETS.push(p); kv.set(PKEY, PRESETS); loadedPreset = p.id; presetSelId = String(p.id); refreshSelects();
}
function loadPreset(){
const el = $("presetSel"), id = (el && el.value) || presetSelId;
const p = PRESETS.find(x => String(x.id) === String(id)); if(!p) return;
S = Object.assign(defaults(), migrate(p.state)); FRESH = p.prompts.map(x => ({text: x.text, meta: x.meta}));
loadedPreset = p.id; presetSelId = String(p.id); save(); render(); renderOut();
}
// ----- saves for locked / clipboard prompts -----
const pinSnap = () => PINNED.map(p => ({text: p.text, meta: p.meta, locked: p.locked, src: p.src}));
function saveLocks(){
if(!PINNED.length) return;
const items = pinSnap(), cur = LSAVES.find(x => x.id === loadedLock);
if(cur && canon(cur.items) === canon(items)) return; // identical to the loaded save: silently ignored
const x = {id: uid(LSAVES), name: stamp(), items};
LSAVES.push(x); kv.set(LKEY, LSAVES); loadedLock = x.id; lockSelId = String(x.id); refreshSelects();
}
function loadLocks(){
const el = $("lockSel"), id = (el && el.value) || lockSelId;
const x = LSAVES.find(v => String(v.id) === String(id)); if(!x) return;
PINNED = x.items.map(i => Object.assign({}, i)); loadedLock = x.id; lockSelId = String(x.id); renderOut();
}
function deletePreset(){
const el = $("presetSel"), id = (el && el.value) || presetSelId;
const i = PRESETS.findIndex(x => String(x.id) === String(id)); if(i < 0) return;
const gone = PRESETS.splice(i, 1)[0]; if(loadedPreset === gone.id) loadedPreset = null;
kv.set(PKEY, PRESETS); presetSelId = newest(PRESETS); refreshSelects();
}
function deleteLocks(){
const el = $("lockSel"), id = (el && el.value) || lockSelId;
const i = LSAVES.findIndex(x => String(x.id) === String(id)); if(i < 0) return;
const gone = LSAVES.splice(i, 1)[0]; if(loadedLock === gone.id) loadedLock = null;
kv.set(LKEY, LSAVES); lockSelId = newest(LSAVES); refreshSelects();
}
// ----- prompt reader: clipboard text -> interface state + a pinned clipboard prompt -----
const norm = s => s.split(",").map(x => x.trim().toLowerCase()).filter(Boolean);
const hasAll = (T, tags) => { const n = norm(tags); return n.length > 0 && n.every(x => T.has(x)); };
const best = (cands, T) => { // full match with the most distinct tags wins
let top = null, topN = 0;
cands.forEach(c => { const n = new Set(norm(c.tags)).size; if(n > topN && hasAll(T, c.tags)){ top = c; topN = n; } });
return top;
};
function applyTokens(T){
const radio = (id, fallback) => { const c = best(GM[id].opts.map((o,i) => ({i, tags: txt(o)})), T); S[id] = c ? c.i : fallback; };
const checks = id => { S[id] = GM[id].opts.map((o,i) => hasAll(T, txt(o)) ? i : -1).filter(i => i >= 0); };
radio("girl_state", 0); radio("pos", -1);
const lc = best(GM.loc.opts.map((o,i) => ({i, tags: txt(o)})), T); S.locOn = !!lc; if(lc) S.loc = lc.i; // no scenery in the prompt -> location off
["girl_detail","girl_extra","boy","legs","sens","cam","cumx","defl"].forEach(checks);
const dog = S.pos >= 0 && txt(GM.pos.opts[S.pos]) === "doggystyle"; // doggystyle keeps forcing arms to none
const a = best(GM.arms.opts.map((o,i) => ({i, tags: txt(o)})), T); S.arms = dog ? -1 : (a ? a.i : -1);
// act (one choice incl. sub modules); entering it applies the usual "faceless nude male" auto-enable rule
const cands = [];
ACTS.forEach(x => { if(x.sub) SUBS[x.k].tags.forEach((row,t) => row.forEach((tags,xi) => cands.push({k: x.k, t, x: xi, tags}))); else if(x.t) cands.push({k: x.k, tags: x.t}); });
const pick = best(cands, T);
S.acts.kind = "none";
if(pick){ if(pick.t !== undefined){ S.acts[pick.k].t = pick.t; S.acts[pick.k].x = pick.x; } setAct(pick.k); }
// panties
const P = S.pants;
if(T.has("no panties") || T.has("nopan")){ P.mode = 1; P.aside = false; }
else {
const w = hasAll(T, "panties, lace_panties, white panties"), b = hasAll(T, "panties, lace_panties, black panties"), as = T.has("panties aside");
if(w || b || as){ P.mode = 0; if(w) P.color = 0; if(b) P.color = 1; P.aside = as; }
}
// surface, cum, quality
const si = GM.surf.opts.findIndex(o => T.has(txt(o))); S.surf.on = si >= 0; if(si >= 0) S.surf.i = si;
if(T.has("night")) S.night = true; else if(T.has("day")) S.night = false;
if(T.has("snow")) S.wx = 2; else if(T.has("rain")) S.wx = 1; else if(T.has("light rays")) S.wx = 0;
S.soft = T.has("soft light");
if(S.locOn) saveLive();
S.cum.on = false;
if(hasAll(T, "cum, cum overflow, ejaculation")){ S.cum.on = true; S.cum.i = 1; } else if(T.has("precum")){ S.cum.on = true; S.cum.i = 0; }
if(T.has("(detailed_hands:1.5)")) S.ph = [0]; else if(T.has("detailed_hands")) S.ph = [];
S.cycle = S.mix = S.mClose = S.mAll = false; // a single prompt: no multi-prompt modes
}
function importPrompt(text){
applyTokens(new Set(text.split(/[,\n]+/).map(t => t.trim().toLowerCase()).filter(Boolean)));
const entry = {text, meta: "", locked: false, src: "clip"};
const k = PINNED.findIndex(p => p.src === "clip" && !p.locked); // one clipboard slot: overwrite it
if(k >= 0) PINNED[k] = entry; else PINNED.push(entry);
save(); render(); generate();
}
async function readClip(){
let text = "";
try{ text = await navigator.clipboard.readText(); }
catch(e){ text = window.prompt("Could not read the clipboard automatically. Paste the prompt here:") || ""; }
text = (text || "").trim(); if(text) importPrompt(text);
}
// ----- prompts header -----
function buildHeader(){
const hdr = $("hdr"); hdr.replaceChildren();
const sel = document.createElement("select"); sel.id = "lockSel"; sel.title = "saved locked prompts";
sel.onchange = () => { lockSelId = sel.value; };
const cnt = document.createElement("small"); cnt.id = "cnt";
hdr.append(sel,
sym("lockLoad", "▲", "ld", "load the selected locked-prompt save", loadLocks),
sym("lockSave", "▼", "sv", "save the locked prompts", saveLocks),
sym("lockDel", "∅", "dl", "delete the selected locked-prompt save", deleteLocks),
sym("lockClear", "⚡", "", "clear all locked and clipboard prompts", () => { PINNED = []; renderOut(); }),
sym("readClip", "☉", "rd", "read a prompt from the clipboard", readClip),
sym("reset", "❌", "", "reset selections (all location settings stay)", () => { const keep = {loc: S.loc, locOn: S.locOn, night: S.night, wx: S.wx, soft: S.soft, surf: S.surf, locMem: S.locMem}; S = Object.assign(defaults(), keep); save(); render(); generate(); }),
sym("reroll", "↻", "rr", "reroll", () => generate()),
cnt);
fillSel(sel, LSAVES, lockSelId);
}
// ----- custom tags: a tag is committed by a trailing comma; stars keep a tag when it leaves the text line -----
const ckey = t => t.toLowerCase();
function parseCommitted(text){ // only tags followed by a comma; the tail after the last comma is still being typed
const i = text.lastIndexOf(",");
if(i < 0) return [];
const seen = new Set(), out = [];
text.slice(0, i).split(",").map(x => x.trim()).filter(Boolean).forEach(t => { const k = ckey(t); if(!seen.has(k)){ seen.add(k); out.push(t); } });
return out;
}
function syncCustom(){
const C = S.custom, live = parseCommitted(C.text), liveKeys = new Set(live.map(ckey));
live.forEach(t => { if(!C.items.some(x => ckey(x.tag) === ckey(t))) C.items.push({tag: t, on: true, star: false}); });
C.items = C.items.filter(x => x.star || liveKeys.has(ckey(x.tag))); // unstarred tags vanish with their text
}
function renderCustom(c){
const input = document.createElement("input"); input.type = "text"; input.id = "customIn"; input.className = "txt";
input.placeholder = "tag one, tag two,"; input.value = S.custom.text; input.autocomplete = "off"; input.spellcheck = false;
const list = document.createElement("div"); list.id = "customList"; list.className = "clist";
const draw = () => {
list.replaceChildren();
S.custom.items.forEach((it, i) => {
const row = document.createElement("div"); row.className = "crow";
const l = document.createElement("label"); l.className = "o";
const cb = document.createElement("input"); cb.type = "checkbox"; cb.name = "cust" + i; cb.checked = it.on;
cb.onchange = () => { it.on = cb.checked; save(); generate(); };
const sp = document.createElement("span"); sp.textContent = it.tag;
l.append(cb, sp);
const st = document.createElement("button"); st.type = "button"; st.className = "star" + (it.star ? " on" : ""); st.textContent = "★";
st.title = it.star ? "unlock: removed once the tag is gone from the text line" : "lock: keep this tag even if it is removed from the text line";
st.onclick = () => { it.star = !it.star; syncCustom(); save(); draw(); generate(); };
row.append(l, st); list.append(row);
});
};
input.oninput = () => { S.custom.text = input.value; syncCustom(); save(); draw(); generate(); }; // no full re-render: typing keeps focus
c.append(input, note("finish a tag with a comma to add it; ★ keeps it"), list); draw();
}
function renderPresets(c){
const row = document.createElement("div"); row.className = "pset";
const sel = document.createElement("select"); sel.id = "presetSel"; sel.title = "saved presets";
sel.onchange = () => { presetSelId = sel.value; };
row.append(sel, sym("presetLoad", "▲", "ld", "load the selected preset (selections + prompts)", loadPreset), sym("presetSave", "▼", "sv", "save this page (selections + prompts)", savePreset), sym("presetDel", "∅", "dl", "delete the selected preset", deletePreset));
c.append(row); fillSel(sel, PRESETS, presetSelId);
}
// ===== 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); if(key === "x" && GENITAL_TARGETS.includes(sp.target[i]) && !S.girl_detail.includes(0)) S.girl_detail = [0]; 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 renderLoc(c, g){ // scenery dropdown + checkbox, and the three switches underneath
const row = document.createElement("div"); row.className = "pset";
const cb = document.createElement("input"); cb.type = "checkbox"; cb.name = "loc_use"; cb.checked = !!S.locOn; cb.title = "use a location";
cb.onchange = () => { S.locOn = cb.checked; if(S.locOn) loadLive(S.loc); save(); render(); generate(); }; // switching back on restores the last used state
const sel = document.createElement("select"); sel.id = "locSel"; sel.disabled = !S.locOn;
g.opts.forEach((o,i) => { const op = document.createElement("option"); op.textContent = lab(o); op.value = String(i); op.selected = S.loc === i; sel.append(op); });
sel.onchange = () => { if(S.locOn) saveLive(); S.loc = +sel.value; if(S.locOn) loadLive(S.loc); save(); render(); generate(); };
row.append(cb, sel); c.append(row);
const mk = (cls, ch, title, fn) => { const b = document.createElement("button"); b.type = "button"; b.className = "sym sw " + cls; b.textContent = ch; b.title = title; b.onclick = fn; return b; };
const WX = [["☀", "light rays"], ["☂", "rain"], ["❆", "snow"]];
const sw = document.createElement("div"); sw.className = "sws";
sw.append(
mk("night" + (S.night ? " on" : ""), "☾", S.night ? "night (adds night,): click for day" : "day (adds day,): click for night", toggleNight),
mk("wx" + S.wx, WX[S.wx][0], "weather: " + WX[S.wx][1] + " (click to cycle light rays / rain / snow)", () => { S.wx = (S.wx + 1) % 3; commitLoc(); }),
mk("soft" + (S.soft ? " on" : ""), "①", S.soft ? "soft light on" : "soft light off", () => { S.soft = !S.soft; commitLoc(); }));
c.append(sw);
}
function renderSurf(c, g){ // surface dropdown + checkbox
const row = document.createElement("div"); row.className = "pset";
const cb = document.createElement("input"); cb.type = "checkbox"; cb.name = "surf_on"; cb.checked = !!S.surf.on; cb.title = "use a surface";
cb.onchange = () => { S.surf.on = cb.checked; commitLoc(); };
const sel = document.createElement("select"); sel.id = "surfSel"; sel.disabled = !S.surf.on;
g.opts.forEach((o,i) => { const op = document.createElement("option"); op.textContent = lab(o); op.value = String(i); op.selected = S.surf.i === i; sel.append(op); });
sel.onchange = () => { S.surf.i = +sel.value; if(S.locOn) saveLive(); save(); generate(); };
row.append(cb, sel); c.append(row);
}
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();
const cards = {};
const place = (m, c) => { cards[m.name] = c; };
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); place(m, c); return; }
if(m.custom==="presets"){ renderPresets(c); place(m, c); return; }
if(m.custom==="custom"){ renderCustom(c); place(m, 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; }
if(g.type==="select"){ renderLoc(c, g); return; }
if(g.type==="surface"){ renderSurf(c, g); return; }
const locked = g.id === "arms" && radioText("pos") === "doggystyle"; // doggystyle forces arms to none
const v = locked ? -1 : S[g.id], dis = (locked || (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();render();generate()}, "none "+dis));
g.opts.forEach((o,i)=> c.append(mkOpt("radio", g.id, o, v===i, ()=>{S[g.id]=i;save();render();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(); }, (g.id==="cam" && (S.mClose||S.mAll) && txt(o)!=="internal") ? "dis" : "")));
} 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);
}
});
const extras = m.extras || (m.extra ? [m.extra] : []);
if(extras.length){
const lockedX = m.name === "Arms" && radioText("pos") === "doggystyle";
const box = document.createElement("div"); box.className = "gate" + (lockedX ? " dis" : "");
extras.forEach(ex=>{
box.append(mkOpt("checkbox", ex.key, ex.label, S[ex.key], e=>{ S[ex.key] = e.target.checked; save(); render(); generate(); }));
box.append(note(lockedX ? "locked: doggystyle forces arms to none" : ex.hint));
});
c.append(box);
}
if(m.hint) c.append(note(m.hint));
place(m, c);
});
const placed = new Set(LAYOUT.flat());
LAYOUT.forEach((names, k)=>{
const col = document.createElement("div"); col.className = "col";
names.forEach(n => cards[n] && col.append(cards[n]));
if(k === LAYOUT.length-1) MODULES.forEach(m => { if(!placed.has(m.name)) col.append(cards[m.name]); }); // anything not listed goes last
root.append(col);
});
}
load(); buildHeader(); 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", id: "hdr" }),
h("div", { id: "outs" })),
h("div", { class: "xcol" }, x));
const panel = h("div", { class: "panel" }, 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();
})();