Files

4419 lines
241 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const LIME = "#4dff6e";
const C = {
lime:LIME, bg0:"#0b0b0b", bg1:"#111111", bg2:"#181818",
bg3:"#222222", border:"#2a2a2a", borderH:"#3c3c3c",
text:"#dedede", muted:"#565656", dim:"#2e2e2e",
warn:"#ffb347", err:"#ff6767",
};
const NODE_W = 980;
const NODE_H = Math.round(NODE_W * 9 / 16);
// ── Workflow node IDs ─────────────────────────────────────────────────────────
// Shared by both t2i_workflow.json and edit_workflow.json
const WF = {
model: "FK:165", // UNETLoader
kvCache: null, // No KV cache for Z-Image
textEnc: "FK:155", // CLIPLoader
vae: "FK:153", // VAELoader
promptPos: "FK:166", // CLIPTextEncode positive
promptNeg: "FK:156", // CLIPTextEncode negative
sampling: "FK:169", // ModelSamplingAuraFlow — receives model input
latent: "FK:170", // EmptySD3LatentImage (T2I) — width/height set here
sampler: "FK:171", // KSampler — receives seed
saveImage: "FK:86", // SaveImage
};
const LS_KEY = "one_node_z_image_state";
const DEFAULT_NEG_PROMPT = "low quality, deformed, blurry, watermark, ugly, bad anatomy, disfigured, mutated, extra limbs, poorly drawn face, bad proportions, gross proportions, jpeg artifacts, overexposed, underexposed";
// ── Resolution presets (Z-Image-friendly, divisible by 16) ──────────────────────
const RES_PRESETS = [
{ label:"1024 × 1024", w:1024, h:1024 },
{ label:"1920 × 1088", w:1920, h:1088 },
{ label:"1088 × 1920", w:1088, h:1920 },
{ label:"1280 × 720", w:1280, h:720 },
{ label:"720 × 1280", w:720, h:1280 },
{ label:"Custom…", w:0, h:0 },
];
function snapRes(v){ return Math.max(16, Math.round(v/16)*16); }
// ── DOM helpers ───────────────────────────────────────────────────────────────
const mk = (tag,css={},props={}) => { const e=document.createElement(tag); Object.assign(e.style,css); Object.assign(e,props); return e; };
const tx = (e,t) => { e.textContent=t; return e; };
const cap = (t) => tx(mk("div",{fontSize:"9px",fontWeight:"700",letterSpacing:".1em",
textTransform:"uppercase",color:C.muted,marginBottom:"5px"}),t);
// ── Notification sound ────────────────────────────────────────────────────────
function playDone(){
try{
const AC=window.AudioContext||/** @type {any} */(window).webkitAudioContext;
const ctx=new AC();
// Two soft sine tones: a gentle rising chime
[[660,0,0.09],[990,0.1,0.07]].forEach(([freq,delay,vol])=>{
const osc=ctx.createOscillator(),gain=ctx.createGain();
osc.connect(gain);gain.connect(ctx.destination);
osc.type="sine";osc.frequency.value=freq;
gain.gain.setValueAtTime(0,ctx.currentTime+delay);
gain.gain.linearRampToValueAtTime(vol,ctx.currentTime+delay+0.03);
gain.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+delay+0.55);
osc.start(ctx.currentTime+delay);osc.stop(ctx.currentTime+delay+0.6);
});
}catch(e){}
}
function fmtErr(v){
try{
if(!v) return "Unknown error.";
if(typeof v === "string") return v;
if(v.message) return String(v.message);
if(v.error){
if(typeof v.error === "string") return v.error;
if(v.error.message) return String(v.error.message);
}
return JSON.stringify(v);
}catch(e){ return String(v); }
}
// ── Dimmer ────────────────────────────────────────────────────────────────────
let _dim=null;
const showDimmer=()=>{ if(!_dim){_dim=mk("div",{position:"fixed",inset:"0",background:"rgba(0,0,0,.7)",zIndex:"999990",display:"none",pointerEvents:"none"});document.body.appendChild(_dim);} _dim.style.display="block"; };
const hideDimmer=()=>{ if(_dim)_dim.style.display="none"; };
// ── Toggle ────────────────────────────────────────────────────────────────────
function Toggle(labelTxt,checked,onChange,activeColor){
const onClr=activeColor||LIME;
const onThumb=activeColor?"#fff":"#111";
const wrap=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",
padding:"9px 0",borderBottom:`1px solid ${C.border}`});
const lbl=mk("span",{fontSize:"12px",color:C.text});tx(lbl,labelTxt);
const track=mk("div",{width:"34px",height:"18px",borderRadius:"9px",
background:checked?onClr:C.dim,cursor:"pointer",position:"relative",transition:"background .2s",flexShrink:"0"});
const thumb=mk("div",{position:"absolute",top:"2px",left:checked?"16px":"2px",
width:"14px",height:"14px",borderRadius:"50%",
background:checked?onThumb:"#888",transition:"left .2s,background .2s"});
track.appendChild(thumb);
let val=checked;
track.onclick=()=>{
val=!val;track.style.background=val?onClr:C.dim;
thumb.style.left=val?"16px":"2px";thumb.style.background=val?onThumb:"#888";onChange(val);
};
wrap.append(lbl,track);
const _setChecked=(v)=>{
val=v;track.style.background=val?onClr:C.dim;
thumb.style.left=val?"16px":"2px";thumb.style.background=val?onThumb:"#888";
};
return{el:wrap,get value(){return val;},_setChecked};
}
// ── Dropdown ──────────────────────────────────────────────────────────────────
function DD(items,selected,onChange){
let val=selected;
const wrap=mk("div",{position:"relative",width:"100%",minWidth:"0",overflow:"hidden"});
const trig=mk("div",{background:C.bg3,border:`1px solid ${C.border}`,borderRadius:"7px",
padding:"0 8px",height:"28px",display:"flex",alignItems:"center",
justifyContent:"space-between",cursor:"pointer",boxSizing:"border-box",
transition:"border-color .15s",userSelect:"none",overflow:"hidden"});
const trigTxt=mk("span",{fontSize:"11px",color:C.text,overflow:"hidden",
textOverflow:"ellipsis",whiteSpace:"nowrap",flex:"1",minWidth:"0"});
tx(trigTxt,val); trigTxt.style.color=val?LIME:C.muted;
const arr=mk("span",{fontSize:"8px",color:C.muted,marginLeft:"5px",flexShrink:"0",transition:"transform .18s"});
tx(arr,"▾");
trig.append(trigTxt,arr);
const panel=mk("div",{display:"none",position:"fixed",background:C.bg1,
border:`1px solid ${C.borderH}`,borderRadius:"8px",zIndex:"999999",
flexDirection:"column",boxShadow:"0 8px 28px rgba(0,0,0,.9)",
overflow:"hidden",minWidth:"140px",maxWidth:"400px"});
const srch=mk("input",{background:C.bg2,border:"none",borderBottom:`1px solid ${C.border}`,
padding:"7px 10px",color:C.text,fontSize:"11px",outline:"none",
width:"100%",boxSizing:"border-box"},{type:"text",placeholder:"Type to filter…"});
const list=mk("div",{overflowY:"auto",maxHeight:"200px"});
const render=q=>{
const _norm=(s)=>(s||"").replace(/\\/g,"/").toLowerCase();
list.innerHTML="";
items.filter(i=>!q||i.toLowerCase().includes(q.toLowerCase())).forEach(item=>{
const isSel=_norm(item)===_norm(val);
const r=mk("div",{padding:"7px 12px",fontSize:"11px",cursor:"pointer",
color:isSel?LIME:C.text,background:isSel?C.bg2:"transparent",
whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",transition:"background .1s"});
tx(r,item);
r.onmouseenter=()=>r.style.background=C.bg3;
r.onmouseleave=()=>r.style.background=_norm(item)===_norm(val)?C.bg2:"transparent";
r.onclick=()=>{val=item;tx(trigTxt,item);trigTxt.style.color=item?LIME:C.muted;close();onChange(item);};
list.appendChild(r);
});
};
const reposition=()=>{
const rect=trig.getBoundingClientRect();
panel.style.left=rect.left+"px";
panel.style.width=Math.max(rect.width,140)+"px";
const ph=Math.min(items.length*28+44,220);
panel.style.top=(rect.top-ph-4>8?rect.top-ph-4:rect.bottom+4)+"px";
};
const open=()=>{
document.body.appendChild(panel);panel.style.display="flex";
reposition();arr.style.transform="rotate(180deg)";
trig.style.borderColor=LIME;showDimmer();
srch.value="";srch.focus();render("");
};
const close=()=>{
panel.style.display="none";
if(panel.parentNode)panel.parentNode.removeChild(panel);
arr.style.transform="";trig.style.borderColor=C.border;hideDimmer();
};
srch.oninput=()=>render(srch.value);
trig.onclick=e=>{e.stopPropagation();panel.style.display==="flex"?close():open();};
document.addEventListener("click",e=>{if(!wrap.contains(e.target)&&!panel.contains(e.target))close();});
trig.onmouseenter=()=>{if(panel.style.display!=="flex")trig.style.background=C.bg2;};
trig.onmouseleave=()=>{if(panel.style.display!=="flex")trig.style.background=C.bg3;};
panel.appendChild(srch);
panel.appendChild(list);
wrap.appendChild(trig);
render("");
return{
el:wrap,get value(){return val;},
set(v){val=v;tx(trigTxt,v);trigTxt.style.color=v?LIME:C.muted;render("");},
updateItems(ni){items=ni;if(!ni.some(i=>(i||"").replace(/\\/g,"/").toLowerCase()===(val||"").replace(/\\/g,"/").toLowerCase())){val=ni[0]||val;tx(trigTxt,val);trigTxt.style.color=val?LIME:C.muted;onChange(val);}render(srch.value||"");},
};
}
// ── Pill button ───────────────────────────────────────────────────────────────
function Pill(txt,active,onClick){
const b=mk("button",{
background:active?LIME:C.bg2,color:active?"#111":C.text,
border:`1px solid ${active?LIME:C.border}`,
borderRadius:"20px",padding:"3px 9px",fontSize:"9px",
fontWeight:active?"700":"400",cursor:"pointer",
transition:"all .14s",outline:"none",whiteSpace:"nowrap",
});
tx(b,txt);
b.onmousedown=()=>b.style.transform="scale(.95)";
b.onmouseup=()=>b.style.transform="";
b.onmouseleave=()=>b.style.transform="";
b.onclick=onClick;
return b;
}
// ── Number input ──────────────────────────────────────────────────────────────
function NI(_label,val,min,max,_step,onChange,width="72px"){
const wrap=mk("div",{
width,height:"28px",background:C.bg2,border:`1px solid ${C.border}`,
borderRadius:"6px",boxSizing:"border-box",display:"flex",alignItems:"center",
padding:"0 7px",transition:"border-color .15s",overflow:"hidden",
});
const inp=mk("input",{
flex:"1 1 0",minWidth:"0",background:"transparent",border:"none",outline:"none",
color:C.text,fontSize:"11px",padding:"0",textAlign:"left",
},{type:"number",min:String(min),max:String(max),value:String(val)});
inp.oninput=()=>{ const v=Math.max(min,Math.min(max,parseFloat(inp.value)||min)); onChange(v); };
inp.onfocus=()=>{ inp.select(); wrap.style.borderColor=LIME; };
inp.onblur=()=>{ inp.value=String(Math.max(min,Math.min(max,parseFloat(inp.value)||min))); wrap.style.borderColor=C.border; };
inp.addEventListener("keydown",e=>{
if(e.key==="Enter"){ inp.blur(); return; }
if(e.key==="ArrowUp"||e.key==="ArrowDown"){
e.preventDefault();
const step=wrap._arrowStep||8;
const cur=Math.max(min,Math.min(max,parseFloat(inp.value)||min));
const next=e.key==="ArrowUp"
? Math.min(max, Math.round((cur+step)/step)*step)
: Math.max(min, Math.round((cur-step)/step)*step);
inp.value=String(next); onChange(next);
}
});
inp.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
wrap.appendChild(inp);
wrap.onclick=()=>inp.focus();
wrap._inp=inp;
wrap.setVal=(v)=>{inp.value=String(v);};
Object.defineProperty(wrap,"numVal",{get(){return parseFloat(inp.value)||min;}});
return wrap;
}
// ── Remove button ─────────────────────────────────────────────────────────────
function mkRmBtn(){
const b=mk("button",{
position:"absolute",top:"4px",right:"4px",width:"18px",height:"18px",
borderRadius:"50%",background:"rgba(0,0,0,.85)",border:`1px solid ${C.border}`,
color:"rgba(255,255,255,.7)",fontSize:"9px",cursor:"pointer",display:"none",
alignItems:"center",justifyContent:"center",padding:"0",
transition:"background .15s, color .15s, border-color .15s",lineHeight:"1",zIndex:"2",
});
tx(b,"✕");
b.onmouseenter=()=>{ b.style.borderColor=LIME; b.style.color=LIME; };
b.onmouseleave=()=>{ b.style.borderColor=C.border; b.style.color="rgba(255,255,255,.7)"; };
return b;
}
// ── Global node-local fullscreen overlay factory (set per node instance) ─────
let _fkActiveFsFactory=null;
// ── Image upload slot ─────────────────────────────────────────────────────────
// Returns {el, name, hasFile(), setDimsLabel(w,h), _restorePreview(name)}
function ImgSlot(optional, onFile, onDims){
const wrap=mk("div",{
width:"88px",height:"88px",borderRadius:"12px",
border:`1.5px dashed ${C.border}`,background:C.bg2,
display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",
cursor:"pointer",position:"relative",
transition:"border-color .18s, background .18s",
overflow:"hidden",flexShrink:"0",boxSizing:"border-box",
});
// Empty state
const icoWrap=mk("div",{
position:"absolute",inset:"0",
display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",
gap:"5px",pointerEvents:"none",
});
const ico=document.createElementNS("http://www.w3.org/2000/svg","svg");
ico.setAttribute("viewBox","0 0 24 24");
ico.setAttribute("width","24");ico.setAttribute("height","24");
ico.setAttribute("fill","none");ico.setAttribute("stroke","currentColor");
ico.setAttribute("stroke-width","1.4");ico.setAttribute("stroke-linecap","round");
ico.setAttribute("stroke-linejoin","round");
ico.style.color=C.muted;ico.style.transition="color .18s";ico.style.pointerEvents="none";
ico.innerHTML=`<rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>`;
const lbl=mk("div",{fontSize:"8px",color:C.muted,pointerEvents:"none",letterSpacing:".04em",fontWeight:"600",transition:"color .18s"});
tx(lbl,"Add image");
if(optional){
const optPill=mk("div",{fontSize:"6px",color:C.muted,letterSpacing:".06em",fontWeight:"700",
border:`1px solid ${C.border}`,borderRadius:"20px",padding:"1px 5px",pointerEvents:"none",
textTransform:"uppercase",background:"transparent",lineHeight:"1.7"});
tx(optPill,"Optional");icoWrap.append(ico,lbl,optPill);icoWrap._optPill=optPill;
} else { icoWrap.append(ico,lbl); }
// Preview image
const prevEl=mk("img",{
position:"absolute",inset:"0",width:"100%",height:"100%",
objectFit:"cover",display:"none",borderRadius:"11px",
});
// Remove button (top-right)
const rm=mkRmBtn();
// Fullscreen button (top-right next to rm, circular, hidden until file loaded)
const fsBtn=mk("button",{
position:"absolute",top:"4px",right:"26px",width:"18px",height:"18px",
borderRadius:"50%",background:"rgba(0,0,0,.85)",border:`1px solid ${C.border}`,
color:"rgba(255,255,255,.7)",cursor:"pointer",display:"none",
alignItems:"center",justifyContent:"center",padding:"0",
transition:"border-color .15s, color .15s",lineHeight:"1",zIndex:"2",
});
fsBtn.innerHTML=`<svg viewBox="0 0 24 24" width="9" height="9" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>`;
fsBtn.onmouseenter=()=>{ fsBtn.style.borderColor=LIME; fsBtn.style.color=LIME; };
fsBtn.onmouseleave=()=>{ fsBtn.style.borderColor=C.border; fsBtn.style.color="rgba(255,255,255,.7)"; };
const inp=mk("input",{display:"none"},{type:"file",accept:"image/*"});
wrap.append(icoWrap,prevEl,fsBtn,rm,inp);
wrap.onmouseenter=()=>{
if(prevEl.style.display==="none"){ wrap.style.borderColor=LIME;wrap.style.background=C.bg1;ico.style.color=LIME;lbl.style.color=LIME;if(icoWrap._optPill){icoWrap._optPill.style.color=LIME;icoWrap._optPill.style.borderColor=LIME;} }
else { wrap.style.borderColor=LIME; }
};
wrap.onmouseleave=()=>{ wrap.style.borderColor=C.border;wrap.style.background=C.bg2;ico.style.color=C.muted;lbl.style.color=C.muted;if(icoWrap._optPill){icoWrap._optPill.style.color=C.muted;icoWrap._optPill.style.borderColor=C.border;} };
wrap.onclick=()=>inp.click();
let _dragDepth=0;
wrap.addEventListener("dragenter",e=>{e.preventDefault();e.stopPropagation();_dragDepth++;wrap.style.borderColor=LIME;wrap.style.background=C.bg1;});
wrap.addEventListener("dragover",e=>{e.preventDefault();e.stopPropagation();});
wrap.addEventListener("dragleave",()=>{ _dragDepth--;if(_dragDepth<=0){_dragDepth=0;wrap.style.borderColor=C.border;wrap.style.background=C.bg2;} });
wrap.addEventListener("drop",e=>{
e.preventDefault();e.stopPropagation();
_dragDepth=0;wrap.style.borderColor=C.border;wrap.style.background=C.bg2;
const f=e.dataTransfer.files[0];if(f&&f.type.startsWith("image/"))_load(f);
});
let _currentName=null;
let _fsSrc="", _fsFileName="";
const _showLoaded=(src,fname)=>{
prevEl.src=src;prevEl.style.display="block";
icoWrap.style.display="none";rm.style.display="flex";fsBtn.style.display="flex";
wrap.style.borderColor=LIME;
_fsSrc=src;_fsFileName=fname;
// Read natural dims once image loads
const _tmpImg=new Image();
_tmpImg.onload=()=>{ if(onDims) onDims(_tmpImg.naturalWidth,_tmpImg.naturalHeight); };
_tmpImg.src=src;
};
fsBtn.onclick=e=>{
e.stopPropagation();
const factory=_fkActiveFsFactory;
if(factory) factory()._open("image",_fsSrc,_fsFileName);
};
const _load=async(file)=>{
const objUrl=URL.createObjectURL(file);
_showLoaded(objUrl,file.name);
// Use ComfyUI image upload endpoint
const fd=new FormData();fd.append("image",file);fd.append("overwrite","true");
try{
const r=await api.fetchApi("/upload/image",{method:"POST",body:fd});
const d=await r.json();_currentName=d.name||file.name;
onFile(_currentName);
}catch(err){console.warn("[ZImageOneNode] upload:",err);_currentName=file.name;onFile(_currentName);}
};
inp.onchange=()=>{if(inp.files[0])_load(inp.files[0]);};
rm.onclick=e=>{
e.stopPropagation();
prevEl.src="";prevEl.style.display="none";
rm.style.display="none";fsBtn.style.display="none";icoWrap.style.display="flex";
wrap.style.borderColor=C.border;
inp.value="";_currentName=null;onFile(null);
if(onDims) onDims(0,0);
};
// Restore a previously-uploaded input image by name (state restore or programmatic set).
// Calls onFile so hasFile() returns true and size controls update correctly.
// Safe to call only after all dependents (resDD, updateSizeControls) are initialized.
const _restorePreview=(name)=>{
if(!name){ rm.onclick({stopPropagation:()=>{}}); return; }
const src=api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=`);
_currentName=name;
_showLoaded(src,name);
// onFile intentionally NOT called here — callers set S.imageXName directly and call
// updateSizeControls/persist themselves to avoid initialization-order issues.
};
// Restore from a fully-formed URL (e.g. output images from gallery)
// storedName is what gets written to S.image1Name — it must be usable with /upload/image later
const _restorePreviewUrl=(url,displayName,storedName)=>{
_currentName=storedName||displayName||url;
_showLoaded(url,displayName||url);
};
return {
el:wrap,
get name(){return _currentName;},
hasFile(){return !!_currentName;},
_restorePreview,
_restorePreviewUrl,
};
}
// ── State helpers ─────────────────────────────────────────────────────────────
function loadState(){
try{ return JSON.parse(localStorage.getItem(LS_KEY)||"{}"); }catch(e){return{};}
}
function saveState(s){
try{ localStorage.setItem(LS_KEY,JSON.stringify(s)); }catch(e){}
}
// ── Active refs for event handlers ────────────────────────────────────────────
let _activeS=null, _activeShowFinal=null, _activeResetBtn=null, _activeShowError=null, _activePromptIdRef=null, _activeShowPreview=null;
// ── API events ────────────────────────────────────────────────────────────────
(()=>{
api.addEventListener("progress",(evt)=>{
const {node,value,max}=evt.detail||{};
if(!_activeS?.generating||!node) return;
const pct=max>0?Math.round(value/max*100):0;
if(_activeSetStage) _activeSetStage("Generating…",`Step ${value}/${max}`,pct);
});
api.addEventListener("execution_success",async()=>{
if(!_activeS?.generating) return;
try{
const r=await api.fetchApi(`/z_image/gallery?offset=0&limit=20&subfolder=one-node-z-image`);
const d=await r.json();
const prev=_activeS?._preRunFiles||new Set();
const v=(d.images||d.videos||[]).find(v=>!prev.has(v.key||((v.subfolder?`${v.subfolder}/`:"")+v.filename)))||(d.images||d.videos||[])[0];
if(v){
const cb=Date.now();
const url=api.apiURL(`/view?filename=${encodeURIComponent(v.filename)}&type=output&subfolder=${encodeURIComponent(v.subfolder||"")}&t=${cb}`);
_activeShowFinal?.(url,v.filename,v.subfolder||"");
}else{
_activeResetBtn?.();
}
}catch(e){
console.error("[ZImageOneNode] execution_success:",e);
_activeResetBtn?.();
}
});
api.addEventListener("execution_error",evt=>{
const errorPromptId=evt.detail?.prompt_id;
if(errorPromptId && _activePromptIdRef && errorPromptId!==_activePromptIdRef()) return;
const msg=fmtErr(evt.detail?.exception_message||evt.detail?.error||evt.detail||"Execution failed.");
_activeShowError?.(msg);
_activeResetBtn?.();
});
api.addEventListener("b_preview",evt=>{
if(!_activeS?.generating) return;
const blob=evt.detail;
if(!blob) return;
const url=URL.createObjectURL(blob);
_activeShowPreview?.(url);
});
})();
let _activeSetStage=null;
// ─────────────────────────────────────────────────────────────────────────────
app.registerExtension({
name:"ZImagePlayground.v1",
async beforeRegisterNodeDef(nodeType,nodeData){
if(nodeData.name!=="ZImageOneNode") return;
nodeType.prototype.onNodeCreated=function(){
this.color=C.bg0;this.bgcolor=C.bg0;this.resizable=false;
this.outputs=[];
if(this.widgets)this.widgets=[];
if(!window.__zimage_nodes) window.__zimage_nodes={};
const nodeId=this.id;
const cached=window.__zimage_nodes[nodeId];
if(cached){
_activeS=cached.S;
_activeShowFinal=cached.fns.showFinal;
_activeShowPreview=cached.fns.showPreview;
_activeResetBtn=cached.fns.resetBtn;
_activeShowError=cached.fns.showError;
_activeSetStage=cached.fns.setStage;
_activePromptIdRef=cached.fns.getPromptId;
this.addDOMWidget("fk_ui","div",cached.root,{
getValue(){return null;},setValue(){},serialize:false,
computeSize(){const slotH=(LiteGraph.NODE_SLOT_HEIGHT||20);const n=(self.inputs||[]).length;return[NODE_W,NODE_H+n*slotH];},
});
this.setSize([NODE_W,NODE_H]);
requestAnimationFrame(()=>{
let el=cached.root;
for(let i=0;i<6;i++){ el=el?.parentElement; if(!el)break; el.querySelectorAll("[class*='bg-node-component-surface']").forEach(b=>b.style.display="none"); }
});
return;
}
this._buildUI();
};
nodeType.prototype.onResize=function(){
const slotH=(LiteGraph.NODE_SLOT_HEIGHT||20);
const n=(this.inputs||[]).length;
this.size=[NODE_W,NODE_H+n*slotH];
};
nodeType.prototype.onDrawConnections=function(){};
nodeType.prototype.getSlotMenuOptions=function(){return[];};
nodeType.prototype._buildUI=function(){
const self=this;
const saved=loadState();
if(!self._fk_S){
self._fk_S={
// Settings
modelVariant: saved.modelVariant||"9b", // "9b" | "9b-kv"
model: saved.model||"",
textEncoder: saved.textEncoder||"",
vae: saved.vae||"",
// Pill mode
pill: saved.pill||"t2i", // "t2i" | "i2i"
// Resolution
resLabel: saved.resLabel||RES_PRESETS[0].label,
resW: saved.resW||1024,
resH: saved.resH||1024,
isCustomRes: saved.isCustomRes||false,
customW: saved.customW||1024,
customH: saved.customH||1024,
useSizeFromImage1: saved.useSizeFromImage1||false,
// Seed
randomizeSeed: saved.randomizeSeed!==undefined?saved.randomizeSeed:true,
seed: saved.seed||0,
i2iImage: saved.i2iImage||null,
i2iDenoise: saved.i2iDenoise!==undefined?saved.i2iDenoise:0.75,
i2iResizeLonger: saved.i2iResizeLonger||0,
promptI2i: saved.promptI2i||"",
advancedUI: saved.advancedUI||false,
steps: saved.steps||4,
cfg: saved.cfg!==undefined?saved.cfg:1,
sampler: saved.sampler||"res_multistep",
scheduler: saved.scheduler||"simple",
denoise: saved.denoise!==undefined?saved.denoise:1,
// Images
image1Name: saved.image1Name||null,
image2Name: saved.image2Name||null,
bgRemovalModel: saved.bgRemovalModel||"", // birefnet model for remove bg
// Prompt — shared (active pill's value) + per-pill storage
prompt: saved.prompt||"",
promptT2i: saved.promptT2i!==undefined?saved.promptT2i:((!saved.pill||saved.pill==="t2i")?saved.prompt||"":""),
// LoRAs
userLoras: saved.userLoras||[{name:"",strength:1.0},{name:"",strength:1.0},{name:"",strength:1.0}],
// Generation state
generating: false,
_pendingMeta: null,
_preRunFiles: new Set(),
soundEnabled: saved.soundEnabled!==undefined?saved.soundEnabled:true,
extLoaders: saved.extLoaders||false,
previewUrl: null,
};
}
const S=self._fk_S;
// Sync S.prompt to the active pill's slot on init (covers first load before _pillPromptKey is available)
{
const _initKey=S.pill==="i2i"?"promptI2i":"promptT2i";
S.prompt=S[_initKey]||S.prompt||"";
S[_initKey]=S.prompt;
}
let soundEnabled=S.soundEnabled;
const persist=()=>{
S.soundEnabled=soundEnabled;
saveState({
modelVariant:S.modelVariant, model:S.model,
textEncoder:S.textEncoder, vae:S.vae,
pill:S.pill,
resLabel:S.resLabel, resW:S.resW, resH:S.resH,
isCustomRes:S.isCustomRes, customW:S.customW, customH:S.customH,
randomizeSeed:S.randomizeSeed, seed:S.seed,
advancedUI:S.advancedUI, steps:S.steps, cfg:S.cfg, sampler:S.sampler, scheduler:S.scheduler, denoise:S.denoise,
image1Name:S.image1Name, image2Name:S.image2Name,
bgRemovalModel:S.bgRemovalModel,
prompt:S.prompt, promptT2i:S.promptT2i, promptI2i:S.promptI2i,
i2iImage:S.i2iImage, i2iDenoise:S.i2iDenoise, i2iResizeLonger:S.i2iResizeLonger,
userLoras:S.userLoras, soundEnabled, extLoaders:S.extLoaders,
});
};
// getW/getH: 0 = "use image size via GetImageSize node", otherwise explicit pixels
const getW=()=>{
return S.isCustomRes?snapRes(S.customW):S.resW;
};
const getH=()=>{
return S.isCustomRes?snapRes(S.customH):S.resH;
};
// Returns actual pixel dims for metadata (reads from dims badge when using image size)
const getEffectiveW=()=>{
if(activePill==="i2i"){
try{
const d=_i2iDims._getDims();
if(d.w&&d.h&&!_i2iUseOrigSize&&S.i2iResizeLonger>0){
return Math.round(d.w*(S.i2iResizeLonger/Math.max(d.w,d.h))/16)*16;
}
return d.w||0;
}catch(ex){}
return S.resW;
}
return S.isCustomRes?snapRes(S.customW):S.resW;
};
const getEffectiveH=()=>{
if(activePill==="i2i"){
try{
const d=_i2iDims._getDims();
if(d.w&&d.h&&!_i2iUseOrigSize&&S.i2iResizeLonger>0){
return Math.round(d.h*(S.i2iResizeLonger/Math.max(d.w,d.h))/16)*16;
}
return d.h||0;
}catch(ex){}
return S.resH;
}
return S.isCustomRes?snapRes(S.customH):S.resH;
};
// ── ROOT ────────────────────────────────────────────────────────────────
if(!document.getElementById("fk-styles")){
const styleEl=document.createElement("style");
styleEl.id="fk-styles";
styleEl.textContent=`
@keyframes fk-gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes fk-error-pulse {
0% { box-shadow: inset 0 0 0 1px rgba(255,103,103,.24), 0 0 0 0 rgba(255,103,103,.10); }
50% { box-shadow: inset 0 0 0 1px rgba(255,103,103,.46), 0 0 0 6px rgba(255,103,103,0); }
100% { box-shadow: inset 0 0 0 1px rgba(255,103,103,.24), 0 0 0 0 rgba(255,103,103,0); }
}
@keyframes fk-light-sweep {
0% { left: -80%; opacity: 0; }
15% { opacity: 1; }
85% { opacity: 1; }
100% { left: 120%; opacity: 0; }
}
@keyframes fk-heart-shake {
0% { transform: scale(1); }
30% { transform: scale(1.22); }
55% { transform: scale(0.95); }
75% { transform: scale(1.1); }
100%{ transform: scale(1); }
}
.fk-heart-anim { animation: fk-heart-shake .6s ease; }
@keyframes fk-manage-flash {
0% { background: linear-gradient(135deg,rgba(26,20,60,0) 0%,rgba(15,52,96,0) 50%,rgba(83,52,131,0) 100%); }
25% { background: linear-gradient(135deg,rgba(26,20,60,.7) 0%,rgba(15,52,96,.5) 50%,rgba(83,52,131,.6) 100%); }
60% { background: linear-gradient(135deg,rgba(26,20,60,.5) 0%,rgba(15,52,96,.35) 50%,rgba(83,52,131,.45) 100%); }
100% { background: linear-gradient(135deg,rgba(26,20,60,.35) 0%,rgba(15,52,96,.2) 50%,rgba(83,52,131,.3) 100%); }
}
.fk-manage-on { background: linear-gradient(135deg,rgba(26,20,60,.35) 0%,rgba(15,52,96,.2) 50%,rgba(83,52,131,.3) 100%) !important; }
.fk-manage-flash { animation: fk-manage-flash .5s ease forwards; }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
input[type=number] { -moz-appearance:textfield; }
/* Nodes 2.0: hide the auto-injected node-type label rendered below the DOM widget */
.fk-root ~ .node_title, .fk-root + .node_title { display:none !important; }
`;
document.head.appendChild(styleEl);
}
const root=mk("div",{width:"100%",background:C.bg0,boxSizing:"border-box",
fontFamily:"-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
color:C.text,overflow:"hidden",position:"relative"});
root.classList.add("fk-root");
// Nodes 2.0 compatibility: inherit border-radius from the DOM widget wrapper so overlays
// clip correctly. The wrapper gets its radius from the litegraph node shape.
const _syncNodeRadius=()=>{
const wrapper=root.parentElement;
if(!wrapper) return;
const r=getComputedStyle(wrapper).borderRadius;
// Only apply if non-zero and different from current
const effective=(r&&r!=="0px")?r:"0px";
root.style.borderRadius=effective;
};
// Sync once after mount and observe changes (theme switches, etc.)
requestAnimationFrame(()=>{
_syncNodeRadius();
if(typeof ResizeObserver!=="undefined"){
new ResizeObserver(_syncNodeRadius).observe(root.parentElement||root);
}
});
const titleH=(typeof LiteGraph!=="undefined"&&LiteGraph.NODE_TITLE_HEIGHT)||30;
const _uiH=NODE_H-titleH-4;
const scrollEl=mk("div",{
width:"100%",height:_uiH+"px",
overflowY:"hidden",overflowX:"hidden",boxSizing:"border-box",
});
const _fwdCv=document.querySelector("canvas.litegraph");
scrollEl.addEventListener("wheel",e=>{
// Always forward wheel to canvas for zoom — node content doesn't scroll
if(_fwdCv) _fwdCv.dispatchEvent(new WheelEvent("wheel",{deltaY:e.deltaY,deltaX:e.deltaX,
clientX:e.clientX,clientY:e.clientY,ctrlKey:e.ctrlKey,metaKey:e.metaKey,
bubbles:true,cancelable:true}));
e.preventDefault();
},{passive:false});
const pad=mk("div",{padding:"12px",display:"flex",flexDirection:"column",
gap:"10px",boxSizing:"border-box",width:"100%",
height:_uiH+"px"});
// ── SETTINGS OVERLAY ──────────────────────────────────────────────────
const settingsOverlay=mk("div",{
position:"absolute",inset:"0",background:"#0a0a0a",
display:"none",flexDirection:"column",padding:"16px",
boxSizing:"border-box",zIndex:"50",borderRadius:"8px",
overflowY:"auto",
opacity:"0",transition:"opacity .22s ease, transform .22s ease",
transform:"translateY(6px)",
});
const settHdr=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",
marginBottom:"16px",flexShrink:"0"});
const settTitle=mk("div",{fontSize:"13px",fontWeight:"700",letterSpacing:".06em",
textTransform:"uppercase",color:C.text});tx(settTitle,"Settings");
const settClose=mk("button",{background:"transparent",border:`1px solid #e05555`,
borderRadius:"6px",padding:"4px 14px",fontSize:"11px",color:"#e05555",
cursor:"pointer",outline:"none",transition:"opacity .15s"});
tx(settClose,"✕ Close");
settClose.onmouseenter=()=>settClose.style.opacity=".7";
settClose.onmouseleave=()=>settClose.style.opacity="1";
const settRefresh=mk("button",{background:"transparent",border:`1px solid ${C.border}`,
borderRadius:"6px",padding:"4px 14px",fontSize:"11px",color:C.muted,
cursor:"pointer",outline:"none",transition:"opacity .15s",marginRight:"8px"});
tx(settRefresh,"↻ Refresh models");
settRefresh.onmouseenter=()=>settRefresh.style.opacity=".7";
settRefresh.onmouseleave=()=>settRefresh.style.opacity="1";
settRefresh.onclick=()=>{ tx(settRefresh,"↻ Refreshing…"); _loadModels().then(()=>tx(settRefresh,"↻ Refresh models")); };
const settBtnRow=mk("div",{display:"flex",alignItems:"center",gap:"0"});
settBtnRow.append(settRefresh,settClose);
settHdr.append(settTitle,settBtnRow);
// ── Model dropdowns ───────────────────────────────────────────────────
const mkPathLabel=(txt)=>mk("div",{fontSize:"10px",color:C.muted,marginTop:"-2px",marginBottom:"5px",lineHeight:"1.3",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},{textContent:txt});
const mkModDD=(capTxt,pathTxt,defVal,onCh,autoKeyword=null)=>{
const wrap=mk("div",{minWidth:"0",overflow:"hidden"});
wrap.appendChild(cap(capTxt));
wrap.appendChild(mkPathLabel(pathTxt));
const row=mk("div",{display:"flex",gap:"4px",alignItems:"center",minWidth:"0",overflow:"hidden"});
const dd=DD([defVal||"—"],[defVal||"—"][0],v=>{onCh(v);persist();});
dd.el.style.flex="1";dd.el.style.minWidth="0";
dd._items=[defVal||""];
const origUpdate=dd.updateItems.bind(dd);
dd.updateItems=ni=>{
dd._items=ni;
const _norm=(s)=>(s||"").replace(/\\/g,"/").toLowerCase();
const currentVal=dd.value||"";
// If the current value exists in the new list, keep it — don't auto-select by keyword
const existsInList=ni.some(i=>_norm(i)===_norm(currentVal));
if(existsInList){
origUpdate(ni); // keeps current selection
} else {
// Current value not in list — try keyword auto-select, else first item
origUpdate(ni);
if(autoKeyword&&ni.length){
const kws=autoKeyword.split(',').map(k=>k.trim().toLowerCase());
const best=ni.find(f=>kws.every(k=>f.toLowerCase().includes(k)));
if(best){dd.set(best);onCh(best);persist();}
}
}
};
row.append(dd.el);
wrap.appendChild(row);
return{wrap,dd};
};
// Row 1: Model / Text Encoder / VAE
const modGrid=mk("div",{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:"8px",marginBottom:"4px"});
const modelF =mkModDD("Model", "/models/diffusion_models", S.model, v=>{S.model=v;if(typeof _kvUpdateNote==="function")_kvUpdateNote();},"z_image");
const teF =mkModDD("Text Encoder", "/models/text_encoders", S.textEncoder, v=>S.textEncoder=v, "qwen");
const vaeF =mkModDD("VAE", "/models/vae", S.vae, v=>S.vae=v, "");
modGrid.append(modelF.wrap,teF.wrap,vaeF.wrap);
// KV info note — shown below Model dropdown when selected model name contains "kv"
const _isBaseModel=()=>(S.model||"").toLowerCase().includes("base");
const _kvNote=mk("div",{fontSize:"9px",color:"#f0a040",marginTop:"0px",marginBottom:"4px",display:"none"});
tx(_kvNote,"⚙ KV model version detected. Settings adjusted for KV model.");
const _baseNote=mk("div",{fontSize:"9px",color:"#f0a040",marginTop:"0px",marginBottom:"8px",display:"none"});
tx(_baseNote,"⚙ Base model detected. Settings adjusted for base model.");
let _advControlsReady=false;
const _kvUpdateNote=()=>{
const name=(S.model||"").toLowerCase();
_kvNote.style.display=name.includes("kv")?"block":"none";
const isBase=name.includes("base");
_baseNote.style.display=isBase?"block":"none";
// Sync advanced control defaults when base model selected
if(!_advControlsReady) return;
if(isBase){
if(S.steps===4||S.steps===20){ S.steps=20; stepsInp.setVal(20); }
if(S.cfg===1||S.cfg===5){ S.cfg=5; cfgInp.setVal(5); }
} else {
if(S.steps===20){ S.steps=4; stepsInp.setVal(4); }
if(S.cfg===5){ S.cfg=1; cfgInp.setVal(1); }
}
};
_kvUpdateNote();
// ── Trigger words system ───────────────────────────────────────────────
// Custom trigger words stored in config.json under key "lora_triggers_custom"
// key = lora basename (no path), value = user-saved trigger string
if(!window.__fkCustomTriggers) window.__fkCustomTriggers=null; // null = not loaded yet
const _loadCustomTriggers=async()=>{
if(window.__fkCustomTriggers!==null) return window.__fkCustomTriggers;
try{
const r=await api.fetchApi("/z_image/config");
const d=await r.json();
window.__fkCustomTriggers=d.lora_triggers_custom||{};
}catch(e){ window.__fkCustomTriggers={}; }
return window.__fkCustomTriggers;
};
const _saveCustomTrigger=async(loraName,triggerText)=>{
const base=loraName.split(/[\\/]/).pop();
if(!window.__fkCustomTriggers) window.__fkCustomTriggers={};
if(triggerText.trim()) window.__fkCustomTriggers[base]=triggerText.trim();
else delete window.__fkCustomTriggers[base];
try{
await api.fetchApi("/z_image/config",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({lora_triggers_custom:window.__fkCustomTriggers}),
});
}catch(e){ console.warn("[ZImageOneNode] save custom trigger:",e); }
};
const _getCustomTrigger=(loraName)=>{
if(!loraName||loraName==="none"||!window.__fkCustomTriggers) return "";
const base=loraName.split(/[\\/]/).pop();
return window.__fkCustomTriggers[base]||"";
};
// ── Preferences ───────────────────────────────────────────────────────
const prefTitle=mk("div",{fontSize:"10px",fontWeight:"700",letterSpacing:".1em",
textTransform:"uppercase",color:C.muted,padding:"10px 0 2px",
borderBottom:`1px solid ${C.border}`,marginBottom:"4px"});
tx(prefTitle,"Preferences");
const soundToggle=Toggle("Notification sound on complete",soundEnabled,v=>{soundEnabled=v;persist();});
const advUIToggle=Toggle("Advanced control (steps, CFG, sampler…)",S.advancedUI,v=>{S.advancedUI=v;persist();_advRefresh();},"#6450b4");
const _slotH=LiteGraph.NODE_SLOT_HEIGHT||20;
const _extInputNames=["model","clip","vae"];
const _extInputColors=["#b39ddb","#80cbc4","#ef9a9a"];
const _applyExtLoaders=(enabled)=>{
const node=app.graph.getNodeById(self.id)||self;
if(!node) return;
if(enabled){
const existing=(node.inputs||[]).filter(i=>_extInputNames.includes(i.name));
if(existing.length===0){
_extInputNames.forEach((name,i)=>{
const type=i===0?"MODEL":i===1?"CLIP":"VAE";
node.addInput(name,type);
const slot=node.inputs[node.inputs.length-1];
if(slot) slot.color_on=_extInputColors[i];
});
}
const n=(node.inputs||[]).length;
node.size=[NODE_W, NODE_H+n*_slotH];
node.setDirtyCanvas(true,true);
} else {
if(node.inputs&&node.inputs.length>0){
for(let i=node.inputs.length-1;i>=0;i--){
if(_extInputNames.includes(node.inputs[i].name)) node.removeInput(i);
}
}
node.size=[NODE_W, NODE_H];
node.setDirtyCanvas(true,true);
}
};
const extLoadersToggle=Toggle("External model/clip/vae inputs (for GGUF etc.)",S.extLoaders||false,v=>{
S.extLoaders=v;persist();
_applyExtLoaders(v);
_refreshExtInputUI();
});
settingsOverlay.append(settHdr,modGrid,_kvNote,_baseNote,prefTitle,soundToggle.el,advUIToggle.el,extLoadersToggle.el);
// ── Overlay helpers ───────────────────────────────────────────────────
const openOverlay=(el)=>{
el.style.display="flex";
el.offsetHeight;
el.style.opacity="1";
el.style.transform="translateY(0)";
};
const closeOverlayFade=(el,cb)=>{
el.style.opacity="0";
el.style.transform="translateY(6px)";
setTimeout(()=>{el.style.display="none";if(cb)cb();},220);
};
// ── TOP BAR ──────────────────────────────────────────────────────────
const topBar=mk("div",{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"6px",marginBottom:"-2px"});
// Gallery button (placeholder — will be wired when gallery is implemented)
const galleryBtn=mk("button",{
background:"linear-gradient(90deg,#1a1a2e,#0f3460,#533483)",
border:"1.5px solid rgba(255,255,255,.15)",
borderRadius:"6px",padding:"4px 11px",cursor:"pointer",color:"#e0e0ff",
fontSize:"11px",fontWeight:"700",display:"flex",alignItems:"center",gap:"5px",
transition:"opacity .15s, filter .15s",outline:"none",
});
const galleryIcon=mk("span",{fontSize:"12px"});tx(galleryIcon,"▦");
const galleryLbl=mk("span");tx(galleryLbl,"Gallery");
galleryBtn.append(galleryIcon,galleryLbl);
galleryBtn.onmouseenter=()=>galleryBtn.style.filter="brightness(1.3)";
galleryBtn.onmouseleave=()=>galleryBtn.style.filter="";
galleryBtn.onclick=()=>{}; // TODO: gallery overlay
// Simple credit
const creditText=mk("div",{fontSize:"9px",color:C.muted,fontStyle:"italic",position:"absolute",bottom:"8px",left:"16px",right:"16px",textAlign:"center",pointerEvents:"none"});
tx(creditText,"node by Adeliox");
// Settings button
const settingsBtn=mk("button",{
background:"transparent",border:`1.5px solid ${C.borderH}`,
borderRadius:"6px",padding:"4px 11px",
cursor:"pointer",color:C.muted,fontSize:"11px",fontWeight:"700",
display:"flex",alignItems:"center",gap:"5px",
transition:"opacity .15s",outline:"none",
});
const settGear=mk("span",{fontSize:"12px",transition:"transform .3s",lineHeight:"1"});tx(settGear,"⚙");
const settLblEl=mk("span");tx(settLblEl,"Settings");
settingsBtn.append(settGear,settLblEl);
settingsBtn.onmouseenter=()=>{settingsBtn.style.borderColor=C.text;settingsBtn.style.color=C.text;settGear.style.transform="rotate(30deg)";};
settingsBtn.onmouseleave=()=>{settingsBtn.style.borderColor=C.borderH;settingsBtn.style.color=C.muted;settGear.style.transform="";};
const _refreshExtInputUI=()=>{
const n=app.graph.getNodeById(self.id);
const isConn=(name)=>{
if(!n||!n.inputs) return false;
const slot=n.inputs.find(i=>i.name===name);
return slot&&slot.link!=null;
};
const dim=(wrap,connected)=>{
wrap.style.opacity=connected?"0.4":"1";
wrap.style.pointerEvents=connected?"none":"";
wrap.title=connected?"Connected externally — disconnect to use dropdown":"";
};
dim(modelF.wrap,isConn("model"));
dim(teF.wrap, isConn("clip"));
dim(vaeF.wrap, isConn("vae"));
};
settingsBtn.onclick=e=>{e.stopPropagation();_refreshExtInputUI();openOverlay(settingsOverlay);};
settClose.onclick=()=>closeOverlayFade(settingsOverlay);
// Fullscreen node button
const fsNodeBtn=mk("button",{
background:"transparent",border:`1.5px solid ${C.borderH}`,
borderRadius:"6px",padding:"4px 8px",
cursor:"pointer",color:C.muted,
display:"flex",alignItems:"center",gap:"4px",
transition:"opacity .15s, border-color .15s, color .15s",outline:"none",
});
fsNodeBtn.title="Fullscreen";
fsNodeBtn.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>`;
fsNodeBtn.onmouseenter=()=>{fsNodeBtn.style.borderColor=LIME;fsNodeBtn.style.color=LIME;};
fsNodeBtn.onmouseleave=()=>{fsNodeBtn.style.borderColor=C.borderH;fsNodeBtn.style.color=C.muted;};
let _inFullscreen=false;
let _fsNodeOverlay=null;
let _rootOrigParent=null,_rootOrigNextSibling=null;
const _enterFullscreen=()=>{
if(_inFullscreen) return;
if(!_fsNodeOverlay){
_fsNodeOverlay=mk("div",{
position:"fixed",inset:"0",zIndex:"99990",
background:"rgba(6,6,8,.97)",
display:"none",flexDirection:"column",
alignItems:"center",justifyContent:"center",
boxSizing:"border-box",overflow:"hidden",
});
// No keydown handler — Esc is blocked globally via capture handler below
document.body.appendChild(_fsNodeOverlay);
}
_rootOrigParent=root.parentNode;
_rootOrigNextSibling=root.nextSibling;
root.style.width=NODE_W+"px";
root.style.height=NODE_H+"px";
root.style.overflow="hidden";
root.style.borderRadius="0";
root.style.position="absolute";
root.style.top="0";root.style.left="0";root.style.margin="0";
const _vw=window.innerWidth,_vh=window.innerHeight;
const _scale=Math.min(_vw/NODE_W,_vh/NODE_H)*0.97;
root.style.transformOrigin="top left";
root.style.transform=`scale(${_scale})`;
const _scW=Math.round(NODE_W*_scale),_scH=Math.round(NODE_H*_scale);
const _scWrap=mk("div",{width:_scW+"px",height:_scH+"px",position:"relative",flexShrink:"0",overflow:"hidden"});
_scWrap.appendChild(root);
_fsNodeOverlay.appendChild(_scWrap);
_fsNodeOverlay._scWrap=_scWrap;
_fsNodeOverlay.style.display="flex";
_fsNodeOverlay.setAttribute("tabindex","-1");
_fsNodeOverlay.focus();
fsNodeBtn.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v5H3M16 3v5h5M8 21v-5H3M16 21v-5h5"/></svg>`;
_inFullscreen=true;
};
const _exitFullscreen=()=>{
if(!_inFullscreen) return;
if(_rootOrigParent){
if(_rootOrigNextSibling) _rootOrigParent.insertBefore(root,_rootOrigNextSibling);
else _rootOrigParent.appendChild(root);
}
root.style.position="";root.style.inset="";root.style.width="100%";root.style.height="";
root.style.borderRadius="";root.style.overflow="hidden";
root.style.transform="";root.style.transformOrigin="";root.style.margin="";
root.style.top="";root.style.left="";
scrollEl.style.height=_uiH+"px";
if(_fsNodeOverlay._scWrap) _fsNodeOverlay._scWrap.remove();
_fsNodeOverlay._scWrap=null;
_fsNodeOverlay.style.display="none";
fsNodeBtn.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>`;
_inFullscreen=false;
};
fsNodeBtn.onclick=()=>{ if(_inFullscreen) _exitFullscreen(); else _enterFullscreen(); };
const topBarLeft=mk("div",{display:"flex",gap:"3px",alignItems:"center",flexWrap:"nowrap"});
const topBarRight=mk("div",{display:"flex",gap:"6px",alignItems:"center",flexShrink:"0"});
topBarRight.append(galleryBtn,settingsBtn,fsNodeBtn);
topBar.append(topBarLeft,topBarRight);
// ── PILLS ─────────────────────────────────────────────────────────────
let activePill=S.pill||"t2i";
const pillT2I =Pill("ZIMG", activePill==="t2i", ()=>setPill("t2i"));
const pillI2I =Pill("Z-I2I", activePill==="i2i", ()=>setPill("i2i"));
topBarLeft.append(pillT2I,pillI2I);
let _promptTARef=null; // set after promptTA is created
const _pillPromptKey=(p)=>p==="i2i"?"promptI2i":"promptT2i";
function setPill(p){
if(_promptTARef&&activePill){
S[_pillPromptKey(activePill)]=_promptTARef.value;
}
activePill=p;S.pill=p;
S.prompt=S[_pillPromptKey(p)];
if(_promptTARef){ _promptTARef.value=S.prompt; if(typeof _promptOvTA!=="undefined"&&_promptOvTA) _promptOvTA.value=S.prompt; }
persist();
[pillT2I,pillI2I].forEach(b=>{
const isActive=
(b===pillT2I&&p==="t2i")||
(b===pillI2I&&p==="i2i");
b.style.background=isActive?LIME:C.bg2;
b.style.color=isActive?"#111":C.text;
b.style.borderColor=isActive?LIME:C.border;
b.style.fontWeight=isActive?"700":"400";
});
updatePillVisibility();
updateSizeControls();
}
// ── MAIN ROW ─────────────────────────────────────────────────────────
const mainRow=mk("div",{display:"flex",gap:"12px",alignItems:"stretch",flex:"1",minHeight:"0"});
const leftPanel=mk("div",{display:"flex",flexDirection:"column",gap:"7px",
width:"300px",flexShrink:"0"});
// ── Node-local fullscreen overlay ────────────────────────────────────
let _nodeFsOv=null;
const _initNodeFsOverlay=()=>{
if(_nodeFsOv) return _nodeFsOv;
const ov=mk("div",{position:"absolute",inset:"0",zIndex:"9999",
background:"rgba(28,28,32,.97)",display:"none",flexDirection:"column",
alignItems:"stretch",borderRadius:"inherit",overflow:"hidden"});
const _nfTopBar=mk("div",{display:"flex",alignItems:"center",
padding:"10px 12px",gap:"10px",flexShrink:"0",
background:"linear-gradient(to bottom,rgba(0,0,0,.7),rgba(0,0,0,0))",
position:"absolute",top:"0",left:"0",right:"0",zIndex:"3"});
const _nfName=mk("div",{fontSize:"11px",fontWeight:"700",color:"#fff",
overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",
letterSpacing:".01em",textAlign:"center",width:"100%",padding:"0 36px",boxSizing:"border-box"});
const _nfCloseBtn=mk("button",{width:"26px",height:"26px",borderRadius:"50%",
position:"absolute",right:"12px",top:"10px",
background:"rgba(255,255,255,.08)",border:"1px solid rgba(255,255,255,.15)",
color:"rgba(255,255,255,.85)",fontSize:"10px",cursor:"pointer",
display:"flex",alignItems:"center",justifyContent:"center",padding:"0",outline:"none"});
_nfCloseBtn.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>`;
_nfCloseBtn.onmouseenter=()=>{_nfCloseBtn.style.background="rgba(255,255,255,.2)";};
_nfCloseBtn.onmouseleave=()=>{_nfCloseBtn.style.background="rgba(255,255,255,.08)";};
_nfTopBar.append(_nfName,_nfCloseBtn);
const _nfMediaWrap=mk("div",{width:"100%",height:"100%",display:"flex",
alignItems:"center",justifyContent:"center",padding:"48px 16px 16px",boxSizing:"border-box"});
const _nfPillRow=mk("div",{display:"flex",gap:"6px",justifyContent:"center",
flexWrap:"wrap",marginTop:"8px"});
const _nfPill=(t)=>{
const p=mk("div",{fontSize:"9px",color:"rgba(255,255,255,.55)",fontWeight:"600",
border:"1px solid rgba(255,255,255,.15)",borderRadius:"20px",
padding:"2px 9px",letterSpacing:".04em",whiteSpace:"nowrap",background:"rgba(255,255,255,.05)"});
tx(p,t);return p;
};
const _nfClose=()=>{
if(ov._cleanupCmp){ov._cleanupCmp();ov._cleanupCmp=null;}
ov.style.display="none";
const img=_nfMediaWrap.querySelector("img");
if(img) img.src="";
_nfMediaWrap.innerHTML="";_nfPillRow.innerHTML="";
// Restore preview action buttons
if(typeof previewUseWrap!=="undefined") previewUseWrap.style.visibility="";
if(typeof previewDelBtn!=="undefined") previewDelBtn.style.visibility="";
};
_nfCloseBtn.onclick=_nfClose;
ov.addEventListener("keydown",e=>{if(e.key==="Escape")_nfClose();});
ov.setAttribute("tabindex","-1");
ov._close=_nfClose;
ov.append(_nfTopBar,_nfMediaWrap);
ov._open=(type,src,name,opts)=>{
_nfMediaWrap.innerHTML="";_nfPillRow.innerHTML="";
tx(_nfName,name||"");
// Hide preview action buttons while fullscreen overlay is open (image-only mode)
if(type==="image"){
if(typeof previewUseWrap!=="undefined"&&previewUseWrap) previewUseWrap.style.visibility="hidden";
if(typeof previewDelBtn!=="undefined"&&previewDelBtn) previewDelBtn.style.visibility="hidden";
}
if(type==="image"){
const outer=mk("div",{display:"flex",flexDirection:"column",alignItems:"center",
justifyContent:"center",gap:"8px",width:"100%",height:"100%"});
const img=mk("img",{maxWidth:"100%",maxHeight:"calc(100% - 56px)",objectFit:"contain",
borderRadius:"8px",boxShadow:"0 4px 24px rgba(0,0,0,.5)",display:"block"});
img.src=src;
img.onload=()=>{
_nfPillRow.innerHTML="";
_nfPillRow.appendChild(_nfPill(`${img.naturalWidth}×${img.naturalHeight} px`));
};
outer.append(img,_nfPillRow);
_nfMediaWrap.appendChild(outer);
} else if(type==="comparer"){
// Full-screen before/after comparer with "Use as input" in top-right
const {genSrc,baseSrc,onUse}=opts||{};
const cWrap=mk("div",{position:"relative",width:"100%",height:"100%",
overflow:"hidden",borderRadius:"8px",cursor:"col-resize",userSelect:"none",
minHeight:"0",flex:"1"});
const cBase=mk("img",{position:"absolute",inset:"0",width:"100%",height:"100%",objectFit:"contain"});
cBase.src=baseSrc||"";
const cGen=mk("div",{position:"absolute",top:"0",left:"0",bottom:"0",overflow:"hidden",width:"100%"});
const cGenImg=mk("img",{position:"absolute",top:"0",left:"0",height:"100%",objectFit:"contain"});
cGen.appendChild(cGenImg);
cGenImg.src=genSrc||"";
const cLine=mk("div",{position:"absolute",top:"0",bottom:"0",width:"2px",
background:LIME,left:"calc(100% - 1px)",boxShadow:"0 0 8px rgba(240,255,65,.5)",
display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",zIndex:"4"});
const cHandle=mk("div",{width:"30px",height:"30px",borderRadius:"50%",background:LIME,
border:"2px solid #111",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center",
boxShadow:"0 2px 10px rgba(0,0,0,.7)",pointerEvents:"none"});
cHandle.innerHTML=`<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#111" stroke-width="2.5" stroke-linecap="round"><path d="M8 4l-4 8 4 8M16 4l4 8-4 8"/></svg>`;
cLine.appendChild(cHandle);
const _fsSetPct=(pct)=>{
pct=Math.max(0,Math.min(100,pct));
cGen.style.width=pct+"%";
cLine.style.left=`calc(${pct}% - 1px)`;
cGenImg.style.width=(cWrap.offsetWidth||900)+"px";
};
let _fsDrag=false;
cWrap.addEventListener("mousedown",e=>{_fsDrag=true;e.preventDefault();});
const _fsMM=(e)=>{if(!_fsDrag)return;const r=cWrap.getBoundingClientRect();_fsSetPct((e.clientX-r.left)/r.width*100);};
const _fsMU=()=>{_fsDrag=false;};
document.addEventListener("mousemove",_fsMM);
document.addEventListener("mouseup",_fsMU);
cWrap.addEventListener("touchstart",()=>{_fsDrag=true;},{passive:true});
cWrap.addEventListener("touchmove",e=>{if(!_fsDrag)return;const r=cWrap.getBoundingClientRect();_fsSetPct((e.touches[0].clientX-r.left)/r.width*100);},{passive:true});
cWrap.addEventListener("touchend",()=>{_fsDrag=false;});
ov._cleanupCmp=()=>{document.removeEventListener("mousemove",_fsMM);document.removeEventListener("mouseup",_fsMU);};
cWrap.append(cBase,cGen,cLine);
_nfMediaWrap.style.position="relative";
_nfMediaWrap.style.padding="0"; // comparer fills full area
_nfMediaWrap.appendChild(cWrap);
cGenImg.onload=()=>{ _fsSetPct(100); };
// "Use as input" button — top-right corner of the overlay
if(onUse){
const useBtn=mk("button",{
position:"absolute",top:"54px",right:"14px",zIndex:"10",
background:"rgba(20,20,20,.82)",color:"rgba(255,255,255,.82)",
border:"1px solid rgba(255,255,255,.18)",
borderRadius:"6px",padding:"5px 12px",fontSize:"10px",fontWeight:"600",
cursor:"pointer",outline:"none",whiteSpace:"nowrap",
backdropFilter:"blur(4px)",letterSpacing:".04em",
boxShadow:"0 2px 8px rgba(0,0,0,.5)",
transition:"background .15s, color .15s, border-color .15s",
});
tx(useBtn,"Use as input");
useBtn.onmouseenter=()=>{useBtn.style.background="rgba(40,40,40,.95)";useBtn.style.color="#fff";useBtn.style.borderColor="rgba(255,255,255,.35)";};
useBtn.onmouseleave=()=>{useBtn.style.background="rgba(20,20,20,.82)";useBtn.style.color="rgba(255,255,255,.82)";useBtn.style.borderColor="rgba(255,255,255,.18)";};
useBtn.onclick=(e)=>{e.stopPropagation();onUse();_nfClose();};
ov.appendChild(useBtn);
}
}
ov.style.display="flex";ov.focus();
};
root.appendChild(ov);
_nodeFsOv=ov;
return ov;
};
_fkActiveFsFactory=_initNodeFsOverlay;
// ── I2I PANEL ─────────────────────────────────────────────────────────
const i2iPanel=mk("div",{display:"none",flexDirection:"column",gap:"5px"});
// _i2iDims: same helper pattern as _fsTargetDims
const _i2iDims=(()=>{
let _w=0,_h=0;
const el=mk("div",{
fontSize:"9px",fontWeight:"700",letterSpacing:".04em",
textAlign:"center",cursor:"pointer",display:"none",
borderRadius:"5px",padding:"2px 6px",boxSizing:"border-box",
background:C.bg3,border:`1px solid ${C.borderH}`,color:LIME,
background:"rgba(240,255,65,.13)",borderColor:"rgba(240,255,65,.5)",
});
el._getDims=()=>({w:_w,h:_h});
el._set=(w,h)=>{ _w=w;_h=h; if(w&&h){ tx(el,`${w}×${h}`);el.style.display="block"; } else el.style.display="none"; };
return el;
})();
let _i2iUseOrigSize=S.i2iResizeLonger<=0; // true = lime badge, locked; false = unlocked
const _i2iResizePreview=mk("span",{fontSize:"9px",fontWeight:"700",color:LIME,letterSpacing:".03em",whiteSpace:"nowrap"});
const _i2iResizeLongerInp=NI("px",S.i2iResizeLonger||1024,64,8192,8,v=>{
S.i2iResizeLonger=Math.round(v)||1024;
_i2iResizeUpdatePreview();
persist();
},52);
const _i2iUseOrigNote=mk("div",{fontSize:"8px",color:LIME,display:"none",marginTop:"0"});
tx(_i2iUseOrigNote,"Using size from Input image.");
const _i2iResizeRow=mk("div",{display:"none",alignItems:"center",gap:"6px",marginTop:"2px"});
const _i2iResizeRowLbl=mk("span",{fontSize:"8px",color:C.muted,whiteSpace:"nowrap",flexShrink:"0"});
tx(_i2iResizeRowLbl,"Scale by longer side");
_i2iResizeRow.append(_i2iResizeRowLbl,_i2iResizeLongerInp,_i2iResizePreview);
const _i2iResizeUpdatePreview=()=>{
const dims=_i2iDims._getDims();
if(!_i2iUseOrigSize&&dims.w&&dims.h&&S.i2iResizeLonger>0){
const scale=S.i2iResizeLonger/Math.max(dims.w,dims.h);
const nw=Math.round(dims.w*scale/16)*16;
const nh=Math.round(dims.h*scale/16)*16;
tx(_i2iResizePreview,`→ ${nw}×${nh}`);
} else {
tx(_i2iResizePreview,"");
}
};
const _i2iApplyState=()=>{
const dims=_i2iDims._getDims();
if(!dims.w||!dims.h) return;
if(_i2iUseOrigSize){
_i2iDims.style.color=LIME;
_i2iDims.style.background="rgba(240,255,65,.13)";
_i2iDims.style.borderColor="rgba(240,255,65,.5)";
_i2iUseOrigNote.style.display="block";
_i2iResizeRow.style.opacity="0.35";
_i2iResizeRow.style.pointerEvents="none";
_i2iResizeLongerInp._inp.disabled=true;
} else {
_i2iDims.style.color=C.text;
_i2iDims.style.background=C.bg3;
_i2iDims.style.borderColor=C.borderH;
_i2iUseOrigNote.style.display="none";
_i2iResizeRow.style.opacity="1";
_i2iResizeRow.style.pointerEvents="auto";
_i2iResizeLongerInp._inp.disabled=false;
if(S.i2iResizeLonger<=0){ S.i2iResizeLonger=_i2iResizeLongerInp.numVal||1024; persist(); }
}
_i2iResizeUpdatePreview();
};
_i2iDims.onclick=()=>{
const dims=_i2iDims._getDims();
if(!dims.w||!dims.h) return;
_i2iUseOrigSize=!_i2iUseOrigSize;
if(_i2iUseOrigSize){ S.i2iResizeLonger=0; persist(); }
_i2iApplyState();
};
const i2iSlotRow=mk("div",{display:"flex",gap:"10px",alignItems:"flex-start"});
const i2iSlotCard=mk("div",{display:"flex",flexDirection:"column",gap:"3px",alignItems:"center"});
const i2iSlot=ImgSlot(false,(name)=>{
S.i2iImage=name||null;
if(!name){
_i2iDims._set(0,0);
_i2iResizeRow.style.display="none";
_i2iUseOrigNote.style.display="none";
_i2iUseOrigSize=true;
S.i2iResizeLonger=0;
}
if(name){ i2iSlot.el.style.borderColor=""; tx(i2iSlotLbl,"Input Image"); i2iSlotLbl.style.color=C.muted; }
persist();
},(w,h)=>{
if(w&&h){
_i2iDims._set(w,h);
_i2iResizeRow.style.display="flex";
_i2iApplyState();
} else {
_i2iDims._set(0,0);
_i2iResizeRow.style.display="none";
_i2iUseOrigNote.style.display="none";
}
});
const i2iSlotLbl=mk("div",{fontSize:"8px",fontWeight:"700",color:C.muted,textTransform:"uppercase",letterSpacing:".07em",textAlign:"center"});
tx(i2iSlotLbl,"Input Image");
i2iSlotCard.append(i2iSlot.el,i2iSlotLbl,_i2iDims);
i2iSlotRow.append(i2iSlotCard);
// Denoise slider — 0 = no change, 100 = full generation
const i2iSliderWrap=mk("div",{display:"flex",flexDirection:"column",gap:"2px"});
const i2iSliderHdr=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between"});
const i2iSliderLbl=mk("div",{fontSize:"8px",fontWeight:"700",color:C.muted,letterSpacing:".05em",textTransform:"uppercase"});
tx(i2iSliderLbl,"Change strength");
const i2iSliderVal=mk("div",{fontSize:"9px",fontWeight:"700",color:LIME});
tx(i2iSliderVal,`${Math.round((S.i2iDenoise||0.75)*100)}%`);
i2iSliderHdr.append(i2iSliderLbl,i2iSliderVal);
const i2iSlider=mk("input",{
width:"100%",cursor:"pointer",accentColor:LIME,height:"18px",display:"block",
},{type:"range",min:"0",max:"100",step:"1",value:String(Math.round((S.i2iDenoise||0.75)*100))});
const _i2iSliderSet=(pct)=>{
pct=Math.max(0,Math.min(100,pct));
i2iSlider.value=String(pct);
S.i2iDenoise=pct/100;
tx(i2iSliderVal,`${pct}%`);
persist();
};
i2iSlider.oninput=()=>_i2iSliderSet(parseInt(i2iSlider.value));
i2iSlider.addEventListener("mouseup",()=>i2iSlider.blur());
i2iSlider.addEventListener("touchend",()=>i2iSlider.blur());
i2iSlider.addEventListener("wheel",(e)=>{
e.preventDefault();e.stopPropagation();
_i2iSliderSet(parseInt(i2iSlider.value)+(e.deltaY<0?1:-1)*(e.shiftKey?10:1));
},{passive:false});
i2iSliderWrap.append(i2iSliderHdr,i2iSlider);
i2iPanel.append(i2iSlotRow,_i2iUseOrigNote,_i2iResizeRow,i2iSliderWrap);
if(S.i2iImage) i2iSlot._restorePreview(S.i2iImage);
// ── (paint/inpaint/sketch features removed) ──────────────────────────
// ── RESOLUTION ───────────────────────────────────────────────────────
const resSect=mk("div",{display:"flex",flexDirection:"column",gap:"4px"});
const _sizeLabelRow=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
_sizeLabelRow.appendChild(cap("Size"));
resSect.appendChild(_sizeLabelRow);
const resDD=DD(RES_PRESETS.map(p=>p.label),S.resLabel,val=>{
const p=RES_PRESETS.find(r=>r.label===val);
if(p&&p.w>0){
S.resLabel=val;S.resW=p.w;S.resH=p.h;S.isCustomRes=false;
customResRow.style.display="none";
}else{
S.resLabel=val;S.isCustomRes=true;
customResRow.style.display="flex";
}
persist();_arRefreshDimsPreview();
});
resSect.appendChild(resDD.el);
const customResRow=mk("div",{display:S.isCustomRes?"flex":"none",gap:"5px",alignItems:"center",marginTop:"5px"});
// Swap W↔H button
const _arSwapBtn=mk("button",{
width:"18px",height:"22px",borderRadius:"4px",flexShrink:"0",
background:"transparent",border:`1px solid ${C.border}`,
color:C.muted,cursor:"pointer",outline:"none",padding:"0",
display:"flex",alignItems:"center",justifyContent:"center",
transition:"border-color .15s,color .15s",
});
_arSwapBtn.innerHTML=`<svg viewBox="0 0 10 14" width="9" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 1L1 3.5L3 6"/><line x1="1" y1="3.5" x2="9" y2="3.5"/><path d="M7 8L9 10.5L7 13"/><line x1="9" y1="10.5" x2="1" y2="10.5"/></svg>`;
_arSwapBtn.title="Swap W and H";
_arSwapBtn.onmouseenter=()=>{_arSwapBtn.style.borderColor=LIME;_arSwapBtn.style.color=LIME;};
_arSwapBtn.onmouseleave=()=>{_arSwapBtn.style.borderColor=C.border;_arSwapBtn.style.color=C.muted;};
_arSwapBtn.onclick=()=>{
const oldW=wInp.numVal||S.customW;
const oldH=hInp.numVal||S.customH;
S.customW=Math.max(1,Math.round(oldH));
S.customH=Math.max(1,Math.round(oldW));
wInp.setVal(S.customW);hInp.setVal(S.customH);
if(_arLocked&&_arRatio) _arRatio=1/_arRatio;
persist();_arRefreshDimsPreview();
};
// Aspect ratio lock
let _arLocked=false;
let _arRatio=null;
const _arLockBtn=mk("button",{
width:"22px",height:"22px",borderRadius:"5px",flexShrink:"0",
background:"transparent",border:`1px solid ${C.border}`,
color:C.muted,cursor:"pointer",outline:"none",padding:"0",
display:"flex",alignItems:"center",justifyContent:"center",
transition:"border-color .15s,color .15s,background .15s",
});
// Flat SVG lock icons
const _lockIconOpen=`<svg viewBox="0 0 12 14" width="11" height="12" fill="currentColor"><rect x="1" y="6" width="10" height="8" rx="1.5"/><path d="M3.5 6V4a2.5 2.5 0 015 0" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>`;
const _lockIconClosed=`<svg viewBox="0 0 12 14" width="11" height="12" fill="currentColor"><rect x="1" y="6" width="10" height="8" rx="1.5"/><path d="M3.5 6V4a2.5 2.5 0 015 0v2" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>`;
_arLockBtn.innerHTML=_lockIconOpen;
const _arSetLocked=(locked)=>{
_arLocked=locked;
_arLockBtn.style.borderColor=locked?LIME:C.border;
_arLockBtn.style.color=locked?LIME:C.muted;
_arLockBtn.style.background=locked?"rgba(240,255,65,.08)":"transparent";
_arLockBtn.innerHTML=locked?_lockIconClosed:_lockIconOpen;
};
// Get best available aspect ratio: current W/H
const _arGetRatio=()=>{
const cw=wInp.numVal||S.customW||1024;
const ch=hInp.numVal||S.customH||1024;
return cw/ch;
};
_arLockBtn.title="Lock aspect ratio";
const snap8=(v)=>Math.max(16,Math.round(v/16)*16);
// W/H inputs store raw values — no snapping in the field itself.
// snap8 is applied only in getEffectiveW/H (workflow) and in the preview label.
const _deactivateImgSize=()=>{};
const wInp=NI("w",S.customW,1,8192,1,v=>{
S.customW=Math.max(1,Math.round(v));
if(_arLocked&&_arRatio){
S.customH=Math.max(1,Math.round(S.customW/_arRatio));
hInp.setVal(S.customH);
}
_deactivateImgSize();
persist(); _arRefreshDimsPreview();
},"80px");
const xLbl=mk("span",{fontSize:"10px",color:C.muted,flexShrink:"0"});tx(xLbl,"×");
const hInp=NI("h",S.customH,1,8192,1,v=>{
S.customH=Math.max(1,Math.round(v));
if(_arLocked&&_arRatio){
S.customW=Math.max(1,Math.round(S.customH*_arRatio));
wInp.setVal(S.customW);
}
_deactivateImgSize();
persist(); _arRefreshDimsPreview();
},"80px");
wInp._inp.addEventListener("keydown",e=>{ if(e.key==="Tab"&&!e.shiftKey){ e.preventDefault(); hInp._inp.focus(); hInp._inp.select(); } });
hInp._inp.addEventListener("keydown",e=>{ if(e.key==="Tab"&&e.shiftKey){ e.preventDefault(); wInp._inp.focus(); wInp._inp.select(); } });
_arLockBtn.onclick=()=>{
const nowLocked=!_arLocked;
if(nowLocked) _arRatio=_arGetRatio();
_arSetLocked(nowLocked);
_arRefreshDimsPreview();
};
// Live dims preview — shows actual W×H after snap, lime colored
const _arDimsPreview=mk("div",{
fontSize:"9px",color:LIME,fontWeight:"600",whiteSpace:"nowrap",
letterSpacing:".02em",opacity:"0.85",alignSelf:"center",
});
const _arRefreshDimsPreview=()=>{
const rawW=S.isCustomRes?(wInp.numVal||S.customW||1024):S.resW;
const rawH=S.isCustomRes?(hInp.numVal||S.customH||1024):S.resH;
const sw=snap8(rawW), sh=snap8(rawH);
// Show snapped dims (what will actually generate); dim if same as raw
const changed=(sw!==rawW||sh!==rawH);
tx(_arDimsPreview,`→ ${sw}×${sh}`);
_arDimsPreview.style.opacity=changed?"1":"0.5";
_arDimsPreview.title=changed?`Input: ${rawW}×${rawH} → snapped to ${sw}×${sh}`:"";
};
_arRefreshDimsPreview();
customResRow.append(wInp,_arSwapBtn,hInp,_arLockBtn,_arDimsPreview);
resSect.appendChild(customResRow);
function updateSizeControls(){
customResRow.style.display=S.isCustomRes?"flex":"none";
}
const seedInp=NI("seed",S.seed||0,0,999999999999,1,v=>{ S.seed=Math.round(v)||0; persist(); },"90px");
// ── Advanced control panel ────────────────────────────────────────────
let SAMPLERS=["euler","euler_cfg_pp","euler_ancestral","euler_ancestral_cfg_pp","heun","heunpp2","exp_heun_2_x0","exp_heun_2_x0_sde","dpm_2","dpm_2_ancestral","lms","dpm_fast","dpm_adaptive","dpmpp_2s_ancestral","dpmpp_2s_ancestral_cfg_pp","dpmpp_sde","dpmpp_sde_gpu","dpmpp_2m","dpmpp_2m_cfg_pp","dpmpp_2m_sde","dpmpp_2m_sde_gpu","dpmpp_2m_sde_heun","dpmpp_2m_sde_heun_gpu","dpmpp_3m_sde","dpmpp_3m_sde_gpu","ddpm","lcm","ipndm","ipndm_v","deis","res_multistep","res_multistep_cfg_pp","res_multistep_ancestral","res_multistep_ancestral_cfg_pp","gradient_estimation","gradient_estimation_cfg_pp","er_sde","seeds_2","seeds_3","sa_solver","sa_solver_pece","ddim","uni_pc","uni_pc_bh2"];
let SCHEDULERS=["simple","sgm_uniform","karras","exponential","ddim_uniform","beta","normal","linear_quadratic","kl_optimal"];
const ADV_BORDER="rgba(100,80,180,.5)";
const ADV_BG="rgba(26,20,60,.55)";
const ADV_LABEL="rgba(160,140,220,.7)";
const advPanel=mk("div",{
display:"none",flexDirection:"column",gap:"4px",
border:`1px solid ${ADV_BORDER}`,borderRadius:"6px",
padding:"5px",background:ADV_BG,
});
const _advNIStyle=(ni)=>{ ni._inp.style.fontSize="9px"; ni._inp.style.padding="1px 2px"; ni._inp.style.height="22px"; return ni; };
const stepsInp=_advNIStyle(NI("steps",S.steps,1,150,1,v=>{S.steps=Math.round(v)||4;persist();},"100%"));
const cfgInp=_advNIStyle(NI("cfg",S.cfg,0,30,0.1,v=>{S.cfg=parseFloat(v.toFixed(2));persist();},"100%"));
_advControlsReady=true;
const _advDDStyle=(dd)=>{
const trig=dd.el.querySelector("div");
if(trig){ trig.style.height="22px"; trig.style.fontSize="9px"; trig.style.padding="0 6px"; }
return dd;
};
const samplerDD=_advDDStyle(DD(SAMPLERS,S.sampler,v=>{S.sampler=v;persist();}));
const schedulerDD=_advDDStyle(DD(SCHEDULERS,S.scheduler,v=>{S.scheduler=v;persist();}));
// Fetch available samplers/schedulers from ComfyUI (includes custom node samplers)
api.fetchApi("/object_info/KSampler").then(r=>r.json()).then(d=>{
const info=d?.KSampler?.input?.required;
const slist=info?.sampler_name?.[0];
const schlist=info?.scheduler?.[0];
if(Array.isArray(slist)&&slist.length){
SAMPLERS=slist;
samplerDD.updateItems(slist);
if(slist.includes(S.sampler)) samplerDD.set(S.sampler);
else{ S.sampler=slist[0]; samplerDD.set(slist[0]); persist(); }
}
if(Array.isArray(schlist)&&schlist.length){
SCHEDULERS=schlist;
schedulerDD.updateItems(schlist);
if(schlist.includes(S.scheduler)) schedulerDD.set(S.scheduler);
else{ S.scheduler=schlist[0]; schedulerDD.set(schlist[0]); persist(); }
}
}).catch(()=>{});
// Seed controls
const _advSeedInp=_advNIStyle(NI("seed",S.seed||0,0,999999999999,1,v=>{
S.seed=Math.round(v)||0; seedInp.setVal(S.seed); persist();
},"60px"));
_advSeedInp._inp.disabled=S.randomizeSeed;
_advSeedInp.style.opacity=S.randomizeSeed?"0.4":"1";
const _advSeedIconLock=`<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
const _advSeedIconDice=`<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="3"/><circle cx="8" cy="8" r="1.3" fill="currentColor"/><circle cx="16" cy="8" r="1.3" fill="currentColor"/><circle cx="12" cy="12" r="1.3" fill="currentColor"/><circle cx="8" cy="16" r="1.3" fill="currentColor"/><circle cx="16" cy="16" r="1.3" fill="currentColor"/></svg>`;
const _advSeedLockBtn=mk("button",{
background:"transparent",border:"none",padding:"0 1px",cursor:"pointer",outline:"none",
display:"flex",alignItems:"center",gap:"3px",flexShrink:"0",color:ADV_LABEL,transition:"color .15s",
});
const _advSeedStateLbl=mk("span",{fontSize:"7px",fontWeight:"700",letterSpacing:".06em",textTransform:"uppercase",whiteSpace:"nowrap",transition:"color .15s"});
const _advSeedRefresh=()=>{
_advSeedLockBtn.innerHTML="";
const ico=mk("span"); ico.innerHTML=S.randomizeSeed?_advSeedIconDice:_advSeedIconLock;
tx(_advSeedStateLbl,S.randomizeSeed?"Random":"Locked");
const col=S.randomizeSeed?ADV_LABEL:"#c0a0ff";
_advSeedLockBtn.style.color=col;
_advSeedStateLbl.style.color=col;
_advSeedLockBtn.append(ico,_advSeedStateLbl);
_advSeedInp._inp.disabled=S.randomizeSeed;
_advSeedInp.style.opacity=S.randomizeSeed?"0.4":"1";
};
_advSeedRefresh();
_advSeedLockBtn.onclick=()=>{ S.randomizeSeed=!S.randomizeSeed; persist(); _advSeedRefresh(); _advRefresh(); };
_advSeedLockBtn.onmouseenter=()=>{ _advSeedLockBtn.style.color="#d0b0ff"; _advSeedStateLbl.style.color="#d0b0ff"; };
_advSeedLockBtn.onmouseleave=()=>_advSeedRefresh();
// Helper: inline label+control pair
const _mkInline=(lbl,el)=>{
const w=mk("div",{display:"flex",alignItems:"center",gap:"3px",flexShrink:"0"});
const l=mk("span",{fontSize:"7px",fontWeight:"700",color:ADV_LABEL,letterSpacing:".06em",textTransform:"uppercase",whiteSpace:"nowrap"});
tx(l,lbl); w.append(l,el); return w;
};
// Single flat row: Steps · CFG · 🎲seed · Sampler · Scheduler
const advRow1=mk("div",{display:"flex",gap:"5px",alignItems:"center",flexWrap:"wrap"});
stepsInp.style.width="34px"; stepsInp.style.minWidth="0";
cfgInp.style.width="30px"; cfgInp.style.minWidth="0";
samplerDD.el.style.width="80px";
schedulerDD.el.style.width="70px";
const _advSeedGroup=mk("div",{display:"flex",alignItems:"center",gap:"2px",flexShrink:"0"});
_advSeedGroup.append(_mkInline("Seed",_advSeedInp),_advSeedLockBtn);
advRow1.append(
_mkInline("Steps",stepsInp),
_mkInline("CFG",cfgInp),
_advSeedGroup,
_mkInline("Sampler",samplerDD.el),
_mkInline("Scheduler",schedulerDD.el),
);
advPanel.append(advRow1);
// Seed locked warning — shown when advanced UI is off but seed is fixed
const _seedLockedWarn=mk("div",{
display:"none",alignItems:"center",gap:"8px",
background:"rgba(160,120,255,.10)",border:"1px solid rgba(160,120,255,.35)",
borderRadius:"6px",padding:"6px 10px",
});
const _seedLockedIcon=mk("span",{fontSize:"12px",flexShrink:"0"});tx(_seedLockedIcon,"🔒");
const _seedLockedText=mk("div",{fontSize:"8px",color:"rgba(200,180,255,.9)",lineHeight:"1.5",flex:"1"});
tx(_seedLockedText,"Seed is locked — you left it fixed.");
const _seedLockedBtn=mk("button",{
background:"rgba(160,120,255,.25)",border:"1px solid rgba(160,120,255,.6)",
borderRadius:"4px",padding:"3px 8px",fontSize:"8px",fontWeight:"700",
color:"#d0b8ff",cursor:"pointer",outline:"none",flexShrink:"0",
transition:"background .15s,border-color .15s",whiteSpace:"nowrap",
});
tx(_seedLockedBtn,"Set to random");
_seedLockedBtn.onmouseenter=()=>{_seedLockedBtn.style.background="rgba(160,120,255,.4)";};
_seedLockedBtn.onmouseleave=()=>{_seedLockedBtn.style.background="rgba(160,120,255,.25)";};
_seedLockedBtn.onclick=()=>{
S.randomizeSeed=true; persist();
_advSeedRefresh();
_advRefresh();
};
_seedLockedWarn.append(_seedLockedIcon,_seedLockedText,_seedLockedBtn);
const _advRefresh=()=>{
advPanel.style.display=S.advancedUI?"flex":"none";
_seedLockedWarn.style.display=(!S.advancedUI&&!S.randomizeSeed)?"flex":"none";
};
_advRefresh();
const genRow=mk("div",{display:"flex",gap:"0",alignItems:"stretch",marginTop:"auto",width:"100%",boxSizing:"border-box"});
const genBtn=mk("button",{
background:LIME,color:"#111",border:"2px solid transparent",borderRadius:"8px",
padding:"0",height:"38px",fontSize:"13px",fontWeight:"700",
cursor:"pointer",flex:"1",letterSpacing:".02em",
transition:"background .3s,color .3s,border-color .3s,box-shadow .15s,transform .1s",
outline:"none",position:"relative",overflow:"hidden",
});
tx(genBtn,"Generate");
const _genSweep=mk("div",{
position:"absolute",top:"0",left:"-80%",width:"50%",height:"100%",
background:"linear-gradient(90deg,transparent 0%,rgba(255,255,255,.75) 50%,transparent 100%)",
transform:"skewX(-20deg)",pointerEvents:"none",opacity:"0",transition:"none",
});
genBtn.appendChild(_genSweep);
genBtn.onmouseenter=()=>{
if(!S.generating){
_genSweep.style.animation="none";void _genSweep.offsetWidth;
_genSweep.style.animation="fk-light-sweep 1s ease forwards";
}
};
const stopBtn=mk("button",{
background:"transparent",border:`1px solid ${C.border}`,borderRadius:"8px",
color:C.muted,fontSize:"12px",cursor:"pointer",
maxWidth:"0",minWidth:"0",width:"0",opacity:"0",padding:"0",height:"38px",
transition:"max-width .25s ease, opacity .25s ease, padding .25s ease",outline:"none",
overflow:"hidden",flexShrink:"0",whiteSpace:"nowrap",
});
tx(stopBtn,"■ Stop");
stopBtn.onmouseenter=()=>{stopBtn.style.borderColor=C.err;stopBtn.style.color=C.err;};
stopBtn.onmouseleave=()=>{stopBtn.style.borderColor=C.border;stopBtn.style.color=C.muted;};
let _activePromptId=null;
stopBtn.onclick=async()=>{
// 1. Interrupt the currently running execution immediately
try{ await api.fetchApi("/interrupt",{method:"POST"}); }catch(e){}
// 2. Delete our prompt from the queue if it's still pending
if(_activePromptId){
try{
await api.fetchApi("/queue",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({delete:[_activePromptId]}),
});
}catch(e){}
_activePromptId=null;
}
resetBtn();
};
genRow.append(genBtn,stopBtn);
// ── LEFT PANEL ASSEMBLY ──────────────────────────────────────────────
leftPanel.append(i2iPanel,resSect,advPanel,_seedLockedWarn,genRow);
// ── RIGHT PANEL — Preview area fills available height ──
const rightPanel=mk("div",{flex:"1",minWidth:"0",display:"flex",flexDirection:"column",overflow:"hidden"});
const previewBox=mk("div",{
width:"100%",flex:"1",minHeight:"0",background:"#000",
borderRadius:"10px",border:`1px solid ${C.border}`,
position:"relative",overflow:"hidden",
});
// Placeholder
const placeholder=mk("div",{
position:"absolute",inset:"0",display:"flex",flexDirection:"column",
alignItems:"center",justifyContent:"center",gap:"10px",
});
const placeholderSvg=document.createElementNS("http://www.w3.org/2000/svg","svg");
placeholderSvg.setAttribute("viewBox","0 0 24 24");placeholderSvg.setAttribute("width","40");placeholderSvg.setAttribute("height","40");
placeholderSvg.setAttribute("fill","none");placeholderSvg.setAttribute("stroke","currentColor");
placeholderSvg.setAttribute("stroke-width","1");placeholderSvg.setAttribute("stroke-linecap","round");
placeholderSvg.style.color=C.border;
placeholderSvg.innerHTML=`<rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>`;
const placeholderLbl=mk("div",{fontSize:"11px",color:C.muted});
tx(placeholderLbl,"Generated image will appear here");
placeholder.append(placeholderSvg,placeholderLbl);
// Final image
const finalImg=mk("img",{
position:"absolute",inset:"0",width:"100%",height:"100%",
objectFit:"contain",display:"none",borderRadius:"10px",
});
// Progress bar — overlaid at the bottom of the preview box
const progWrap=mk("div",{
position:"absolute",bottom:"0",left:"0",right:"0",
background:"linear-gradient(transparent,rgba(0,0,0,.88))",
padding:"16px 14px 12px",display:"none",
flexDirection:"column",gap:"4px",boxSizing:"border-box",pointerEvents:"none",
});
const progTop=mk("div",{display:"flex",justifyContent:"space-between",alignItems:"center"});
const progStageL=mk("div",{fontSize:"11px",fontWeight:"600",color:C.text,textAlign:"center",flex:"1"});
tx(progStageL,"Generating…");
const progPct=mk("div",{fontSize:"10px",color:C.muted,flexShrink:"0"});tx(progPct,"0%");
progTop.append(progStageL,progPct);
const progBar=mk("div",{height:"3px",borderRadius:"2px",background:"rgba(255,255,255,.15)",overflow:"hidden",marginTop:"4px"});
const progFill=mk("div",{height:"100%",background:LIME,width:"0%",transition:"width .3s ease",borderRadius:"2px"});
progBar.appendChild(progFill);
const progDetailL=mk("div",{fontSize:"9px",color:"rgba(255,255,255,.5)",textAlign:"center",marginTop:"2px"});
progWrap.append(progTop,progBar,progDetailL);
const setStage=(l,d,p)=>{
tx(progStageL,l);tx(progDetailL,d);
progFill.style.width=p+"%";tx(progPct,Math.round(p)+"%");
};
// ── Image Comparer — always active after I2I generation ──────────────
// Generated image fills the box; Image 1 revealed from the right by dragging divider left.
// Divider starts at 100% (full generated shown), user drags left to reveal Image 1.
const comparerWrap=mk("div",{
position:"absolute",inset:"0",display:"none",cursor:"col-resize",
userSelect:"none",borderRadius:"10px",overflow:"hidden",
});
// Image 1 (reference) — full-size background, visible on the right of divider
const comparerBase=mk("img",{
position:"absolute",inset:"0",width:"100%",height:"100%",objectFit:"contain",
});
// Generated image — clipped to left portion
const comparerGen=mk("div",{
position:"absolute",top:"0",left:"0",bottom:"0",overflow:"hidden",
width:"100%", // starts at 100% so only generated is visible
});
const comparerGenImg=mk("img",{
position:"absolute",top:"0",left:"0",height:"100%",objectFit:"contain",
});
comparerGen.appendChild(comparerGenImg);
// Divider line
const comparerLine=mk("div",{
position:"absolute",top:"0",bottom:"0",width:"2px",
background:LIME,left:"calc(100% - 1px)",
boxShadow:"0 0 8px rgba(240,255,65,.5)",
display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",
gap:"6px",zIndex:"4",
});
// Handle circle on divider
const comparerHandle=mk("div",{
width:"30px",height:"30px",borderRadius:"50%",background:LIME,
border:"2px solid #111",flexShrink:"0",
display:"flex",alignItems:"center",justifyContent:"center",
boxShadow:"0 2px 10px rgba(0,0,0,.7)",pointerEvents:"none",
});
comparerHandle.innerHTML=`<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#111" stroke-width="2.5" stroke-linecap="round"><path d="M8 4l-4 8 4 8M16 4l4 8-4 8"/></svg>`;
comparerLine.append(comparerHandle);
comparerWrap.append(comparerBase,comparerGen,comparerLine);
// Drag logic
let _cmpDragging=false;
const _cmpSetPct=(pct)=>{
pct=Math.max(0,Math.min(100,pct));
comparerGen.style.width=pct+"%";
comparerLine.style.left=`calc(${pct}% - 1px)`;
comparerGenImg.style.width=(comparerWrap.offsetWidth||620)+"px";
};
comparerWrap.addEventListener("mousedown",e=>{
_cmpDragging=true;e.preventDefault();
});
document.addEventListener("mousemove",e=>{
if(!_cmpDragging) return;
const r=comparerWrap.getBoundingClientRect();
_cmpSetPct((e.clientX-r.left)/r.width*100);
});
document.addEventListener("mouseup",()=>{ _cmpDragging=false; });
comparerWrap.addEventListener("touchstart",()=>{_cmpDragging=true;},{passive:true});
comparerWrap.addEventListener("touchmove",e=>{
if(!_cmpDragging) return;
const r=comparerWrap.getBoundingClientRect();
_cmpSetPct((e.touches[0].clientX-r.left)/r.width*100);
},{passive:true});
comparerWrap.addEventListener("touchend",()=>{ _cmpDragging=false; });
// "Use as…" dropdown — top-right of previewBox, visible after generation
const previewUseWrap=mk("div",{
position:"absolute",top:"10px",right:"10px",zIndex:"5",display:"none",
});
const previewUseBtn=mk("button",{
background:"rgba(20,20,20,.88)",color:"rgba(255,255,255,.88)",
border:"1px solid rgba(255,255,255,.22)",
borderRadius:"6px",padding:"4px 11px",fontSize:"9px",fontWeight:"600",
cursor:"pointer",outline:"none",whiteSpace:"nowrap",
backdropFilter:"blur(4px)",letterSpacing:".04em",
boxShadow:"0 2px 8px rgba(0,0,0,.5)",
display:"flex",alignItems:"center",gap:"5px",
transition:"background .15s,color .15s,border-color .15s",
});
previewUseBtn.innerHTML=`Use as… <svg viewBox="0 0 10 6" width="8" height="8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><polyline points="1,1 5,5 9,1"/></svg>`;
previewUseBtn.onmouseenter=()=>{previewUseBtn.style.background="rgba(40,40,40,.97)";previewUseBtn.style.color="#fff";previewUseBtn.style.borderColor="rgba(255,255,255,.4)";};
previewUseBtn.onmouseleave=()=>{previewUseBtn.style.background="rgba(20,20,20,.88)";previewUseBtn.style.color="rgba(255,255,255,.88)";previewUseBtn.style.borderColor="rgba(255,255,255,.22)";};
const previewUseDrop=mk("div",{
position:"absolute",top:"calc(100% + 4px)",right:"0",
background:C.bg1,border:`1px solid ${C.borderH}`,borderRadius:"8px",
minWidth:"160px",overflow:"hidden",display:"none",zIndex:"200",
boxShadow:"0 4px 20px rgba(0,0,0,.7)",flexDirection:"column",
});
const _mkPUSection=(label)=>{ const h=mk("div",{padding:"6px 12px 3px",fontSize:"8px",fontWeight:"700",letterSpacing:".08em",textTransform:"uppercase",color:C.muted,userSelect:"none"});tx(h,label);return h; };
const _mkPUItem=(label,icon,fn)=>{ const row=mk("div",{padding:"7px 12px",fontSize:"10px",fontWeight:"500",color:C.text,cursor:"pointer",display:"flex",alignItems:"center",gap:"7px",transition:"background .1s,color .1s",userSelect:"none"});const ico=mk("span",{fontSize:"11px",width:"14px",textAlign:"center",flexShrink:"0",color:C.muted});tx(ico,icon);const lbl=mk("span");tx(lbl,label);row.append(ico,lbl);row.onmouseenter=()=>{row.style.background="rgba(240,255,65,.10)";row.style.color=LIME;ico.style.color=LIME;};row.onmouseleave=()=>{row.style.background="";row.style.color=C.text;ico.style.color=C.muted;};row.onclick=()=>{previewUseDrop.style.display="none";_puDropOpen=false;fn();};return row; };
const _mkPUDivider=()=>mk("div",{height:"1px",background:C.border,margin:"2px 0"});
const _getLastSrc=()=>_lastGenObj||(_galImages&&_galImages[0]);
const _puUpload=async(fn)=>{ const v=_getLastSrc();if(!v)return;try{const n=await _uploadOutputToInput(v);fn(n);}catch(e){console.warn("[ZImageOneNode] use-as:",e);} };
previewUseDrop.append(
_mkPUSection("I2I"),
_mkPUItem("I2I slot","⟳",()=>_puUpload(n=>{setPill("i2i");S.i2iImage=n;i2iSlot._restorePreview(n);persist();})),
);
let _puDropOpen=false;
previewUseBtn.onclick=e=>{ e.stopPropagation();_puDropOpen=!_puDropOpen;previewUseDrop.style.display=_puDropOpen?"flex":"none"; };
document.addEventListener("click",()=>{ if(_puDropOpen){previewUseDrop.style.display="none";_puDropOpen=false;} });
previewUseDrop.addEventListener("click",e=>e.stopPropagation());
previewUseWrap.append(previewUseBtn,previewUseDrop);
// ── Delete helper ─────────────────────────────────────────────────────
// Confirm popover for delete — shows "Sure? Yes / Keep" near the trigger button
const _confirmPop=mk("div",{
position:"fixed",zIndex:"99999",
background:"#18181c",border:`1px solid rgba(255,80,80,.45)`,
borderRadius:"10px",padding:"10px 14px",
display:"none",flexDirection:"column",alignItems:"center",gap:"8px",
boxShadow:"0 6px 24px rgba(0,0,0,.7)",
minWidth:"110px",
});
const _confirmTxt=mk("div",{fontSize:"11px",fontWeight:"600",color:"rgba(255,200,200,.9)",
letterSpacing:".02em",whiteSpace:"nowrap"});
tx(_confirmTxt,"Sure?");
const _confirmBtns=mk("div",{display:"flex",gap:"6px"});
const _mkConfBtn=(label,bg,hoverBg,color,border)=>{
const b=mk("button",{
background:bg,border:`1px solid ${border}`,borderRadius:"6px",
padding:"4px 13px",fontSize:"10px",fontWeight:"700",
color,cursor:"pointer",outline:"none",letterSpacing:".04em",
transition:"background .12s,border-color .12s",
});
tx(b,label);
b.onmouseenter=()=>{b.style.background=hoverBg;};
b.onmouseleave=()=>{b.style.background=bg;};
return b;
};
const _confirmYes=_mkConfBtn("Yes","rgba(180,30,30,.8)","rgba(220,40,40,.95)","#ffb0b0","rgba(255,80,80,.5)");
const _confirmKeep=_mkConfBtn("Keep","rgba(255,255,255,.06)","rgba(255,255,255,.13)","rgba(255,255,255,.7)","rgba(255,255,255,.18)");
_confirmBtns.append(_confirmYes,_confirmKeep);
_confirmPop.append(_confirmTxt,_confirmBtns);
document.body.appendChild(_confirmPop);
let _confirmResolve=null;
const _showConfirm=(anchorEl)=>new Promise(res=>{
_confirmResolve=res;
const r=anchorEl.getBoundingClientRect();
// Position above the button, centered
_confirmPop.style.display="flex";
const pw=_confirmPop.offsetWidth||120;
let left=r.left+r.width/2-pw/2;
let top=r.top-_confirmPop.offsetHeight-8;
if(top<8) top=r.bottom+8;
left=Math.max(8,Math.min(left,window.innerWidth-pw-8));
_confirmPop.style.left=left+"px";
_confirmPop.style.top=top+"px";
});
const _hideConfirm=()=>{ _confirmPop.style.display="none"; _confirmResolve=null; };
_confirmYes.onclick=()=>{ const r=_confirmResolve; _hideConfirm(); r?.(true); };
_confirmKeep.onclick=()=>{ const r=_confirmResolve; _hideConfirm(); r?.(false); };
document.addEventListener("keydown",e=>{ if(e.key==="Escape"&&_confirmPop.style.display!=="none"){ _hideConfirm(); _confirmResolve?.(false); } });
document.addEventListener("click",e=>{ if(_confirmPop.style.display!=="none"&&!_confirmPop.contains(e.target)) { _hideConfirm(); _confirmResolve?.(false); } },{capture:true});
const _deleteImage=async(imgObj,anchorEl,onSuccess)=>{
if(!imgObj||!imgObj.filename) return;
const ok=await _showConfirm(anchorEl);
if(!ok) return;
try{
const r=await api.fetchApi("/z_image/delete",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({filename:imgObj.filename,subfolder:imgObj.subfolder||""}),
});
const d=await r.json();
if(d.ok){ onSuccess?.(); }
else{ console.warn("[ZImageOneNode] delete failed:",(d.error||"unknown")); }
}catch(e){ console.warn("[ZImageOneNode] delete error:",fmtErr(e)); }
};
// Preview delete button — bottom-right of previewBox
const previewDelBtn=mk("button",{
position:"absolute",bottom:"10px",right:"10px",zIndex:"5",
width:"28px",height:"28px",borderRadius:"8px",
background:"rgba(180,30,30,.75)",border:"1px solid rgba(255,80,80,.35)",
color:"rgba(255,200,200,.9)",cursor:"pointer",outline:"none",
display:"none",alignItems:"center",justifyContent:"center",padding:"0",
backdropFilter:"blur(4px)",transition:"background .15s,border-color .15s",
});
previewDelBtn.title="Delete image";
previewDelBtn.innerHTML=`<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>`;
previewDelBtn.onmouseenter=()=>{previewDelBtn.style.background="rgba(220,40,40,.95)";previewDelBtn.style.borderColor="rgba(255,80,80,.7)";};
previewDelBtn.onmouseleave=()=>{previewDelBtn.style.background="rgba(180,30,30,.75)";previewDelBtn.style.borderColor="rgba(255,80,80,.35)";};
previewDelBtn.onclick=()=>{
const src=_getLastSrc();
if(!src) return;
_deleteImage(src,previewDelBtn,()=>{
// Hide preview, show placeholder, clear _lastGenObj
finalImg.src="";finalImg.style.display="none";
comparerWrap.style.display="none";
previewUseWrap.style.display="none";
previewDelBtn.style.display="none";
placeholder.style.display="flex";
_lastGenObj=null;
_galNeedsRefresh=true;
});
};
// Comparer activates automatically in EDIT mode after generation
previewBox.append(placeholder,finalImg,comparerWrap,previewUseWrap,previewDelBtn,progWrap);
rightPanel.appendChild(previewBox);
mainRow.append(leftPanel,rightPanel);
// ── PROMPT ───────────────────────────────────────────────────────────
const promptWrap=mk("div",{display:"flex",flexDirection:"column",gap:"5px"});
const promptHdr=mk("div",{display:"flex",alignItems:"center",gap:"5px"});
const promptCap=cap("Prompt");
// ── LoRA overlay ──────────────────────────────────────────────────────
const _ulOverlay=mk("div",{
position:"fixed",inset:"0",zIndex:"99998",display:"none",
alignItems:"center",justifyContent:"center",
});
const _ulBg=mk("div",{position:"absolute",inset:"0",background:"rgba(0,0,0,.7)"});
const _ulPanel=mk("div",{
position:"relative",
background:"linear-gradient(145deg,#111 0%,#0d0d0d 100%)",
border:`1px solid rgba(240,255,65,.18)`,
borderRadius:"16px",padding:"20px 22px 22px",width:"520px",
boxShadow:"0 20px 60px rgba(0,0,0,.95),inset 0 1px 0 rgba(255,255,255,.04)",
display:"flex",flexDirection:"column",gap:"16px",
});
const _ulPHdr=mk("div",{display:"flex",alignItems:"center",gap:"8px"});
const _ulPTitle=mk("div",{fontSize:"12px",fontWeight:"700",color:"#fff",flex:"1",
letterSpacing:".06em",textTransform:"uppercase"});
tx(_ulPTitle,"LoRA");
const _ulPClose=mk("button",{background:"none",border:"none",cursor:"pointer",
color:C.muted,fontSize:"16px",lineHeight:"1",padding:"0",outline:"none",flexShrink:"0"});
tx(_ulPClose,"×");
_ulPClose.onmouseenter=()=>_ulPClose.style.color="#fff";
_ulPClose.onmouseleave=()=>_ulPClose.style.color=C.muted;
_ulPClose.onclick=()=>{ _ulOverlay.style.display="none"; hideDimmer(); };
_ulBg.onclick=()=>{ _ulOverlay.style.display="none"; hideDimmer(); };
const _ulRefreshBtn=mk("button",{background:"none",border:"none",cursor:"pointer",
color:C.muted,fontSize:"13px",lineHeight:"1",padding:"0 6px 0 0",outline:"none",flexShrink:"0"});
tx(_ulRefreshBtn,"↻");
_ulRefreshBtn.title="Refresh model list";
_ulRefreshBtn.onmouseenter=()=>_ulRefreshBtn.style.color="#fff";
_ulRefreshBtn.onmouseleave=()=>_ulRefreshBtn.style.color=C.muted;
_ulRefreshBtn.onclick=()=>{ tx(_ulRefreshBtn,"↻"); _loadModels(); };
_ulPHdr.append(_ulPTitle,_ulRefreshBtn,_ulPClose);
const _ulPSub=mk("div",{width:"100%",height:"1px",background:"rgba(240,255,65,.10)",marginTop:"-6px"});
const _ulRows=mk("div",{display:"flex",flexDirection:"column",gap:"10px"});
const _mkULRow=(idx)=>{
const row=mk("div",{display:"flex",flexDirection:"column",gap:"4px"});
const rowLbl=mk("div",{fontSize:"7px",color:"rgba(240,255,65,.5)",fontWeight:"700",
letterSpacing:".1em",textTransform:"uppercase"});
tx(rowLbl,`SLOT ${idx+1}`);
const rowCtrl=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
// Trigger words area — shown below the control row
const trigRow=mk("div",{display:"none",flexDirection:"column",gap:"5px",
marginTop:"2px",padding:"8px 10px",background:"rgba(240,255,65,.04)",
border:`1px solid rgba(240,255,65,.12)`,borderRadius:"8px",
});
const trigTopRow=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
const trigLbl=mk("div",{fontSize:"9px",fontWeight:"600",color:C.muted,whiteSpace:"nowrap",flexShrink:"0"});
tx(trigLbl,"Trigger words:");
const trigVal=mk("div",{fontSize:"10px",color:LIME,flex:"1",minWidth:"0",
wordBreak:"break-word",lineHeight:"1.4"});
tx(trigVal,"—");
trigTopRow.append(trigLbl,trigVal);
// Custom trigger input row
const trigCustomRow=mk("div",{display:"flex",alignItems:"center",gap:"5px",marginTop:"2px"});
const trigCustomLbl=mk("div",{fontSize:"8px",color:C.muted,whiteSpace:"nowrap",flexShrink:"0"});
tx(trigCustomLbl,"Custom:");
const trigCustomInp=mk("input",{
flex:"1",background:"rgba(255,255,255,.06)",border:`1px solid rgba(255,255,255,.12)`,
borderRadius:"5px",color:C.text,fontSize:"10px",padding:"3px 7px",
outline:"none",transition:"border-color .15s",
},{type:"text",placeholder:"Add custom trigger words…"});
trigCustomInp.onfocus=()=>trigCustomInp.style.borderColor=LIME;
trigCustomInp.onblur=()=>trigCustomInp.style.borderColor="rgba(255,255,255,.12)";
const trigSaveBtn=mk("button",{
background:"rgba(255,255,255,.07)",border:`1px solid rgba(255,255,255,.15)`,
borderRadius:"5px",cursor:"pointer",color:C.muted,fontSize:"9px",fontWeight:"700",
padding:"3px 9px",outline:"none",transition:"all .15s",flexShrink:"0",whiteSpace:"nowrap",
});
tx(trigSaveBtn,"Save");
trigSaveBtn.onmouseenter=()=>{trigSaveBtn.style.background="rgba(240,255,65,.12)";trigSaveBtn.style.borderColor=LIME;trigSaveBtn.style.color=LIME;};
trigSaveBtn.onmouseleave=()=>{trigSaveBtn.style.background="rgba(255,255,255,.07)";trigSaveBtn.style.borderColor="rgba(255,255,255,.15)";trigSaveBtn.style.color=C.muted;};
trigSaveBtn.onclick=async()=>{
const name=ulDD.value;
if(!name||name==="none") return;
const txt=trigCustomInp.value.trim();
await _saveCustomTrigger(name,txt);
// Update displayed value and clear input field
tx(trigVal,txt||"—");
trigCustomInp.value="";
trigSaveBtn.style.color=LIME; tx(trigSaveBtn,"Saved ✓");
setTimeout(()=>{ trigSaveBtn.style.color=C.muted; tx(trigSaveBtn,"Save"); },1500);
};
trigCustomRow.append(trigCustomLbl,trigCustomInp,trigSaveBtn);
trigRow.append(trigTopRow,trigCustomRow);
// Load and display trigger words when lora changes
const _refreshTrigWords=async(loraName)=>{
if(!loraName||loraName==="none"){ trigRow.style.display="none"; return; }
trigRow.style.display="flex";
tx(trigVal,"…");
trigCustomInp.value="";
await _loadCustomTriggers();
const custom=_getCustomTrigger(loraName);
if(custom){
// Custom always wins over metadata
tx(trigVal,custom);
} else {
try{
const r=await api.fetchApi(`/z_image/lora_triggers?name=${encodeURIComponent(loraName)}`);
const d=await r.json();
tx(trigVal,(d.ok&&d.triggers?.length)?d.triggers.join(", "):"—");
}catch(e){ tx(trigVal,"—"); }
}
};
const ulDD=DD(["none"],"none",v=>{
const has=v&&v!=="none";
S.userLoras[idx].name=has?v:"";
if(!has){ S.userLoras[idx].strength=0; ulStr.value="0"; }
else if(S.userLoras[idx].strength===0){ S.userLoras[idx].strength=1; ulStr.value="1"; }
_ulUpdateBtn();persist();
_refreshTrigWords(v);
});
ulDD.el.style.flex="1";ulDD.el.style.minWidth="0";
const ulStr=mk("input",{
width:"44px",textAlign:"center",background:"rgba(255,255,255,.06)",
border:`1px solid rgba(255,255,255,.1)`,borderRadius:"6px",
color:LIME,fontSize:"10px",fontWeight:"700",
padding:"5px 0",outline:"none",transition:"border-color .15s",flexShrink:"0",
},{type:"number",step:"0.05",value:String(S.userLoras[idx].name&&S.userLoras[idx].name!=="none"?S.userLoras[idx].strength||1:0)});
ulStr.onfocus=()=>ulStr.style.borderColor=LIME;
ulStr.onblur=()=>{ S.userLoras[idx].strength=isNaN(+ulStr.value)?1:+ulStr.value;
ulStr.value=String(S.userLoras[idx].strength);persist(); };
ulStr.oninput=()=>{ S.userLoras[idx].strength=+ulStr.value||0;persist(); };
const ulClr=mk("button",{
background:"rgba(255,80,80,.08)",border:"1px solid rgba(255,80,80,.25)",borderRadius:"6px",
cursor:"pointer",color:"rgba(255,100,100,.7)",fontSize:"9px",fontWeight:"700",
padding:"5px 8px",outline:"none",transition:"all .15s",flexShrink:"0",
});
tx(ulClr,"CLR");
ulClr.onmouseenter=()=>{ ulClr.style.background="rgba(255,80,80,.18)";ulClr.style.color="#ff6666"; };
ulClr.onmouseleave=()=>{ ulClr.style.background="rgba(255,80,80,.08)";ulClr.style.color="rgba(255,100,100,.7)"; };
ulClr.onclick=()=>{
S.userLoras[idx]={name:"",strength:0};ulDD.set("none");ulStr.value="0";
_ulUpdateBtn();persist();
trigRow.style.display="none";
};
rowCtrl.append(ulDD.el,ulStr,ulClr);
row.append(rowLbl,rowCtrl,trigRow);
row._dd=ulDD;row._str=ulStr;
// Restore trigger words display if lora already selected
if(S.userLoras[idx].name&&S.userLoras[idx].name!=="none"){
_refreshTrigWords(S.userLoras[idx].name);
}
return row;
};
const _ulRowEls=[_mkULRow(0),_mkULRow(1),_mkULRow(2)];
_ulRowEls.forEach(r=>_ulRows.appendChild(r));
// Info note at bottom of panel
const _ulInfoNote=mk("div",{
fontSize:"9px",color:C.muted,lineHeight:"1.5",
padding:"8px 10px",background:"rgba(255,255,255,.03)",
borderRadius:"6px",border:`1px solid ${C.border}`,
});
tx(_ulInfoNote,"✦ Trigger words are applied automatically if saved and set for the selected LoRA.");
_ulPanel.append(_ulPHdr,_ulPSub,_ulRows,_ulInfoNote);
_ulOverlay.append(_ulBg,_ulPanel);
root.appendChild(_ulOverlay);
// ── Collect trigger words for all active LoRAs at generate time ────────
const _buildPromptWithTriggers=async(basePrompt)=>{
await _loadCustomTriggers();
const trigParts=[];
for(const ul of S.userLoras){
if(!ul.name||ul.name==="none"||!(+(ul.strength||0)>0)) continue;
// Custom trigger words override metadata
const custom=_getCustomTrigger(ul.name);
if(custom){ trigParts.push(custom); continue; }
// Try metadata
try{
const r=await api.fetchApi(`/z_image/lora_triggers?name=${encodeURIComponent(ul.name)}`);
const d=await r.json();
if(d.ok&&d.triggers?.length) trigParts.push(d.triggers.join(", "));
}catch(e){}
}
if(!trigParts.length) return basePrompt;
const prefix=trigParts.join(", ");
return basePrompt.trim()?`${prefix}, ${basePrompt.trim()}`:prefix;
};
// ── Add LoRA button — identical style/layout to LTX node ─────────────
const _ulBtn=mk("button",{
background:"linear-gradient(135deg,rgba(240,255,65,.10),rgba(240,255,65,.04))",
border:"1.5px solid rgba(240,255,65,.35)",cursor:"pointer",
padding:"2px 8px 2px 6px",color:LIME,outline:"none",
display:"flex",alignItems:"center",gap:"5px",borderRadius:"5px",
transition:"all .15s",flexShrink:"0",marginLeft:"auto",
boxShadow:"0 0 0 0 rgba(240,255,65,0)",
});
const _ulBtnIco=document.createElementNS("http://www.w3.org/2000/svg","svg");
_ulBtnIco.setAttribute("viewBox","0 0 24 24");_ulBtnIco.setAttribute("width","9");_ulBtnIco.setAttribute("height","9");
_ulBtnIco.setAttribute("fill","none");_ulBtnIco.setAttribute("stroke","currentColor");
_ulBtnIco.setAttribute("stroke-width","2");_ulBtnIco.setAttribute("stroke-linecap","round");
_ulBtnIco.innerHTML=`<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>`;
const _ulBtnTxt=mk("span",{fontSize:"9px",fontWeight:"700",letterSpacing:".04em"});
tx(_ulBtnTxt,"Add LoRA");
const _ulBtnBadge=mk("span",{fontSize:"7px",fontWeight:"700",background:LIME,color:"#111",
borderRadius:"20px",padding:"0 4px",lineHeight:"1.6",display:"none",flexShrink:"0"});
_ulBtn.append(_ulBtnIco,_ulBtnTxt,_ulBtnBadge);
_ulBtn.onmouseenter=()=>{ _ulBtn.style.background="linear-gradient(135deg,rgba(240,255,65,.18),rgba(240,255,65,.08))";_ulBtn.style.borderColor=LIME;_ulBtn.style.boxShadow="0 0 8px rgba(240,255,65,.12)"; };
_ulBtn.onmouseleave=()=>{ _ulBtn.style.background="linear-gradient(135deg,rgba(240,255,65,.10),rgba(240,255,65,.04))";_ulBtn.style.borderColor="rgba(240,255,65,.35)";_ulBtn.style.boxShadow="0 0 0 0 rgba(240,255,65,0)"; };
_ulBtn.onclick=()=>{ _ulOverlay.style.display="flex";showDimmer(); };
const _ulUpdateBtn=()=>{
const n=S.userLoras.filter(l=>l.name&&l.name!=="none").length;
tx(_ulBtnBadge,String(n));
_ulBtnBadge.style.display=n>0?"":"none";
_ulBtn.style.borderColor=n>0?LIME:"rgba(240,255,65,.35)";
_ulBtn.style.color=n>0?LIME:LIME;
};
_ulUpdateBtn();
// ── Expand prompt button — identical style to LTX node ────────────────
const _promptExpandBtn=mk("button",{
background:"none",border:`1px solid ${C.border}`,cursor:"pointer",
padding:"2px 7px 2px 5px",color:C.muted,outline:"none",
display:"flex",alignItems:"center",gap:"5px",borderRadius:"5px",
transition:"color .15s,border-color .15s",flexShrink:"0",
});
_promptExpandBtn.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg><span style="font-size:9px;font-weight:700;letter-spacing:.04em">Expand prompt</span>`;
_promptExpandBtn.onmouseenter=()=>{ _promptExpandBtn.style.color="#fff";_promptExpandBtn.style.borderColor="#555"; };
_promptExpandBtn.onmouseleave=()=>{ _promptExpandBtn.style.color=C.muted;_promptExpandBtn.style.borderColor=C.border; };
// Error chip (shown when error is minimized)
const errMinChip=mk("button",{
display:"none",alignItems:"center",gap:"4px",
background:"none",border:`1px solid rgba(255,103,103,.35)`,
borderRadius:"5px",padding:"2px 7px",cursor:"pointer",outline:"none",
color:"rgba(255,103,103,.8)",fontSize:"9px",fontWeight:"700",
letterSpacing:".03em",transition:"border-color .15s,color .15s",flexShrink:"0",
});
tx(errMinChip,"⚠ Error");
// ── Get Inspired overlay ──────────────────────────────────────────────
// Per-pill prompt suggestions
// INSPIRE_BY_PILL is loaded from config.json on first Discover open.
// This object serves as the default — written to config.json if discover_prompts is empty.
let INSPIRE_BY_PILL={
t2i:{
categories:[],
},
i2i:{categories:[{cat:"I2I",items:[]}]},
};
// ── Get Inspired overlay — inside root (position:absolute like _promptOverlay) ──
const _inspireOverlay=mk("div",{
position:"absolute",inset:"0",zIndex:"260",background:C.bg0,
display:"none",flexDirection:"column",
padding:"12px",boxSizing:"border-box",gap:"0",
opacity:"0",transition:"opacity 0.15s ease",overflow:"hidden",
});
// Manage mode gradient flash layer — sits behind content, above bg0
const _inspireManageFlash=mk("div",{
position:"absolute",inset:"0",zIndex:"0",pointerEvents:"none",
background:"transparent",transition:"background .4s ease",
});
_inspireOverlay.appendChild(_inspireManageFlash);
const _closeInspire=()=>{
_inspireOverlay.style.opacity="0";
setTimeout(()=>_inspireOverlay.style.display="none",160);
};
// Header (static)
const _inspireHdr=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",
marginBottom:"10px",flexShrink:"0"});
const _inspireTitleEl=mk("div",{fontSize:"10px",fontWeight:"700",color:C.muted,
letterSpacing:".07em",textTransform:"uppercase"});
tx(_inspireTitleEl,"✦ Discover");
let _inspireShowFull=false;
let _t2iTemplates=[]; // persistent across _buildInspireBody rebuilds
let _t2iTemplatesLoaded=false;
const _inspireShowFullBtn=mk("button",{
background:"rgba(83,52,131,.15)",border:"1px solid rgba(83,52,131,.4)",borderRadius:"5px",
padding:"3px 8px",fontSize:"8px",fontWeight:"700",letterSpacing:".05em",textTransform:"uppercase",
cursor:"pointer",outline:"none",color:"rgba(180,160,220,.7)",transition:"all .15s",marginRight:"6px",flexShrink:"0",
});
tx(_inspireShowFullBtn,"Edit mode (E)");
_inspireShowFullBtn.style.display="none";
const _inspireShowFullUpdate=()=>{
tx(_inspireShowFullBtn,_inspireShowFull?"Exit edit mode":"Edit mode (E)");
_inspireShowFullBtn.style.background=_inspireShowFull?"rgba(180,140,255,.15)":"rgba(83,52,131,.15)";
_inspireShowFullBtn.style.borderColor=_inspireShowFull?"rgba(220,180,255,.8)":"rgba(83,52,131,.4)";
_inspireShowFullBtn.style.color=_inspireShowFull?"#f0e8ff":"rgba(180,160,220,.7)";
};
_inspireShowFullBtn.onmouseenter=()=>{
_inspireShowFullBtn.style.background="linear-gradient(90deg,rgba(26,26,46,.8),rgba(15,52,96,.6),rgba(83,52,131,.6))";
_inspireShowFullBtn.style.borderColor="rgba(180,140,255,.8)";
_inspireShowFullBtn.style.color="#e0e0ff";
};
_inspireShowFullBtn.onmouseleave=()=>_inspireShowFullUpdate();
_inspireShowFullBtn.onclick=async()=>{
_inspireShowFull=!_inspireShowFull; _inspireShowFullUpdate();
await _loadDiscoverPrompts(); _buildInspireBody();
if(_inspireShowFull){
_inspireManageFlash.style.transition="none";
_inspireManageFlash.style.background="linear-gradient(135deg,rgba(26,20,60,.9) 0%,rgba(15,52,96,.7) 50%,rgba(83,52,131,.8) 100%)";
void _inspireManageFlash.offsetWidth;
_inspireManageFlash.style.transition="background .6s ease";
_inspireManageFlash.style.background="linear-gradient(135deg,rgba(26,20,60,.45) 0%,rgba(15,52,96,.25) 50%,rgba(83,52,131,.35) 100%)";
} else {
_inspireManageFlash.style.transition="background .3s ease";
_inspireManageFlash.style.background="transparent";
}
};
const _inspireCloseBtn=mk("button",{background:"none",border:"none",cursor:"pointer",
color:C.muted,fontSize:"14px",lineHeight:"1",padding:"2px 4px",outline:"none",
display:"flex",alignItems:"center",borderRadius:"4px",transition:"color .15s"});
tx(_inspireCloseBtn,"×");
_inspireCloseBtn.onmouseenter=()=>_inspireCloseBtn.style.color="#fff";
_inspireCloseBtn.onmouseleave=()=>_inspireCloseBtn.style.color=C.muted;
_inspireCloseBtn.onclick=_closeInspire;
const _inspireHdrRight=mk("div",{display:"flex",alignItems:"center"});
_inspireHdrRight.append(_inspireShowFullBtn,_inspireCloseBtn);
_inspireHdr.append(_inspireTitleEl,_inspireHdrRight);
// Dynamic content area — rebuilt each time overlay opens
const _inspireBody=mk("div",{display:"flex",flexDirection:"column",flex:"1",minHeight:"0",gap:"0"});
const _usePrompt=(p)=>{
S.prompt=p;
if(_promptTARef) _promptTARef.value=p;
if(typeof _promptOvTA!=="undefined") _promptOvTA.value=p;
persist();_closeInspire();
};
const _buildInspireBody=()=>{
_inspireBody.innerHTML="";
const def=INSPIRE_BY_PILL[activePill]||INSPIRE_BY_PILL.t2i;
// ── Categories mode (new format) ────────────────────────────────────
if(def.categories){
const hasShortLabels=def.categories.some(({items})=>items.some(it=>typeof it==="object"&&it.label!==it.prompt));
// T2I: show toggle for templates (name ≠ prompt). EDIT also has short labels.
_inspireShowFullBtn.style.display=(hasShortLabels||activePill==="t2i"||activePill==="i2i")?"":"none";
const scroll=mk("div",{
flex:"1",overflowY:"auto",display:"flex",flexDirection:"column",gap:"12px",
scrollbarWidth:"thin",scrollbarColor:`${C.border} transparent`,
});
scroll.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
// ── T2I Templates ────────────────────────────────────────────────
if(activePill==="t2i"){
// Use persistent vars so rebuilds don't lose state
const _templates=_t2iTemplates;
let _editIdx=-1;
let _tmplPillsRow=null;
const _saveTmpl=async()=>{
try{ await api.fetchApi("/z_image/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({t2i_templates:_templates})}); }
catch(e){ console.warn("[ZImageOneNode] save templates:",e); }
};
// Form (create/edit) — framed, compact
const tmplForm=mk("div",{
display:"none",flexDirection:"column",gap:"6px",marginBottom:"6px",
border:`1px solid rgba(240,255,65,.3)`,borderRadius:"16px",
padding:"10px 12px",background:"rgba(240,255,65,.04)",boxSizing:"border-box",
});
const tmplFormTitle=mk("div",{fontSize:"8px",fontWeight:"700",color:LIME,letterSpacing:".08em",textTransform:"uppercase",marginBottom:"2px"});
tx(tmplFormTitle,"New Template");
const tmplNameInp=mk("input",{
background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"999px",
color:C.text,fontSize:"10px",padding:"6px 14px",outline:"none",
transition:"border-color .15s",width:"100%",boxSizing:"border-box",
},{placeholder:"Title…"});
tmplNameInp.onfocus=()=>tmplNameInp.style.borderColor=LIME;
tmplNameInp.onblur=()=>tmplNameInp.style.borderColor=C.border;
const tmplPromptTA=mk("textarea",{
background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"16px",
color:C.text,fontSize:"10px",padding:"8px 14px",outline:"none",lineHeight:"1.5",
resize:"vertical",minHeight:"60px",fontFamily:"inherit",
transition:"border-color .15s",width:"100%",boxSizing:"border-box",
},{placeholder:"Prompt…"});
tmplPromptTA.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
tmplPromptTA.onfocus=()=>tmplPromptTA.style.borderColor=LIME;
tmplPromptTA.onblur=()=>tmplPromptTA.style.borderColor=C.border;
const tmplFormBtns=mk("div",{display:"flex",gap:"6px",justifyContent:"flex-end"});
const tmplCancelBtn=mk("button",{
background:"transparent",border:`1px solid ${C.borderH}`,borderRadius:"999px",
padding:"5px 16px",fontSize:"9px",fontWeight:"700",color:C.muted,
cursor:"pointer",outline:"none",transition:"border-color .15s,color .15s",
});
tx(tmplCancelBtn,"Cancel");
tmplCancelBtn.onmouseenter=()=>{tmplCancelBtn.style.borderColor=C.text;tmplCancelBtn.style.color=C.text;};
tmplCancelBtn.onmouseleave=()=>{tmplCancelBtn.style.borderColor=C.borderH;tmplCancelBtn.style.color=C.muted;};
const tmplSaveBtn=mk("button",{
background:LIME,color:"#111",border:"none",borderRadius:"999px",
padding:"5px 16px",fontSize:"9px",fontWeight:"700",cursor:"pointer",outline:"none",
transition:"opacity .15s",
});
tx(tmplSaveBtn,"Save");
tmplSaveBtn.onmouseenter=()=>tmplSaveBtn.style.opacity=".85";
tmplSaveBtn.onmouseleave=()=>tmplSaveBtn.style.opacity="1";
tmplFormBtns.append(tmplCancelBtn,tmplSaveBtn);
tmplFormBtns.style.justifyContent="flex-end";
tmplForm.append(tmplFormTitle,tmplNameInp,tmplPromptTA,tmplFormBtns);
// Header: "MY TEMPLATES" only
const tmplHdr=mk("div",{display:"flex",alignItems:"center",marginBottom:"5px"});
const tmplHdrLbl=mk("div",{fontSize:"9px",fontWeight:"700",letterSpacing:".1em",textTransform:"uppercase",color:C.muted});
tx(tmplHdrLbl,"My Templates");
tmplHdr.append(tmplHdrLbl);
// Pills container + details area (same pattern as categories)
_tmplPillsRow=mk("div",{display:"flex",flexWrap:"wrap",gap:"6px",minHeight:"16px"});
const _tmplDetailsArea=mk("div",{display:"flex",flexDirection:"column",gap:"4px",marginTop:"2px"});
let _tmplOpenIdx=-1;
const _renderTmplPills=()=>{
_tmplPillsRow.innerHTML=""; _tmplDetailsArea.innerHTML=""; _tmplOpenIdx=-1;
_templates.forEach((t,idx)=>{
// Pill
const pill=mk("button",{padding:"7px 14px",borderRadius:"999px",cursor:"pointer",fontSize:"10px",fontWeight:"500",lineHeight:"1.5",border:`1px solid ${C.border}`,background:C.bg1,color:C.text,outline:"none",transition:"background .12s,border-color .12s,color .12s"});
tx(pill,t.name);
pill.onmouseenter=()=>{ if(_tmplOpenIdx!==idx){pill.style.background="rgba(240,255,65,.10)";pill.style.borderColor=LIME;pill.style.color=LIME;} };
pill.onmouseleave=()=>{ if(_tmplOpenIdx!==idx){pill.style.background=C.bg1;pill.style.borderColor=C.border;pill.style.color=C.text;} };
// Detail panel
const detail=mk("div",{display:"none",flexDirection:"column",gap:"6px",border:`1px solid rgba(180,140,255,.4)`,borderRadius:"12px",padding:"10px 14px",background:"rgba(26,20,60,.55)",boxSizing:"border-box"});
const detailTop=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
const detailLbl=mk("span",{fontSize:"9px",fontWeight:"700",color:"rgba(255,255,255,.85)",flex:"1"});tx(detailLbl,t.name);
// Copy
const dCopyBtn=mk("button",{background:"transparent",border:`1px solid rgba(100,220,120,.4)`,borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(100,220,120,.8)",cursor:"pointer",outline:"none",transition:"all .15s",flexShrink:"0"});
tx(dCopyBtn,"Copy");
dCopyBtn.onmouseenter=()=>{dCopyBtn.style.background="rgba(100,220,120,.12)";dCopyBtn.style.borderColor="rgba(100,220,120,.8)";};
dCopyBtn.onmouseleave=()=>{dCopyBtn.style.background="transparent";dCopyBtn.style.borderColor="rgba(100,220,120,.4)";};
dCopyBtn.onclick=(e)=>{ e.stopPropagation(); navigator.clipboard.writeText(t.prompt).then(()=>{ tx(dCopyBtn,"✓ Copied"); setTimeout(()=>tx(dCopyBtn,"Copy"),1500); }).catch(()=>{}); };
// Edit
const dEditBtn=mk("button",{background:"transparent",border:`1px solid rgba(160,140,220,.4)`,borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(160,140,220,.8)",cursor:"pointer",outline:"none",transition:"all .15s",flexShrink:"0"});
tx(dEditBtn,"Edit");
dEditBtn.onmouseenter=()=>{dEditBtn.style.background="rgba(160,140,220,.1)";dEditBtn.style.borderColor="rgba(160,140,220,.8)";dEditBtn.style.color="#e0d0ff";};
dEditBtn.onmouseleave=()=>{dEditBtn.style.background="transparent";dEditBtn.style.borderColor="rgba(160,140,220,.4)";dEditBtn.style.color="rgba(160,140,220,.8)";};
// Delete
const dDelWrap=mk("div",{position:"relative",display:"inline-flex",flexShrink:"0"});
const dDelBtn=mk("button",{background:"transparent",border:"1px solid rgba(220,80,80,.4)",borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(220,80,80,.8)",cursor:"pointer",outline:"none",transition:"all .15s"});
tx(dDelBtn,"Delete");
dDelBtn.onmouseenter=()=>{dDelBtn.style.background="rgba(220,80,80,.1)";dDelBtn.style.borderColor="rgba(220,80,80,.8)";};
dDelBtn.onmouseleave=()=>{dDelBtn.style.background="transparent";dDelBtn.style.borderColor="rgba(220,80,80,.4)";};
const dDelPop=mk("div",{display:"none",position:"absolute",right:"0",top:"calc(100% + 4px)",background:C.bg1,border:`1px solid ${C.borderH}`,borderRadius:"7px",padding:"6px 8px",zIndex:"10",whiteSpace:"nowrap",boxShadow:"0 4px 12px rgba(0,0,0,.5)",flexDirection:"column",gap:"5px",alignItems:"center"});
const dDelQ=mk("div",{fontSize:"9px",color:C.text,fontWeight:"600"});tx(dDelQ,"Sure?");
const dDelBtns=mk("div",{display:"flex",gap:"4px"});
const dDelYes=mk("button",{background:"rgba(160,25,25,.7)",border:"1px solid rgba(255,80,80,.3)",borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(255,180,180,.9)",cursor:"pointer",outline:"none"});tx(dDelYes,"Yes");
const dDelNo=mk("button",{background:C.bg3,border:`1px solid ${C.borderH}`,borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:C.muted,cursor:"pointer",outline:"none"});tx(dDelNo,"Keep");
dDelBtns.append(dDelYes,dDelNo); dDelPop.append(dDelQ,dDelBtns); dDelWrap.append(dDelBtn,dDelPop);
dDelBtn.onclick=(e)=>{ e.stopPropagation(); dDelPop.style.display=dDelPop.style.display==="flex"?"none":"flex"; };
dDelYes.onclick=async(e)=>{ e.stopPropagation(); dDelPop.style.display="none"; _templates.splice(idx,1); await _saveTmpl(); _renderTmplPills(); };
dDelNo.onclick=(e)=>{ e.stopPropagation(); dDelPop.style.display="none"; };
const dUseBtn=mk("button",{background:LIME,color:"#111",border:"none",borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",cursor:"pointer",outline:"none",transition:"opacity .15s",flexShrink:"0"});
tx(dUseBtn,"Use"); dUseBtn.onmouseenter=()=>dUseBtn.style.opacity=".85"; dUseBtn.onmouseleave=()=>dUseBtn.style.opacity="1";
dUseBtn.onclick=(e)=>{ e.stopPropagation(); _usePrompt(t.prompt); };
detailTop.append(detailLbl,dUseBtn,dCopyBtn,dEditBtn,dDelWrap);
const detailPromptTxt=mk("div",{fontSize:"9px",color:"rgba(200,185,230,.7)",lineHeight:"1.55",fontStyle:"italic"});tx(detailPromptTxt,t.prompt);
// Inline edit form
const dEditForm=mk("div",{display:"none",flexDirection:"column",gap:"6px",borderTop:`1px solid rgba(180,140,255,.2)`,paddingTop:"8px"});
const dETitleInp=mk("input",{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"999px",color:C.text,fontSize:"10px",padding:"6px 14px",outline:"none",transition:"border-color .15s",width:"100%",boxSizing:"border-box"},{placeholder:"Title…"});
dETitleInp.onfocus=()=>dETitleInp.style.borderColor=LIME; dETitleInp.onblur=()=>dETitleInp.style.borderColor=C.border;
const dEPromptTA=mk("textarea",{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"16px",color:C.text,fontSize:"10px",padding:"8px 14px",outline:"none",lineHeight:"1.5",resize:"vertical",minHeight:"60px",fontFamily:"inherit",transition:"border-color .15s",width:"100%",boxSizing:"border-box"},{placeholder:"Prompt…"});
dEPromptTA.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
dEPromptTA.onfocus=()=>dEPromptTA.style.borderColor=LIME; dEPromptTA.onblur=()=>dEPromptTA.style.borderColor=C.border;
const dECancelBtn=mk("button",{background:"transparent",border:`1px solid ${C.borderH}`,borderRadius:"999px",padding:"5px 16px",fontSize:"9px",fontWeight:"700",color:C.muted,cursor:"pointer",outline:"none",transition:"all .15s"});
tx(dECancelBtn,"Cancel"); dECancelBtn.onmouseenter=()=>{dECancelBtn.style.borderColor=C.text;dECancelBtn.style.color=C.text;}; dECancelBtn.onmouseleave=()=>{dECancelBtn.style.borderColor=C.borderH;dECancelBtn.style.color=C.muted;};
const dEUpdateBtn=mk("button",{background:LIME,color:"#111",border:"none",borderRadius:"999px",padding:"5px 16px",fontSize:"9px",fontWeight:"700",cursor:"pointer",outline:"none",transition:"opacity .15s"});
tx(dEUpdateBtn,"Update"); dEUpdateBtn.onmouseenter=()=>dEUpdateBtn.style.opacity=".85"; dEUpdateBtn.onmouseleave=()=>dEUpdateBtn.style.opacity="1";
const dEBtmRow=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
dEBtmRow.append(mk("div",{flex:"1"}),dECancelBtn,dEUpdateBtn);
dEditForm.append(dETitleInp,dEPromptTA,dEBtmRow);
const _tmplSetEditMode=(on)=>{ dUseBtn.style.display=on?"none":""; dCopyBtn.style.display=on?"none":""; dEditBtn.style.display=on?"none":""; dDelWrap.style.display=on?"none":"inline-flex"; };
dEditBtn.onclick=(e)=>{ e.stopPropagation(); const opening=dEditForm.style.display!=="flex"; dETitleInp.value=t.name; dEPromptTA.value=t.prompt; dEditForm.style.display=opening?"flex":"none"; _tmplSetEditMode(opening); if(opening) setTimeout(()=>dETitleInp.focus(),30); };
dECancelBtn.onclick=()=>{ dEditForm.style.display="none"; _tmplSetEditMode(false); };
dEUpdateBtn.onclick=async()=>{ const n=dETitleInp.value.trim(),p=dEPromptTA.value.trim(); if(!n||!p) return; _templates[idx]={name:n,prompt:p}; await _saveTmpl(); _renderTmplPills(); _tmplOpenIdx=idx; const det=_tmplDetailsArea.children[idx]; if(det){det.style.display="flex";det.dataset.open="1";} const pil=_tmplPillsRow.querySelectorAll("button:not([data-addpill])")[idx]; if(pil){pil.style.background="rgba(180,140,255,.15)";pil.style.borderColor="rgba(180,140,255,.6)";pil.style.color="#e0d0ff";} };
detail.append(detailTop,detailPromptTxt,dEditForm);
_tmplDetailsArea.appendChild(detail);
const _closeTmplDetail=()=>{ detail.style.display="none"; _tmplOpenIdx=-1; pill.style.background=C.bg1;pill.style.borderColor=C.border;pill.style.color=C.text; };
pill.onclick=()=>{
if(!_inspireShowFull){ _usePrompt(t.prompt); return; }
if(_tmplOpenIdx===idx){ _closeTmplDetail(); return; }
_tmplDetailsArea.querySelectorAll("div[data-open]").forEach(d=>{d.style.display="none";delete d.dataset.open;});
_tmplPillsRow.querySelectorAll("button").forEach(p2=>{p2.style.background=C.bg1;p2.style.borderColor=C.border;p2.style.color=C.text;});
_tmplOpenIdx=idx; detail.style.display="flex"; detail.dataset.open="1";
pill.style.background="rgba(180,140,255,.15)";pill.style.borderColor="rgba(180,140,255,.6)";pill.style.color="#e0d0ff";
};
_tmplPillsRow.appendChild(pill);
});
// "+ Add" pill always at the end
const tmplAddPill=mk("button",{
padding:"5px 12px",borderRadius:"999px",cursor:"pointer",
fontSize:"10px",fontWeight:"600",lineHeight:"1.5",
border:`1px dashed rgba(240,255,65,.4)`,
background:"rgba(240,255,65,.05)",color:"rgba(240,255,65,.6)",
outline:"none",transition:"all .15s",flexShrink:"0",
});
tx(tmplAddPill,"+ Add"); tmplAddPill.dataset.addpill="1";
tmplAddPill.onmouseenter=()=>{ tmplAddPill.style.borderColor=LIME;tmplAddPill.style.background="rgba(240,255,65,.12)";tmplAddPill.style.color=LIME; };
tmplAddPill.onmouseleave=()=>{ tmplAddPill.style.borderColor="rgba(240,255,65,.4)";tmplAddPill.style.background="rgba(240,255,65,.05)";tmplAddPill.style.color="rgba(240,255,65,.6)"; };
tmplAddPill.onclick=()=>{
_editIdx=-1; tmplNameInp.value=""; tmplPromptTA.value=S.prompt||"";
tx(tmplFormTitle,"New Template"); tx(tmplSaveBtn,"Save");
tmplForm.style.display=tmplForm.style.display==="flex"?"none":"flex";
if(tmplForm.style.display==="flex") setTimeout(()=>tmplNameInp.focus(),30);
};
_tmplPillsRow.appendChild(tmplAddPill);
};
tmplCancelBtn.onclick=()=>{ tmplForm.style.display="none"; };
tmplSaveBtn.onclick=async()=>{
const name=tmplNameInp.value.trim(), prompt=tmplPromptTA.value.trim();
if(!name||!prompt) return;
if(_editIdx>=0) _templates[_editIdx]={name,prompt};
else _templates.push({name,prompt});
await _saveTmpl();
tmplForm.style.display="none";
_editIdx=-1; _renderTmplPills();
};
// Load & render — only fetch from server once
if(!_t2iTemplatesLoaded){
_t2iTemplatesLoaded=true;
(async()=>{
try{ const r=await api.fetchApi("/z_image/config"); const d=await r.json(); _t2iTemplates.length=0; (d.t2i_templates||[]).forEach(t=>_t2iTemplates.push(t)); }
catch(e){}
_renderTmplPills();
})();
} else {
_renderTmplPills();
}
const tmplSection=mk("div",{});
tmplSection.append(tmplHdr,tmplForm,_tmplPillsRow,_tmplDetailsArea);
scroll.appendChild(tmplSection);
}
let _hasDual=false;
def.categories.forEach(({cat,items})=>{
// Category label — if cat contains "(note)", render note part smaller without uppercase
const catLbl=mk("div",{
display:"flex",alignItems:"baseline",gap:"5px",flexWrap:"wrap",
marginBottom:"5px",
});
const parenIdx=cat.indexOf("(");
if(parenIdx>0){
const mainPart=mk("span",{fontSize:"9px",fontWeight:"700",letterSpacing:".1em",textTransform:"uppercase",color:C.muted});
tx(mainPart,cat.slice(0,parenIdx).trim());
const notePart=mk("span",{fontSize:"8px",fontWeight:"400",color:C.muted,opacity:".7",textTransform:"none",letterSpacing:".01em"});
tx(notePart,cat.slice(parenIdx));
catLbl.append(mainPart,notePart);
} else {
catLbl.style.fontSize="9px";catLbl.style.fontWeight="700";
catLbl.style.letterSpacing=".1em";catLbl.style.textTransform="uppercase";
catLbl.style.color=C.muted;
tx(catLbl,cat);
}
// Pills always render as pills; in preview mode click shows inline detail instead of inserting
const pillsRow=mk("div",{display:"flex",flexWrap:"wrap",gap:"6px"});
// Container for detail panels (below pills row)
const detailsArea=mk("div",{display:"flex",flexDirection:"column",gap:"4px",marginTop:"2px"});
let _openDetailIdx=-1; // which pill's detail is currently open
items.forEach(({label,prompt,dual},itemIdx)=>{
if(dual) _hasDual=true;
const pill=mk("button",{
padding:dual?"5px 10px 5px 8px":"7px 14px",
borderRadius:"999px",cursor:"pointer",
fontSize:"10px",fontWeight:"500",lineHeight:"1.5",
border:`1px solid ${dual?"rgba(100,160,255,.35)":C.border}`,
background:dual?"rgba(80,120,220,.08)":C.bg1,
color:dual?"rgba(140,190,255,.9)":C.text,
outline:"none",textAlign:"left",
display:"flex",alignItems:"center",gap:"6px",
transition:"background .12s,border-color .12s,color .12s",
});
if(dual){
const badge=mk("span",{fontSize:"7px",fontWeight:"800",letterSpacing:".06em",background:"rgba(100,160,255,.2)",color:"rgba(140,190,255,.8)",border:"1px solid rgba(100,160,255,.3)",borderRadius:"4px",padding:"1px 4px",flexShrink:"0",lineHeight:"1.6"});
tx(badge,"2 imgs"); const lbl=mk("span");tx(lbl,label); pill.append(badge,lbl);
pill.onmouseenter=()=>{ pill.style.background="rgba(80,140,255,.18)";pill.style.borderColor="rgba(100,160,255,.7)";pill.style.color="#9bbfff"; };
pill.onmouseleave=()=>{ if(_openDetailIdx!==itemIdx){pill.style.background="rgba(80,120,220,.08)";pill.style.borderColor="rgba(100,160,255,.35)";pill.style.color="rgba(140,190,255,.9)";} };
} else {
tx(pill,label);
pill.onmouseenter=()=>{ if(_openDetailIdx!==itemIdx){pill.style.background="rgba(240,255,65,.10)";pill.style.borderColor=LIME;pill.style.color=LIME;} };
pill.onmouseleave=()=>{ if(_openDetailIdx!==itemIdx){pill.style.background=C.bg1;pill.style.borderColor=C.border;pill.style.color=C.text;} };
}
// Detail panel (shown in preview mode)
const detail=mk("div",{
display:"none",flexDirection:"column",gap:"6px",
border:`1px solid rgba(180,140,255,.4)`,borderRadius:"12px",
padding:"10px 14px",background:"rgba(26,20,60,.55)",boxSizing:"border-box",
});
const detailTop=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
const detailLbl=mk("span",{fontSize:"9px",fontWeight:"700",color:"rgba(255,255,255,.85)",flex:"1"});
tx(detailLbl,label);
// Copy button
const detailCopyBtn=mk("button",{background:"transparent",border:`1px solid rgba(100,220,120,.4)`,borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(100,220,120,.8)",cursor:"pointer",outline:"none",transition:"all .15s",flexShrink:"0"});
tx(detailCopyBtn,"Copy");
detailCopyBtn.onmouseenter=()=>{detailCopyBtn.style.background="rgba(100,220,120,.12)";detailCopyBtn.style.borderColor="rgba(100,220,120,.8)";};
detailCopyBtn.onmouseleave=()=>{detailCopyBtn.style.background="transparent";detailCopyBtn.style.borderColor="rgba(100,220,120,.4)";};
detailCopyBtn.onclick=(e)=>{ e.stopPropagation(); navigator.clipboard.writeText(prompt).then(()=>{ tx(detailCopyBtn,"✓ Copied"); setTimeout(()=>tx(detailCopyBtn,"Copy"),1500); }).catch(()=>{}); };
// Edit button
const detailEditBtn=mk("button",{background:"transparent",border:`1px solid rgba(160,140,220,.4)`,borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(160,140,220,.8)",cursor:"pointer",outline:"none",transition:"all .15s",flexShrink:"0"});
tx(detailEditBtn,"Edit");
detailEditBtn.onmouseenter=()=>{detailEditBtn.style.background="rgba(160,140,220,.1)";detailEditBtn.style.borderColor="rgba(160,140,220,.8)";detailEditBtn.style.color="#e0d0ff";};
detailEditBtn.onmouseleave=()=>{detailEditBtn.style.background="transparent";detailEditBtn.style.borderColor="rgba(160,140,220,.4)";detailEditBtn.style.color="rgba(160,140,220,.8)";};
// Delete button with confirm popover
const detailDelWrap=mk("div",{position:"relative",display:"inline-flex",flexShrink:"0"});
const detailDelBtn=mk("button",{background:"transparent",border:"1px solid rgba(220,80,80,.4)",borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(220,80,80,.8)",cursor:"pointer",outline:"none",transition:"all .15s"});
tx(detailDelBtn,"Delete");
detailDelBtn.onmouseenter=()=>{detailDelBtn.style.background="rgba(220,80,80,.1)";detailDelBtn.style.borderColor="rgba(220,80,80,.8)";};
detailDelBtn.onmouseleave=()=>{detailDelBtn.style.background="transparent";detailDelBtn.style.borderColor="rgba(220,80,80,.4)";};
const detailDelPop=mk("div",{display:"none",position:"absolute",right:"0",top:"calc(100% + 4px)",background:C.bg1,border:`1px solid ${C.borderH}`,borderRadius:"7px",padding:"6px 8px",zIndex:"10",whiteSpace:"nowrap",boxShadow:"0 4px 12px rgba(0,0,0,.5)",flexDirection:"column",gap:"5px",alignItems:"center"});
const detailDelQ=mk("div",{fontSize:"9px",color:C.text,fontWeight:"600"});tx(detailDelQ,"Sure?");
const detailDelBtns=mk("div",{display:"flex",gap:"4px"});
const detailDelYes=mk("button",{background:"rgba(160,25,25,.7)",border:"1px solid rgba(255,80,80,.3)",borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:"rgba(255,180,180,.9)",cursor:"pointer",outline:"none"});tx(detailDelYes,"Yes");
const detailDelNo=mk("button",{background:C.bg3,border:`1px solid ${C.borderH}`,borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",color:C.muted,cursor:"pointer",outline:"none"});tx(detailDelNo,"Keep");
detailDelBtns.append(detailDelYes,detailDelNo); detailDelPop.append(detailDelQ,detailDelBtns); detailDelWrap.append(detailDelBtn,detailDelPop);
detailDelBtn.onclick=(e)=>{ e.stopPropagation(); detailDelPop.style.display=detailDelPop.style.display==="flex"?"none":"flex"; };
detailDelYes.onclick=async(e)=>{ e.stopPropagation(); detailDelPop.style.display="none"; items.splice(itemIdx,1); await _saveDiscoverPrompts(); _inspirePromptsLoaded=false; _buildInspireBody(); };
detailDelNo.onclick=(e)=>{ e.stopPropagation(); detailDelPop.style.display="none"; };
const detailUseBtn=mk("button",{background:LIME,color:"#111",border:"none",borderRadius:"999px",padding:"2px 10px",fontSize:"8px",fontWeight:"700",cursor:"pointer",outline:"none",transition:"opacity .15s",flexShrink:"0"});
tx(detailUseBtn,"Use"); detailUseBtn.onmouseenter=()=>detailUseBtn.style.opacity=".85"; detailUseBtn.onmouseleave=()=>detailUseBtn.style.opacity="1";
detailUseBtn.onclick=(e)=>{ e.stopPropagation(); _usePrompt(prompt); };
detailTop.append(detailLbl,detailUseBtn,detailCopyBtn,detailEditBtn,detailDelWrap);
const detailPromptTxt=mk("div",{fontSize:"9px",color:"rgba(200,185,230,.7)",lineHeight:"1.55",fontStyle:"italic"});
tx(detailPromptTxt,prompt);
// Edit form inside detail
const editForm=mk("div",{display:"none",flexDirection:"column",gap:"6px",borderTop:`1px solid rgba(180,140,255,.2)`,paddingTop:"8px"});
const editTitleInp=mk("input",{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"999px",color:C.text,fontSize:"10px",padding:"6px 14px",outline:"none",transition:"border-color .15s",width:"100%",boxSizing:"border-box"},{placeholder:"Title…"});
editTitleInp.onfocus=()=>editTitleInp.style.borderColor=LIME; editTitleInp.onblur=()=>editTitleInp.style.borderColor=C.border;
const editPromptTA=mk("textarea",{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"16px",color:C.text,fontSize:"10px",padding:"8px 14px",outline:"none",lineHeight:"1.5",resize:"vertical",minHeight:"60px",fontFamily:"inherit",transition:"border-color .15s",width:"100%",boxSizing:"border-box"},{placeholder:"Prompt…"});
editPromptTA.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
editPromptTA.onfocus=()=>editPromptTA.style.borderColor=LIME; editPromptTA.onblur=()=>editPromptTA.style.borderColor=C.border;
const editBtns=mk("div",{display:"flex",gap:"6px",justifyContent:"flex-end"});
const editCancelBtn=mk("button",{background:"transparent",border:`1px solid ${C.borderH}`,borderRadius:"999px",padding:"5px 16px",fontSize:"9px",fontWeight:"700",color:C.muted,cursor:"pointer",outline:"none",transition:"all .15s"});
tx(editCancelBtn,"Cancel"); editCancelBtn.onmouseenter=()=>{editCancelBtn.style.borderColor=C.text;editCancelBtn.style.color=C.text;}; editCancelBtn.onmouseleave=()=>{editCancelBtn.style.borderColor=C.borderH;editCancelBtn.style.color=C.muted;};
const editUpdateBtn=mk("button",{background:LIME,color:"#111",border:"none",borderRadius:"999px",padding:"5px 16px",fontSize:"9px",fontWeight:"700",cursor:"pointer",outline:"none",transition:"opacity .15s"});
tx(editUpdateBtn,"Update"); editUpdateBtn.onmouseenter=()=>editUpdateBtn.style.opacity=".85"; editUpdateBtn.onmouseleave=()=>editUpdateBtn.style.opacity="1";
let _editDualVal=false;
const editDualChk={get checked(){return _editDualVal;},set checked(v){_editDualVal=v;_editDualRefresh();}};
const editDualToggle=mk("button",{background:"transparent",border:`1px solid rgba(160,140,220,.35)`,borderRadius:"999px",padding:"3px 10px",fontSize:"8px",fontWeight:"600",color:"rgba(160,140,220,.6)",cursor:"pointer",outline:"none",transition:"all .15s",textAlign:"left"});
tx(editDualToggle,"Requires 2 images");
const _editDualRefresh=()=>{
if(_editDualVal){editDualToggle.style.background="rgba(100,160,255,.15)";editDualToggle.style.borderColor="rgba(100,160,255,.7)";editDualToggle.style.color="#9bbfff";}
else{editDualToggle.style.background="transparent";editDualToggle.style.borderColor="rgba(160,140,220,.35)";editDualToggle.style.color="rgba(160,140,220,.6)";}
};
editDualToggle.onclick=(e)=>{e.stopPropagation();_editDualVal=!_editDualVal;_editDualRefresh();};
editBtns.append(editCancelBtn,editUpdateBtn);
const editBottomRow=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
if(activePill==="i2i") editDualToggle.style.display="none";
editBottomRow.append(editDualToggle,mk("div",{flex:"1"}),editBtns);
editForm.append(editTitleInp,editPromptTA,editBottomRow);
const _setDetailEditMode=(on)=>{ detailUseBtn.style.display=on?"none":""; detailCopyBtn.style.display=on?"none":""; detailEditBtn.style.display=on?"none":""; detailDelWrap.style.display=on?"none":"inline-flex"; };
detailEditBtn.onclick=(e)=>{ e.stopPropagation(); const opening=editForm.style.display!=="flex"; editTitleInp.value=label; editPromptTA.value=prompt; editDualChk.checked=!!dual; editForm.style.display=opening?"flex":"none"; _setDetailEditMode(opening); if(opening) setTimeout(()=>editTitleInp.focus(),30); };
editCancelBtn.onclick=()=>{ editForm.style.display="none"; _setDetailEditMode(false); };
editUpdateBtn.onclick=async()=>{
const t=editTitleInp.value.trim(),p=editPromptTA.value.trim(); if(!t||!p) return;
const entry={label:t,prompt:p}; if(editDualChk.checked) entry.dual=true;
items[itemIdx]=entry; await _saveDiscoverPrompts(); _inspirePromptsLoaded=false; _buildInspireBody();
};
detail.append(detailTop,detailPromptTxt,editForm);
detailsArea.appendChild(detail);
const _closeDetail=()=>{
detail.style.display="none"; _openDetailIdx=-1;
pill.style.background=dual?"rgba(80,120,220,.08)":C.bg1;
pill.style.borderColor=dual?"rgba(100,160,255,.35)":C.border;
pill.style.color=dual?"rgba(140,190,255,.9)":C.text;
};
pill.onclick=()=>{
if(!_inspireShowFull){ _usePrompt(prompt); return; }
if(_openDetailIdx===itemIdx){ _closeDetail(); return; }
// Close previously open detail
detailsArea.querySelectorAll("div[data-detail-open]").forEach(d=>{ d.style.display="none"; delete d.dataset.detailOpen; });
// Reset all pills in this category
pillsRow.querySelectorAll("button").forEach(p2=>{ p2.style.background=C.bg1;p2.style.borderColor=C.border;p2.style.color=C.text; });
_openDetailIdx=itemIdx;
detail.style.display="flex"; detail.dataset.detailOpen="1";
pill.style.background="rgba(180,140,255,.15)"; pill.style.borderColor="rgba(180,140,255,.6)"; pill.style.color="#e0d0ff";
};
pillsRow.appendChild(pill);
});
// ── + Add pill at end of each category ──────────────────────
const addPill=mk("button",{
padding:"5px 12px",borderRadius:"999px",cursor:"pointer",
fontSize:"10px",fontWeight:"600",lineHeight:"1.5",
border:`1px dashed rgba(240,255,65,.4)`,
background:"rgba(240,255,65,.05)",color:"rgba(240,255,65,.6)",
outline:"none",transition:"all .15s",flexShrink:"0",
});
tx(addPill,"+ Add");
addPill.onmouseenter=()=>{ addPill.style.borderColor=LIME;addPill.style.background="rgba(240,255,65,.12)";addPill.style.color=LIME; };
addPill.onmouseleave=()=>{ addPill.style.borderColor="rgba(240,255,65,.4)";addPill.style.background="rgba(240,255,65,.05)";addPill.style.color="rgba(240,255,65,.6)"; };
// Inline form (hidden, appears below pills row)
const addForm=mk("div",{
display:"none",flexDirection:"column",gap:"6px",marginTop:"4px",
border:`1px solid rgba(240,255,65,.3)`,borderRadius:"16px",
padding:"10px 12px",background:"rgba(240,255,65,.04)",boxSizing:"border-box",
});
const addFormTitle=mk("div",{fontSize:"8px",fontWeight:"700",color:LIME,letterSpacing:".08em",textTransform:"uppercase",marginBottom:"2px"});
tx(addFormTitle,"Add to "+cat.split("(")[0].trim());
const addTitleInp=mk("input",{
background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"999px",
color:C.text,fontSize:"10px",padding:"6px 14px",outline:"none",
transition:"border-color .15s",width:"100%",boxSizing:"border-box",
},{placeholder:"Title…"});
addTitleInp.onfocus=()=>addTitleInp.style.borderColor=LIME;
addTitleInp.onblur=()=>addTitleInp.style.borderColor=C.border;
const addPromptTA=mk("textarea",{
background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"16px",
color:C.text,fontSize:"10px",padding:"8px 14px",outline:"none",lineHeight:"1.5",
resize:"vertical",minHeight:"60px",fontFamily:"inherit",
transition:"border-color .15s",width:"100%",boxSizing:"border-box",
},{placeholder:"Prompt…"});
addPromptTA.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
addPromptTA.onfocus=()=>addPromptTA.style.borderColor=LIME;
addPromptTA.onblur=()=>addPromptTA.style.borderColor=C.border;
const addFormBtns=mk("div",{display:"flex",gap:"6px",justifyContent:"flex-end"});
const addCancelBtn=mk("button",{background:"transparent",border:`1px solid ${C.borderH}`,borderRadius:"999px",padding:"5px 16px",fontSize:"9px",fontWeight:"700",color:C.muted,cursor:"pointer",outline:"none",transition:"all .15s"});
tx(addCancelBtn,"Cancel");
addCancelBtn.onmouseenter=()=>{addCancelBtn.style.borderColor=C.text;addCancelBtn.style.color=C.text;};
addCancelBtn.onmouseleave=()=>{addCancelBtn.style.borderColor=C.borderH;addCancelBtn.style.color=C.muted;};
const addSaveBtn=mk("button",{background:LIME,color:"#111",border:"none",borderRadius:"999px",padding:"5px 16px",fontSize:"9px",fontWeight:"700",cursor:"pointer",outline:"none",transition:"opacity .15s"});
tx(addSaveBtn,"Save");
addSaveBtn.onmouseenter=()=>addSaveBtn.style.opacity=".85";
addSaveBtn.onmouseleave=()=>addSaveBtn.style.opacity="1";
// Dual checkbox
let _addDualVal=false;
const addDualChk={get checked(){return _addDualVal;},set checked(v){_addDualVal=v;_addDualRefresh();}};
const addDualToggle=mk("button",{background:"transparent",border:`1px solid rgba(160,140,220,.35)`,borderRadius:"999px",padding:"3px 10px",fontSize:"8px",fontWeight:"600",color:"rgba(160,140,220,.6)",cursor:"pointer",outline:"none",transition:"all .15s"});
tx(addDualToggle,"Requires 2 images");
const _addDualRefresh=()=>{
if(_addDualVal){addDualToggle.style.background="rgba(100,160,255,.15)";addDualToggle.style.borderColor="rgba(100,160,255,.7)";addDualToggle.style.color="#9bbfff";}
else{addDualToggle.style.background="transparent";addDualToggle.style.borderColor="rgba(160,140,220,.35)";addDualToggle.style.color="rgba(160,140,220,.6)";}
};
addDualToggle.onclick=(e)=>{e.stopPropagation();_addDualVal=!_addDualVal;_addDualRefresh();};
addFormBtns.append(addCancelBtn,addSaveBtn);
const addBottomRow=mk("div",{display:"flex",alignItems:"center",gap:"6px"});
if(activePill==="i2i") addDualToggle.style.display="none";
addBottomRow.append(addDualToggle,mk("div",{flex:"1"}),addFormBtns);
addForm.append(addFormTitle,addTitleInp,addPromptTA,addBottomRow);
addPill.onclick=()=>{
addTitleInp.value=""; addPromptTA.value=""; _addDualVal=false; _addDualRefresh();
addForm.style.display=addForm.style.display==="flex"?"none":"flex";
if(addForm.style.display==="flex") setTimeout(()=>addTitleInp.focus(),30);
};
addCancelBtn.onclick=()=>{ addForm.style.display="none"; };
addSaveBtn.onclick=async()=>{
const title=addTitleInp.value.trim(), prompt=addPromptTA.value.trim();
if(!title||!prompt) return;
const entry={label:title,prompt};
if(addDualChk.checked) entry.dual=true;
items.push(entry);
await _saveDiscoverPrompts();
_inspirePromptsLoaded=false;
addForm.style.display="none";
_buildInspireBody();
};
pillsRow.append(addPill);
const section=mk("div",{});
section.append(catLbl,pillsRow,detailsArea,addForm);
scroll.appendChild(section);
});
_inspireBody.appendChild(scroll);
// Legend for dual-input pills
if(_hasDual){
const legend=mk("div",{
display:"flex",alignItems:"center",gap:"5px",
marginTop:"8px",flexShrink:"0",
});
const badge=mk("span",{
fontSize:"7px",fontWeight:"800",letterSpacing:".06em",
background:"rgba(100,160,255,.2)",color:"rgba(140,190,255,.8)",
border:"1px solid rgba(100,160,255,.3)",borderRadius:"4px",
padding:"1px 4px",lineHeight:"1.6",flexShrink:"0",
});
tx(badge,"2 imgs");
const legendTxt=mk("span",{fontSize:"9px",color:C.muted,lineHeight:"1.5"});
tx(legendTxt,"requires both Image 1 and Image 2 to be loaded");
legend.append(badge,legendTxt);
_inspireBody.appendChild(legend);
}
// Optional note — shown below the prompt pills
if(def.note){
const noteLbl=mk("div",{
fontSize:"9px",color:"#f0a040",lineHeight:"1.5",marginTop:"8px",flexShrink:"0",
});
tx(noteLbl,"⚠ "+def.note);
_inspireBody.appendChild(noteLbl);
}
return;
}
// ── Tabs mode (original format) ─────────────────────────────────────
_inspireShowFullBtn.style.display="";
const tabs=def.tabs||[];
const tabRow=mk("div",{display:"flex",gap:"4px",marginBottom:"8px",flexShrink:"0"});
const showTabs=tabs.length>1;
tabRow.style.display=showTabs?"flex":"none";
const pages=tabs.map(({items},i)=>{
const page=mk("div",{
flex:"1",overflowY:"auto",display:i===0?"flex":"none",
flexDirection:"column",gap:"4px",
scrollbarWidth:"thin",scrollbarColor:`${C.border} transparent`,
});
page.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
if(!items||items.length===0){
const empty=mk("div",{fontSize:"11px",color:C.muted,padding:"20px 0",textAlign:"center"});
tx(empty,"No suggestions for this mode yet.");
page.appendChild(empty);
} else {
items.forEach(prompt=>{
const row=mk("div",{
padding:"8px 10px",borderRadius:"7px",cursor:"pointer",
fontSize:"10px",lineHeight:"1.55",
border:`1px solid ${C.border}`,background:C.bg1,
transition:"background .12s,border-color .12s,color .12s",
boxSizing:"border-box",
});
if(_inspireShowFull){
row.style.color=C.text;
tx(row,prompt);
let _copied=false;
row.onmouseenter=()=>{ row.style.background=C.bg3;row.style.borderColor="rgba(100,160,255,.6)";row.style.color="#9bbfff"; };
row.onmouseleave=()=>{ row.style.background=C.bg1;row.style.borderColor=C.border;row.style.color=C.text; if(_copied){_copied=false;} };
row.onclick=()=>{
navigator.clipboard.writeText(prompt).then(()=>{
row.style.borderColor="rgba(100,220,120,.7)";row.style.color="#7ddd9a";
setTimeout(()=>{ row.style.borderColor=C.border;row.style.color=C.text; },1200);
}).catch(()=>{});
};
} else {
row.style.color=C.text;
// In normal mode items are plain strings — show truncated label
const short=prompt.length>72?prompt.slice(0,70).trimEnd()+"…":prompt;
tx(row,short);
row.title=prompt;
row.onmouseenter=()=>{ row.style.background=C.bg3;row.style.borderColor=LIME;row.style.color="#fff"; };
row.onmouseleave=()=>{ row.style.background=C.bg1;row.style.borderColor=C.border;row.style.color=C.text; };
row.onclick=()=>_usePrompt(prompt);
}
page.appendChild(row);
});
}
return page;
});
if(showTabs){
tabs.forEach((_tab,i)=>{
const cat=_tab.cat;
const tab=mk("button",{
border:`1px solid ${i===0?LIME:C.border}`,cursor:"pointer",
padding:"3px 10px",fontSize:"9px",fontWeight:"700",letterSpacing:".04em",
borderRadius:"5px",outline:"none",transition:"all .15s",
color:i===0?LIME:C.muted,
background:i===0?"rgba(240,255,65,.08)":"transparent",
});
tx(tab,cat);
tab.onclick=()=>{
tabRow.querySelectorAll("button").forEach((t,j)=>{
t.style.color=j===i?LIME:C.muted;
t.style.borderColor=j===i?LIME:C.border;
t.style.background=j===i?"rgba(240,255,65,.08)":"transparent";
});
pages.forEach((p,j)=>p.style.display=j===i?"flex":"none");
};
tabRow.appendChild(tab);
});
}
_inspireBody.append(tabRow,...pages);
};
let _inspirePromptsLoaded=false;
const _saveDiscoverPrompts=async()=>{
try{
const toSave={};
["i2i"].forEach(pill=>{
const d=INSPIRE_BY_PILL[pill];
if(d&&d.categories) toSave[pill]={categories:d.categories};
else if(d&&d.tabs) toSave[pill]={tabs:d.tabs};
});
await api.fetchApi("/z_image/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({discover_prompts:toSave})});
}catch(e){ console.warn("[ZImageOneNode] save discover_prompts:",e); }
};
// Autofill prompts loaded from config.json — fallbacks used if config missing
let _autofillPrompts={
i2i:"Enhance, transform, or re-imagine this image based on your prompt while preserving the core composition.",
};
const _loadDiscoverPrompts=async()=>{
if(_inspirePromptsLoaded) return;
_inspirePromptsLoaded=true;
try{
const r=await api.fetchApi("/z_image/config");
const d=await r.json();
if(d.autofill_prompts&&Object.keys(d.autofill_prompts).length){
_autofillPrompts={..._autofillPrompts,...d.autofill_prompts};
}
if(d.discover_prompts){
["i2i"].forEach(pill=>{
if(d.discover_prompts[pill]) INSPIRE_BY_PILL[pill]=d.discover_prompts[pill];
});
}
}catch(e){ console.warn("[ZImageOneNode] load discover_prompts:",e); }
};
const _openInspire=async()=>{
await _loadDiscoverPrompts();
_buildInspireBody();
_inspireOverlay.style.display="flex";
_inspireOverlay.getBoundingClientRect();
_inspireOverlay.style.opacity="1";
};
_inspireHdr.style.position="relative"; _inspireHdr.style.zIndex="1";
_inspireBody.style.position="relative"; _inspireBody.style.zIndex="1";
_inspireOverlay.append(_inspireHdr,_inspireBody);
// ── Get Inspired button ───────────────────────────────────────────────
const _inspireBtn=mk("button",{
background:"none",border:`1px solid ${C.border}`,cursor:"pointer",
padding:"2px 7px",color:C.muted,outline:"none",
display:"flex",alignItems:"center",gap:"4px",borderRadius:"5px",
transition:"color .15s,border-color .15s",flexShrink:"0",
});
_inspireBtn.innerHTML=`<span style="font-size:9px;font-weight:700;letter-spacing:.04em">✦ Discover</span>`;
_inspireBtn.onmouseenter=()=>{_inspireBtn.style.color="#fff";_inspireBtn.style.borderColor="#555";};
_inspireBtn.onmouseleave=()=>{_inspireBtn.style.color=C.muted;_inspireBtn.style.borderColor=C.border;};
_inspireBtn.onclick=_openInspire;
// Order: cap | Get Inspired | errChip | [spacer via marginLeft:auto on _ulBtn] | Add LoRA | Expand prompt
promptCap.style.marginBottom="0";
promptHdr.append(promptCap,_inspireBtn,errMinChip,_ulBtn,_promptExpandBtn);
promptWrap.appendChild(promptHdr);
const promptTA=mk("textarea",{
width:"100%",height:"80px",resize:"none",
background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"8px",
color:C.text,fontSize:"12px",padding:"9px 12px",
boxSizing:"border-box",outline:"none",lineHeight:"1.55",
fontFamily:"inherit",transition:"border-color .15s",display:"block",
},{placeholder:"Describe what you want to generate…"});
promptTA.value=S.prompt;
_promptTARef=promptTA;
promptTA.onfocus=()=>promptTA.style.borderColor=LIME;
promptTA.onblur=()=>promptTA.style.borderColor=C.border;
promptTA.oninput=()=>{S.prompt=promptTA.value;S[_pillPromptKey(activePill)]=promptTA.value;persist();};
promptTA.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault();promptTA.blur();}});
promptTA.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
// ── Error panel ───────────────────────────────────────────────────────
const errPanel=mk("div",{display:"none",borderRadius:"8px",overflow:"hidden"});
const errMain=mk("div",{background:"linear-gradient(180deg,rgba(255,103,103,.12),rgba(255,103,103,.05))",
border:`1px solid rgba(255,103,103,.34)`,borderRadius:"8px",
padding:"10px 12px",boxSizing:"border-box",
animation:"fk-error-pulse 1.8s ease-in-out infinite"});
const errTopRow=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:"6px"});
const errTitle=mk("div",{fontSize:"11px",fontWeight:"700",color:C.err});tx(errTitle,"Generation error");
const _errMinBtn=mk("button",{
background:"none",border:"none",cursor:"pointer",padding:"2px 6px",
color:C.muted,fontSize:"9px",fontWeight:"700",letterSpacing:".04em",
outline:"none",borderRadius:"4px",textTransform:"uppercase",
border:`1px solid rgba(255,103,103,.25)`,transition:"color .15s,border-color .15s",
});
tx(_errMinBtn,"Hide");
errTopRow.append(errTitle,_errMinBtn);
const errMsg=mk("div",{fontSize:"11px",lineHeight:"1.55",color:C.text,whiteSpace:"pre-wrap",wordBreak:"break-word"});
const errHint=mk("div",{fontSize:"10px",lineHeight:"1.5",color:C.warn,marginTop:"6px"});
tx(errHint,"Check the console log for details and make sure the correct models are selected in Settings.");
errMain.append(errTopRow,errMsg,errHint);
errPanel.appendChild(errMain);
let _errMinimized=false;
const _toggleErrMin=()=>{
_errMinimized=!_errMinimized;
errPanel.style.display=_errMinimized?"none":"block";
errMain.style.display=_errMinimized?"none":"block";
errMinChip.style.display=_errMinimized?"flex":"none";
promptTA.style.display=_errMinimized?"block":"none";
tx(_errMinBtn,_errMinimized?"Show":"Hide");
};
_errMinBtn.onclick=_toggleErrMin;
errMinChip.onclick=_toggleErrMin;
function showError(msg){
_errMinimized=false;tx(_errMinBtn,"Hide");
errMsg.textContent=msg||"Unknown error.";
errMain.style.display="block";errMinChip.style.display="none";
promptTA.style.display="none";errPanel.style.display="block";
}
function clearError(){
_errMinimized=false;errPanel.style.display="none";promptTA.style.display="block";
errMinChip.style.display="none";
}
promptWrap.append(promptTA,errPanel);
// ── Prompt expand overlay ─────────────────────────────────────────────
const _promptOverlay=mk("div",{
position:"absolute",inset:"0",zIndex:"250",background:C.bg0,
display:"none",flexDirection:"column",
padding:"14px",boxSizing:"border-box",gap:"8px",
opacity:"0",transition:"opacity 0.15s ease",
});
const _promptOvHdr=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});
const _promptOvCap=mk("div",{fontSize:"10px",fontWeight:"700",color:C.muted,letterSpacing:".07em",textTransform:"uppercase"});
tx(_promptOvCap,"Prompt");
const _promptCollapseBtn=mk("button",{
background:"none",border:"none",cursor:"pointer",padding:"2px 4px",
color:C.muted,lineHeight:"1",outline:"none",
display:"flex",alignItems:"center",borderRadius:"4px",transition:"color .15s",
});
_promptCollapseBtn.innerHTML=`<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="10" y1="14" x2="3" y2="21"/><line x1="21" y1="3" x2="14" y2="10"/></svg>`;
_promptCollapseBtn.onmouseenter=()=>_promptCollapseBtn.style.color=LIME;
_promptCollapseBtn.onmouseleave=()=>_promptCollapseBtn.style.color=C.muted;
_promptOvHdr.append(_promptOvCap,_promptCollapseBtn);
const _promptOvTA=mk("textarea",{
flex:"1",width:"100%",resize:"none",minHeight:"0",
background:C.bg2,border:`1px solid ${C.border}`,borderRadius:"8px",
color:C.text,fontSize:"12px",padding:"10px 12px",
boxSizing:"border-box",outline:"none",lineHeight:"1.6",
fontFamily:"inherit",transition:"border-color .15s",
},{placeholder:"Describe what you want to generate…"});
_promptOvTA.addEventListener("wheel",e=>e.stopPropagation(),{passive:true});
_promptOvTA.onfocus=()=>_promptOvTA.style.borderColor=LIME;
_promptOvTA.onblur=()=>_promptOvTA.style.borderColor=C.border;
_promptOvTA.oninput=()=>{S.prompt=_promptOvTA.value;S[_pillPromptKey(activePill)]=_promptOvTA.value;promptTA.value=_promptOvTA.value;persist();};
const _openPromptOverlay=()=>{
_promptOvTA.value=S.prompt;_promptOverlay.style.display="flex";
_promptOverlay.getBoundingClientRect();_promptOverlay.style.opacity="1";
setTimeout(()=>{_promptOvTA.focus();_promptOvTA.setSelectionRange(_promptOvTA.value.length,_promptOvTA.value.length);},50);
};
const _closePromptOverlay=()=>{
S.prompt=_promptOvTA.value;promptTA.value=_promptOvTA.value;persist();
_promptOverlay.style.opacity="0";
setTimeout(()=>_promptOverlay.style.display="none",160);
};
_promptExpandBtn.onclick=_openPromptOverlay;
_promptCollapseBtn.onclick=_closePromptOverlay;
_promptOverlay.append(_promptOvHdr,_promptOvTA);
_promptOverlay.addEventListener("keydown",e=>{if(e.key==="Escape")_closePromptOverlay();});
// ── GENERATION ────────────────────────────────────────────────────────
const _resetGenBtn=()=>{
genBtn.disabled=false;tx(genBtn,"Generate");
genBtn.style.background=LIME;genBtn.style.backgroundSize="";
genBtn.style.animation="none";genBtn.style.color="#111";
genBtn.style.border="2px solid transparent";
stopBtn.style.maxWidth="0";stopBtn.style.minWidth="0";stopBtn.style.width="0";stopBtn.style.opacity="0";stopBtn.style.padding="0";stopBtn.style.marginLeft="0";
progWrap.style.display="none";
if(_lastGenObj) previewDelBtn.style.display="flex";
};
const resetBtn=()=>{
S.generating=false;S._pendingMeta=null;_activePromptId=null;
S._preRunFiles=new Set();persist();_resetGenBtn();
};
let _lastGenObj=null; // {filename, subfolder} of the most recently generated image
let _previewBlobUrl=null;
const showPreview=(url)=>{
if(_previewBlobUrl) URL.revokeObjectURL(_previewBlobUrl);
_previewBlobUrl=url;
placeholder.style.display="none";
finalImg.src=url;
finalImg.style.display="block";
};
const showFinal=(url,filename,subfolder)=>{
if(_previewBlobUrl){ URL.revokeObjectURL(_previewBlobUrl); _previewBlobUrl=null; }
clearError();S.generating=false;S.previewUrl=null;_activePromptId=null;persist();
_resetGenBtn();
if(soundEnabled)playDone();
_galNeedsRefresh=true;
if(filename) _lastGenObj={filename,subfolder:subfolder||""};
placeholder.style.display="none";
// Save metadata — use snapshot captured at Generate click time
const meta=S._pendingMeta?{v:1,...S._pendingMeta}:{v:1,prompt:S.prompt,w:getEffectiveW(),h:getEffectiveH(),mode:activePill};
if(filename){
api.fetchApi("/z_image/save_meta",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({filename,subfolder:subfolder||"",meta}),
}).catch(e=>console.warn("[ZImageOneNode] save_meta:",e));
}
const _showComparer=(img1InputName)=>{
finalImg.style.display="none";
comparerGenImg.src=url;
comparerGenImg.style.width=(previewBox.offsetWidth||620)+"px";
comparerBase.src=api.apiURL(`/view?filename=${encodeURIComponent(img1InputName)}&type=input&subfolder=`);
comparerWrap.style.display="block";
previewUseWrap.style.display="block";
_cmpSetPct(100);
};
// Use snapshot mode/image so switching pills mid-generation doesn't corrupt comparer
const _snapMode=S._pendingMeta?.mode||activePill;
const _snapImg1=S._pendingMeta?.image1||null;
if(_snapMode==="i2i"&&_snapImg1){
_showComparer(_snapImg1);
} else {
comparerWrap.style.display="none";
finalImg.src=url;finalImg.style.display="block";
}
previewUseWrap.style.display="block";
if(filename) previewDelBtn.style.display="flex";
};
const _slotErr=(slot,lbl)=>{ slot.el.style.borderColor="#e05555"; tx(lbl,"Required!"); lbl.style.color="#e05555"; };
genBtn.onclick=async()=>{
if(!S.prompt.trim()){showError("Please enter a prompt.");return;}
if(activePill==="i2i"&&!i2iSlot.hasFile()){_slotErr(i2iSlot,i2iSlotLbl);return;}
const _hasExtModel=(()=>{ const n=app.graph.getNodeById(self.id); const inputs=n?.inputs||[]; const slot=inputs.find(i=>i.name==="model"); return slot?.link!=null; })();
if(!S.model&&!_hasExtModel){showError("No model selected. Open Settings and choose a model.");return;}
clearError();S.generating=true;
let _snapMode=activePill;
S._pendingMeta={
prompt:S.prompt,
w:getEffectiveW(), h:getEffectiveH(),
mode:_snapMode,
image1:activePill==="i2i"?(S.i2iImage||null):null,
i2iDenoise:activePill==="i2i"?S.i2iDenoise:undefined,
userLoras:S.userLoras.filter(l=>l.name&&l.name!=="none"&&+(l.strength||0)>0).map(l=>({n:l.name.split(/[\\/]/).pop(),s:l.strength})),
...(S.advancedUI?{steps:S.steps||4, cfg:S.cfg!==undefined?S.cfg:1,
sampler:S.sampler||"res_multistep", scheduler:S.scheduler||"simple",
advancedUI:true}:{}),
seed:S.seed||0, randomizeSeed:S.randomizeSeed,
};
S._preRunFiles=new Set();persist();
// Keep existing image visible until the new one arrives
const _hadImage=finalImg.style.display!=="none"||comparerWrap.style.display!=="none";
if(!_hadImage) placeholder.style.display="none";
progWrap.style.display="flex";
setStage("Generating…","Preparing workflow…",0);
genBtn.disabled=true;tx(genBtn,"Generating…");
genBtn.style.background="linear-gradient(270deg,#4dff6e,#00e5ff,#a259ff,#4dff6e)";
genBtn.style.backgroundSize="300% 300%";
genBtn.style.animation="fk-gradient 2.4s ease infinite";
genBtn.style.color=LIME;genBtn.style.border="2px solid transparent";
previewDelBtn.style.display="none";
requestAnimationFrame(()=>{
stopBtn.style.maxWidth="120px";stopBtn.style.minWidth="";stopBtn.style.width="";stopBtn.style.opacity="1";stopBtn.style.padding="0 14px";stopBtn.style.marginLeft="6px";
});
// Snapshot pre-run gallery files so we can identify the new output
try{
const prevR=await api.fetchApi("/z_image/gallery?offset=0&limit=200&subfolder=one-node-z-image");
const prevD=await prevR.json();
S._preRunFiles=new Set((prevD.images||[]).map(v=>v.key||((v.subfolder?`${v.subfolder}/`:"")+v.filename)));
}catch(e){ S._preRunFiles=new Set(); }
// Load correct workflow
const isI2IMode=activePill==="i2i";
let wfUrl;
if(isI2IMode) wfUrl="/z_image/workflow_i2i";
else wfUrl="/z_image/workflow_t2i";
let wfData;
try{
const r=await api.fetchApi(wfUrl);
if(!r.ok) throw new Error("HTTP "+r.status);
wfData=await r.json();
}catch(e){
showError("Could not load workflow (HTTP 404 = restart ComfyUI): "+fmtErr(e));resetBtn();return;
}
const prompt=JSON.parse(JSON.stringify(wfData));
const set=(id,key,val)=>{ if(prompt[id]) prompt[id].inputs[key]=val; };
const _isBase=_isBaseModel();
const _setAdv=(samplerNodeId,skipDenoise)=>{
const steps=S.advancedUI?(S.steps||4):(_isBase?20:4);
const cfg=S.advancedUI?(S.cfg!==undefined?S.cfg:1):(_isBase?5:1);
set(samplerNodeId,"steps",steps);
set(samplerNodeId,"cfg",cfg);
if(S.advancedUI){
set(samplerNodeId,"sampler_name",S.sampler||"res_multistep");
set(samplerNodeId,"scheduler",S.scheduler||"simple");
if(!skipDenoise) set(samplerNodeId,"denoise",S.denoise!==undefined?S.denoise:1);
}
};
// Build effective prompt — trigger words from all active LoRAs prepended
const _effectivePrompt=await _buildPromptWithTriggers(S.prompt||"");
const useKV=false; // KV cache not available for Z-Image
// ── External model/clip/vae input detection ─────────────────────────
// If the node has optional inputs wired from outside (e.g. a GGUF loader),
// skip internal loaders and use the external node's output instead.
// The external node is serialized and added to the prompt so ComfyUI can find it.
const _selfNode=app.graph.getNodeById(self.id);
const _extSlot=(name)=>{
if(!_selfNode) return null;
const inputs=_selfNode.inputs||[];
const slot=inputs.find(i=>i.name===name);
if(!slot||slot.link==null) return null;
const link=app.graph.links[slot.link];
if(!link) return null;
// Serialize the external node and all its upstream dependencies into prompt
const _addNodeToPrompt=(nodeId)=>{
if(prompt[String(nodeId)]) return; // already added
const extNode=app.graph.getNodeById(nodeId);
if(!extNode) return;
const serialized={class_type:extNode.comfyClass||extNode.type,inputs:{},_meta:{title:extNode.title||extNode.type}};
// Add widget values as inputs
(extNode.widgets||[]).forEach(w=>{ if(w.name) serialized.inputs[w.name]=w.value; });
// Add connected inputs
(extNode.inputs||[]).forEach((inp,i)=>{
if(inp.link!=null){
const l=app.graph.links[inp.link];
if(l){ _addNodeToPrompt(l.origin_id); serialized.inputs[inp.name]=[String(l.origin_id),l.origin_slot||0]; }
}
});
prompt[String(nodeId)]=serialized;
};
_addNodeToPrompt(link.origin_id);
return [String(link.origin_id),link.origin_slot||0];
};
const extModel=_extSlot("model");
const extClip =_extSlot("clip");
const extVae =_extSlot("vae");
// ── LoRA chain helper ───────────────────────────────────────────────
const _applyLoRAs=(chainSrc,idPrefix)=>{
const toPrev=(p)=>typeof p==="string"?[p,0]:p;
let prev=chainSrc;
(S.userLoras||[]).forEach((ul,i)=>{
if(!ul.name||ul.name==="none"||!(+(ul.strength||0)>0)) return;
const id=`${idPrefix}UL${i+1}`;
prompt[id]={
inputs:{lora_name:ul.name,strength_model:+(ul.strength??1.0),model:toPrev(prev)},
class_type:"LoraLoaderModelOnly",
_meta:{title:`User LoRA ${i+1}`},
};
prev=[id,0];
});
return prev;
};
if(isI2IMode){
// ── I2I workflow patching ──────────────────────────────────────────
if(extModel){ delete prompt["FK:165"]; }
else set("FK:165","unet_name",S.model||"z_image_turbo_bf16.safetensors");
if(extClip){ delete prompt["FK:155"]; prompt["FK:166"].inputs.clip=extClip; prompt["FK:156"].inputs.clip=extClip; }
else set("FK:155","clip_name",S.textEncoder||"qwen_3_4b.safetensors");
if(extVae){ delete prompt["FK:153"]; prompt["FKI2I:vae"].inputs.vae=extVae; prompt["FK:152"].inputs.vae=extVae; }
else set("FK:153","vae_name",S.vae||"ae.safetensors");
set("FK:166","text", _effectivePrompt);
set("FKI2I:img","image", S.i2iImage||"placeholder.png");
set("FK:86","filename_prefix","one-node-z-image/FK");
// Resize input image by longer side if enabled
if(S.i2iResizeLonger>0){
const dims=_i2iDims._getDims();
if(dims.w&&dims.h){
const scale=S.i2iResizeLonger/Math.max(dims.w,dims.h);
const newW=Math.round(dims.w*scale/16)*16;
const newH=Math.round(dims.h*scale/16)*16;
prompt["FKI2I:scale"]={
class_type:"ImageScale",
inputs:{image:["FKI2I:img",0],upscale_method:"lanczos",width:newW,height:newH,crop:"center"},
_meta:{title:"Scale I2I Input"},
};
prompt["FKI2I:vae"].inputs.pixels=["FKI2I:scale",0];
}
}
// Model chain → ModelSamplingAuraFlow → KSampler
let i2iModelSrc=extModel||"FK:165";
const i2iLoraRef=_applyLoRAs(i2iModelSrc,"FK:");
set("FK:169","model",typeof i2iLoraRef==="string"?[i2iLoraRef,0]:i2iLoraRef);
const seed=S.randomizeSeed?Math.floor(Math.random()*999999999999):S.seed;
if(S.randomizeSeed){S.seed=seed;seedInp.setVal(seed);_advSeedInp.setVal(seed);_advSeedRefresh();persist();}
set("FK:171","seed",seed);
set("FK:171","denoise",S.i2iDenoise!==undefined?S.i2iDenoise:0.75);
_setAdv("FK:171",true); // skipDenoise=true — denoise controlled by slider, not advanced
} else {
// ── T2I: original model chain ─────────────────────────────────────
if(extModel){ delete prompt[WF.model]; }
else set(WF.model,"unet_name",S.model||"z_image_turbo_bf16.safetensors");
if(extClip){ delete prompt[WF.textEnc]; prompt[WF.promptPos].inputs.clip=extClip; prompt[WF.promptNeg].inputs.clip=extClip; }
else set(WF.textEnc,"clip_name",S.textEncoder||"qwen_3_4b.safetensors");
if(extVae){ delete prompt[WF.vae]; if(prompt["FK:132"]) prompt["FK:132"].inputs.vae=extVae; if(prompt["FK:232"]) prompt["FK:232"].inputs.vae=extVae; prompt["FK:152"].inputs.vae=extVae; }
else set(WF.vae,"vae_name",S.vae||"ae.safetensors");
// Build chain: UNETLoader → (LoRAs?) → ModelSamplingAuraFlow
let modelSrc=extModel||WF.model;
const finalModelRef=_applyLoRAs(modelSrc,"FK:");
set(WF.sampling,"model",typeof finalModelRef==="string"?[finalModelRef,0]:finalModelRef);
const seed=S.randomizeSeed?Math.floor(Math.random()*999999999999):S.seed;
if(S.randomizeSeed){S.seed=seed;seedInp.setVal(seed);_advSeedInp.setVal(seed);_advSeedRefresh();persist();}
set(WF.sampler,"seed",seed); _setAdv(WF.sampler);
set(WF.promptPos,"text",_effectivePrompt);
set(WF.promptNeg,"text",DEFAULT_NEG_PROMPT);
set(WF.saveImage,"filename_prefix","one-node-z-image/FK");
// ── T2I ──────────────────────────────────────────────────────────
if(activePill==="t2i"){
const w=getW(), h=getH();
set(WF.latent,"width", w||1024);
set(WF.latent,"height", h||1024);
}
}
try{
const resp=await api.fetchApi("/prompt",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({prompt,client_id:api.clientId,extra_data:{enable_previews:true}}),
});
const result=await resp.json();
const wfErrs=Object.entries(result.node_errors||{}).filter(([k])=>k!==String(self.id));
if(result.error){
showError(fmtErr(result.error));resetBtn();
}else if(wfErrs.length){
showError(fmtErr(wfErrs[0][1]));resetBtn();
}else{
_activePromptId=result.prompt_id||null;
console.log("[ZImageOneNode] queued:",result.prompt_id);
}
}catch(err){
showError(fmtErr(err));resetBtn();
}
};
// ── PILL VISIBILITY ───────────────────────────────────────────────────
function updatePillVisibility(){
i2iPanel.style.display=activePill==="i2i"?"flex":"none";
resSect.style.display=activePill==="i2i"?"none":"flex";
updateSizeControls();
}
// ── mkHeart helper (used by gallery) ─────────────────────────────────
const _mkHeart=(size)=>{
const svg=document.createElementNS("http://www.w3.org/2000/svg","svg");
svg.setAttribute("viewBox","0 0 24 24");svg.setAttribute("width",size||"12px");svg.setAttribute("height",size||"12px");
svg.style.fill="currentColor";svg.style.display="block";svg.style.flexShrink="0";
const p=document.createElementNS("http://www.w3.org/2000/svg","path");
p.setAttribute("d","M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z");
svg.appendChild(p);return svg;
};
// ── GALLERY OVERLAY ───────────────────────────────────────────────────
const galleryOverlay=mk("div",{
position:"absolute",inset:"0",background:"#0a0a0a",
display:"none",flexDirection:"column",padding:"16px",
boxSizing:"border-box",zIndex:"50",borderRadius:"8px",
opacity:"0",transition:"opacity .22s ease, transform .22s ease",
transform:"translateY(6px)",
});
// Gallery header
const galHdr=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",
marginBottom:"12px",flexShrink:"0",gap:"8px"});
const galTitle=mk("div",{fontSize:"13px",fontWeight:"700",letterSpacing:".06em",
textTransform:"uppercase",color:C.text});tx(galTitle,"Gallery");
const galHdrRight=mk("div",{display:"flex",gap:"6px",alignItems:"center"});
// Favorites toggle
let _galFavOnly=false;
const galFavBtn=mk("button",{background:"transparent",border:`1px solid ${C.border}`,
borderRadius:"6px",padding:"4px 10px",fontSize:"11px",color:C.muted,
cursor:"pointer",outline:"none",transition:"border-color .15s,color .15s,background .15s",
display:"flex",alignItems:"center",gap:"5px"});
galFavBtn.appendChild(_mkHeart("11px"));
const _galFavLbl=mk("span");tx(_galFavLbl,"Favorites");galFavBtn.appendChild(_galFavLbl);
const _setGalFavBtn=(active)=>{
_galFavOnly=active;
galFavBtn.style.background=active?`rgba(240,255,65,.18)`:"transparent";
galFavBtn.style.borderColor=active?LIME:C.border;
galFavBtn.style.color=active?LIME:C.muted;
galleryOverlay.style.background=active?
"linear-gradient(180deg,rgba(240,255,65,.06) 0%,rgba(240,255,65,.02) 40%,rgba(0,0,0,0) 100%), #0a0a0a":
"#0a0a0a";
};
galFavBtn.onmouseenter=()=>{if(!_galFavOnly){galFavBtn.style.borderColor=LIME;galFavBtn.style.color=LIME;}};
galFavBtn.onmouseleave=()=>{if(!_galFavOnly){galFavBtn.style.borderColor=C.border;galFavBtn.style.color=C.muted;}};
galFavBtn.onclick=()=>{ _setGalFavBtn(!_galFavOnly); galLoad(true); };
const galRefreshBtn=mk("button",{background:C.bg3,border:`1px solid ${C.border}`,
borderRadius:"6px",padding:"4px 10px",fontSize:"11px",color:C.muted,
cursor:"pointer",outline:"none",transition:"border-color .15s,color .15s"});
tx(galRefreshBtn,"↺ Refresh");
galRefreshBtn.onmouseenter=()=>{galRefreshBtn.style.borderColor=C.text;galRefreshBtn.style.color=C.text;};
galRefreshBtn.onmouseleave=()=>{galRefreshBtn.style.borderColor=C.border;galRefreshBtn.style.color=C.muted;};
const galClose=mk("button",{background:"transparent",border:`1px solid #e05555`,
borderRadius:"6px",padding:"4px 14px",fontSize:"11px",color:"#e05555",
cursor:"pointer",outline:"none",transition:"opacity .15s"});
tx(galClose,"✕ Close");
galClose.onmouseenter=()=>galClose.style.opacity=".7";
galClose.onmouseleave=()=>galClose.style.opacity="1";
galHdrRight.append(galFavBtn,galRefreshBtn,galClose);
galHdr.append(galTitle,galHdrRight);
// Grid + scroll area
const galScroll=mk("div",{flex:"1",overflowY:"auto",scrollbarWidth:"thin",
scrollbarColor:`${C.border} transparent`});
const galGrid=mk("div",{
display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(100px,1fr))",
gap:"6px",paddingBottom:"12px",
});
const galEmpty=mk("div",{fontSize:"12px",color:C.muted,textAlign:"center",
padding:"40px 0",display:"none"});
tx(galEmpty,"No images found.");
// Infinite scroll sentinel inside galScroll — must be a child of the scroll container
const galSentinel=mk("div",{height:"2px",flexShrink:"0",marginTop:"4px"});
// Loading spinner shown below grid while fetching next page
const galMoreWrap=mk("div",{display:"none",justifyContent:"center",alignItems:"center",
padding:"14px 0",gap:"8px",flexShrink:"0"});
const galSpinner=mk("div",{
width:"16px",height:"16px",borderRadius:"50%",flexShrink:"0",
border:"2px solid rgba(240,255,65,.2)",borderTopColor:LIME,
animation:"fk-galSpin .7s linear infinite",
});
if(!document.getElementById("fk-galspin-style")){
const ss=document.createElement("style");ss.id="fk-galspin-style";
ss.textContent="@keyframes fk-galSpin{to{transform:rotate(360deg)}}";
document.head.appendChild(ss);
}
const galMoreBtn=mk("div"); // kept for compatibility
galMoreWrap.appendChild(galSpinner);
// Sentinel and spinner are inside galScroll so IntersectionObserver root works correctly
galScroll.append(galGrid,galEmpty,galMoreWrap,galSentinel);
let _galLoading=false;
const _galIo=new IntersectionObserver(entries=>{
if(!entries[0].isIntersecting) return;
if(_galLoading||_galFavOnly||_galOffset>=_galTotal) return;
galLoad(false,GAL_MORE);
},{root:galScroll,threshold:0});
_galIo.observe(galSentinel);
galleryOverlay.append(galHdr,galScroll);
// ── LIGHTBOX ──────────────────────────────────────────────────────────
const lightbox=mk("div",{
position:"absolute",inset:"0",background:"#0a0a0a",
display:"none",flexDirection:"column",zIndex:"60",borderRadius:"8px",
boxSizing:"border-box",
});
// Top bar: filename (left) + close (right)
const lbTop=mk("div",{display:"flex",alignItems:"center",justifyContent:"space-between",
padding:"8px 10px 4px",flexShrink:"0",gap:"8px"});
const lbFilename=mk("div",{fontSize:"11px",color:C.muted,flex:"1",
overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});
const lbClose=mk("button",{background:"transparent",border:`1px solid #e05555`,
borderRadius:"6px",padding:"3px 12px",fontSize:"11px",color:"#e05555",
cursor:"pointer",outline:"none",transition:"opacity .15s"});
tx(lbClose,"✕");
lbClose.onmouseenter=()=>lbClose.style.opacity=".7";
lbClose.onmouseleave=()=>lbClose.style.opacity="1";
lbTop.append(lbFilename,lbClose);
// Body: arrows + image wrap as flex siblings (full-height arrows like LTX node)
const lbBody=mk("div",{display:"flex",alignItems:"center",flex:"1",
minHeight:"0",position:"relative",gap:"8px",padding:"0 8px"});
const lbImgWrap=mk("div",{flex:"1",minWidth:"0",display:"flex",alignItems:"center",
justifyContent:"center",overflow:"hidden",height:"100%",minHeight:"0"});
const lbImg=mk("img",{maxWidth:"100%",maxHeight:"100%",objectFit:"contain",
borderRadius:"8px",display:"block"});
lbImgWrap.appendChild(lbImg);
// Prev / Next arrows — flex siblings, full height
const lbArrowL=mk("button",{background:"rgba(255,255,255,.08)",border:"none",
borderRadius:"6px",width:"36px",flexShrink:"0",alignSelf:"stretch",
cursor:"pointer",fontSize:"18px",color:C.text,outline:"none",
transition:"background .15s",display:"flex",alignItems:"center",justifyContent:"center"});
tx(lbArrowL,"‹");
lbArrowL.onmouseenter=()=>lbArrowL.style.background="rgba(255,255,255,.15)";
lbArrowL.onmouseleave=()=>lbArrowL.style.background="rgba(255,255,255,.08)";
const lbArrowR=mk("button",{background:"rgba(255,255,255,.08)",border:"none",
borderRadius:"6px",width:"36px",flexShrink:"0",alignSelf:"stretch",
cursor:"pointer",fontSize:"18px",color:C.text,outline:"none",
transition:"background .15s",display:"flex",alignItems:"center",justifyContent:"center"});
tx(lbArrowR,"›");
lbArrowR.onmouseenter=()=>lbArrowR.style.background="rgba(255,255,255,.15)";
lbArrowR.onmouseleave=()=>lbArrowR.style.background="rgba(255,255,255,.08)";
lbBody.append(lbArrowL,lbImgWrap,lbArrowR);
// Metadata panel — shown below image
const lbMeta=mk("div",{
flexShrink:"0",margin:"0 8px 4px",
background:"linear-gradient(180deg,rgba(240,255,65,.07),rgba(240,255,65,.02))",
border:"1px solid rgba(240,255,65,.2)",borderRadius:"10px",
padding:"9px 12px",display:"none",flexDirection:"column",gap:"6px",
});
// Prompt row
const lbPromptRow=mk("div",{display:"flex",flexDirection:"column",gap:"3px"});
const lbPromptLbl=mk("div",{fontSize:"9px",color:C.muted,fontWeight:"700",
letterSpacing:".08em",textTransform:"uppercase"});
tx(lbPromptLbl,"Prompt");
const lbPromptText=mk("div",{fontSize:"10px",color:C.text,lineHeight:"1.5",
maxHeight:"42px",overflowY:"auto",scrollbarWidth:"thin"});
lbPromptRow.append(lbPromptLbl,lbPromptText);
// Info chips row: resolution chip + input thumb + restore btn + fav + open folder
const lbInfoRow=mk("div",{display:"flex",gap:"6px",alignItems:"stretch",flexWrap:"wrap"});
const _lbChip=(label,val)=>{
const chip=mk("div",{display:"flex",flexDirection:"column",gap:"1px",
background:C.bg3,borderRadius:"5px",padding:"4px 8px",minWidth:"50px"});
const cl=mk("div",{fontSize:"8px",color:C.muted,fontWeight:"700",
letterSpacing:".07em",textTransform:"uppercase"});
tx(cl,label);
const cv=mk("div",{fontSize:"10px",color:C.text,fontWeight:"600"});
tx(cv,val||"—");
chip.append(cl,cv);
return chip;
};
const lbChipRes=_lbChip("Size","—");
const lbChipMode=_lbChip("Mode","—");
const lbChipAdv=mk("div",{
display:"none",alignItems:"center",gap:"4px",
background:C.bg3,borderRadius:"5px",padding:"4px 8px",
fontSize:"9px",color:C.muted,fontWeight:"700",letterSpacing:".05em",
cursor:"default",title:"Advanced settings saved",
});
lbChipAdv.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg><span>ADV</span>`;
// Input image thumbnails (shown when meta has image1 / image2)
const lbImgThumb=mk("img",{height:"40px",borderRadius:"4px",objectFit:"cover",
display:"none",alignSelf:"center",border:`1px solid ${C.border}`});
const lbImgThumb2=mk("img",{height:"40px",borderRadius:"4px",objectFit:"cover",
display:"none",alignSelf:"center",border:`1px solid ${C.border}`});
// Restore button
const lbRestoreBtn=mk("button",{
background:LIME,color:"#111",border:"none",borderRadius:"5px",
padding:"0 14px",fontSize:"11px",fontWeight:"700",
cursor:"pointer",outline:"none",transition:"opacity .15s",
display:"none",alignSelf:"stretch",whiteSpace:"nowrap",
alignItems:"center",justifyContent:"center"});
tx(lbRestoreBtn,"Load settings into UI");
lbRestoreBtn.onmouseenter=()=>lbRestoreBtn.style.opacity=".85";
lbRestoreBtn.onmouseleave=()=>lbRestoreBtn.style.opacity="1";
// Fav button
let _lbFavActive=false;
const lbFavBtn=mk("button",{
background:"rgba(20,20,30,.85)",border:"1px solid rgba(240,255,65,.2)",
borderRadius:"6px",width:"40px",height:"40px",flexShrink:"0",
cursor:"pointer",outline:"none",
transition:"background .2s,border-color .2s,color .2s",
display:"flex",alignItems:"center",justifyContent:"center",
color:"rgba(240,255,65,.35)",alignSelf:"stretch"});
const _lbFavApplyStyle=(hover)=>{
if(_lbFavActive){
lbFavBtn.style.background=hover?"rgba(240,255,65,.22)":"rgba(240,255,65,.15)";
lbFavBtn.style.borderColor=hover?LIME:LIME;
lbFavBtn.style.color=hover?"#fff":LIME;
lbFavBtn.style.boxShadow=hover?"0 0 10px rgba(240,255,65,.3)":"0 0 6px rgba(240,255,65,.15)";
} else {
lbFavBtn.style.background=hover?"rgba(240,255,65,.08)":"rgba(20,20,30,.85)";
lbFavBtn.style.borderColor=hover?"rgba(240,255,65,.5)":"rgba(240,255,65,.2)";
lbFavBtn.style.color=hover?"rgba(240,255,65,.85)":"rgba(240,255,65,.35)";
lbFavBtn.style.boxShadow="none";
}
};
lbFavBtn.onmouseenter=()=>_lbFavApplyStyle(true);
lbFavBtn.onmouseleave=()=>_lbFavApplyStyle(false);
lbFavBtn.appendChild(_mkHeart("14px"));
// Open folder button
const lbOpenBtn=mk("button",{
background:"transparent",border:`1px solid ${C.border}`,
borderRadius:"6px",padding:"0 10px",fontSize:"10px",color:C.muted,
cursor:"pointer",outline:"none",transition:"border-color .15s,color .15s",
alignSelf:"stretch",display:"flex",alignItems:"center",gap:"4px",
marginLeft:"auto",whiteSpace:"nowrap"});
// Folder SVG icon
const _lbFolderSvg=(()=>{
const s=document.createElementNS("http://www.w3.org/2000/svg","svg");
s.setAttribute("viewBox","0 0 24 24");s.setAttribute("width","12");s.setAttribute("height","12");
s.style.fill="currentColor";s.style.flexShrink="0";
const p=document.createElementNS("http://www.w3.org/2000/svg","path");
p.setAttribute("d","M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2z");
s.appendChild(p);return s;
})();
lbOpenBtn.appendChild(_lbFolderSvg);
const _lbOpenLbl=mk("span");tx(_lbOpenLbl,"Show in folder");lbOpenBtn.appendChild(_lbOpenLbl);
lbOpenBtn.onmouseenter=()=>{lbOpenBtn.style.borderColor=C.text;lbOpenBtn.style.color=C.text;};
lbOpenBtn.onmouseleave=()=>{lbOpenBtn.style.borderColor=C.border;lbOpenBtn.style.color=C.muted;};
lbOpenBtn.onclick=async()=>{
const v=_lbActiveImg; if(!v) return;
try{
await api.fetchApi("/z_image/open_folder",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({filename:v.filename,subfolder:v.subfolder||""}),
});
}catch(e){ console.warn("[ZImageOneNode] open_folder:",e); }
};
// ── "Use as…" dropdown button ─────────────────────────────────────────
const _lbUseWrap=mk("div",{position:"relative",alignSelf:"stretch",display:"flex"});
const _lbUseBtn=mk("button",{
background:C.bg3,color:C.text,border:`1px solid ${C.borderH}`,
borderRadius:"5px",padding:"0 11px",fontSize:"10px",fontWeight:"600",
cursor:"pointer",outline:"none",whiteSpace:"nowrap",
display:"flex",alignItems:"center",gap:"5px",
transition:"border-color .15s,color .15s,background .15s",
});
_lbUseBtn.innerHTML=`Use as… <svg viewBox="0 0 10 6" width="8" height="8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><polyline points="1,1 5,5 9,1"/></svg>`;
// Dropdown panel — appears above the button (bottom-anchored)
const _lbUseDrop=mk("div",{
position:"absolute",bottom:"calc(100% + 5px)",left:"0",
background:C.bg1,border:`1px solid ${C.borderH}`,borderRadius:"8px",
minWidth:"170px",overflow:"hidden",display:"none",zIndex:"200",
boxShadow:"0 4px 20px rgba(0,0,0,.7)",flexDirection:"column",
});
// Section header inside dropdown
const _mkDropSection=(label)=>{
const h=mk("div",{
padding:"6px 12px 3px",fontSize:"8px",fontWeight:"700",letterSpacing:".08em",
textTransform:"uppercase",color:C.muted,userSelect:"none",
});
tx(h,label);return h;
};
// Clickable slot item inside dropdown
const _mkDropItem=(label,icon,onClick)=>{
const row=mk("div",{
padding:"7px 12px",fontSize:"10px",fontWeight:"500",color:C.text,
cursor:"pointer",display:"flex",alignItems:"center",gap:"7px",
transition:"background .1s,color .1s",userSelect:"none",
});
const ico=mk("span",{fontSize:"11px",width:"14px",textAlign:"center",flexShrink:"0",color:C.muted});
tx(ico,icon);
const lbl=mk("span",{}); tx(lbl,label);
row.append(ico,lbl);
row.onmouseenter=()=>{row.style.background="rgba(240,255,65,.10)";row.style.color=LIME;ico.style.color=LIME;};
row.onmouseleave=()=>{row.style.background="";row.style.color=C.text;ico.style.color=C.muted;};
row.onclick=()=>{ _lbCloseDrop(); onClick(); };
return row;
};
// Thin divider between sections
const _mkDropDivider=()=>mk("div",{height:"1px",background:C.border,margin:"2px 0"});
// Build dropdown items
_lbUseDrop.append(
_mkDropSection("I2I"),
_mkDropItem("I2I slot","⟳",()=>{ const v=_lbActiveImg;if(v)_loadIntoI2ISlot(v); }),
);
let _lbDropOpen=false;
const _lbCloseDrop=()=>{
_lbDropOpen=false;
_lbUseDrop.style.display="none";
};
const _lbToggleDrop=()=>{
_lbDropOpen=!_lbDropOpen;
_lbUseDrop.style.display=_lbDropOpen?"flex":"none";
};
_lbUseBtn.onmouseenter=()=>{_lbUseBtn.style.borderColor=LIME;_lbUseBtn.style.color=LIME;_lbUseBtn.style.background=C.bg2;};
_lbUseBtn.onmouseleave=()=>{_lbUseBtn.style.borderColor=C.borderH;_lbUseBtn.style.color=C.text;_lbUseBtn.style.background=C.bg3;};
_lbUseBtn.onclick=(e)=>{ e.stopPropagation(); _lbToggleDrop(); };
// Close dropdown when clicking outside
document.addEventListener("click",()=>{ if(_lbDropOpen) _lbCloseDrop(); });
_lbUseDrop.addEventListener("click",e=>e.stopPropagation());
_lbUseWrap.append(_lbUseBtn,_lbUseDrop);
// Delete button in lightbox
const lbDelBtn=mk("button",{
background:"rgba(160,25,25,.7)",border:"1px solid rgba(255,80,80,.3)",
borderRadius:"6px",width:"42px",height:"40px",flexShrink:"0",
cursor:"pointer",outline:"none",transition:"background .15s,border-color .15s",
display:"flex",alignItems:"center",justifyContent:"center",color:"rgba(255,180,180,.9)",
alignSelf:"stretch"});
lbDelBtn.title="Delete image";
lbDelBtn.innerHTML=`<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>`;
lbDelBtn.onmouseenter=()=>{lbDelBtn.style.background="rgba(210,35,35,.9)";lbDelBtn.style.borderColor="rgba(255,80,80,.6)";};
lbDelBtn.onmouseleave=()=>{lbDelBtn.style.background="rgba(160,25,25,.7)";lbDelBtn.style.borderColor="rgba(255,80,80,.3)";};
lbDelBtn.onclick=()=>{
const v=_lbActiveImg; if(!v) return;
const delIdx=_lbIdx;
_deleteImage(v,lbDelBtn,()=>{
// Remove from _galImages and _lbNavList
const galIdx=_galImages.indexOf(v);
if(galIdx!==-1){ _galImages.splice(galIdx,1); }
_galTotal=Math.max(0,_galTotal-1);
delete _galMetas[v.filename];
// Remove from navList too (may differ from galImages in fav-only mode)
const navIdx=_lbNavList.indexOf(v);
if(navIdx!==-1){ _lbNavList.splice(navIdx,1); }
// Rebuild grid
galGrid.innerHTML="";
if(_galFavOnly){
const favs=_galImages.filter(img=>_galMetas[img.filename]?.favorite===true);
_galAppend(favs,0);
tx(galTitle,`Gallery (${favs.length} fav)`);
galEmpty.style.display=favs.length?"none":"block";
} else {
_galAppend(_galImages,0);
tx(galTitle,`Gallery (${_galTotal})`);
galEmpty.style.display=_galImages.length?"none":"block";
}
// Navigate to next image or close
if(_lbNavList.length===0){ _lbClose(); }
else { _lbNav(Math.min(delIdx,_lbNavList.length-1)); }
});
};
lbInfoRow.append(lbChipRes,lbChipMode,lbChipAdv,lbImgThumb,lbImgThumb2,lbRestoreBtn,_lbUseWrap,lbFavBtn,lbDelBtn,lbOpenBtn);
// LoRA row — subtle, hidden when no loras
const lbLoraRow=mk("div",{display:"none",gap:"4px",flexWrap:"wrap",alignItems:"center"});
const lbLoraLbl=mk("span",{fontSize:"8px",color:C.muted,fontWeight:"700",
letterSpacing:".07em",textTransform:"uppercase",marginRight:"2px"});
tx(lbLoraLbl,"LoRA");
lbLoraRow.appendChild(lbLoraLbl);
lbMeta.append(lbPromptRow,lbLoraRow,lbInfoRow);
// Bottom counter
const lbBottom=mk("div",{display:"flex",justifyContent:"center",
alignItems:"center",padding:"4px 10px 8px",flexShrink:"0",position:"relative"});
const lbCounter=mk("div",{fontSize:"10px",color:C.muted});
const lbFShortcut=mk("div",{
position:"absolute",right:"12px",display:"flex",alignItems:"center",gap:"4px",
});
const _lbFKbd=mk("span",{fontSize:"7px",fontWeight:"700",color:"#111",background:C.muted,borderRadius:"3px",padding:"0px 3px",letterSpacing:".02em",lineHeight:"1.7"});
tx(_lbFKbd,"F");
const _lbFLbl=mk("span",{fontSize:"7px",color:C.muted,whiteSpace:"nowrap"});
tx(_lbFLbl,"Fullscreen");
lbFShortcut.append(_lbFKbd,_lbFLbl);
lbBottom.append(lbCounter,lbFShortcut);
lightbox.append(lbTop,lbBody,lbMeta,lbBottom);
galleryOverlay.appendChild(lightbox);
// ── Gallery state ────────────────────────────────────────────────────
let _galImages=[];
let _galTotal=0;
let _galOffset=0;
let _galNeedsRefresh=false;
let _galMetas={};
let _lbActiveImg=null;
let _loraList=[];
let _lbIdx=0;
const GAL_LIMIT=50;
const GAL_MORE=50;
const _galImgUrl=(v)=>`/view?filename=${encodeURIComponent(v.filename)}&type=output&subfolder=${encodeURIComponent(v.subfolder||"")}`;
const _fetchMeta=async(v,force=false)=>{
const key=v.filename;
if(!force&&_galMetas[key]!==undefined) return _galMetas[key];
try{
const r=await api.fetchApi(`/z_image/meta?filename=${encodeURIComponent(v.filename)}&subfolder=${encodeURIComponent(v.subfolder||"")}`);
const d=await r.json();
_galMetas[key]=d.ok?d.meta:null;
}catch(e){ _galMetas[key]=null; }
return _galMetas[key];
};
const _setLbFav=(isFav)=>{
_lbFavActive=isFav;
_lbFavApplyStyle(false);
lightbox.style.background=isFav?
"linear-gradient(180deg,rgba(240,255,65,.18) 0%,rgba(240,255,65,.06) 50%,rgba(0,0,0,0) 100%), #0a0a0a":
"#0a0a0a";
};
const _lbClose=()=>{ lightbox.style.display="none"; _lbActiveImg=null; };
// Active navigation list — all images or favorites-only depending on current filter
let _lbNavList=[];
const _lbNav=async(i)=>{
if(!_lbNavList.length) return;
// Load more when reaching end in non-fav mode
if(i>=_lbNavList.length&&_galOffset<_galTotal&&!_galFavOnly){
await galLoad(false,GAL_MORE);
_lbNavList=_galImages; // refresh after load
}
i=Math.max(0,Math.min(_lbNavList.length-1,i));
lbShow(_lbNavList[i],i);
};
lbArrowL.onclick=()=>_lbNav(_lbIdx-1);
lbArrowR.onclick=()=>_lbNav(_lbIdx+1);
// Keyboard handler for lightbox — capture phase so we beat ComfyUI canvas handlers
document.addEventListener("keydown",(e)=>{
if(lightbox.style.display!=="flex") return;
if(e.key==="Escape"){ e.preventDefault(); e._lbHandled=true; _lbClose(); return; }
if(e.key==="ArrowLeft"){ e.preventDefault(); e.stopPropagation(); _lbNav(_lbIdx-1); return; }
if(e.key==="ArrowRight"){ e.preventDefault(); e.stopPropagation(); _lbNav(_lbIdx+1); return; }
if(e.key==="f"||e.key==="F"){
e.preventDefault();
if(!document.fullscreenElement) lbImg.requestFullscreen().catch(()=>{});
else document.exitFullscreen().catch(()=>{});
}
},{capture:true});
// Open lightbox
const lbShow=async(v,idx)=>{
_lbActiveImg=v;
_lbIdx=idx??0;
tx(lbFilename,v.filename);
lbImg.src=_galImgUrl(v)+"&t="+v.mtime;
lbMeta.style.display="none";lbRestoreBtn.style.display="none";
lbPromptText.textContent="";
lightbox.style.display="flex";
const total=_lbNavList.length||_galImages.length;
tx(lbCounter,`${_lbIdx+1} / ${total}`);
lbArrowL.style.opacity=_lbIdx>0?"1":".25";
lbArrowR.style.opacity=_lbIdx<total-1?"1":".25";
const meta=await _fetchMeta(v);
// Reset lora row
while(lbLoraRow.children.length>1) lbLoraRow.removeChild(lbLoraRow.lastChild);
lbLoraRow.style.display="none";
if(meta){
lbPromptText.textContent=meta.prompt||"(no prompt saved)";
const w=meta.w,h=meta.h;
tx(lbChipRes.querySelector("div:last-child"),w&&h?`${w}×${h}`:"—");
tx(lbChipMode.querySelector("div:last-child"),(meta.mode||"").toUpperCase()||"—");
const _hasAdv=meta.advancedUI===true;
lbChipAdv.style.display=_hasAdv?"flex":"none";
if(_hasAdv) lbChipAdv.title=`Steps:${meta.steps||8} CFG:${meta.cfg??1} Sampler:${meta.sampler||"res_multistep"}`;
if(meta.image1){
lbImgThumb.src=api.apiURL(`/view?filename=${encodeURIComponent(meta.image1)}&type=input&subfolder=`);
lbImgThumb.style.display="block";
} else { lbImgThumb.style.display="none"; }
if(meta.image2){
lbImgThumb2.src=api.apiURL(`/view?filename=${encodeURIComponent(meta.image2)}&type=input&subfolder=`);
lbImgThumb2.style.display="block";
} else { lbImgThumb2.style.display="none"; }
// LoRA chips
if(Array.isArray(meta.userLoras)&&meta.userLoras.length){
meta.userLoras.forEach(ul=>{
const chip=mk("span",{
fontSize:"9px",color:C.muted,background:C.bg3,
borderRadius:"4px",padding:"2px 6px",lineHeight:"1.6",
border:`1px solid ${C.border}`,whiteSpace:"nowrap",
});
const name=(ul.n||"").replace(/\.safetensors$/i,"");
chip.textContent=`${name} ×${+(ul.s??1).toFixed(2)}`;
lbLoraRow.appendChild(chip);
});
lbLoraRow.style.display="flex";
}
lbMeta.style.display="flex";
lbRestoreBtn.style.display="flex";
lbRestoreBtn.onclick=()=>_lbApplyMeta(meta);
_setLbFav(meta.favorite===true);
} else {
lbPromptText.textContent="⚠ No metadata.";
tx(lbChipRes.querySelector("div:last-child"),"—");
tx(lbChipMode.querySelector("div:last-child"),"—");
lbChipAdv.style.display="none";
lbImgThumb.style.display="none";
lbImgThumb2.style.display="none";
lbMeta.style.display="flex";
_setLbFav(false);
}
};
// Resolve an image name from metadata: if it's an output path (contains "/"), upload to input first.
const _resolveMetaImage=async(name)=>{
if(!name) return null;
if(!name.includes("/")) return name;
const parts=name.split("/");
const filename=parts[parts.length-1];
const subfolder=parts.slice(0,-1).join("/");
try{
return await _uploadOutputToInput({filename,subfolder});
}catch(e){
console.warn("[ZImageOneNode] resolveMetaImage:",e);
return filename;
}
};
// Apply meta — restores prompt, mode, resolution AND image slots (the source images stored in metadata).
// "Use as Image 1/2" buttons handle loading the currently-viewed gallery image into a slot instead.
const _lbApplyMeta=async(meta)=>{
// Close gallery immediately — don't wait for async image uploads
lightbox.style.display="none"; _lbActiveImg=null;
closeOverlayFade(galleryOverlay);
try{
const mode=meta.mode||"t2i";
// Map meta.mode → pill name
const pillMap={"i2i":"i2i","t2i":"t2i"};
const pill=pillMap[mode]||"t2i";
// Prompt
if(meta.prompt){
S.prompt=meta.prompt; S[_pillPromptKey(pill)]=meta.prompt;
promptTA.value=meta.prompt; _promptOvTA.value=meta.prompt;
}
// Pill
setPill(pill);
// Resolution (T2I / Edit only)
if(meta.w&&meta.h&&pill==="t2i"){
const preset=RES_PRESETS.find(r=>r.w===meta.w&&r.h===meta.h);
if(preset){ S.resLabel=preset.label;S.resW=preset.w;S.resH=preset.h;S.isCustomRes=false;
resDD.set(preset.label);customResRow.style.display="none"; }
else { S.isCustomRes=true;S.customW=meta.w;S.customH=meta.h;S.resLabel="Custom…";
resDD.set("Custom…");customResRow.style.display="flex";
wInp.setVal(meta.w);hInp.setVal(meta.h); }
}
// Image slots — by mode
const _ri=async(name)=>{ try{ return await _resolveMetaImage(name); }catch(e){ return null; } };
if(mode==="i2i"){
if(meta.image1){ const n=await _ri(meta.image1); if(n){S.i2iImage=n;i2iSlot._restorePreview(n);} }
if(meta.i2iDenoise!==undefined){ S.i2iDenoise=meta.i2iDenoise; _i2iSliderSet(Math.round(meta.i2iDenoise*100)); }
}
// Advanced params — only restore if explicitly saved with advancedUI:true
if(meta.advancedUI===true){
if(meta.steps){ S.steps=meta.steps; stepsInp.setVal(meta.steps); }
if(meta.cfg!==undefined){ S.cfg=meta.cfg; cfgInp.setVal(meta.cfg); }
if(meta.sampler){ S.sampler=meta.sampler; samplerDD.set(meta.sampler); }
if(meta.scheduler){ S.scheduler=meta.scheduler; schedulerDD.set(meta.scheduler); }
if(!S.advancedUI){ S.advancedUI=true; _advRefresh(); advUIToggle._setChecked(true); }
}
if(meta.seed!==undefined&&meta.randomizeSeed===false){
S.randomizeSeed=false; S.seed=meta.seed;
seedInp.setVal(meta.seed); _advSeedInp.setVal(meta.seed); _advSeedRefresh();
}
// LoRAs — always reset all slots first, then apply what meta has
_ulRowEls.forEach((r,i)=>{
S.userLoras[i]={name:"",strength:0};
r._dd.set("none"); r._str.value="0";
});
if(Array.isArray(meta.userLoras)&&meta.userLoras.length&&_loraList.length){
const nd=(s)=>(s||"").replace(/\\/g,"/").toLowerCase();
const loraOpts=["none",..._loraList];
meta.userLoras.forEach((ul,i)=>{
if(i>=_ulRowEls.length) return;
const basename=nd(ul.n||"").split("/").pop();
const match=loraOpts.find(o=>nd(o)===nd(ul.n||""))||
loraOpts.find(o=>nd(o).split("/").pop()===basename);
if(match&&match!=="none"){
S.userLoras[i].name=match; S.userLoras[i].strength=+(ul.s??1);
_ulRowEls[i]._dd.set(match); _ulRowEls[i]._str.value=String(S.userLoras[i].strength);
}
});
}
_ulUpdateBtn();
updateSizeControls(); persist();
}catch(err){
console.warn("[ZImageOneNode] _lbApplyMeta error:",err);
}
};
lbFavBtn.onclick=async()=>{
const v=_lbActiveImg; if(!v) return;
const newFav=!(_galMetas[v.filename]?.favorite===true);
try{
const r=await api.fetchApi("/z_image/update_meta",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({filename:v.filename,subfolder:v.subfolder||"",patch:{favorite:newFav}}),
});
const d=await r.json();
if(d.ok){
const cached=_galMetas[v.filename]||{};
cached.favorite=newFav;_galMetas[v.filename]=cached;
_setLbFav(newFav);
lbFavBtn.classList.remove("fk-heart-anim");
void lbFavBtn.offsetWidth;
lbFavBtn.classList.add("fk-heart-anim");
galGrid.querySelectorAll("[data-filename]").forEach(cell=>{
if(cell.dataset.filename===v.filename){
const ico=cell.querySelector("._favico");
if(ico){ ico.style.opacity=newFav?"1":"0"; }
}
});
if(_galFavOnly&&!newFav) galGrid.querySelector(`[data-filename="${CSS.escape(v.filename)}"]`)?.remove();
}
}catch(e){ console.warn("[ZImageOneNode] fav:",e); }
};
lbClose.onclick=_lbClose;
// Upload an output image into ComfyUI's input folder so LoadImage can reference it.
// Returns the uploaded input filename, or null on failure.
const _uploadOutputToInput=async(v)=>{
const outputUrl=api.apiURL(`/view?filename=${encodeURIComponent(v.filename)}&type=output&subfolder=${encodeURIComponent(v.subfolder||"")}`);
const resp=await fetch(outputUrl);
if(!resp.ok) throw new Error("fetch "+resp.status);
const blob=await resp.blob();
const fd=new FormData();
fd.append("image",new File([blob],v.filename,{type:blob.type||"image/png"}));
fd.append("overwrite","true");
const up=await api.fetchApi("/upload/image",{method:"POST",body:fd});
const upd=await up.json();
return upd.name||v.filename;
};
const _loadIntoI2ISlot=async(v)=>{
if(activePill!=="i2i") setPill("i2i");
let inputName;
try{ inputName=await _uploadOutputToInput(v); }
catch(err){ console.warn("[ZImageOneNode] load-into-i2i:",err); inputName=v.filename; }
S.i2iImage=inputName; i2iSlot._restorePreview(inputName);
persist();
lightbox.style.display="none";_lbActiveImg=null;
closeOverlayFade(galleryOverlay);
};
// mkApplyIcon for Load button (same as LTX node)
const _mkApplyIcon=(size)=>{
size=size||"13px";
const svg=document.createElementNS("http://www.w3.org/2000/svg","svg");
svg.setAttribute("viewBox","0 0 24 24");svg.setAttribute("width",size);svg.setAttribute("height",size);
svg.style.fill="currentColor";svg.style.display="block";svg.style.flexShrink="0";
const r1=document.createElementNS("http://www.w3.org/2000/svg","rect");
r1.setAttribute("x","3");r1.setAttribute("y","3");r1.setAttribute("width","13");r1.setAttribute("height","13");
r1.setAttribute("rx","2");r1.setAttribute("fill","none");r1.setAttribute("stroke","currentColor");r1.setAttribute("stroke-width","2");
const r2=document.createElementNS("http://www.w3.org/2000/svg","rect");
r2.setAttribute("x","8");r2.setAttribute("y","8");r2.setAttribute("width","13");r2.setAttribute("height","13");r2.setAttribute("rx","2");
svg.appendChild(r1);svg.appendChild(r2);return svg;
};
// Build / append grid cells
const _galAppend=(images,startIdx)=>{
images.forEach((v,i)=>{
const idx=startIdx+i;
const cell=mk("div",{
position:"relative",borderRadius:"8px",overflow:"hidden",
background:C.bg2,border:`1px solid ${C.border}`,
cursor:"pointer",transition:"border-color .15s",
aspectRatio:"1/1",
});
cell.dataset.filename=v.filename;
cell.dataset.idx=String(idx);
// Thumbnail
const thumb=mk("img",{
width:"100%",height:"100%",objectFit:"cover",
display:"block",background:C.bg3,position:"absolute",inset:"0",
});
thumb.loading="lazy";
thumb.src=_galImgUrl(v)+"&t="+v.mtime;
// Hover overlay
const ov=mk("div",{position:"absolute",inset:"0",background:"rgba(0,0,0,.35)",
opacity:"0",transition:"opacity .15s",pointerEvents:"none"});
// Filename strip at bottom
const strip=mk("div",{
position:"absolute",bottom:"0",left:"0",right:"0",
padding:"3px 6px",fontSize:"8px",color:"#ccc",
background:"rgba(0,0,0,.65)",
overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",
});
tx(strip,v.filename);
// Fav indicator
const favIco=mk("div",{
position:"absolute",bottom:"22px",right:"5px",
opacity:"0",transition:"opacity .15s",
color:LIME,pointerEvents:"none",
textShadow:"0 1px 2px rgba(0,0,0,.9)",
});
favIco.className="_favico";
favIco.appendChild(_mkHeart("11px"));
cell.append(thumb,ov,strip,favIco);
cell.onmouseenter=()=>{ cell.style.borderColor=LIME;ov.style.opacity="1"; };
cell.onmouseleave=()=>{ cell.style.borderColor=C.border;ov.style.opacity="0"; };
cell.onclick=()=>{ _lbNavList=images; lbShow(v,idx); };
if(v.favorite===true){
favIco.style.opacity="1";
cell.style.background="linear-gradient(180deg,rgba(240,255,65,.12) 0%,rgba(240,255,65,.04) 100%)";
cell.style.borderColor="rgba(240,255,65,.4)";
}
galGrid.appendChild(cell);
});
};
const galLoad=async(reset=true,limit=GAL_LIMIT)=>{
if(_galLoading&&!reset) return;
_galLoading=true;
galMoreWrap.style.display="flex";
if(reset){ _galImages=[];_galMetas={};_galOffset=0;galGrid.innerHTML=""; }
tx(galTitle,`Gallery (loading…)`);
try{
if(_galFavOnly){
// Fast path: single request, server resolves favorites index
const r=await api.fetchApi(`/z_image/gallery?offset=${_galOffset}&limit=${limit}&subfolder=one-node-z-image&favonly=1`);
const d=await r.json();
const newImgs=d.images||[];
_galTotal=d.total||0;
_galImages.push(...newImgs);
_galOffset=_galImages.length;
_galAppend(newImgs,_galImages.length-newImgs.length);
galEmpty.style.display=_galImages.length?"none":"block";
tx(galTitle,`Gallery (${_galTotal} fav)`);
galMoreWrap.style.display=_galOffset<_galTotal?"flex":"none";
} else {
const r=await api.fetchApi(`/z_image/gallery?offset=${_galOffset}&limit=${limit}&subfolder=one-node-z-image`);
const d=await r.json();
const newImgs=d.images||[];
_galTotal=d.total||0;
const startIdx=_galImages.length;
_galImages.push(...newImgs);
_galOffset=_galImages.length;
galEmpty.style.display=_galImages.length?"none":"block";
_galAppend(newImgs,startIdx);
tx(galTitle,`Gallery (${_galTotal})`);
galMoreWrap.style.display=_galOffset<_galTotal?"flex":"none";
}
}catch(e){
tx(galTitle,"Gallery (error)");
console.warn("[ZImageOneNode] gallery:",e);
}finally{
_galLoading=false;
if(_galOffset>=_galTotal) galMoreWrap.style.display="none";
}
};
galMoreBtn.onclick=()=>galLoad(false,GAL_MORE); // kept for compatibility
galRefreshBtn.onclick=()=>galLoad(true);
galClose.onclick=()=>closeOverlayFade(galleryOverlay,()=>{ lightbox.style.display="none"; _lbActiveImg=null; });
galleryBtn.onclick=e=>{
e.stopPropagation();
openOverlay(galleryOverlay);
if(_galNeedsRefresh||_galImages.length===0){ galLoad(true); _galNeedsRefresh=false; }
};
// Mark gallery as needing refresh after a successful generation
// _galNeedsRefresh is set to true in showFinal so gallery auto-refreshes on next open
// ── ASSEMBLE ─────────────────────────────────────────────────────────
pad.append(topBar,mainRow,promptWrap,creditText);
root.appendChild(settingsOverlay);
root.appendChild(galleryOverlay);
root.appendChild(_promptOverlay);
root.appendChild(_inspireOverlay);
scrollEl.appendChild(pad);
root.appendChild(scrollEl);
// ── Esc in any number/text input → blur (dismiss focus) ──────────────
root.addEventListener("keydown",(e)=>{
if(e.key!=="Escape") return;
const t=e.target;
if(t&&(t.tagName==="INPUT")&&t.type!=="range"){ e.stopPropagation(); t.blur(); }
},true);
const _creditEl=mk("div",{
position:"absolute",bottom:"5px",left:"12px",right:"12px",
fontSize:"8px",color:"#555",pointerEvents:"none",
letterSpacing:".04em",userSelect:"none",zIndex:"1",
display:"flex",alignItems:"center",justifyContent:"space-between",
});
const _shortcutsEl=mk("span",{color:"#444",letterSpacing:".03em"});
tx(_shortcutsEl,"D · discover  G · gallery  Space · generate  F · fullscreen");
const _creditTxt=mk("div",{fontSize:"8px",color:"#555",letterSpacing:".04em",whiteSpace:"nowrap"});
tx(_creditTxt,"created by Adeliox");
_creditEl.style.justifyContent="";
_creditEl.innerHTML="";
_creditEl.style.pointerEvents="auto";
// Left: shortcuts toggle button + expanded bar
const _scLeft=mk("div",{flex:"1",display:"flex",alignItems:"center",gap:"6px"});
const _scToggleBtn=mk("button",{
background:"transparent",border:`1px solid ${C.border}`,borderRadius:"4px",
padding:"1px 6px",fontSize:"7px",fontWeight:"600",color:C.muted,
cursor:"pointer",outline:"none",letterSpacing:".05em",textTransform:"uppercase",
transition:"border-color .15s,color .15s",whiteSpace:"nowrap",flexShrink:"0",
});
tx(_scToggleBtn,"Shortcuts");
_scToggleBtn.onmouseenter=()=>{_scToggleBtn.style.borderColor=C.text;_scToggleBtn.style.color=C.text;};
_scToggleBtn.onmouseleave=()=>{_scToggleBtn.style.borderColor=C.border;_scToggleBtn.style.color=C.muted;};
const _scBar=mk("div",{display:"none",alignItems:"center",flexShrink:"0",gap:"0",cursor:"pointer"});
[["D","Discover"],["F","Fullscreen preview"],["G","Gallery"],["Esc","Exit prompt"],["Space","Generate"]].forEach(([key,desc],idx)=>{
if(idx>0){ const sep=mk("div",{width:"1px",height:"8px",background:C.border,margin:"0 6px",flexShrink:"0"});_scBar.appendChild(sep); }
const item=mk("div",{display:"flex",alignItems:"center",gap:"3px"});
const kbd=mk("span",{fontSize:"7px",fontWeight:"700",color:"#111",background:C.muted,borderRadius:"3px",padding:"0px 3px",letterSpacing:".02em",flexShrink:"0",lineHeight:"1.7"});
tx(kbd,key);
const lbl=mk("span",{fontSize:"7px",color:C.muted,whiteSpace:"nowrap"});
tx(lbl,desc);
item.append(kbd,lbl);
_scBar.appendChild(item);
});
_scBar.onclick=()=>{ _scBar.style.display="none"; _scToggleBtn.style.display=""; };
_scToggleBtn.onclick=()=>{ _scToggleBtn.style.display="none"; _scBar.style.display="flex"; };
_scLeft.append(_scToggleBtn,_scBar);
// Right: credit
const _creditRight=mk("div",{flex:"1",display:"flex",justifyContent:"flex-end",pointerEvents:"none"});
_creditRight.appendChild(_creditTxt);
_creditEl.append(_scLeft,_creditRight);
root.appendChild(_creditEl);
// Initialize visibility
updatePillVisibility();
updateSizeControls();
// ── F key → fullscreen current preview image inside the node overlay ──
// Listen on document; only fire when mouse is over this node's root element.
let _mouseOverRoot=false;
root.addEventListener("mouseenter",()=>{ _mouseOverRoot=true; });
root.addEventListener("mouseleave",()=>{ _mouseOverRoot=false; });
const _fKeyHandler=(e)=>{
if(e.key!=="f"&&e.key!=="F") return;
if(!_mouseOverRoot) return;
// Don't fire when typing in a text field
const tag=(document.activeElement||{}).tagName||"";
if(tag==="INPUT"||tag==="TEXTAREA") return;
// Don't fire when any overlay is open inside the node
if(settingsOverlay.style.display!=="none") return;
if(galleryOverlay.style.display!=="none") return;
if(_promptOverlay.style.display!=="none") return;
if(_inspireOverlay.style.display!=="none") return;
if(_nodeFsOv&&_nodeFsOv.style.display!=="none"){_nodeFsOv._close();return;}
// Get currently visible image
let src="", name="", fsType="image", fsOpts=null;
if(comparerWrap.style.display!=="none"&&comparerGenImg.src){
src=comparerGenImg.src;
name="Before / After";
fsType="comparer";
fsOpts={
genSrc:comparerGenImg.src,
baseSrc:comparerBase.src,
};
} else if(finalImg.style.display!=="none"&&finalImg.src){
src=finalImg.src;
name=finalImg.src.split("/").pop().split("?")[0]||"Image";
}
if(!src) return;
e.preventDefault();
e.stopPropagation();
_initNodeFsOverlay()._open(fsType,src,name,fsOpts);
};
document.addEventListener("keydown",_fKeyHandler);
// ── D key → open/close Discover (Get Inspired) ───────────────────────
document.addEventListener("keydown",(e)=>{
if(e.key!=="d"&&e.key!=="D") return;
if(!_mouseOverRoot) return;
const tag=(document.activeElement||{}).tagName||"";
if(tag==="INPUT"||tag==="TEXTAREA") return;
if(settingsOverlay.style.display!=="none") return;
if(galleryOverlay.style.display!=="none") return;
if(_nodeFsOv&&_nodeFsOv.style.display!=="none") return;
e.preventDefault();e.stopPropagation();
if(_inspireOverlay.style.display!=="none"){ _closeInspire(); }
else { _openInspire(); }
});
// ── G key → open Gallery (grid view) ─────────────────────────────────
document.addEventListener("keydown",(e)=>{
if(e.key!=="g"&&e.key!=="G") return;
if(!_mouseOverRoot) return;
const tag=(document.activeElement||{}).tagName||"";
if(tag==="INPUT"||tag==="TEXTAREA") return;
if(settingsOverlay.style.display!=="none") return;
if(_promptOverlay.style.display!=="none") return;
if(_inspireOverlay.style.display!=="none") return;
if(_nodeFsOv&&_nodeFsOv.style.display!=="none") return;
// If gallery is open (grid view, no lightbox) → close it
if(galleryOverlay.style.display!=="none"&&lightbox.style.display==="none"){
closeOverlayFade(galleryOverlay,()=>{ lightbox.style.display="none"; _lbActiveImg=null; });
e.preventDefault();e.stopPropagation();return;
}
if(galleryOverlay.style.display!=="none") return;
e.preventDefault();e.stopPropagation();
galleryBtn.click();
});
// ── Space → trigger Generate (main UI only) ───────────────────────────
document.addEventListener("keydown",(e)=>{
if(e.code!=="Space") return;
if(!_mouseOverRoot) return;
const tag=(document.activeElement||{}).tagName||"";
if(tag==="INPUT"||tag==="TEXTAREA") return;
if(settingsOverlay.style.display!=="none") return;
if(galleryOverlay.style.display!=="none") return;
if(_promptOverlay.style.display!=="none") return;
if(_inspireOverlay.style.display!=="none") return;
if(_nodeFsOv&&_nodeFsOv.style.display!=="none") return;
e.preventDefault();e.stopPropagation();
genBtn.click();
});
// (fullscreen Esc guard is applied per-handler below)
// ── Escape → close Get Inspired overlay ──────────────────────────────
document.addEventListener("keydown",(e)=>{
if(e.key!=="Escape") return;
if(_inspireOverlay.style.display==="none") return;
e.preventDefault();e.stopPropagation();
_closeInspire();
});
// ── E → toggle Edit mode in Discover overlay ──────────────────────────
document.addEventListener("keydown",(e)=>{
if(e.key!=="e"&&e.key!=="E") return;
if(_inspireOverlay.style.display==="none") return;
const tag=(document.activeElement||{}).tagName||"";
if(tag==="INPUT"||tag==="TEXTAREA") return;
e.preventDefault();e.stopPropagation();
_inspireShowFullBtn.click();
});
// ── Escape in gallery grid → close gallery ────────────────────────────
document.addEventListener("keydown",(e)=>{
if(e.key!=="Escape") return;
if(!_mouseOverRoot) return;
if(galleryOverlay.style.display==="none") return;
if(e._lbHandled) return; // lightbox already handled this Escape — stay in grid
e.preventDefault();e.stopPropagation();
closeOverlayFade(galleryOverlay,()=>{ lightbox.style.display="none"; _lbActiveImg=null; });
});
// ── Escape → close Settings / Help overlays ──────────────────────────
document.addEventListener("keydown",(e)=>{
if(e.key!=="Escape") return;
if(settingsOverlay.style.display!=="none"){ e.preventDefault();e.stopPropagation();closeOverlayFade(settingsOverlay);return; }
});
// Fetch models
const _loadModels=()=>api.fetchApi("/z_image/models")
.then(r=>r.json())
.then(d=>{
const _norm=(s)=>(s||"").replace(/\\/g,"/").toLowerCase();
// pick: saved value takes priority; keyword auto-select only when no saved value matches
const pick=(list,saved,kw)=>{
if(!list?.length) return saved;
const ns=_norm(saved||"");
// 1. exact match
let r=list.find(i=>_norm(i)===ns);
if(r) return r;
// 2. basename match
if(ns){
const base=ns.split("/").pop();
r=list.find(i=>_norm(i).split("/").pop()===base);
if(r) return r;
}
// 3. no saved value — use keyword auto-select
if(kw){
const kws=kw.split(',').map(k=>k.trim().toLowerCase());
r=list.find(f=>kws.every(k=>_norm(f).includes(k)));
if(r) return r;
}
// 4. fallback: first item
return list[0]||saved;
};
const modelList=(d.diffusion_models||[]).filter(f=>f!=="none");
if(modelList.length){const v=pick(modelList,S.model,"z_image");S.model=v;modelF.dd.updateItems(modelList);modelF.dd.set(v);}
else{S.model="";modelF.dd.updateItems(["none"]);modelF.dd.set("none");}
const teList=(d.text_encoders||[]).filter(f=>f!=="none");
if(teList.length){const v=pick(teList,S.textEncoder,"qwen");S.textEncoder=v;teF.dd.updateItems(teList);teF.dd.set(v);}
else{S.textEncoder="";teF.dd.updateItems(["none"]);teF.dd.set("none");}
const vaeList=(d.vaes||[]).filter(f=>f!=="none");
if(vaeList.length){const v=pick(vaeList,S.vae,"");S.vae=v;vaeF.dd.updateItems(vaeList);vaeF.dd.set(v);}
else{S.vae="";vaeF.dd.updateItems(["none"]);vaeF.dd.set("none");}
persist();
// Populate LoRA dropdowns
const loraList=(d.loras||[]).filter(f=>f!=="none");
_loraList=loraList;
const loraOpts=["none",...loraList];
_ulRowEls.forEach((r,i)=>{
r._dd.updateItems(loraOpts);
const saved=S.userLoras[i].name;
if(saved&&saved!=="none"&&loraList.length){
const nd=(s)=>s.replace(/\\/g,"/").toLowerCase();
const match=loraOpts.find(o=>nd(o)===nd(saved))||
loraOpts.find(o=>nd(o).split("/").pop()===nd(saved).split("/").pop());
if(match){r._dd.set(match);S.userLoras[i].name=match;}
else{r._dd.set("none");S.userLoras[i].name="";}
} else{r._dd.set("none");S.userLoras[i].name="";}
});
_ulUpdateBtn();
persist();
})
.catch(e=>console.warn("[ZImageOneNode] models:",e));
_loadModels();
if(S.extLoaders) _applyExtLoaders(true);
// Auto-refresh Settings dropdowns when connections change
self.onConnectionsChange=function(){ _refreshExtInputUI(); };
const _slotHInit=(self.inputs||[]).length*(LiteGraph.NODE_SLOT_HEIGHT||20);
this.addDOMWidget("fk_ui","div",root,{
getValue(){return null;},setValue(){},serialize:false,
computeSize(){const slotH=(LiteGraph.NODE_SLOT_HEIGHT||20);const n=(self.inputs||[]).length;return[NODE_W,NODE_H+n*slotH];},
});
this.setSize([NODE_W,NODE_H+_slotHInit]);
// Nodes 2.0: hide the auto-injected node-type name badge rendered in the node footer.
// The badge has class "bg-node-component-surface" (Tailwind) and contains the node type string.
const _hideNodes2Badge=()=>{
// Walk up from root to find the node-level container (up to 6 levels)
let el=root;
for(let i=0;i<6;i++){
el=el?.parentElement;
if(!el) break;
el.querySelectorAll("[class*='bg-node-component-surface']").forEach(badge=>{
badge.style.display="none";
});
}
};
requestAnimationFrame(()=>{
_hideNodes2Badge();
if(typeof MutationObserver!=="undefined"){
let obs=root;
for(let i=0;i<4;i++) obs=obs?.parentElement||obs;
new MutationObserver(_hideNodes2Badge).observe(obs,{childList:true,subtree:true});
}
});
if(!window.__zimage_nodes) window.__zimage_nodes={};
window.__zimage_nodes[this.id]={
root,S,
fns:{showFinal,showPreview,resetBtn,setStage,showError,clearError,getPromptId:()=>_activePromptId},
};
_activeS=S;
_activeShowFinal=showFinal;
_activeShowPreview=showPreview;
_activeResetBtn=resetBtn;
_activeSetStage=setStage;
_activeShowError=showError;
_activePromptIdRef=()=>_activePromptId;
};
},
});