img-v2 / index.html
patrickligardes
initial commit
30ec3a0
Raw
History Blame Contribute Delete
60.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Editor</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:#0a0a0c;--panel:#131316;--panel2:#0e0e11;--canvas:#08080a;
--border:#2a2323;--border2:#3f2f2f;
--text:#e4e4e7;--muted:#a1a1aa;--dim:#6b6b75;--faint:#4a4a52;
--accent:#FF3333;--accent2:#FF5555;--accent3:#FF8080;
--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:linear-gradient(135deg,#FF0000,#FF3333,#FF8080);
border-radius:8px;display:flex;align-items:center;justify-content:center;
box-shadow:0 2px 8px rgba(255,0,0,.35);flex-shrink:0;
}
.menubar-logo svg{width:15px;height:15px;fill:#fff}
.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,0,0,.14);color:var(--accent2);border:1px solid rgba(255,0,0,.25);
letter-spacing:.4px;white-space:nowrap;
}
.menubar-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border-color:rgba(34,197,94,.25)}
.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:#4ade80;box-shadow:0 0 6px rgba(74,222,128,.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:var(--accent);color:#fff;
border:1px solid rgba(255,255,255,.15);
transition:all .15s ease;flex-shrink:0;
}
.gh-btn:hover{background:var(--accent2)}
.gh-btn svg{fill:#fff;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,0,0,.12);color:var(--accent3);border-color:rgba(255,0,0,.25)}
.rail-btn.active{background:rgba(255,0,0,.2);color:var(--accent2);border-color:rgba(255,0,0,.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,0,0,.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,0,0,.35)}
.canvas-stage{
flex:1;position:relative;overflow:hidden;min-height:0;
display:flex;align-items:center;justify-content:center;
background:
repeating-conic-gradient(#0c0c0f 0% 25%, #08080a 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,.7),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,.7),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(8,8,10,.9);
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,#FF0000,#FF3333,#FF0000);
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,0,0,.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,0,0,.03);gap:3px;
}
.fs-add:hover{border-color:var(--accent);background:rgba(255,0,0,.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,0,0,.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,0,0,.07);border:1px solid rgba(255,0,0,.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,0,0,.15);border-color:rgba(255,0,0,.35);color:var(--accent2)}
.suggestion-chip.active{background:rgba(255,0,0,.25);border-color:var(--accent);color:#fff}
.btn-run{
display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
background:linear-gradient(135deg,#FF0000,#CC0000);border:none;border-radius:9px;
padding:12px 24px;cursor:pointer;font-size:14px;font-weight:700;font-family:'Inter',sans-serif;
color:#fff;transition:all .2s ease;letter-spacing:-.2px;
box-shadow:0 4px 16px rgba(255,0,0,.3),inset 0 1px 0 rgba(255,255,255,.1);
}
.btn-run:hover:not(:disabled){
background:linear-gradient(135deg,#FF3333,#FF0000);transform:translateY(-1px);
box-shadow:0 6px 24px rgba(255,0,0,.45),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:#fff}
.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:linear-gradient(135deg,#FF0000,#CC0000);
border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,0,0,.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:linear-gradient(135deg,#FF0000,#CC0000);
border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,0,0,.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(--accent3)}
.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,0,0,.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-badge{
display:inline-flex;padding:2px 7px;background:rgba(255,0,0,.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,0,0,.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,0,0,.1);color:var(--accent2);
}
.sb-pill.error{background:rgba(239,68,68,.12);color:#f87171}
.sb-pill.done{background:rgba(34,197,94,.12);color:#4ade80}
.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,#FF0000,#CC0000);color:#fff;border:1px solid rgba(255,255,255,.15)}
.toast-notification .toast-icon{font-size:15px;line-height:1}
/* ══ Lightbox ══ */
.lightbox{
position:fixed;inset:0;background:rgba(0,0,0,.92);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(8,8,10,.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,0,0,.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="white" 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">Image Editor</div>
<span class="menubar-badge fast">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>
</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&hellip;</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="#3f2f2f" stroke-width="2" stroke-dasharray="4 3"/>
<polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,0,0,0.1)" stroke="#3f2f2f" stroke-width="1.5"/>
<circle cx="28" cy="30" r="6" fill="rgba(255,0,0,0.12)" stroke="#3f2f2f" 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., transform into anime, upscale, change lighting…"></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">
<button id="custom-run-btn" class="btn-run">
<svg viewBox="0 0 24 24" fill="white" 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">
<span class="sb-link">Image Editor</span>
<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 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 = [
['Cartoon', 'Transform the image into a dotted cartoon style.'],
['B&W', 'Convert it to black and white.'],
['Cinematic', 'Add cinematic lighting with warm orange tones and film grain.'],
['Anime', 'Transform into anime style illustration.'],
['Oil Paint', 'Apply oil painting effect with visible brush strokes.'],
['Enhance', 'Enhance and upscale with more detail and clarity.'],
['Watercolor', 'Make it look like a watercolor painting with soft edges.'],
['Sunset', 'Add dramatic sunset sky and warm lighting.'],
['Pencil', 'Convert to detailed pencil sketch with cross-hatching and shading.'],
['Pop Art', 'Apply pop art style with bold colors and halftone patterns.'],
['Retro', 'Apply a vintage retro film look with faded colors and light leaks.'],
['Neon', 'Add neon glow effects with vibrant colors against a dark background.'],
['Pixel', 'Convert to pixel art style with a retro 16-bit aesthetic.'],
['Minimalist', 'Simplify into a clean minimalist illustration with flat colors.'],
['Low Poly', 'Convert to low poly 3D geometric art style.'],
['Comic', 'Transform into comic book style with bold outlines and cel shading.'],
];
/* ── 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 = '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 = '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 = 'edit_' + (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: examples ── */
function renderConfig(cfg) {
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-badge">' + ex.n_images + ' img' + (ex.n_images > 1 ? 's' : '') + '</span>'
+ '</div>'
+ '<div class="example-prompt-text"></div>'
+ '</div>';
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(); }
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 payload = {
images_b64_json: JSON.stringify(images.map(img => img.b64)),
prompt: usedPrompt,
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, 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 if (out && out.status === 'blocked') {
// NCII safety guard triggered
hideLoader('Blocked', 'error');
showToast(out.message || 'Request blocked by safety guard', 'warning');
} 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>