Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FLUX.2 Klein Studio</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#000000;--panel:#0a0a0a;--panel2:#111111;--canvas:#050505; | |
| --border:#1f1f1f;--border2:#2a2a2a; | |
| --text:#ffffff;--muted:#b0b0b0;--dim:#707070;--faint:#404040; | |
| --accent:#ffffff;--accent2:#e0e0e0;--accent3:#a0a0a0; | |
| --menubar-h:48px;--statusbar-h:30px;--rail-w:52px;--inspector-w:360px;--filmstrip-h:132px; | |
| } | |
| html,body{height:100%} | |
| body{ | |
| background:var(--bg);font-family:'Inter',system-ui,-apple-system,sans-serif; | |
| font-size:13px;color:var(--text);overflow:hidden; | |
| } | |
| /* ══ Layout grid ══ */ | |
| .studio{ | |
| display:grid;height:100vh; | |
| grid-template-rows:var(--menubar-h) 1fr var(--statusbar-h); | |
| grid-template-columns:var(--rail-w) 1fr var(--inspector-w); | |
| grid-template-areas: | |
| "menubar menubar menubar" | |
| "rail workspace inspector" | |
| "statusbar statusbar statusbar"; | |
| } | |
| /* ══ Menu bar ══ */ | |
| .menubar{ | |
| grid-area:menubar;display:flex;align-items:center;gap:14px; | |
| background:var(--panel);border-bottom:1px solid var(--border); | |
| padding:0 16px;z-index:50; | |
| } | |
| .menubar-logo{ | |
| width:28px;height:28px;background:#ffffff; | |
| border-radius:8px;display:flex;align-items:center;justify-content:center; | |
| box-shadow:0 2px 8px rgba(255,255,255,.25);flex-shrink:0; | |
| } | |
| .menubar-logo svg{width:15px;height:15px;fill:#000} | |
| .menubar-title{font-size:14px;font-weight:700;letter-spacing:-.2px;white-space:nowrap} | |
| .menubar-title span{color:var(--dim);font-weight:500} | |
| .menubar-badge{ | |
| font-size:10px;font-weight:700;padding:2px 8px;border-radius:12px; | |
| background:rgba(255,255,255,.1);color:var(--accent2);border:1px solid rgba(255,255,255,.2); | |
| letter-spacing:.4px;white-space:nowrap; | |
| } | |
| .menubar-badge.fast{background:rgba(255,255,255,.1);color:var(--accent2);border-color:rgba(255,255,255,.2)} | |
| .menubar-spacer{flex:1} | |
| .menubar-status{ | |
| display:flex;align-items:center;gap:7px;font-family:'JetBrains Mono',monospace; | |
| font-size:11px;color:var(--dim); | |
| } | |
| .menubar-status .dot{width:7px;height:7px;border-radius:50%;background:#fff;box-shadow:0 0 6px rgba(255,255,255,.6)} | |
| .menubar-status .dot.off{background:#f87171;box-shadow:0 0 6px rgba(248,113,113,.5)} | |
| .gh-btn{ | |
| display:inline-flex;align-items:center;gap:6px; | |
| padding:5px 13px;border-radius:7px;text-decoration:none; | |
| font-size:12px;font-weight:600;background:#fff;color:#000; | |
| border:1px solid #333; | |
| transition:all .15s ease;flex-shrink:0; | |
| } | |
| .gh-btn:hover{background:#e0e0e0} | |
| .gh-btn svg{fill:#000;width:13px;height:13px} | |
| /* ══ Left icon rail ══ */ | |
| .rail{ | |
| grid-area:rail;background:var(--panel);border-right:1px solid var(--border); | |
| display:flex;flex-direction:column;align-items:center;padding:10px 0;gap:4px; | |
| overflow-y:auto;z-index:40; | |
| } | |
| .rail-btn{ | |
| width:38px;height:38px;border-radius:9px;border:1px solid transparent; | |
| background:transparent;color:var(--dim);cursor:pointer; | |
| display:flex;align-items:center;justify-content:center; | |
| transition:all .15s ease;position:relative; | |
| } | |
| .rail-btn:hover:not(:disabled){background:rgba(255,255,255,.1);color:var(--accent2);border-color:rgba(255,255,255,.25)} | |
| .rail-btn.active{background:rgba(255,255,255,.2);color:var(--accent2);border-color:rgba(255,255,255,.4)} | |
| .rail-btn:disabled{opacity:.3;cursor:not-allowed} | |
| .rail-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round} | |
| .rail-btn svg.fill{fill:currentColor;stroke:none} | |
| .rail-sep{width:24px;height:1px;background:var(--border);margin:6px 0} | |
| .rail-label{ | |
| font-size:8px;font-weight:700;color:var(--faint);letter-spacing:1px; | |
| text-transform:uppercase;margin:8px 0 2px;user-select:none; | |
| } | |
| /* ══ Workspace ══ */ | |
| .workspace{ | |
| grid-area:workspace;display:flex;flex-direction:column;min-width:0;min-height:0; | |
| background:var(--canvas); | |
| } | |
| .canvas-head{ | |
| display:flex;align-items:center;gap:10px;padding:8px 16px; | |
| border-bottom:1px solid var(--border);background:var(--panel2);flex-shrink:0; | |
| } | |
| .seg-control{ | |
| display:inline-flex;background:var(--panel);border:1px solid var(--border); | |
| border-radius:8px;padding:2px;gap:2px; | |
| } | |
| .seg-btn{ | |
| padding:4px 14px;font-size:11px;font-weight:600;color:var(--dim); | |
| background:transparent;border:none;border-radius:6px;cursor:pointer; | |
| font-family:'Inter',sans-serif;transition:all .15s;letter-spacing:.2px; | |
| } | |
| .seg-btn:hover:not(.active){color:var(--muted)} | |
| .seg-btn.active{background:rgba(255,255,255,.18);color:var(--accent2)} | |
| .canvas-meta{ | |
| font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint); | |
| overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1; | |
| } | |
| .canvas-actions{display:flex;align-items:center;gap:6px} | |
| .out-seed-chip{ | |
| display:none;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:500; | |
| color:var(--dim);background:var(--panel);border:1px solid var(--border); | |
| border-radius:6px;padding:4px 9px;cursor:pointer;transition:all .15s; | |
| } | |
| .out-seed-chip.visible{display:inline-flex} | |
| .out-seed-chip:hover{color:var(--accent3);border-color:rgba(255,255,255,.35)} | |
| .canvas-stage{ | |
| flex:1;position:relative;overflow:hidden;min-height:0; | |
| display:flex;align-items:center;justify-content:center; | |
| background: | |
| repeating-conic-gradient(#0c0c0c 0% 25%, #050505 0% 50%) 0 0/24px 24px; | |
| } | |
| .canvas-stage img.modern-out-img{ | |
| max-width:calc(100% - 48px);max-height:calc(100% - 48px); | |
| box-shadow:0 12px 48px rgba(0,0,0,.9),0 0 0 1px var(--border); | |
| border-radius:4px;cursor:zoom-in;image-rendering:auto; | |
| animation:canvasIn .3s ease; | |
| } | |
| @keyframes canvasIn{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}} | |
| .canvas-placeholder{ | |
| text-align:center;color:var(--faint);user-select:none; | |
| display:flex;flex-direction:column;align-items:center;gap:14px;padding:24px; | |
| } | |
| .canvas-placeholder svg{width:72px;height:72px;opacity:.5} | |
| .canvas-placeholder .cp-title{font-size:15px;font-weight:600;color:var(--dim)} | |
| .canvas-placeholder .cp-sub{font-size:12px;line-height:1.7;max-width:340px} | |
| .canvas-placeholder kbd{ | |
| padding:1px 6px;background:var(--panel);border:1px solid var(--border2); | |
| border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted); | |
| } | |
| /* Compare slider */ | |
| .compare-wrap{ | |
| position:relative;max-width:calc(100% - 48px);max-height:calc(100% - 48px);overflow:hidden; | |
| display:none;user-select:none;touch-action:none;cursor:ew-resize; | |
| box-shadow:0 12px 48px rgba(0,0,0,.9),0 0 0 1px var(--border);border-radius:4px; | |
| } | |
| .compare-wrap.visible{display:block} | |
| .compare-wrap img{display:block;max-width:100%;max-height:100%;pointer-events:none} | |
| .compare-top{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden} | |
| .compare-top img{position:absolute;top:0;left:0;height:100%;width:auto;max-width:none!important;max-height:none!important} | |
| .compare-divider{position:absolute;top:0;bottom:0;width:2px;background:#fff;box-shadow:0 0 8px rgba(0,0,0,.6);pointer-events:none} | |
| .compare-handle{ | |
| position:absolute;top:50%;transform:translate(-50%,-50%); | |
| width:32px;height:32px;border-radius:50%;background:#fff; | |
| display:flex;align-items:center;justify-content:center; | |
| box-shadow:0 2px 8px rgba(0,0,0,.5);color:#111;font-size:13px;font-weight:800; | |
| } | |
| .compare-label{ | |
| position:absolute;top:8px;padding:2px 8px;border-radius:4px; | |
| background:rgba(0,0,0,.65);color:#fff;font-size:10px;font-weight:600; | |
| font-family:'JetBrains Mono',monospace;pointer-events:none; | |
| } | |
| .compare-label.before{left:8px} | |
| .compare-label.after{right:8px} | |
| /* Loader */ | |
| .modern-loader{ | |
| display:none;position:absolute;inset:0;background:rgba(0,0,0,.92); | |
| z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(5px); | |
| } | |
| .modern-loader.active{display:flex} | |
| .loader-spinner{ | |
| width:38px;height:38px;border:3px solid var(--border);border-top-color:var(--accent); | |
| border-radius:50%;animation:spin .8s linear infinite; | |
| } | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .loader-text{font-size:13px;color:var(--muted);font-weight:500;text-align:center;padding:0 16px} | |
| .loader-bar-track{width:220px;height:4px;background:var(--border);border-radius:2px;overflow:hidden} | |
| .loader-bar-fill{ | |
| height:100%;width:100%;background:linear-gradient(90deg,#ffffff,#e0e0e0,#ffffff); | |
| background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px; | |
| transition:width .3s ease; | |
| } | |
| .loader-bar-fill.determinate{animation:none;background:var(--accent)} | |
| @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} | |
| /* ══ Filmstrip ══ */ | |
| .filmstrip{ | |
| height:var(--filmstrip-h);flex-shrink:0;background:var(--panel); | |
| border-top:1px solid var(--border);display:flex;min-height:0;overflow:hidden; | |
| } | |
| .filmstrip-section{ | |
| display:flex;flex-direction:column;min-width:0;padding:8px 0 8px 14px; | |
| } | |
| .filmstrip-section.inputs{flex:1.2;border-right:1px solid var(--border)} | |
| .filmstrip-section.history{flex:1} | |
| .filmstrip-label{ | |
| font-size:9px;font-weight:700;color:var(--faint);letter-spacing:1.2px; | |
| text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px; | |
| user-select:none;flex-shrink:0; | |
| } | |
| .filmstrip-label .count{ | |
| font-family:'JetBrains Mono',monospace;font-weight:500;color:var(--dim); | |
| background:var(--panel2);border:1px solid var(--border);border-radius:4px;padding:0 5px;font-size:9px; | |
| } | |
| .filmstrip-row{ | |
| display:flex;gap:8px;overflow-x:auto;overflow-y:hidden;flex:1; | |
| align-items:flex-start;padding-bottom:4px;padding-right:14px; | |
| } | |
| .filmstrip-row::-webkit-scrollbar{height:5px} | |
| .filmstrip-row::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} | |
| .fs-thumb{ | |
| position:relative;flex-shrink:0;width:76px;height:76px;border-radius:8px;overflow:hidden; | |
| border:2px solid var(--border);cursor:pointer;transition:all .15s;background:var(--panel2); | |
| animation:thumbIn .25s ease; | |
| } | |
| @keyframes thumbIn{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}} | |
| .fs-thumb:hover{border-color:var(--border2);transform:translateY(-2px)} | |
| .fs-thumb.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(255,255,255,.25)} | |
| .fs-thumb img{width:100%;height:100%;object-fit:cover} | |
| .fs-badge{ | |
| position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.75);color:#fff; | |
| padding:1px 6px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600; | |
| } | |
| .fs-remove{ | |
| position:absolute;top:4px;right:4px;width:20px;height:20px;background:rgba(0,0,0,.8); | |
| color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer; | |
| display:none;align-items:center;justify-content:center;font-size:10px;transition:all .15s;line-height:1; | |
| } | |
| .fs-thumb:hover .fs-remove{display:flex} | |
| .fs-remove:hover{background:#ef4444;border-color:#ef4444} | |
| .fs-add{ | |
| flex-shrink:0;width:76px;height:76px;border-radius:8px;border:2px dashed var(--border2); | |
| display:flex;flex-direction:column;align-items:center;justify-content:center; | |
| cursor:pointer;transition:all .2s;background:rgba(255,255,255,.03);gap:3px; | |
| } | |
| .fs-add:hover{border-color:var(--accent);background:rgba(255,255,255,.08)} | |
| .fs-add .add-icon{font-size:22px;color:var(--dim);font-weight:300} | |
| .fs-add .add-text{font-size:9px;color:var(--dim);font-weight:600;text-transform:uppercase;letter-spacing:.5px} | |
| .fs-empty{ | |
| flex-shrink:0;display:flex;align-items:center;height:76px; | |
| color:var(--faint);font-size:11px;font-style:italic;padding-right:12px; | |
| } | |
| /* ══ Inspector ══ */ | |
| .inspector{ | |
| grid-area:inspector;background:var(--panel);border-left:1px solid var(--border); | |
| display:flex;flex-direction:column;min-height:0;z-index:40; | |
| } | |
| .inspector-tabs{ | |
| display:flex;border-bottom:1px solid var(--border);flex-shrink:0; | |
| } | |
| .insp-tab{ | |
| flex:1;padding:11px 8px;font-size:11px;font-weight:700;color:var(--dim); | |
| background:transparent;border:none;border-bottom:2px solid transparent; | |
| cursor:pointer;font-family:'Inter',sans-serif;text-transform:uppercase;letter-spacing:.8px; | |
| transition:all .15s; | |
| } | |
| .insp-tab:hover:not(.active){color:var(--muted)} | |
| .insp-tab.active{color:var(--accent2);border-bottom-color:var(--accent)} | |
| .insp-page{display:none;flex:1;overflow-y:auto;min-height:0;flex-direction:column} | |
| .insp-page.active{display:flex} | |
| .insp-section{border-bottom:1px solid var(--border);padding:14px 18px} | |
| .insp-section-title{ | |
| font-size:10px;font-weight:700;color:var(--dim);text-transform:uppercase; | |
| letter-spacing:1px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between; | |
| } | |
| .char-count{font-size:9px;font-weight:500;color:var(--faint);font-family:'JetBrains Mono',monospace;text-transform:none;letter-spacing:0} | |
| .modern-textarea{ | |
| width:100%;background:var(--panel2);border:1px solid var(--border);border-radius:8px; | |
| padding:10px 13px;font-family:'Inter',sans-serif;font-size:13px;color:var(--text); | |
| resize:vertical;outline:none;min-height:64px;transition:border-color .2s;line-height:1.55; | |
| } | |
| .modern-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,255,255,.15)} | |
| .modern-textarea::placeholder{color:var(--faint)} | |
| .modern-textarea.error-flash{ | |
| border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease; | |
| } | |
| @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}} | |
| .suggestions-wrap{display:flex;flex-wrap:wrap;gap:5px} | |
| .suggestion-chip{ | |
| display:inline-flex;align-items:center;padding:4px 11px; | |
| background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.18);border-radius:14px; | |
| color:var(--accent3);font-size:11px;font-weight:500;font-family:'Inter',sans-serif; | |
| cursor:pointer;transition:all .15s;white-space:nowrap; | |
| } | |
| .suggestion-chip:hover{background:rgba(255,255,255,.15);border-color:rgba(255,255,255,.35);color:var(--accent2)} | |
| .suggestion-chip.active{background:rgba(255,255,255,.25);border-color:var(--accent);color:#000} | |
| .lora-native-select{ | |
| width:100%;background:var(--panel2); | |
| border:1px solid var(--border2);border-radius:8px; | |
| padding:9px 34px 9px 13px; | |
| font-family:'Inter',sans-serif;font-size:12px;font-weight:600;color:var(--text); | |
| outline:none;appearance:none;-webkit-appearance:none; | |
| background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); | |
| background-repeat:no-repeat;background-position:right 11px center; | |
| cursor:pointer;transition:border-color .2s;color-scheme:dark; | |
| } | |
| .lora-native-select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,255,255,.15)} | |
| .lora-native-select option{background:var(--panel);color:var(--text)} | |
| .btn-run{ | |
| display:flex;align-items:center;justify-content:center;gap:8px;width:100%; | |
| background:#ffffff;border:none;border-radius:9px; | |
| padding:12px 24px;cursor:pointer;font-size:14px;font-weight:700;font-family:'Inter',sans-serif; | |
| color:#000;transition:all .2s ease;letter-spacing:-.2px; | |
| box-shadow:0 4px 16px rgba(255,255,255,.15),inset 0 1px 0 rgba(255,255,255,.1); | |
| } | |
| .btn-run:hover:not(:disabled){ | |
| background:#e0e0e0;transform:translateY(-1px); | |
| box-shadow:0 6px 24px rgba(255,255,255,.25),inset 0 1px 0 rgba(255,255,255,.15); | |
| } | |
| .btn-run:active:not(:disabled){transform:translateY(0)} | |
| .btn-run:disabled{opacity:.55;cursor:not-allowed} | |
| .btn-run svg{width:16px;height:16px;fill:#000} | |
| .btn-cancel{ | |
| display:flex;align-items:center;justify-content:center;gap:6px;width:100%; | |
| margin-top:8px;background:transparent;border:1px solid var(--border2);border-radius:9px; | |
| padding:8px 24px;cursor:pointer;font-size:12px;font-weight:600;font-family:'Inter',sans-serif; | |
| color:var(--muted);transition:all .15s; | |
| } | |
| .btn-cancel:hover{border-color:#ef4444;color:#ef4444;background:rgba(239,68,68,.08)} | |
| .run-hint{ | |
| text-align:center;font-size:10px;color:var(--faint);margin-top:8px; | |
| font-family:'JetBrains Mono',monospace; | |
| } | |
| .slider-row{display:flex;align-items:center;gap:9px;min-height:26px;margin-bottom:10px} | |
| .slider-row:last-child{margin-bottom:0} | |
| .slider-row label{font-size:12px;font-weight:500;color:var(--muted);min-width:64px;flex-shrink:0} | |
| .slider-row input[type="range"]{ | |
| flex:1;-webkit-appearance:none;appearance:none;height:5px;background:var(--border); | |
| border-radius:3px;outline:none;min-width:0; | |
| } | |
| .slider-row input[type="range"]::-webkit-slider-thumb{ | |
| -webkit-appearance:none;width:14px;height:14px;background:#ffffff; | |
| border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,255,255,.4);transition:transform .15s; | |
| } | |
| .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)} | |
| .slider-row input[type="range"]::-moz-range-thumb{ | |
| width:14px;height:14px;background:#ffffff; | |
| border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,255,255,.4); | |
| } | |
| .slider-val{ | |
| min-width:48px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px; | |
| font-weight:500;padding:2px 7px;background:var(--panel2);border:1px solid var(--border); | |
| border-radius:5px;color:var(--muted);flex-shrink:0; | |
| } | |
| .dice-btn{ | |
| display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px; | |
| background:var(--panel2);border:1px solid var(--border);border-radius:5px;cursor:pointer; | |
| color:var(--dim);transition:all .15s;flex-shrink:0;padding:0; | |
| } | |
| .dice-btn:hover{border-color:var(--accent);color:var(--accent2)} | |
| .dice-btn svg{width:13px;height:13px;fill:currentColor} | |
| .checkbox-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);margin-bottom:10px} | |
| .checkbox-row input[type="checkbox"]{accent-color:var(--accent);width:15px;height:15px;cursor:pointer} | |
| .checkbox-row label{color:var(--muted);font-size:12px;cursor:pointer} | |
| /* Examples page */ | |
| .examples-list{padding:12px 14px;display:flex;flex-direction:column;gap:10px} | |
| .example-card{ | |
| background:var(--panel2);border:1px solid var(--border); | |
| border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;flex-shrink:0; | |
| display:flex;align-items:stretch; | |
| } | |
| .example-card:hover{border-color:var(--accent);box-shadow:0 4px 14px rgba(255,255,255,.15)} | |
| .example-card.loading{opacity:.5;pointer-events:none} | |
| .example-thumb-wrap{width:96px;flex-shrink:0;background:var(--panel);overflow:hidden;display:flex} | |
| .example-thumb-wrap img{width:100%;height:100%;object-fit:cover} | |
| .example-thumb-placeholder{ | |
| width:100%;height:100%;display:flex;align-items:center;justify-content:center; | |
| background:var(--panel);color:var(--border2);font-size:10px; | |
| } | |
| .example-meta{padding:6px 10px 2px;display:flex;align-items:center;gap:5px;flex-wrap:wrap} | |
| .example-lora-badge{ | |
| display:inline-flex;padding:2px 7px;background:rgba(255,255,255,.1);border-radius:4px; | |
| font-size:9px;font-weight:600;color:var(--accent2);font-family:'JetBrains Mono',monospace; | |
| white-space:nowrap;border:1px solid rgba(255,255,255,.25); | |
| } | |
| .example-body{flex:1;display:flex;flex-direction:column;justify-content:center;min-width:0} | |
| .example-prompt-text{ | |
| padding:4px 10px 9px;font-size:11px;color:var(--muted);line-height:1.45; | |
| display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; | |
| } | |
| /* ══ Status bar ══ */ | |
| .statusbar{ | |
| grid-area:statusbar;background:var(--panel);border-top:1px solid var(--border); | |
| display:flex;align-items:center;gap:4px;padding:0 14px;font-size:11px;z-index:50; | |
| } | |
| .statusbar .sb-section{ | |
| padding:0 10px;display:flex;align-items:center;font-family:'JetBrains Mono',monospace; | |
| font-size:11px;color:var(--faint);overflow:hidden;white-space:nowrap;gap:6px; | |
| } | |
| .statusbar .sb-right{margin-left:auto;display:flex;align-items:center;gap:10px} | |
| .sb-pill{ | |
| padding:2px 12px;border-radius:5px;font-weight:600;font-size:10px; | |
| background:rgba(255,255,255,.1);color:var(--accent2); | |
| } | |
| .sb-pill.error{background:rgba(239,68,68,.12);color:#f87171} | |
| .sb-pill.done{background:rgba(255,255,255,.12);color:#fff} | |
| .sb-link{color:var(--faint);text-decoration:none;font-family:'JetBrains Mono',monospace;font-size:10px} | |
| .sb-link:hover{color:var(--accent3)} | |
| /* ══ Toast ══ */ | |
| .toast-notification{ | |
| position:fixed;top:60px;left:50%;transform:translateX(-50%) translateY(-140%); | |
| z-index:9999;padding:10px 22px;border-radius:9px;font-family:'Inter',sans-serif; | |
| font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px; | |
| box-shadow:0 8px 24px rgba(0,0,0,.5); | |
| transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none; | |
| } | |
| .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto} | |
| .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)} | |
| .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)} | |
| .toast-notification.info{background:linear-gradient(135deg,#ffffff,#e0e0e0);color:#000;border:1px solid rgba(0,0,0,.15)} | |
| .toast-notification .toast-icon{font-size:15px;line-height:1} | |
| /* ══ Lightbox ══ */ | |
| .lightbox{ | |
| position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:10000; | |
| display:none;align-items:center;justify-content:center;cursor:zoom-out; | |
| backdrop-filter:blur(6px);animation:fadeIn .2s ease; | |
| } | |
| .lightbox.visible{display:flex} | |
| .lightbox img{max-width:94vw;max-height:94vh;border-radius:6px;box-shadow:0 20px 60px rgba(0,0,0,.8)} | |
| @keyframes fadeIn{from{opacity:0}to{opacity:1}} | |
| /* ══ Drop overlay ══ */ | |
| .drop-overlay{ | |
| position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.85); | |
| display:none;align-items:center;justify-content:center;pointer-events:none; | |
| backdrop-filter:blur(3px); | |
| } | |
| .drop-overlay.visible{display:flex} | |
| .drop-overlay-inner{ | |
| border:3px dashed var(--accent);border-radius:22px;padding:56px 76px; | |
| font-size:19px;font-weight:700;color:var(--accent2);background:rgba(255,255,255,.06); | |
| } | |
| /* ══ Scrollbars ══ */ | |
| ::-webkit-scrollbar{width:8px;height:8px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px} | |
| ::-webkit-scrollbar-thumb:hover{background:var(--border2)} | |
| /* ══ Touch devices ══ */ | |
| @media (pointer:coarse){ | |
| .fs-remove{display:flex} | |
| .rail-btn{width:44px;height:44px} | |
| .rail-btn svg{width:19px;height:19px} | |
| } | |
| /* ══ Responsive ══ */ | |
| @media(max-width:960px){ | |
| html,body{height:auto} | |
| body{overflow:auto!important} | |
| .studio{ | |
| display:flex;flex-direction:column;height:auto;min-height:100vh; | |
| } | |
| .menubar{ | |
| flex-wrap:wrap;gap:8px;padding:10px 12px;min-height:var(--menubar-h); | |
| position:sticky;top:0; | |
| } | |
| .menubar-title{font-size:13px} | |
| .menubar-status{display:none} | |
| .menubar-spacer{flex:1} | |
| .gh-btn{padding:6px 10px} | |
| .gh-btn span{display:none} | |
| .rail{ | |
| flex-direction:row;justify-content:flex-start;flex-wrap:nowrap; | |
| overflow-x:auto;overflow-y:hidden;border-right:none; | |
| border-bottom:1px solid var(--border);padding:6px 10px;gap:6px; | |
| } | |
| .rail-sep{width:1px;height:26px;margin:0 4px;flex-shrink:0} | |
| .rail-label{display:none} | |
| .rail-btn{flex-shrink:0} | |
| .workspace{min-height:0} | |
| .canvas-head{flex-wrap:wrap;gap:8px;padding:8px 12px} | |
| .canvas-meta{flex-basis:100%;order:3;font-size:10px} | |
| .canvas-stage{min-height:42vh} | |
| .canvas-stage img.modern-out-img{max-width:calc(100% - 20px);max-height:52vh} | |
| .compare-wrap{max-width:calc(100% - 20px);max-height:52vh} | |
| .canvas-placeholder .cp-sub{font-size:11px;max-width:280px} | |
| .filmstrip{flex-direction:column;height:auto;max-height:none} | |
| .filmstrip-section.inputs{border-right:none;border-bottom:1px solid var(--border)} | |
| .filmstrip-section{padding:8px 0 8px 12px} | |
| .fs-thumb{width:68px;height:68px} | |
| .fs-add{width:68px;height:68px} | |
| .fs-empty{height:68px} | |
| .inspector{border-left:none;border-top:1px solid var(--border)} | |
| .inspector-tabs{position:sticky;top:0;background:var(--panel);z-index:30} | |
| .insp-tab{padding:13px 8px;font-size:12px} | |
| .insp-page{overflow-y:visible} | |
| .insp-section{padding:14px 16px} | |
| .modern-textarea{font-size:16px} | |
| .btn-run{padding:14px 24px;font-size:15px} | |
| .suggestion-chip{padding:6px 13px;font-size:12px} | |
| .statusbar{flex-wrap:wrap;height:auto;min-height:var(--statusbar-h);padding:4px 10px;gap:2px} | |
| .sb-link{display:none} | |
| .toast-notification{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px} | |
| .drop-overlay-inner{padding:32px 28px;font-size:15px;border-radius:16px;margin:0 16px} | |
| .example-card{flex-direction:column} | |
| .example-thumb-wrap{width:100%;height:96px} | |
| } | |
| @media(max-width:400px){ | |
| .menubar-badge.fast{display:none} | |
| .seg-btn{padding:4px 12px} | |
| .canvas-stage{min-height:36vh} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="studio"> | |
| <!-- ══ Menu bar ══ --> | |
| <div class="menubar"> | |
| <div class="menubar-logo"> | |
| <svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg> | |
| </div> | |
| <div class="menubar-title">FLUX.2 Klein <span>/ Studio</span></div> | |
| <span class="menubar-badge">9B</span> | |
| <span class="menubar-badge fast">4-STEP FAST</span> | |
| <div class="menubar-spacer"></div> | |
| <div class="menubar-status"><span class="dot" id="conn-dot"></span><span id="conn-text">connecting…</span></div> | |
| <a href="https://github.com/PRITHIVSAKTHIUR/FLUX.2-Klein-LoRA-Studio" | |
| target="_blank" rel="noopener" class="gh-btn"> | |
| <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> | |
| <span>GitHub</span> | |
| </a> | |
| </div> | |
| <!-- ══ Left icon rail ══ --> | |
| <div class="rail"> | |
| <div class="rail-label">Input</div> | |
| <button class="rail-btn" id="tb-upload" title="Upload images"> | |
| <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> | |
| </button> | |
| <button class="rail-btn" id="tb-remove" title="Remove selected image" disabled> | |
| <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| </button> | |
| <button class="rail-btn" id="tb-clear" title="Clear all images" disabled> | |
| <svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> | |
| </button> | |
| <div class="rail-sep"></div> | |
| <div class="rail-label">Result</div> | |
| <button class="rail-btn" id="compare-btn" title="Compare before / after" disabled> | |
| <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg> | |
| </button> | |
| <button class="rail-btn" id="use-as-input-btn" title="Use result as input (chain edits)" disabled> | |
| <svg viewBox="0 0 24 24"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg> | |
| </button> | |
| <button class="rail-btn" id="dl-btn-output" title="Download result" disabled> | |
| <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | |
| </button> | |
| <button class="rail-btn" id="zoom-btn" title="Zoom result" disabled> | |
| <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg> | |
| </button> | |
| </div> | |
| <!-- ══ Workspace ══ --> | |
| <div class="workspace"> | |
| <div class="canvas-head"> | |
| <div class="seg-control"> | |
| <button class="seg-btn active" id="seg-result">Result</button> | |
| <button class="seg-btn" id="seg-input">Input</button> | |
| </div> | |
| <div class="canvas-meta" id="canvas-meta">No image loaded</div> | |
| <div class="canvas-actions"> | |
| <span id="out-seed-chip" class="out-seed-chip" title="Click to copy seed"></span> | |
| </div> | |
| </div> | |
| <div class="canvas-stage" id="output-image-container"> | |
| <div class="modern-loader" id="output-loader"> | |
| <div class="loader-spinner"></div> | |
| <div class="loader-text" id="loader-text">Processing image…</div> | |
| <div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div> | |
| </div> | |
| <div class="canvas-placeholder" id="output-placeholder"> | |
| <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#2a2a2a" stroke-width="2" stroke-dasharray="4 3"/> | |
| <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,255,255,0.05)" stroke="#2a2a2a" stroke-width="1.5"/> | |
| <circle cx="28" cy="30" r="6" fill="rgba(255,255,255,0.05)" stroke="#2a2a2a" stroke-width="1.5"/> | |
| </svg> | |
| <div class="cp-title">Canvas is empty</div> | |
| <div class="cp-sub"> | |
| Drop images anywhere, paste with <kbd>⌘/Ctrl+V</kbd>, or use the rail to upload. | |
| Then write an instruction and press <kbd>⌘/Ctrl</kbd>+<kbd>↵</kbd>. | |
| </div> | |
| </div> | |
| <div class="compare-wrap" id="compare-wrap"> | |
| <img id="compare-after-img" alt="after"> | |
| <div class="compare-top" id="compare-top"><img id="compare-before-img" alt="before"></div> | |
| <div class="compare-divider" id="compare-divider"><div class="compare-handle">⇄</div></div> | |
| <span class="compare-label before">Before</span> | |
| <span class="compare-label after">After</span> | |
| </div> | |
| </div> | |
| <!-- Filmstrip --> | |
| <div class="filmstrip"> | |
| <div class="filmstrip-section inputs"> | |
| <div class="filmstrip-label">Inputs <span class="count" id="tb-image-count">0</span></div> | |
| <div class="filmstrip-row" id="image-gallery-grid"></div> | |
| </div> | |
| <div class="filmstrip-section history"> | |
| <div class="filmstrip-label">History <span class="count" id="history-count">0</span></div> | |
| <div class="filmstrip-row" id="history-strip"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ══ Inspector ══ --> | |
| <div class="inspector"> | |
| <div class="inspector-tabs"> | |
| <button class="insp-tab active" id="tab-edit">Edit</button> | |
| <button class="insp-tab" id="tab-examples">Examples</button> | |
| </div> | |
| <!-- Edit page --> | |
| <div class="insp-page active" id="page-edit"> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count">0</span></div> | |
| <textarea id="custom-prompt-input" class="modern-textarea" rows="3" | |
| placeholder="e.g., Change weather to stormy, face swap, ghost mannequin…"></textarea> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Quick Prompts</span></div> | |
| <div class="suggestions-wrap" id="suggestions-wrap"></div> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Style / LoRA</span></div> | |
| <select id="custom-lora-select" class="lora-native-select"></select> | |
| </div> | |
| <div class="insp-section"> | |
| <button id="custom-run-btn" class="btn-run"> | |
| <svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg> | |
| <span id="run-btn-label">Edit Image</span> | |
| </button> | |
| <button id="custom-cancel-btn" class="btn-cancel" style="display:none;">Cancel</button> | |
| <div class="run-hint">⌘/Ctrl + Enter</div> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Advanced</span></div> | |
| <div class="slider-row"> | |
| <label>Seed</label> | |
| <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0"> | |
| <span class="slider-val" id="custom-seed-val">0</span> | |
| <button class="dice-btn" id="dice-btn" title="Random seed"> | |
| <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM7.5 18c-.83 0-1.5-.67-1.5-1.5S6.67 15 7.5 15s1.5.67 1.5 1.5S8.33 18 7.5 18zm0-9C6.67 9 6 8.33 6 7.5S6.67 6 7.5 6 9 6.67 9 7.5 8.33 9 7.5 9zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm0-9c-.83 0-1.5-.67-1.5-1.5S15.67 6 16.5 6s1.5.67 1.5 1.5S17.33 9 16.5 9z"/></svg> | |
| </button> | |
| </div> | |
| <div class="checkbox-row"> | |
| <input type="checkbox" id="custom-randomize" checked> | |
| <label for="custom-randomize">Randomize seed</label> | |
| </div> | |
| <div class="slider-row"> | |
| <label>Guidance</label> | |
| <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0"> | |
| <span class="slider-val" id="custom-guidance-val">1.0</span> | |
| </div> | |
| <div class="slider-row"> | |
| <label>Steps</label> | |
| <input type="range" id="custom-steps" min="1" max="50" step="1" value="4"> | |
| <span class="slider-val" id="custom-steps-val">4</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Examples page --> | |
| <div class="insp-page" id="page-examples"> | |
| <div class="examples-list" id="examples-scroll"></div> | |
| </div> | |
| </div> | |
| <!-- ══ Status bar ══ --> | |
| <div class="statusbar"> | |
| <div class="sb-section" id="sb-image-count">No inputs</div> | |
| <div class="sb-right"> | |
| <a class="sb-link" href="https://huggingface.co/black-forest-labs/FLUX.2-klein-9B" target="_blank" rel="noopener">FLUX.2-klein-9B</a> | |
| <span class="sb-pill" id="sb-status">Ready</span> | |
| </div> | |
| </div> | |
| </div> | |
| <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" /> | |
| <!-- Lightbox --> | |
| <div class="lightbox" id="lightbox"><img id="lightbox-img" alt="zoom"></div> | |
| <!-- Global drop overlay --> | |
| <div class="drop-overlay" id="drop-overlay"><div class="drop-overlay-inner">Drop images to upload</div></div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| /* ── DOM refs ── */ | |
| const galleryGrid = document.getElementById('image-gallery-grid'); | |
| const fileInput = document.getElementById('custom-file-input'); | |
| const btnUpload = document.getElementById('tb-upload'); | |
| const btnRemove = document.getElementById('tb-remove'); | |
| const btnClear = document.getElementById('tb-clear'); | |
| const promptInput = document.getElementById('custom-prompt-input'); | |
| const charCount = document.getElementById('char-count'); | |
| const loraSelect = document.getElementById('custom-lora-select'); | |
| const runBtn = document.getElementById('custom-run-btn'); | |
| const runBtnLabel = document.getElementById('run-btn-label'); | |
| const cancelBtn = document.getElementById('custom-cancel-btn'); | |
| const imgCountTb = document.getElementById('tb-image-count'); | |
| const imgCountSb = document.getElementById('sb-image-count'); | |
| const sbStatus = document.getElementById('sb-status'); | |
| const loader = document.getElementById('output-loader'); | |
| const loaderText = document.getElementById('loader-text'); | |
| const loaderFill = document.getElementById('loader-bar-fill'); | |
| const outBody = document.getElementById('output-image-container'); | |
| const outPh = document.getElementById('output-placeholder'); | |
| const dlBtn = document.getElementById('dl-btn-output'); | |
| const compareBtn = document.getElementById('compare-btn'); | |
| const useAsInputBtn= document.getElementById('use-as-input-btn'); | |
| const zoomBtn = document.getElementById('zoom-btn'); | |
| const seedChip = document.getElementById('out-seed-chip'); | |
| const historyStrip = document.getElementById('history-strip'); | |
| const historyCount = document.getElementById('history-count'); | |
| const seedSlider = document.getElementById('custom-seed'); | |
| const seedVal = document.getElementById('custom-seed-val'); | |
| const lightbox = document.getElementById('lightbox'); | |
| const lightboxImg = document.getElementById('lightbox-img'); | |
| const dropOverlay = document.getElementById('drop-overlay'); | |
| const compareWrap = document.getElementById('compare-wrap'); | |
| const compareTop = document.getElementById('compare-top'); | |
| const compareDivider = document.getElementById('compare-divider'); | |
| const compareBeforeImg = document.getElementById('compare-before-img'); | |
| const compareAfterImg = document.getElementById('compare-after-img'); | |
| const canvasMeta = document.getElementById('canvas-meta'); | |
| const segResult = document.getElementById('seg-result'); | |
| const segInput = document.getElementById('seg-input'); | |
| const connDot = document.getElementById('conn-dot'); | |
| const connText = document.getElementById('conn-text'); | |
| /* ── State (multi-image) ── */ | |
| let images = []; | |
| let selectedIdx = -1; | |
| let toastTimer = null; | |
| let running = false; | |
| let currentJob = null; | |
| let currentResult = null; // {b64, seed, prompt, beforeB64} | |
| let history = []; | |
| let historyIdx = -1; | |
| let compareMode = false; | |
| let viewMode = 'result'; // 'result' | 'input' | |
| const SUGGESTIONS = [ | |
| ['Storm', 'Change the weather to stormy.'], | |
| ['Relight', 'Relight the image to remove all existing lighting conditions and replace them with neutral, uniform illumination. Apply soft, evenly distributed lighting with no directional shadows, no harsh highlights, and no dramatic contrast. Maintain the original identity of all subjects exactly.'], | |
| ['Face Swap', 'head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. Remove the head from Picture 1 completely and replace it with the head from Picture 2. FROM PICTURE 2 (strictly preserve identity): Facial structure, all facial features, hair, skin. The replaced head must seamlessly match Picture 1\'s lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k.'], | |
| ['Polaroid', 'cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed by prithivMLmods preserving realistic texture and details'], | |
| ['Ghost', '3Dghostmannequin'], | |
| ]; | |
| /* ── Toast ── */ | |
| function showToast(message, type) { | |
| let toast = document.getElementById('app-toast'); | |
| if (!toast) { | |
| toast = document.createElement('div'); | |
| toast.id = 'app-toast'; | |
| toast.className = 'toast-notification'; | |
| toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>'; | |
| document.body.appendChild(toast); | |
| } | |
| const icon = toast.querySelector('.toast-icon'); | |
| const text = toast.querySelector('.toast-text'); | |
| toast.className = 'toast-notification ' + (type || 'error'); | |
| icon.textContent = type === 'warning' ? '⚠' : type === 'info' ? 'ℹ' : '✗'; | |
| text.textContent = message; | |
| if (toastTimer) clearTimeout(toastTimer); | |
| void toast.offsetWidth; | |
| toast.classList.add('visible'); | |
| toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500); | |
| } | |
| function flashPromptError() { | |
| promptInput.classList.add('error-flash'); | |
| promptInput.focus(); | |
| setTimeout(() => promptInput.classList.remove('error-flash'), 800); | |
| } | |
| function setStatus(text, cls) { | |
| sbStatus.textContent = text; | |
| sbStatus.className = 'sb-pill' + (cls ? ' ' + cls : ''); | |
| } | |
| /* ── View mode ── */ | |
| function setViewMode(mode) { | |
| viewMode = mode; | |
| segResult.classList.toggle('active', mode === 'result'); | |
| segInput.classList.toggle('active', mode === 'input'); | |
| renderCanvas(); | |
| } | |
| segResult.addEventListener('click', () => setViewMode('result')); | |
| segInput.addEventListener('click', () => setViewMode('input')); | |
| function getCanvasImg() { return outBody.querySelector('img.modern-out-img'); } | |
| function renderCanvas() { | |
| exitCompareMode(); | |
| let img = getCanvasImg(); | |
| const showResult = viewMode === 'result' && currentResult; | |
| const showInput = viewMode === 'input' && images.length > 0; | |
| if (showResult || showInput) { | |
| outPh.style.display = 'none'; | |
| if (!img) { | |
| img = document.createElement('img'); | |
| img.className = 'modern-out-img'; | |
| img.addEventListener('click', () => { | |
| if (img.src) { lightboxImg.src = img.src; lightbox.classList.add('visible'); } | |
| }); | |
| outBody.appendChild(img); | |
| } | |
| img.style.display = ''; | |
| if (showResult) { | |
| img.src = currentResult.b64; | |
| canvasMeta.textContent = (currentResult.lora || 'result') + ' · seed ' + currentResult.seed; | |
| } else { | |
| const i = selectedIdx >= 0 ? selectedIdx : 0; | |
| img.src = images[i].b64; | |
| canvasMeta.textContent = 'input #' + (i + 1) + ' · ' + (images[i].name || 'image'); | |
| } | |
| } else { | |
| if (img) img.style.display = 'none'; | |
| outPh.style.display = ''; | |
| canvasMeta.textContent = images.length > 0 | |
| ? images.length + ' input' + (images.length > 1 ? 's' : '') + ' ready — run an edit' | |
| : 'No image loaded'; | |
| } | |
| updateRailState(); | |
| } | |
| function updateRailState() { | |
| const hasResult = !!currentResult; | |
| dlBtn.disabled = !hasResult; | |
| zoomBtn.disabled = !hasResult && images.length === 0; | |
| useAsInputBtn.disabled = !hasResult; | |
| compareBtn.disabled = !(hasResult && currentResult.beforeB64); | |
| btnRemove.disabled = selectedIdx < 0; | |
| btnClear.disabled = images.length === 0; | |
| } | |
| /* ── Inputs filmstrip ── */ | |
| function updateCounts() { | |
| const n = images.length; | |
| imgCountTb.textContent = n; | |
| imgCountSb.textContent = n > 0 ? n + ' input' + (n > 1 ? 's' : '') + ' · ' + history.length + ' result' + (history.length === 1 ? '' : 's') : 'No inputs'; | |
| } | |
| function addImage(b64, name) { | |
| images.push({id: Date.now() + Math.random(), b64, name}); | |
| if (selectedIdx < 0) selectedIdx = 0; | |
| renderFilmstrip(); updateCounts(); | |
| if (!currentResult || viewMode === 'input') renderCanvas(); | |
| } | |
| function removeImage(idx) { | |
| images.splice(idx, 1); | |
| if (selectedIdx === idx) selectedIdx = images.length ? 0 : -1; | |
| else if (selectedIdx > idx) selectedIdx--; | |
| renderFilmstrip(); updateCounts(); renderCanvas(); | |
| } | |
| function clearAll() { | |
| images = []; selectedIdx = -1; | |
| renderFilmstrip(); updateCounts(); renderCanvas(); | |
| } | |
| function renderFilmstrip() { | |
| galleryGrid.innerHTML = ''; | |
| if (images.length === 0) { | |
| const empty = document.createElement('div'); | |
| empty.className = 'fs-empty'; | |
| empty.textContent = 'Drop or paste images…'; | |
| galleryGrid.appendChild(empty); | |
| } | |
| images.forEach((img, i) => { | |
| const t = document.createElement('div'); | |
| t.className = 'fs-thumb' + (i === selectedIdx ? ' selected' : ''); | |
| t.innerHTML = '<img src="' + img.b64 + '" alt="">' + | |
| '<span class="fs-badge">#' + (i + 1) + '</span>' + | |
| '<button class="fs-remove">✕</button>'; | |
| t.addEventListener('click', (e) => { | |
| if (e.target.closest('.fs-remove')) return; | |
| selectedIdx = i; | |
| renderFilmstrip(); | |
| setViewMode('input'); | |
| }); | |
| t.querySelector('.fs-remove').addEventListener('click', (e) => { | |
| e.stopPropagation(); removeImage(i); | |
| }); | |
| galleryGrid.appendChild(t); | |
| }); | |
| const add = document.createElement('div'); | |
| add.className = 'fs-add'; | |
| add.innerHTML = '<span class="add-icon">+</span><span class="add-text">Add</span>'; | |
| add.addEventListener('click', () => fileInput.click()); | |
| galleryGrid.appendChild(add); | |
| updateRailState(); | |
| } | |
| function processFiles(files) { | |
| let added = 0; | |
| Array.from(files).forEach(file => { | |
| if (!file.type.startsWith('image/')) return; | |
| added++; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => addImage(e.target.result, file.name); | |
| reader.readAsDataURL(file); | |
| }); | |
| return added; | |
| } | |
| fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; }); | |
| btnUpload.addEventListener('click', () => fileInput.click()); | |
| btnRemove.addEventListener('click', () => { if (selectedIdx >= 0) removeImage(selectedIdx); }); | |
| btnClear.addEventListener('click', clearAll); | |
| /* ── Drop anywhere ── */ | |
| let dragDepth = 0; | |
| window.addEventListener('dragenter', (e) => { | |
| if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return; | |
| e.preventDefault(); | |
| dragDepth++; | |
| dropOverlay.classList.add('visible'); | |
| }); | |
| window.addEventListener('dragleave', (e) => { | |
| if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return; | |
| dragDepth = Math.max(0, dragDepth - 1); | |
| if (dragDepth === 0) dropOverlay.classList.remove('visible'); | |
| }); | |
| window.addEventListener('dragover', (e) => { e.preventDefault(); }); | |
| window.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dragDepth = 0; | |
| dropOverlay.classList.remove('visible'); | |
| if (e.dataTransfer.files.length) { | |
| const n = processFiles(e.dataTransfer.files); | |
| if (n > 0) showToast('Added ' + n + ' image' + (n > 1 ? 's' : ''), 'info'); | |
| } | |
| }); | |
| /* ── Paste ── */ | |
| document.addEventListener('paste', (e) => { | |
| if (e.target === promptInput) return; | |
| const items = e.clipboardData && e.clipboardData.items; | |
| if (!items) return; | |
| const files = []; | |
| for (const item of items) { | |
| if (item.kind === 'file' && item.type.startsWith('image/')) { | |
| const f = item.getAsFile(); | |
| if (f) files.push(f); | |
| } | |
| } | |
| if (files.length) { | |
| e.preventDefault(); | |
| processFiles(files); | |
| showToast('Pasted ' + files.length + ' image' + (files.length > 1 ? 's' : ''), 'info'); | |
| } | |
| }); | |
| /* ── Inspector tabs ── */ | |
| const tabEdit = document.getElementById('tab-edit'); | |
| const tabExamples = document.getElementById('tab-examples'); | |
| const pageEdit = document.getElementById('page-edit'); | |
| const pageExamples = document.getElementById('page-examples'); | |
| tabEdit.addEventListener('click', () => { | |
| tabEdit.classList.add('active'); tabExamples.classList.remove('active'); | |
| pageEdit.classList.add('active'); pageExamples.classList.remove('active'); | |
| }); | |
| tabExamples.addEventListener('click', () => { | |
| tabExamples.classList.add('active'); tabEdit.classList.remove('active'); | |
| pageExamples.classList.add('active'); pageEdit.classList.remove('active'); | |
| }); | |
| /* ── Suggestion chips ── */ | |
| const suggWrap = document.getElementById('suggestions-wrap'); | |
| SUGGESTIONS.forEach(([label, prompt]) => { | |
| const chip = document.createElement('button'); | |
| chip.className = 'suggestion-chip'; | |
| chip.textContent = label; | |
| chip.addEventListener('click', () => { | |
| promptInput.value = prompt; | |
| updateCharCount(); | |
| suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active')); | |
| chip.classList.add('active'); | |
| promptInput.focus(); | |
| }); | |
| suggWrap.appendChild(chip); | |
| }); | |
| function updateCharCount() { charCount.textContent = promptInput.value.length; } | |
| promptInput.addEventListener('input', () => { | |
| updateCharCount(); | |
| suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active')); | |
| }); | |
| updateCharCount(); | |
| /* ── Sliders ── */ | |
| function bindSlider(id) { | |
| const slider = document.getElementById(id); | |
| const valSpan = document.getElementById(id + '-val'); | |
| slider.addEventListener('input', () => { valSpan.textContent = slider.value; }); | |
| } | |
| bindSlider('custom-seed'); | |
| bindSlider('custom-guidance'); | |
| bindSlider('custom-steps'); | |
| document.getElementById('dice-btn').addEventListener('click', () => { | |
| const s = Math.floor(Math.random() * 2147483647); | |
| seedSlider.value = s; seedVal.textContent = s; | |
| }); | |
| /* ── Loader ── */ | |
| function showLoader(text) { | |
| loaderText.textContent = text || 'Processing image…'; | |
| loaderFill.classList.remove('determinate'); | |
| loaderFill.style.width = '100%'; | |
| loader.classList.add('active'); | |
| setStatus('Processing…'); | |
| } | |
| function setLoaderProgress(text, frac) { | |
| loaderText.textContent = text; | |
| if (frac != null && frac >= 0 && frac <= 1) { | |
| loaderFill.classList.add('determinate'); | |
| loaderFill.style.width = Math.max(4, Math.round(frac * 100)) + '%'; | |
| } | |
| } | |
| function hideLoader(statusText, cls) { | |
| loader.classList.remove('active'); | |
| setStatus(statusText || 'Done', cls || 'done'); | |
| } | |
| /* ── Result / history ── */ | |
| function showResult(entry) { | |
| currentResult = entry; | |
| setViewMode('result'); | |
| dlBtn.disabled = false; | |
| useAsInputBtn.disabled = false; | |
| zoomBtn.disabled = false; | |
| if (entry.beforeB64) compareBtn.disabled = false; | |
| if (entry.seed != null) { | |
| seedChip.textContent = 'seed ' + entry.seed; | |
| seedChip.classList.add('visible'); | |
| } | |
| } | |
| function renderHistory() { | |
| historyStrip.innerHTML = ''; | |
| historyCount.textContent = history.length; | |
| if (history.length === 0) { | |
| const empty = document.createElement('div'); | |
| empty.className = 'fs-empty'; | |
| empty.textContent = 'No results yet'; | |
| historyStrip.appendChild(empty); | |
| return; | |
| } | |
| history.forEach((entry, i) => { | |
| const t = document.createElement('div'); | |
| t.className = 'fs-thumb' + (i === historyIdx ? ' selected' : ''); | |
| t.title = (entry.lora || '') + ' · seed ' + entry.seed; | |
| const im = document.createElement('img'); | |
| im.src = entry.b64; | |
| t.appendChild(im); | |
| t.addEventListener('click', () => { historyIdx = i; showResult(history[i]); renderHistory(); }); | |
| historyStrip.appendChild(t); | |
| }); | |
| } | |
| function pushHistory(entry) { | |
| history.push(entry); | |
| historyIdx = history.length - 1; | |
| renderHistory(); | |
| updateCounts(); | |
| } | |
| /* ── Compare slider ── */ | |
| function enterCompareMode() { | |
| if (!currentResult || !currentResult.beforeB64) return; | |
| compareMode = true; | |
| compareBtn.classList.add('active'); | |
| const img = getCanvasImg(); | |
| if (img) img.style.display = 'none'; | |
| compareAfterImg.src = currentResult.b64; | |
| compareBeforeImg.src = currentResult.beforeB64; | |
| compareWrap.classList.add('visible'); | |
| requestAnimationFrame(() => setComparePos(0.5)); | |
| } | |
| function exitCompareMode() { | |
| if (!compareMode) return; | |
| compareMode = false; | |
| compareBtn.classList.remove('active'); | |
| compareWrap.classList.remove('visible'); | |
| const img = getCanvasImg(); | |
| if (img && (viewMode === 'result' && currentResult || viewMode === 'input' && images.length)) img.style.display = ''; | |
| } | |
| function setComparePos(frac) { | |
| frac = Math.max(0, Math.min(1, frac)); | |
| const w = compareWrap.clientWidth; | |
| const x = frac * w; | |
| compareTop.style.clipPath = 'inset(0 ' + (w - x) + 'px 0 0)'; | |
| compareDivider.style.left = x + 'px'; | |
| } | |
| compareBtn.addEventListener('click', () => { compareMode ? exitCompareMode() : enterCompareMode(); }); | |
| let compareDragging = false; | |
| function compareMove(clientX) { | |
| const rect = compareWrap.getBoundingClientRect(); | |
| setComparePos((clientX - rect.left) / rect.width); | |
| } | |
| compareWrap.addEventListener('pointerdown', (e) => { | |
| compareDragging = true; | |
| compareWrap.setPointerCapture(e.pointerId); | |
| compareMove(e.clientX); | |
| }); | |
| compareWrap.addEventListener('pointermove', (e) => { if (compareDragging) compareMove(e.clientX); }); | |
| compareWrap.addEventListener('pointerup', () => { compareDragging = false; }); | |
| compareWrap.addEventListener('pointercancel', () => { compareDragging = false; }); | |
| /* ── Lightbox ── */ | |
| lightbox.addEventListener('click', () => lightbox.classList.remove('visible')); | |
| document.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.classList.remove('visible'); }); | |
| zoomBtn.addEventListener('click', () => { | |
| const img = getCanvasImg(); | |
| const src = (viewMode === 'result' && currentResult) ? currentResult.b64 | |
| : (images.length ? images[selectedIdx >= 0 ? selectedIdx : 0].b64 : null); | |
| if (src) { lightboxImg.src = src; lightbox.classList.add('visible'); } | |
| }); | |
| /* ── Download / chain / seed ── */ | |
| dlBtn.addEventListener('click', () => { | |
| if (currentResult && currentResult.b64) { | |
| const a = document.createElement('a'); | |
| a.href = currentResult.b64; | |
| a.download = 'flux_klein_' + (currentResult.seed != null ? currentResult.seed : 'output') + '.png'; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); | |
| } | |
| }); | |
| useAsInputBtn.addEventListener('click', () => { | |
| if (!currentResult) return; | |
| addImage(currentResult.b64, 'edit_seed_' + (currentResult.seed != null ? currentResult.seed : 'x') + '.png'); | |
| showToast('Result added as input image', 'info'); | |
| }); | |
| seedChip.addEventListener('click', () => { | |
| if (currentResult && currentResult.seed != null) { | |
| navigator.clipboard.writeText(String(currentResult.seed)).then( | |
| () => showToast('Seed copied: ' + currentResult.seed, 'info'), () => {}); | |
| } | |
| }); | |
| /* ── Backend connection ── */ | |
| let client = null; | |
| try { | |
| client = await Client.connect(window.location.origin); | |
| setStatus('Ready'); | |
| connDot.classList.remove('off'); | |
| connText.textContent = 'connected'; | |
| } catch (e) { | |
| console.error('Failed to connect to Gradio backend:', e); | |
| setStatus('Offline', 'error'); | |
| connDot.classList.add('off'); | |
| connText.textContent = 'offline'; | |
| showToast('Could not connect to the backend API', 'error'); | |
| } | |
| /* ── Config: LoRAs + examples ── */ | |
| function renderConfig(cfg) { | |
| loraSelect.innerHTML = ''; | |
| cfg.loras.forEach(name => { | |
| const opt = document.createElement('option'); | |
| opt.value = name; opt.textContent = name; | |
| loraSelect.appendChild(opt); | |
| }); | |
| if (cfg.default_lora) loraSelect.value = cfg.default_lora; | |
| const list = document.getElementById('examples-scroll'); | |
| list.innerHTML = ''; | |
| cfg.examples.forEach(ex => { | |
| const card = document.createElement('div'); | |
| card.className = 'example-card'; | |
| const thumbs = ex.thumbs.map(t => | |
| t ? '<img src="' + t + '" alt="">' : '<div class="example-thumb-placeholder">Preview</div>' | |
| ).join(''); | |
| const promptShort = ex.prompt.length > 90 ? ex.prompt.slice(0, 90) + '…' : ex.prompt; | |
| card.innerHTML = | |
| '<div class="example-thumb-wrap">' + thumbs + '</div>' | |
| + '<div class="example-body">' | |
| + '<div class="example-meta">' | |
| + '<span class="example-lora-badge"></span>' | |
| + '</div>' | |
| + '<div class="example-prompt-text"></div>' | |
| + '</div>'; | |
| card.querySelector('.example-lora-badge').textContent = ex.lora; | |
| card.querySelector('.example-prompt-text').textContent = promptShort; | |
| card.addEventListener('click', () => loadExample(card, ex.idx)); | |
| list.appendChild(card); | |
| }); | |
| } | |
| async function loadExample(card, idx) { | |
| if (!client) { showToast('Backend not connected', 'error'); return; } | |
| document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading')); | |
| card.classList.add('loading'); | |
| showToast('Loading example…', 'info'); | |
| try { | |
| const result = await client.predict('/load_example', { idx: idx }); | |
| const data = result.data[0]; | |
| if (data && data.status === 'ok' && data.images && data.images.length > 0) { | |
| clearAll(); | |
| if (data.prompt) { promptInput.value = data.prompt; updateCharCount(); } | |
| if (data.lora) loraSelect.value = data.lora; | |
| data.images.forEach((b64, i) => { | |
| const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i + 1) + '.jpg'); | |
| addImage(b64, name); | |
| }); | |
| tabEdit.click(); | |
| showToast('Example loaded — ' + data.images.length + ' image(s)', 'info'); | |
| } else { | |
| showToast('Could not load example images', 'error'); | |
| } | |
| } catch (e) { | |
| console.error('Example load error:', e); | |
| showToast('Could not load example images', 'error'); | |
| } finally { | |
| card.classList.remove('loading'); | |
| } | |
| } | |
| try { | |
| const cfg = await fetch('api/config').then(r => r.json()); | |
| renderConfig(cfg); | |
| } catch (e) { | |
| console.error('Failed to load config:', e); | |
| } | |
| /* ── Run ── */ | |
| function validateBeforeRun() { | |
| const promptVal = promptInput.value.trim(); | |
| const hasImages = images.length > 0; | |
| if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; } | |
| if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; } | |
| if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; } | |
| return true; | |
| } | |
| function setRunningUI(on) { | |
| running = on; | |
| runBtn.disabled = on; | |
| runBtnLabel.textContent = on ? 'Editing…' : 'Edit Image'; | |
| cancelBtn.style.display = on ? 'flex' : 'none'; | |
| } | |
| async function runEdit() { | |
| if (running) return; | |
| if (!client) { showToast('Backend not connected', 'error'); return; } | |
| if (!validateBeforeRun()) return; | |
| setRunningUI(true); | |
| setViewMode('result'); | |
| showLoader('Submitting to queue…'); | |
| const beforeB64 = images[0] ? images[0].b64 : null; | |
| const usedPrompt = promptInput.value; | |
| const usedLora = loraSelect.value; | |
| const payload = { | |
| images_b64_json: JSON.stringify(images.map(img => img.b64)), | |
| prompt: usedPrompt, | |
| lora_adapter: usedLora, | |
| seed: parseInt(seedSlider.value) || 0, | |
| randomize_seed: document.getElementById('custom-randomize').checked, | |
| guidance_scale: parseFloat(document.getElementById('custom-guidance').value), | |
| steps: parseInt(document.getElementById('custom-steps').value), | |
| }; | |
| try { | |
| currentJob = client.submit('/edit_image', payload); | |
| for await (const msg of currentJob) { | |
| if (msg.type === 'status') { | |
| if (msg.status === 'pending') { | |
| const pos = (msg.position != null && msg.position >= 0) | |
| ? 'In queue — position ' + (msg.position + 1) + (msg.queue_size ? ' of ' + msg.queue_size : '') | |
| : 'Waiting in queue…'; | |
| setLoaderProgress(pos, null); | |
| setStatus('Queued'); | |
| } else if (msg.status === 'generating') { | |
| setStatus('Processing…'); | |
| const pd = msg.progress_data; | |
| if (pd && pd.length > 0) { | |
| const p = pd[pd.length - 1]; | |
| if (p.index != null && p.length) { | |
| const pct = Math.round((p.index / p.length) * 100); | |
| setLoaderProgress('Generating… ' + p.index + '/' + p.length + ' (' + pct + '%)', p.index / p.length); | |
| } else { | |
| setLoaderProgress('Generating…', null); | |
| } | |
| } else { | |
| setLoaderProgress('Generating image…', null); | |
| } | |
| } else if (msg.status === 'error') { | |
| throw new Error(msg.message || 'Generation failed'); | |
| } | |
| } else if (msg.type === 'data') { | |
| const out = msg.data && msg.data[0]; | |
| if (out && out.image) { | |
| const entry = { b64: out.image, seed: out.seed, prompt: usedPrompt, lora: usedLora, beforeB64 }; | |
| showResult(entry); | |
| pushHistory(entry); | |
| if (out.seed != null) { | |
| seedSlider.value = out.seed; | |
| seedVal.textContent = out.seed; | |
| } | |
| hideLoader('Done', 'done'); | |
| showToast('Image edited successfully', 'info'); | |
| } else { | |
| hideLoader('Done', 'done'); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Edit error:', e); | |
| hideLoader('Error', 'error'); | |
| const msg = (e && e.message) ? e.message : 'Generation failed'; | |
| showToast(msg.length > 120 ? msg.slice(0, 120) + '…' : msg, 'error'); | |
| } finally { | |
| setRunningUI(false); | |
| currentJob = null; | |
| } | |
| } | |
| runBtn.addEventListener('click', runEdit); | |
| cancelBtn.addEventListener('click', async () => { | |
| if (currentJob) { | |
| try { await currentJob.cancel(); } catch (e) { console.warn('Cancel error:', e); } | |
| currentJob = null; | |
| } | |
| setRunningUI(false); | |
| hideLoader('Cancelled'); | |
| showToast('Generation cancelled', 'warning'); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { | |
| e.preventDefault(); | |
| runEdit(); | |
| } | |
| }); | |
| renderFilmstrip(); | |
| renderHistory(); | |
| updateCounts(); | |
| renderCanvas(); | |
| </script> | |
| </body> | |
| </html> |