akhaliq's picture
akhaliq HF Staff
Photoshop-style studio page (Edit/Generate/Workflow tabs)
1be787b verified
Raw History Blame Contribute Delete
37.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>MOSS Image Edit Studio</title>
<style>
:root {
--bg: #0e0e10; --panel: #16171a; --panel2: #101114; --raised: #1e2024;
--border: rgba(255,255,255,0.07); --highlight: rgba(255,255,255,0.05);
--text: rgba(255,255,255,0.87); --muted: rgba(255,255,255,0.5); --dim: rgba(255,255,255,0.35);
--accent: #4f9d63; --accent2: #6cc184; --accent-soft: rgba(79,157,99,0.14);
--radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
[hidden] { display: none !important; }
body {
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
background: var(--bg); color: var(--text); height: 100vh;
display: flex; flex-direction: column; overflow: hidden;
font-size: 13px; -webkit-font-smoothing: antialiased;
}
/* ---------- top bar ---------- */
.topbar {
display: flex; align-items: center; gap: 16px; padding: 8px 16px;
background: var(--panel); border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.brand { font-weight: 700; font-size: 0.92rem; white-space: nowrap; letter-spacing: -0.01em; }
.mode-tabs { display: flex; gap: 2px; background: var(--panel2); border: 1px solid var(--border); border-radius: 9px; padding: 3px; }
.mode-tabs button {
border: none; border-radius: 7px; padding: 6px 16px; font-weight: 550; font-size: 0.82rem;
background: transparent; color: var(--muted); cursor: pointer; transition: color 0.15s, background 0.15s;
}
.mode-tabs button:hover { color: var(--text); }
.mode-tabs button.active { background: var(--raised); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.45); }
.spacer { flex: 1; }
.ml-badge { color: var(--muted); text-decoration: none; font-size: 0.8rem; font-weight: 500; transition: color 0.15s; white-space: nowrap; }
.ml-badge:hover { color: var(--text); }
.ml-cta {
color: var(--accent2); text-decoration: none; font-size: 0.8rem; font-weight: 600;
padding: 5px 13px; border: 1px solid var(--accent); border-radius: 999px;
background: var(--accent-soft); transition: filter 0.15s; white-space: nowrap;
}
.ml-cta:hover { filter: brightness(1.2); }
/* ---------- main ---------- */
.main { display: flex; flex: 1; min-height: 0; }
.rail {
width: 52px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center;
gap: 6px; padding: 10px 0; background: var(--panel); border-right: 1px solid var(--border);
}
.rail button {
width: 38px; height: 38px; border: 1px solid transparent; border-radius: 8px;
background: transparent; color: var(--muted); cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: color 0.15s, background 0.15s;
}
.rail button:hover { background: var(--panel2); color: var(--text); }
.rail button.active { color: var(--accent2); background: var(--accent-soft); }
.viewport { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: auto; position: relative; }
.doc-grid { flex: 1; min-height: 0; display: flex; gap: 16px; width: 100%; align-items: stretch; justify-content: center; padding: 20px 20px 10px; }
.doc {
flex: 1; min-width: 0; max-width: 640px; display: flex; flex-direction: column;
background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
}
.doc figcaption {
padding: 8px 12px; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.16em;
color: var(--muted); border-bottom: 1px solid var(--border); text-transform: uppercase;
display: flex; align-items: center;
}
.doc .stage { position: relative; flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.doc img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.drop-hint { color: var(--dim); font-size: 0.85rem; text-align: center; padding: 20px; }
.drop-hint b { color: var(--muted); display: block; margin-bottom: 6px; }
.workflow-view { position: absolute; inset: 0; padding: 12px; }
.workflow-view iframe { width: 100%; height: 100%; border: none; border-radius: 10px; background: #0c0d10; }
/* Photoshop checkerboard document stage */
.doc .stage {
background-color: #17171b;
background-image:
linear-gradient(45deg, #1f1f25 25%, transparent 25%),
linear-gradient(-45deg, #1f1f25 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #1f1f25 75%),
linear-gradient(-45deg, transparent 75%, #1f1f25 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
}
.doc img.zoomed { max-width: none; max-height: none; }
.doc .stage.scrollable { overflow: auto; justify-content: flex-start; align-items: flex-start; }
.zoom-btn {
margin-left: auto; border: 1px solid var(--border); background: transparent; color: var(--muted);
border-radius: 5px; font-size: 0.62rem; padding: 2px 8px; cursor: pointer; letter-spacing: 0.08em;
}
.zoom-btn:hover { color: var(--text); border-color: var(--accent2); }
.panel .panel-title { cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: space-between; }
.panel .panel-title::after { content: "▾"; font-size: 0.7rem; color: var(--dim); }
.panel.collapsed .panel-title::after { content: "▸"; }
.panel.collapsed .panel-body { display: none; }
/* ---------- panels ---------- */
.panels {
width: 320px; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px;
padding: 12px; overflow-y: auto; background: var(--panel); border-left: 1px solid var(--border);
}
.panel { background: var(--panel2); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; }
.panel-title { font-size: 0.62rem; font-weight: 650; letter-spacing: 0.12em; color: var(--dim); text-transform: uppercase; margin-bottom: 10px; }
.panel label { display: block; font-size: 0.72rem; font-weight: 550; color: var(--muted); margin: 10px 0 4px; }
.panel label:first-of-type { margin-top: 0; }
textarea, input[type="text"], input[type="number"] {
width: 100%; background: var(--raised); color: var(--text); border: 1px solid var(--border);
border-radius: 7px; padding: 8px 10px; font-family: inherit; font-size: 0.83rem; resize: vertical;
transition: border-color 0.15s, box-shadow 0.15s;
}
textarea:focus, input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.row { display: flex; gap: 8px; align-items: center; }
.row > * { flex: 1; }
input[type="range"] {
-webkit-appearance: none; appearance: none; width: 100%; height: 3px;
border-radius: 2px; background: var(--raised); outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%;
background: #fff; border: none; box-shadow: 0 1px 4px rgba(0,0,0,0.55); cursor: pointer;
transition: transform 0.12s;
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
input[type="range"]::-moz-range-thumb {
width: 14px; height: 14px; border-radius: 50%; background: #fff; border: none;
box-shadow: 0 1px 4px rgba(0,0,0,0.55); cursor: pointer;
}
.switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.switch input { display: none; }
.switch .sw {
display: inline-block;
width: 34px; height: 20px; border-radius: 999px; background: var(--raised);
border: 1px solid var(--border); position: relative; flex-shrink: 0; transition: background 0.18s, border-color 0.18s;
}
.switch .sw::after {
content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
border-radius: 50%; background: var(--muted); transition: transform 0.18s, background 0.18s;
}
.switch input:checked + .sw { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .sw::after { transform: translateX(14px); background: #fff; }
.switch .sw-label { font-size: 0.75rem; color: var(--muted); }
.ref-slot {
height: 72px; border: 1px dashed var(--border); border-radius: 6px; margin-top: 8px;
display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;
color: var(--dim); font-size: 0.72rem; background: #17171c;
}
.ref-slot:hover { border-color: var(--accent2); }
.ref-slot img { width: 100%; height: 100%; object-fit: cover; }
.hint { font-size: 0.68rem; color: var(--dim); margin-top: 8px; line-height: 1.4; }
#run-btn {
margin-top: 4px; padding: 12px; border: none; border-radius: 8px; cursor: pointer;
background: var(--accent); color: white;
font-weight: 600; font-size: 0.9rem; letter-spacing: 0.01em;
transition: filter 0.15s, transform 0.05s;
}
#run-btn:hover { filter: brightness(1.12); }
#run-btn:active { transform: translateY(1px); }
#run-btn:disabled { opacity: 0.5; cursor: wait; }
/* ---------- status bar ---------- */
.statusbar {
display: flex; align-items: center; gap: 16px; padding: 7px 16px; flex-shrink: 0;
background: var(--panel); border-top: 1px solid var(--border);
font-size: 0.75rem; color: var(--muted);
}
.statusbar .pulse { width: 8px; height: 8px; border-radius: 50%; background: #4fd1a5; }
.statusbar.busy .pulse { background: var(--accent2); animation: pulse 1s infinite alternate; }
@keyframes pulse { from { opacity: 0.4; } to { opacity: 1; } }
#file-input { display: none; }
/* ---------- layers, split view, doc bar, history ---------- */
.layer-row {
display: flex; align-items: center; gap: 10px; padding: 6px 8px;
border: 1px solid var(--border); border-radius: 6px; margin-top: 6px;
background: #17171c; cursor: pointer;
}
.layer-row:hover { border-color: var(--accent2); }
.layer-row .thumb {
width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0;
background: #101014; border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
color: var(--dim); font-size: 0.65rem; text-align: center;
}
.layer-row .thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 3px; }
.layer-row .lname { flex: 1; font-size: 0.78rem; color: var(--text); }
.layer-row .lsub { font-size: 0.62rem; color: var(--dim); display: block; }
.layer-row .ldel {
border: none; background: transparent; color: var(--dim); cursor: pointer;
font-size: 0.9rem; padding: 2px 6px; border-radius: 4px;
}
.layer-row .ldel:hover { color: #ff6f91; background: rgba(255,111,145,0.1); }
.doc-bar {
display: flex; align-items: center; gap: 10px; padding: 6px 12px;
border-top: 1px solid var(--border); background: var(--panel);
font-size: 0.72rem; color: var(--muted); flex-shrink: 0;
}
.doc-bar .spacer { flex: 1; }
.doc-bar button {
border: 1px solid var(--border); background: transparent; color: var(--muted);
border-radius: 5px; padding: 3px 10px; font-size: 0.68rem; cursor: pointer; letter-spacing: 0.06em;
}
.doc-bar button:hover, .doc-bar button.active { color: var(--text); border-color: var(--accent2); }
.doc-bar button.active { background: rgba(108,193,132,0.12); color: var(--accent2); }
.split-wrap { position: absolute; inset: 0; overflow: hidden; }
.split-wrap img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.split-divider {
position: absolute; top: 0; bottom: 0; width: 3px; background: var(--accent2);
cursor: ew-resize; z-index: 5; box-shadow: 0 0 8px rgba(108,193,132,0.6);
}
.split-divider::after {
content: "◂ ▸"; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
background: var(--accent2); color: white; border-radius: 999px; padding: 4px 7px;
font-size: 0.6rem; white-space: nowrap;
}
.history-strip {
display: flex; gap: 8px; padding: 8px 12px; overflow-x: auto; flex-shrink: 0;
background: var(--panel); border-top: 1px solid var(--border); align-items: center;
}
.history-strip .hlabel { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.14em; color: var(--muted); text-transform: uppercase; white-space: nowrap; }
.history-chip {
width: 52px; height: 52px; border-radius: 6px; border: 1px solid var(--border);
overflow: hidden; cursor: pointer; flex-shrink: 0; position: relative;
}
.history-chip:hover { border-color: var(--accent2); }
.history-chip img { width: 100%; height: 100%; object-fit: cover; }
.history-chip .hseed {
position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.65);
color: #ddd; font-size: 0.52rem; padding: 1px 3px; text-align: center;
}
/* ---------- examples ---------- */
.ex-row { display: flex; gap: 8px; overflow-x: auto; }
.ex-chip {
flex: 1; min-width: 76px; border: 1px solid var(--border); border-radius: 8px;
overflow: hidden; cursor: pointer; background: var(--raised); padding: 0;
text-align: left; transition: border-color 0.15s;
}
.ex-chip:hover { border-color: var(--accent); }
.ex-chip img { width: 100%; height: 64px; object-fit: cover; display: block; }
.ex-chip span { display: block; padding: 5px 7px; font-size: 0.62rem; color: var(--muted); line-height: 1.25; }
.ex-prompt {
display: block; width: 100%; text-align: left; margin-top: 6px; padding: 8px 10px;
background: var(--raised); border: 1px solid var(--border); border-radius: 7px;
color: var(--text); font-size: 0.75rem; cursor: pointer; transition: border-color 0.15s;
}
.ex-prompt:hover { border-color: var(--accent); }
@media (max-width: 980px) {
.doc-grid { flex-direction: column; }
.panels { width: 260px; }
}
@media (max-width: 860px) {
html, body { height: auto !important; overflow: visible !important; }
body { display: block; }
.topbar { flex-wrap: wrap; gap: 8px; padding: 8px 12px; }
.brand { font-size: 0.88rem; }
.topbar .ml-badge { display: none; }
.main { display: block; height: auto; min-height: 0; }
.rail {
flex-direction: row; width: 100%; justify-content: center; gap: 10px;
padding: 6px 10px; border-right: none; border-bottom: 1px solid var(--border);
}
.rail button { width: 44px; height: 44px; font-size: 1.2rem; }
.viewport { display: block; padding: 10px; overflow: visible; }
.doc-grid { flex-direction: column; align-items: stretch; height: auto; }
.doc { max-width: 100%; width: 100%; }
.doc .stage { min-height: 220px; }
.workflow-view { position: static; height: 78vh; padding: 0 0 10px; }
.panels {
width: 100%; border-left: none; border-top: 1px solid var(--border);
padding: 10px 12px; overflow-y: visible;
}
.mode-tabs button { padding: 6px 13px; font-size: 0.8rem; }
.statusbar { position: sticky; bottom: 0; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="brand">🎨 MOSS Image Edit Studio</div>
<div class="mode-tabs">
<button id="mode-edit" class="active">Edit</button>
<button id="mode-generate">Generate</button>
<button id="mode-workflow">Workflow</button>
</div>
<div class="spacer"></div>
<a class="ml-badge" href="https://huggingface.co/chinoll/Moss-Image-Edit" target="_blank" rel="noopener">MOSS Image Edit LoRA</a>
<a class="ml-cta" href="https://hf.co/chat?mode=ml-intern" target="_blank" rel="noopener">✨ Built with ML Intern</a>
</header>
<div class="main">
<div class="rail">
<button id="tool-upload" title="Upload source image"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></button>
<button id="tool-clear-doc" title="Clear document"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
</div>
<div class="viewport">
<div class="workflow-view" id="workflow-view" hidden>
<iframe src="/workflow/?ui=canvas" title="Workflow canvas"></iframe>
</div>
<div class="doc-grid">
<figure class="doc" id="doc-before">
<figcaption id="before-caption">Source</figcaption>
<div class="stage" id="before-stage">
<img id="source-img" alt="" hidden />
<div class="drop-hint" id="source-hint"><b>No source image</b>Use the upload tool or drop a file here</div>
</div>
</figure>
<figure class="doc" id="doc-after">
<figcaption id="after-caption">Result</figcaption>
<div class="stage" id="after-stage">
<img id="result-img" alt="" hidden />
<div class="split-wrap" id="split-wrap" hidden>
<img id="split-source" alt="" style="clip-path: inset(0 50% 0 0)" />
<div class="split-divider" id="split-divider" style="left:50%"></div>
</div>
<div class="drop-hint" id="result-hint"><b>Nothing yet</b>Run to see the result</div>
</div>
<div class="doc-bar">
<span id="result-meta">—</span>
<div class="spacer"></div>
<button id="split-toggle" hidden>SPLIT</button>
</div>
</figure>
</div>
<div class="history-strip" id="history-strip"><span class="hlabel">History</span></div>
</div>
<aside class="panels">
<!-- edit mode -->
<div id="edit-panels">
<section class="panel">
<div class="panel-title">Examples</div>
<div class="ex-row">
<button class="ex-chip" data-img="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/007/image.webp" data-instruction="Change the scene to a snowy winter day, keeping the person unchanged.">
<img src="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/007/image.webp" alt="" /><span>Snowy winter day</span>
</button>
<button class="ex-chip" data-img="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/003/image.webp" data-instruction="Change the lighting to warm golden-hour sunlight.">
<img src="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/003/image.webp" alt="" /><span>Golden-hour light</span>
</button>
<button class="ex-chip" data-img="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/011/image.webp" data-instruction="Turn the outfit into a bright red dress, preserving everything else.">
<img src="https://huggingface.co/spaces/OpenMOSS-Team/MOSS-Image/resolve/main/assets/images/011/image.webp" alt="" /><span>Red outfit</span>
</button>
</div>
</section>
<section class="panel">
<div class="panel-title">Instruction</div>
<label for="instruction">What should change?</label>
<textarea id="instruction" rows="3" placeholder="Change the background to a sunny beach, preserving the person."></textarea>
<label for="edit-negative">Negative prompt (optional)</label>
<textarea id="edit-negative" rows="2" placeholder="watermark, blurry"></textarea>
</section>
<section class="panel">
<div class="panel-title">Layers</div>
<div class="layer-row" id="layer-source">
<div class="thumb" id="layer-source-thumb">BG</div>
<div class="lname">Background<span class="lsub">source · image 1</span></div>
</div>
<div class="ref-slot" data-ref="2">+ Reference 2</div>
<div class="ref-slot" data-ref="3">+ Reference 3</div>
<div class="ref-slot" data-ref="4">+ Reference 4</div>
<div class="hint">With multiple references, name each image's role — e.g. "Dress the person in image 1 in the outfit from image 2."</div>
</section>
</div>
<!-- generate mode -->
<div id="generate-panels" hidden>
<section class="panel">
<div class="panel-title">Examples</div>
<button class="ex-prompt" data-prompt="A cinematic close-up portrait of an elderly fisherman with a weathered face, soft window light, shallow depth of field">🎣 Cinematic fisherman portrait</button>
<button class="ex-prompt" data-prompt="A cozy bookstore cafe interior at golden hour, warm lamp light, tall shelves of books, steam rising from a cup">📚 Cozy bookstore cafe</button>
<button class="ex-prompt" data-prompt="A majestic snow leopard resting on a rocky ledge at dawn, cold morning mist, telephoto wildlife photography">🐆 Snow leopard in the mist</button>
<button class="ex-prompt" data-prompt="An isometric diorama of a tiny moss-covered shrine in a bamboo forest, soft volumetric light, miniature photography style">🎋 Moss shrine diorama</button>
</section>
<section class="panel">
<div class="panel-title">Prompt</div>
<label for="gen-prompt">Describe the image</label>
<textarea id="gen-prompt" rows="4" placeholder="a golden retriever puppy sitting in a lavender field at golden hour"></textarea>
<label for="gen-negative">Negative prompt (optional)</label>
<textarea id="gen-negative" rows="2">watermark, bad anatomy</textarea>
</section>
</div>
<!-- shared advanced -->
<section class="panel">
<div class="panel-title">Advanced</div>
<div class="row">
<div><label for="seed">Seed</label><input type="number" id="seed" value="-1" min="-1" /></div>
<label class="switch"><input type="checkbox" id="randomize" checked /><span class="sw"></span><span class="sw-label">Randomize</span></label>
</div>
<div class="row" style="margin-top:10px">
<div><label>Steps <span id="steps-val">30</span></label><input type="range" id="steps" min="1" max="100" value="30" /></div>
<div><label>CFG <span id="cfg-val">4</span></label><input type="range" id="cfg" min="1" max="10" step="0.5" value="4" /></div>
</div>
<div class="row" style="margin-top:10px">
<div><label>Shift <span id="shift-val">5</span></label><input type="range" id="shift" min="1" max="10" step="0.5" value="5" /></div>
<div id="num-images-wrap" hidden><label>Images <span id="num-images-val">1</span></label><input type="range" id="num-images" min="1" max="4" step="1" value="1" /></div>
</div>
<div class="row" style="margin-top:10px">
<div><label>Width <span id="width-val">1024</span></label><input type="range" id="width" min="512" max="1536" step="16" value="1024" /></div>
<div><label>Height <span id="height-val">1024</span></label><input type="range" id="height" min="512" max="1536" step="16" value="1024" /></div>
</div>
<div class="row" id="ref-edge-row" style="margin-top:10px">
<div><label>Reference max edge <span id="ref-edge-val">512</span></label><input type="range" id="ref-edge" min="256" max="1024" step="16" value="512" /></div>
</div>
</section>
<button id="run-btn">Edit Image</button>
</aside>
</div>
<footer class="statusbar" id="statusbar">
<div class="pulse"></div>
<span id="status">Idle</span>
<div class="spacer"></div>
<span id="seed-out"></span>
</footer>
<input type="file" id="file-input" accept="image/*" />
<script type="module">
import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const $ = (id) => document.getElementById(id);
const state = {
mode: "edit",
sourcePath: null,
refPaths: { 2: null, 3: null, 4: null },
app: null,
};
// ---------- client ----------
async function getApp() {
if (!state.app) state.app = await client(location.origin);
return state.app;
}
async function uploadFile(file) {
const form = new FormData();
form.append("files", file);
const resp = await fetch("/gradio_api/upload", { method: "POST", body: form });
if (!resp.ok) throw new Error(`Upload failed (${resp.status})`);
const paths = await resp.json();
return Array.isArray(paths) ? paths[0] : paths;
}
function setStatus(text, busy = false) {
$("status").textContent = text;
$("statusbar").classList.toggle("busy", busy);
$("run-btn").disabled = busy;
}
// ---------- mode ----------
function setMode(mode) {
state.mode = mode;
$("mode-edit").classList.toggle("active", mode === "edit");
$("mode-generate").classList.toggle("active", mode === "generate");
$("mode-workflow").classList.toggle("active", mode === "workflow");
const workflow = mode === "workflow";
$("workflow-view").hidden = !workflow;
document.querySelector(".doc-grid").style.display = workflow ? "none" : "";
document.querySelector(".rail").style.display = workflow ? "none" : "";
document.querySelector(".panels").style.display = workflow ? "none" : "";
document.getElementById("history-strip").style.display = workflow ? "none" : "";
if (workflow) return;
$("edit-panels").hidden = mode !== "edit";
$("generate-panels").hidden = mode !== "generate";
$("doc-before").style.display = mode === "edit" ? "" : "none";
$("ref-edge-row").style.display = mode === "edit" ? "" : "none";
$("num-images-wrap").hidden = mode !== "generate";
$("steps").value = mode === "edit" ? 30 : 50;
$("steps-val").textContent = $("steps").value;
$("cfg").value = mode === "edit" ? 4 : 3;
$("cfg-val").textContent = $("cfg").value;
$("run-btn").textContent = mode === "edit" ? "Edit Image" : "Generate Image";
renderDocs(mode);
renderHistory(mode);
}
$("mode-edit").onclick = () => setMode("edit");
$("mode-generate").onclick = () => setMode("generate");
$("mode-workflow").onclick = () => setMode("workflow");
// ---------- examples ----------
document.querySelectorAll(".ex-chip").forEach((chip) => {
chip.onclick = async () => {
setStatus("Loading example…", true);
try {
const blob = await (await fetch(chip.dataset.img)).blob();
const file = new File([blob], "example.webp", { type: blob.type || "image/webp" });
state.sourcePath = await uploadFile(file);
showSource(file);
$("instruction").value = chip.dataset.instruction;
setMode("edit");
setStatus("Example loaded — hit Edit Image");
} catch (err) {
setStatus(String(err));
}
};
});
document.querySelectorAll(".ex-prompt").forEach((btn) => {
btn.onclick = () => {
setMode("generate");
$("gen-prompt").value = btn.dataset.prompt;
setStatus("Example prompt loaded — hit Generate Image");
};
});
// Collapsible panels (Photoshop dock behavior) + document zoom toggle.
document.querySelectorAll(".panel .panel-title").forEach((title) => {
const body = document.createElement("div");
body.className = "panel-body";
[...title.parentElement.children].forEach((child) => {
if (child !== title) body.appendChild(child);
});
title.parentElement.appendChild(body);
title.onclick = () => title.parentElement.classList.toggle("collapsed");
});
document.querySelectorAll(".doc figcaption").forEach((caption) => {
const btn = document.createElement("button");
btn.className = "zoom-btn";
btn.textContent = "FIT";
btn.onclick = () => {
const img = caption.parentElement.querySelector("img");
const stage = caption.parentElement.querySelector(".stage");
const zoomed = img.classList.toggle("zoomed");
stage.classList.toggle("scrollable", zoomed);
btn.textContent = zoomed ? "1:1" : "FIT";
};
caption.appendChild(btn);
});
// ---------- uploads ----------
let uploadTarget = "source";
function pickFile(target) {
uploadTarget = target;
$("file-input").click();
}
$("tool-upload").onclick = () => pickFile("source");
document.querySelectorAll(".ref-slot").forEach((slot) => {
slot.onclick = () => pickFile(slot.dataset.ref);
});
$("file-input").onchange = async (e) => {
const file = e.target.files[0];
e.target.value = "";
if (!file) return;
setStatus("Uploading…", true);
try {
const path = await uploadFile(file);
if (uploadTarget === "source") {
state.sourcePath = path;
showSource(file);
} else {
state.refPaths[uploadTarget] = path;
const slot = document.querySelector(`.ref-slot[data-ref="${uploadTarget}"]`);
slot.innerHTML = "";
const img = document.createElement("img");
img.src = URL.createObjectURL(file);
slot.appendChild(img);
}
setStatus("Idle");
} catch (err) {
setStatus(String(err));
}
};
$("before-stage").addEventListener("dragover", (e) => e.preventDefault());
$("before-stage").addEventListener("drop", async (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (!file) return;
setStatus("Uploading…", true);
try {
state.sourcePath = await uploadFile(file);
showSource(file);
setStatus("Idle");
} catch (err) {
setStatus(String(err));
}
});
// Per-mode document state: Edit and Generate keep separate result panes so
// a run in one tab never bleeds into the other.
const docState = {
edit: { source: null, result: null, history: [] },
generate: { source: null, result: null, history: [] },
};
function renderDocs(mode) {
const d = docState[mode];
const si = $("source-img"), ri = $("result-img");
if (d.source) {
si.src = d.source;
si.hidden = false;
$("source-hint").style.display = "none";
} else {
si.hidden = true;
$("source-hint").style.display = "";
}
if (d.result) {
ri.src = d.result;
ri.hidden = false;
$("result-hint").style.display = "none";
$("split-toggle").hidden = mode !== "edit";
} else {
ri.hidden = true;
$("result-hint").style.display = "";
$("split-toggle").hidden = true;
}
}
function showSource(file) {
const img = $("source-img");
img.src = URL.createObjectURL(file);
img.hidden = false;
$("source-hint").style.display = "none";
docState.edit.source = img.src;
// Layers panel: the source is the Background layer.
const st = $("layer-source-thumb");
st.innerHTML = "";
const stImg = document.createElement("img");
stImg.src = img.src;
st.appendChild(stImg);
}
$("tool-clear-doc").onclick = () => {
state.sourcePath = null;
docState.edit.source = null;
docState.edit.result = null;
$("source-img").hidden = true;
$("source-hint").style.display = "";
$("result-img").hidden = true;
$("result-hint").style.display = "";
};
// ---------- slider readouts ----------
$("steps").oninput = () => ($("steps-val").textContent = $("steps").value);
$("cfg").oninput = () => ($("cfg-val").textContent = $("cfg").value);
$("shift").oninput = () => ($("shift-val").textContent = $("shift").value);
$("width").oninput = () => ($("width-val").textContent = $("width").value);
$("height").oninput = () => ($("height-val").textContent = $("height").value);
$("ref-edge").oninput = () => ($("ref-edge-val").textContent = $("ref-edge").value);
$("num-images").oninput = () => ($("num-images-val").textContent = $("num-images").value);
function unwrap(result) {
const data = Array.isArray(result?.data) ? result.data[0] : result?.data;
return data ?? {};
}
// ---------- history (per mode) ----------
function renderHistory(mode) {
const strip = $("history-strip");
strip.innerHTML = '<span class="hlabel">History</span>';
docState[mode].history.forEach((h) => {
const chip = document.createElement("div");
chip.className = "history-chip";
chip.title = h.instruction || "";
const img = document.createElement("img");
img.src = h.uri;
const tag = document.createElement("div");
tag.className = "hseed";
tag.textContent = h.seed ?? "";
chip.appendChild(img);
chip.appendChild(tag);
chip.onclick = () => {
if (mode === "edit") $("instruction").value = h.instruction;
else $("gen-prompt").value = h.instruction;
$("seed").value = h.seed ?? -1;
$("randomize").checked = false;
docState[mode].result = h.uri;
$("result-img").src = h.uri;
$("result-img").hidden = false;
$("result-hint").style.display = "none";
$("split-toggle").hidden = mode !== "edit";
};
strip.appendChild(chip);
});
}
function pushHistory(uri, seed, instruction, mode) {
docState[mode].history.unshift({ uri, seed, instruction, mode });
if (docState[mode].history.length > 8) docState[mode].history.pop();
renderHistory(mode);
}
// ---------- before/after split view ----------
let splitOn = false;
$("split-toggle").onclick = () => {
if ($("result-img").hidden) return;
splitOn = !splitOn;
$("split-toggle").classList.toggle("active", splitOn);
$("split-wrap").hidden = !splitOn;
if (splitOn && !$("source-img").hidden) $("split-source").src = $("source-img").src;
};
(function () {
const divider = $("split-divider");
let dragging = false;
divider.addEventListener("pointerdown", (e) => { dragging = true; e.preventDefault(); });
window.addEventListener("pointermove", (e) => {
if (!dragging) return;
const stage = $("after-stage").getBoundingClientRect();
const pct = Math.min(97, Math.max(3, ((e.clientX - stage.left) / stage.width) * 100));
divider.style.left = pct + "%";
$("split-source").style.clipPath = `inset(0 ${100 - pct}% 0 0)`;
});
window.addEventListener("pointerup", () => { dragging = false; });
})();
function showResult(result) {
const payload = unwrap(result);
const url = typeof payload.image_data === "string" ? payload.image_data : null;
if (!url) { setStatus("Run finished but no image came back (see console)."); return false; }
const img = $("result-img");
img.src = url;
img.hidden = false;
$("result-hint").style.display = "none";
docState[state.mode].result = url;
if (payload.seed != null) $("seed-out").textContent = "seed " + payload.seed;
$("split-toggle").hidden = state.mode !== "edit";
$("result-meta").textContent = payload.status || (state.mode === "edit" ? "Edited" : "Generated");
const instruction = state.mode === "edit" ? $("instruction").value : $("gen-prompt").value;
const uris = Array.isArray(payload.images_data) && payload.images_data.length
? payload.images_data : [url];
uris.forEach((uri) => pushHistory(uri, payload.seed, instruction, state.mode));
return true;
}
$("run-btn").onclick = async () => {
const app = await getApp();
const seed = parseInt($("seed").value, 10);
const randomize = $("randomize").checked;
try {
if (state.mode === "edit") {
if (!state.sourcePath) { setStatus("Upload a source image first."); return; }
setStatus("Editing… (first run also loads the weights onto the GPU)", true);
const result = await app.predict("/edit", {
source: state.sourcePath,
instruction: $("instruction").value,
negative_prompt: $("edit-negative").value,
ref2: state.refPaths[2],
ref3: state.refPaths[3],
ref4: state.refPaths[4],
seed: Number.isFinite(seed) ? seed : -1,
randomize,
steps: parseInt($("steps").value, 10),
cfg: parseFloat($("cfg").value),
shift: parseFloat($("shift").value),
width: parseInt($("width").value, 10),
height: parseInt($("height").value, 10),
reference_max_edge: parseInt($("ref-edge").value, 10),
});
if (showResult(result)) setStatus("Done");
} else {
setStatus("Generating… (first run also loads the weights onto the GPU)", true);
const result = await app.predict("/generate", {
prompt: $("gen-prompt").value,
negative_prompt: $("gen-negative").value,
seed: Number.isFinite(seed) ? seed : -1,
randomize,
steps: parseInt($("steps").value, 10),
cfg: parseFloat($("cfg").value),
shift: parseFloat($("shift").value),
width: parseInt($("width").value, 10),
height: parseInt($("height").value, 10),
num_images: parseInt($("num-images").value, 10),
});
if (showResult(result)) setStatus("Done");
}
} catch (err) {
setStatus(err?.message || String(err));
}
};
setMode("edit");
</script>
</body>
</html>