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=``; 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=``; 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=``; 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=``; _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=``; _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=``; _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=``; 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=``; _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=``; const _lockIconClosed=``; _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=``; const _advSeedIconDice=``; 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=``; 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=``; 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… `; 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=``; 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=``; 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=`Expand prompt`; _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=`✦ Discover`; _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=``; _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=`ADV`; // 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… `; // 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=``; 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=_lbIdx1) 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; }; }, });