Align Studio display chrome to the Classic | Studio reference mock.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>AIGen — Display mock (Classic | Studio)</title>
|
||||
<link rel="stylesheet" href="./tokens/display.css" />
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: ui-sans-serif, system-ui, Inter, sans-serif; background: #0e0e10; color: #f3f1ea; }
|
||||
.app { min-height: 100vh; display: flex; flex-direction: column; gap: var(--chrome-gap); padding: var(--chrome-gap); }
|
||||
header.top {
|
||||
height: var(--header-h); display: flex; align-items: center; gap: 12px;
|
||||
border: 1px solid var(--line); border-radius: var(--radius); padding: 0 12px; background: var(--bg-raise);
|
||||
}
|
||||
.brand { font-weight: 650; letter-spacing: .02em; }
|
||||
.grow { flex: 1; }
|
||||
.pill { font-size: var(--type-sm); color: #b8e0a8; border: 1px solid #2d4a2a; background: #142016; border-radius: 999px; padding: 4px 10px; }
|
||||
.nav { color: var(--muted); font-size: var(--type-sm); display: flex; gap: 14px; }
|
||||
.nav b { color: var(--text); font-weight: 600; }
|
||||
.identity-header, .classic-hide { display: flex; gap: 10px; align-items: center; font-size: var(--type-sm); color: var(--muted); }
|
||||
.rail { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.seg {
|
||||
height: var(--rail-h); padding: 0 12px; border-radius: var(--radius);
|
||||
border: 1px solid var(--line); background: var(--bg-slot); color: var(--muted); cursor: pointer;
|
||||
}
|
||||
.seg[aria-pressed="true"] { background: #2a1f0c; border-color: var(--amber); color: var(--amber); }
|
||||
.seg:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.classic-selects { display: flex; gap: 8px; }
|
||||
.classic-selects select {
|
||||
background: var(--bg-slot); color: var(--text); border: 1px solid var(--line);
|
||||
border-radius: var(--radius); height: var(--rail-h); padding: 0 8px;
|
||||
}
|
||||
.grid { display: grid; grid-template-columns: 240px 1fr 260px; gap: var(--chrome-gap); flex: 1; }
|
||||
.col { background: var(--bg-raise); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--card-pad); }
|
||||
.slot { background: var(--bg-slot); border: 1px dashed var(--line); border-radius: var(--radius); min-height: 72px; margin-bottom: 8px; padding: 8px; font-size: var(--type-sm); color: var(--muted); }
|
||||
.slot strong { display: block; color: var(--text); margin-bottom: 4px; }
|
||||
.viewer { position: relative; background: #111; border-radius: var(--radius); min-height: 320px; display: grid; place-items: center; color: var(--muted); border: 1px solid var(--line); }
|
||||
.eye-viewer { position: absolute; top: 8px; right: 8px; background: #000a; border: 1px solid var(--line); border-radius: 6px; padding: 4px; }
|
||||
.strip { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.tile { width: 72px; height: 48px; background: #222; border-radius: 6px; border: 1px solid var(--line); position: relative; }
|
||||
.eye-tile { position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; }
|
||||
textarea, input {
|
||||
width: 100%; background: var(--bg-slot); color: var(--text); border: 1px solid var(--line);
|
||||
border-radius: var(--radius); padding: 8px; font: inherit;
|
||||
}
|
||||
.cta { width: 100%; height: 44px; border: 0; border-radius: var(--radius); background: var(--amber); color: #1a1204; font-weight: 700; cursor: pointer; margin-top: 10px; }
|
||||
.hint { font-size: 11px; color: var(--muted); margin: 6px 0 10px; }
|
||||
.settings {
|
||||
position: fixed; right: 16px; bottom: 16px; width: 360px; background: var(--bg-raise);
|
||||
border: 1px solid var(--line); border-radius: 10px; padding: 14px; box-shadow: 0 12px 40px #0008;
|
||||
}
|
||||
.cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.card {
|
||||
border: 1px solid var(--line); border-radius: 8px; padding: 10px; background: var(--bg-slot); cursor: pointer; text-align: left; color: inherit;
|
||||
}
|
||||
.card[aria-pressed="true"] { border-color: var(--amber); background: #2a1f0c; }
|
||||
.card small { display: block; color: var(--muted); margin-top: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" data-display="studio" id="root">
|
||||
<header class="top">
|
||||
<span class="brand">AIGen</span>
|
||||
<span class="muted" style="color:var(--muted);font-size:12px">/ Inbox</span>
|
||||
<div class="grow"></div>
|
||||
<div class="identity-header">
|
||||
<label><input type="checkbox" checked /> Lock face</label>
|
||||
<label><input type="checkbox" checked /> Lock outfit</label>
|
||||
</div>
|
||||
<div class="classic-hide">
|
||||
<label><input type="checkbox" /> Hide input</label>
|
||||
<label><input type="checkbox" /> Hide output</label>
|
||||
</div>
|
||||
<nav class="nav"><span>Library</span><b>Queue</b><span>Settings</span></nav>
|
||||
<span class="pill">5080 Ready</span>
|
||||
</header>
|
||||
|
||||
<div class="rail rail-task" role="tablist">
|
||||
<button class="seg" aria-pressed="false">Image</button>
|
||||
<button class="seg" aria-pressed="false">Edit</button>
|
||||
<button class="seg" aria-pressed="false">Refine</button>
|
||||
<button class="seg" aria-pressed="true">Video</button>
|
||||
<button class="seg" aria-pressed="false">Extend</button>
|
||||
<button class="seg" aria-pressed="false">Music</button>
|
||||
</div>
|
||||
<div class="rail rail-model">
|
||||
<button class="seg" aria-pressed="true">MiniMax H3</button>
|
||||
<button class="seg" disabled title="xAIGen only">LTX</button>
|
||||
</div>
|
||||
<div class="classic-selects">
|
||||
<select><option>Video</option><option>Image</option><option>Extend</option><option>Music</option></select>
|
||||
<select><option>MiniMax H3</option><option disabled>LTX (xAIGen only)</option></select>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<aside class="col">
|
||||
<div class="slot"><strong>Hero</strong>car_bee still</div>
|
||||
<div class="slot"><strong>Start</strong>Select still or clip</div>
|
||||
<div class="slot"><strong>End</strong>Optional</div>
|
||||
<div class="identity-slots">
|
||||
<label><input type="checkbox" checked /> Lock face</label><br />
|
||||
<label><input type="checkbox" checked /> Lock outfit</label>
|
||||
<p class="hint">hero encode + lock text — not a MiniMax face/clothes split on stills</p>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="col">
|
||||
<div class="viewer">
|
||||
<button class="eye-viewer" title="Toggle viewer">eye</button>
|
||||
viewer · last frame / waveform
|
||||
</div>
|
||||
<div class="strip">
|
||||
<div class="tile"><span class="eye-tile">👁</span></div>
|
||||
<div class="tile"><span class="eye-tile">👁</span></div>
|
||||
<div class="tile"><span class="eye-tile">👁</span></div>
|
||||
<div class="tile"><span class="eye-tile">👁</span></div>
|
||||
</div>
|
||||
</main>
|
||||
<aside class="col">
|
||||
<textarea rows="5">car_bee walks the Great Hall, cinematic</textarea>
|
||||
<p class="hint">YuE2 target length is a plan hint, not a trim.</p>
|
||||
<input value="seed unlocked" />
|
||||
<button class="cta">Generate</button>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="settings">
|
||||
<strong>Settings → Display</strong>
|
||||
<p class="hint">Layout only. Jobs, engines, and library IDs stay the same.</p>
|
||||
<div class="cards">
|
||||
<button class="card" id="btnClassic" aria-pressed="false">Classic<small>packed header, dropdowns</small></button>
|
||||
<button class="card" id="btnStudio" aria-pressed="true">Studio<small>task rail, model rail, slots + eyes</small></button>
|
||||
</div>
|
||||
</aside>
|
||||
<script>
|
||||
const root = document.getElementById("root");
|
||||
const classic = document.getElementById("btnClassic");
|
||||
const studio = document.getElementById("btnStudio");
|
||||
function setMode(mode) {
|
||||
root.setAttribute("data-display", mode);
|
||||
classic.setAttribute("aria-pressed", mode === "classic" ? "true" : "false");
|
||||
studio.setAttribute("aria-pressed", mode === "studio" ? "true" : "false");
|
||||
}
|
||||
classic.onclick = () => setMode("classic");
|
||||
studio.onclick = () => setMode("studio");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 3l18 18"/>
|
||||
<path d="M10.6 10.6A3 3 0 0 0 12 15a3 3 0 0 0 2.4-4.4"/>
|
||||
<path d="M9.9 5.1A11 11 0 0 1 12 5c6.5 0 10 7 10 7a18 18 0 0 1-3.2 3.8"/>
|
||||
<path d="M6.1 6.1C3.7 7.8 2 12 2 12s3.5 7 10 7a10.5 10.5 0 0 0 4.2-.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 446 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="4" y="11" width="16" height="10" rx="2"/>
|
||||
<path d="M8 11V8a4 4 0 0 1 8 0v3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 302 B |
@@ -0,0 +1,56 @@
|
||||
/* AIGen display tokens — Classic | Studio
|
||||
Apply on [data-display] wrapper. Do not fork components. */
|
||||
|
||||
:root,
|
||||
[data-display="studio"] {
|
||||
--bg: #0e0e10;
|
||||
--bg-raise: #161618;
|
||||
--bg-slot: #1c1c1f;
|
||||
--line: #2a2a2e;
|
||||
--text: #f3f1ea;
|
||||
--muted: #9a9588;
|
||||
--amber: #f5a524;
|
||||
--amber-dim: #c4841c;
|
||||
--radius: 8px;
|
||||
--chrome-gap: 16px;
|
||||
--rail-h: 40px;
|
||||
--card-pad: 14px;
|
||||
--header-h: 52px;
|
||||
--type: 14px;
|
||||
--type-sm: 12px;
|
||||
}
|
||||
|
||||
[data-display="classic"] {
|
||||
--chrome-gap: 8px;
|
||||
--rail-h: 32px;
|
||||
--card-pad: 10px;
|
||||
--header-h: 44px;
|
||||
--type: 13px;
|
||||
}
|
||||
|
||||
[data-display] {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: var(--type);
|
||||
}
|
||||
|
||||
[data-display="classic"] .rail-task,
|
||||
[data-display="classic"] .rail-model {
|
||||
display: none;
|
||||
}
|
||||
[data-display="studio"] .classic-selects {
|
||||
display: none;
|
||||
}
|
||||
[data-display="classic"] .identity-slots {
|
||||
display: none;
|
||||
}
|
||||
[data-display="studio"] .identity-header {
|
||||
display: none;
|
||||
}
|
||||
[data-display="classic"] .eye-tile,
|
||||
[data-display="classic"] .eye-viewer {
|
||||
display: none;
|
||||
}
|
||||
[data-display="studio"] .classic-hide {
|
||||
display: none;
|
||||
}
|
||||
+302
-77
@@ -1,118 +1,343 @@
|
||||
/* Studio 2 display skins — Classic | Studio. Layout tokens only; palette matches main.css. */
|
||||
/* AIGen display tokens — Classic | Studio
|
||||
Apply on [data-display] wrapper. Do not fork components.
|
||||
Copied from studio2-display-ux/tokens/display.css */
|
||||
|
||||
:root,
|
||||
[data-display='classic'],
|
||||
[data-display='studio'] {
|
||||
--display-bg: #09090b;
|
||||
--display-fg: #e4e4e7;
|
||||
--display-muted: #a1a1aa;
|
||||
--display-border: rgba(255, 255, 255, 0.1);
|
||||
--display-amber: #fbbf24;
|
||||
--display-amber-fg: #09090b;
|
||||
--display-radius: 8px;
|
||||
--display-focus: rgba(252, 211, 77, 0.5);
|
||||
--bg: #0e0e10;
|
||||
--bg-raise: #161618;
|
||||
--bg-slot: #1c1c1f;
|
||||
--line: #2a2a2e;
|
||||
--text: #f3f1ea;
|
||||
--muted: #9a9588;
|
||||
--amber: #f5a524;
|
||||
--amber-dim: #c4841c;
|
||||
--radius: 8px;
|
||||
--chrome-gap: 16px;
|
||||
--rail-h: 40px;
|
||||
--card-pad: 14px;
|
||||
--header-h: 52px;
|
||||
--type: 14px;
|
||||
--type-sm: 12px;
|
||||
}
|
||||
|
||||
[data-display='classic'] {
|
||||
--chrome-gap: 0.5rem;
|
||||
--rail-h: 2.25rem;
|
||||
--card-pad: 0.75rem;
|
||||
--display-type-title: 1.75rem;
|
||||
--display-type-body: 0.875rem;
|
||||
--display-viewer-radius: 0.75rem;
|
||||
--chrome-gap: 8px;
|
||||
--rail-h: 32px;
|
||||
--card-pad: 10px;
|
||||
--header-h: 44px;
|
||||
--type: 13px;
|
||||
}
|
||||
|
||||
[data-display='studio'] {
|
||||
--chrome-gap: 1rem;
|
||||
--rail-h: 3rem;
|
||||
--card-pad: 1rem;
|
||||
--display-type-title: 1.875rem;
|
||||
--display-type-body: 0.9375rem;
|
||||
--display-viewer-radius: 8px;
|
||||
[data-display] {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: var(--type);
|
||||
}
|
||||
|
||||
[data-display] .display-title {
|
||||
font-size: var(--display-type-title);
|
||||
}
|
||||
|
||||
[data-display] .display-body {
|
||||
font-size: var(--display-type-body);
|
||||
}
|
||||
|
||||
[data-display] .display-stack {
|
||||
[data-display] .display-app {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
flex-direction: column;
|
||||
gap: var(--chrome-gap);
|
||||
padding: var(--chrome-gap);
|
||||
}
|
||||
|
||||
[data-display] .display-card {
|
||||
border-radius: var(--display-radius);
|
||||
border: 1px solid var(--display-border);
|
||||
[data-display] .display-top {
|
||||
display: flex;
|
||||
height: var(--header-h);
|
||||
min-height: var(--header-h);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-raise);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
[data-display] .display-brand {
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
[data-display] .display-grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-display] .display-nav {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
font-size: var(--type-sm);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
[data-display] .display-nav a {
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-display] .display-nav a:hover,
|
||||
[data-display] .display-nav a.router-link-active {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-display] .display-nav a:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-display] .identity-header,
|
||||
[data-display] .classic-hide,
|
||||
[data-display] .identity-slots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: var(--type-sm);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
[data-display] .identity-slots {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[data-display] .identity-slots .hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
[data-display] .rail {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-display] .seg {
|
||||
height: var(--rail-h);
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-slot);
|
||||
padding: 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: var(--type);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-display] .seg:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
||||
}
|
||||
|
||||
[data-display] .seg[aria-pressed='true'] {
|
||||
border-color: var(--amber);
|
||||
background: #2a1f0c;
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
[data-display] .seg:disabled,
|
||||
[data-display] .seg[aria-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
[data-display] .classic-selects {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-display] .classic-selects select {
|
||||
height: var(--rail-h);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-slot);
|
||||
padding: 0 8px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-display] .classic-selects select:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
||||
}
|
||||
|
||||
[data-display] .display-grid {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
gap: var(--chrome-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
[data-display] .display-grid {
|
||||
grid-template-columns: minmax(14rem, 15rem) minmax(0, 1fr) minmax(16rem, 18rem);
|
||||
}
|
||||
[data-display] .display-grid--music {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(16rem, 18rem);
|
||||
}
|
||||
}
|
||||
|
||||
[data-display] .display-col {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-raise);
|
||||
padding: var(--card-pad);
|
||||
}
|
||||
|
||||
[data-display] .display-viewer {
|
||||
border-radius: var(--display-viewer-radius);
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 320px;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #111;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
[data-display] .display-rail {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
min-height: var(--rail-h);
|
||||
}
|
||||
|
||||
[data-display] .display-rail__seg {
|
||||
min-height: var(--rail-h);
|
||||
border-radius: var(--display-radius);
|
||||
border: 1px solid var(--display-border);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: var(--display-type-body);
|
||||
color: var(--display-fg);
|
||||
background: transparent;
|
||||
[data-display] .eye-viewer {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #000a;
|
||||
padding: 4px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
[data-display] .display-rail__seg:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--display-focus);
|
||||
[data-display] .eye-viewer:focus-visible,
|
||||
[data-display] .eye-tile:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
||||
}
|
||||
|
||||
[data-display] .display-rail__seg[aria-pressed='true'],
|
||||
[data-display] .display-rail__seg[data-active='true'] {
|
||||
border-color: var(--display-amber);
|
||||
background: var(--display-amber);
|
||||
color: var(--display-amber-fg);
|
||||
[data-display] .display-strip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
[data-display] .display-rail__seg:disabled,
|
||||
[data-display] .display-rail__seg[aria-disabled='true'] {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
[data-display] .display-tile {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
[data-display='studio'] .display-rail__seg {
|
||||
font-weight: 600;
|
||||
padding: 0.625rem 1.25rem;
|
||||
[data-display] .display-tile[data-active='true'] {
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
[data-display='classic'] .display-rail__seg {
|
||||
min-height: 2.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-weight: 500;
|
||||
[data-display] .eye-tile {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
display: grid;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
background: #000a;
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-display] .display-cta {
|
||||
border-radius: var(--display-radius);
|
||||
background: var(--display-amber);
|
||||
color: var(--display-amber-fg);
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: var(--radius);
|
||||
background: var(--amber);
|
||||
color: #1a1204;
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-display] .display-cta:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--display-focus);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
||||
}
|
||||
|
||||
[data-display] .display-control {
|
||||
border-radius: var(--display-radius);
|
||||
[data-display] .display-cta:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[data-display] .display-field {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-slot);
|
||||
padding: 8px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-display] .display-field:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
||||
}
|
||||
|
||||
[data-display] .display-gpu-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #2d4a2a;
|
||||
background: #142016;
|
||||
padding: 4px 10px;
|
||||
font-size: var(--type-sm);
|
||||
color: #b8e0a8;
|
||||
}
|
||||
|
||||
[data-display] .display-gpu-pill[data-tone='busy'] {
|
||||
border-color: color-mix(in srgb, var(--amber) 40%, transparent);
|
||||
background: #2a1f0c;
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
[data-display] .display-gpu-pill[data-tone='idle'],
|
||||
[data-display] .display-gpu-pill[data-tone='off'] {
|
||||
border-color: var(--line);
|
||||
background: var(--bg-slot);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Skin toggles — keep after base rules so display:none wins */
|
||||
[data-display='classic'] .rail-task,
|
||||
[data-display='classic'] .rail-model {
|
||||
display: none !important;
|
||||
}
|
||||
[data-display='studio'] .classic-selects {
|
||||
display: none !important;
|
||||
}
|
||||
[data-display='classic'] .identity-slots {
|
||||
display: none !important;
|
||||
}
|
||||
[data-display='studio'] .identity-header {
|
||||
display: none !important;
|
||||
}
|
||||
[data-display='classic'] .eye-tile,
|
||||
[data-display='classic'] .eye-viewer {
|
||||
display: none !important;
|
||||
}
|
||||
[data-display='studio'] .classic-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
@click="setDisplayMode('classic')"
|
||||
>
|
||||
<span class="block font-display text-lg font-semibold text-zinc-50">Classic</span>
|
||||
<span class="mt-1 block text-xs text-zinc-400">Packed header, compact selects, tight gaps.</span>
|
||||
<span class="mt-1 block text-xs text-zinc-400">packed header, dropdowns</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -46,7 +46,7 @@
|
||||
@click="setDisplayMode('studio')"
|
||||
>
|
||||
<span class="block font-display text-lg font-semibold text-zinc-50">Studio</span>
|
||||
<span class="mt-1 block text-xs text-zinc-400">Task and model rails, locks by slots, eye toggles.</span>
|
||||
<span class="mt-1 block text-xs text-zinc-400">task rail, model rail, slots + eyes</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+154
-179
@@ -1,259 +1,233 @@
|
||||
<template>
|
||||
<main class="display-stack w-full min-w-0 p-4 md:p-6 xl:p-8" :data-display="displayMode">
|
||||
<header class="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="display-title font-display font-bold tracking-tight text-zinc-50">Studio 2</h1>
|
||||
<p class="display-body mt-1 text-zinc-400">One hero. Every shot.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<label class="display-body text-zinc-300">Folder
|
||||
<select v-model="form.folderId" class="display-control ml-2 border border-white/10 bg-zinc-900 px-3 py-2 outline-none ring-amber-300/40 focus:ring-2">
|
||||
<main class="display-app w-full min-w-0" :data-display="displayMode">
|
||||
<header class="display-top">
|
||||
<span class="display-brand">{{ instanceName }}</span>
|
||||
<label class="text-[var(--type-sm)] text-[var(--muted)]">Folder
|
||||
<select v-model="form.folderId" class="ml-2 h-[var(--rail-h)] rounded-[var(--radius)] border border-[var(--line)] bg-[var(--bg-slot)] px-2 text-[var(--text)] outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--amber)_50%,transparent)]">
|
||||
<option v-for="f in library.folders" :key="f.id" :value="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div v-if="isClassic && form.mode!=='music'" class="flex flex-wrap items-center gap-3 text-sm text-zinc-300" aria-label="Identity locks">
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-amber-400">Lock face</label>
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-amber-400">Lock outfit</label>
|
||||
<div class="display-grow" />
|
||||
<div v-if="form.mode!=='music'" class="identity-header" aria-label="Identity locks">
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-[var(--amber)]">Lock face</label>
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
|
||||
</div>
|
||||
<div v-if="isClassic && xaigen" class="flex flex-wrap items-center gap-3 text-sm text-zinc-300" aria-label="Preview hide">
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideInput" type="checkbox" class="accent-amber-400">Hide input</label>
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideOutput" type="checkbox" class="accent-amber-400">Hide output</label>
|
||||
<div v-if="xaigen" class="classic-hide" aria-label="Preview hide">
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideInput" type="checkbox" class="accent-[var(--amber)]">Hide input</label>
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideOutput" type="checkbox" class="accent-[var(--amber)]">Hide output</label>
|
||||
</div>
|
||||
<nav class="flex flex-wrap items-center gap-3 text-sm" aria-label="Studio links">
|
||||
<Studio2GpuStatus :status="gpu" />
|
||||
<NuxtLink to="/queue" class="text-zinc-300 outline-none ring-amber-300/40 focus-visible:ring-2">Queue</NuxtLink>
|
||||
<NuxtLink to="/library" class="text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2">Library</NuxtLink>
|
||||
<NuxtLink to="/settings" class="text-zinc-300 outline-none ring-amber-300/40 focus-visible:ring-2">Settings</NuxtLink>
|
||||
<nav class="display-nav" aria-label="Studio links">
|
||||
<NuxtLink to="/library">Library</NuxtLink>
|
||||
<NuxtLink to="/queue">Queue</NuxtLink>
|
||||
<NuxtLink to="/settings">Settings</NuxtLink>
|
||||
</nav>
|
||||
</div>
|
||||
<span class="display-gpu-pill" :data-tone="gpu.tone" role="status" :title="gpu.detail">{{ gpu.label }}</span>
|
||||
</header>
|
||||
|
||||
<div class="display-stack min-w-0">
|
||||
<label v-if="isClassic" class="display-body block text-zinc-300">Task
|
||||
<select :value="form.mode" class="display-control mt-1 w-full max-w-xs border border-white/10 bg-zinc-900 px-3 py-2 capitalize outline-none ring-amber-300/40 focus:ring-2" @change="onTaskSelect">
|
||||
<option v-for="task in tasks" :key="task" :value="task" class="capitalize">{{ task }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<nav v-else class="display-rail" aria-label="Tasks">
|
||||
<nav class="rail rail-task" aria-label="Tasks">
|
||||
<button
|
||||
v-for="task in tasks"
|
||||
:key="task"
|
||||
type="button"
|
||||
class="display-rail__seg capitalize"
|
||||
class="seg capitalize"
|
||||
:aria-pressed="form.mode===task"
|
||||
@click="chooseTask(task)"
|
||||
>{{ task }}</button>
|
||||
</nav>
|
||||
|
||||
<div v-if="form.mode!=='music'" class="min-w-0">
|
||||
<label v-if="isClassic" class="display-body block text-zinc-300">Model
|
||||
<select v-model="form.engine" class="display-control mt-1 w-full max-w-md border border-white/10 bg-zinc-900 px-3 py-2 outline-none ring-amber-300/40 focus:ring-2">
|
||||
<option v-for="opt in engineOptions" :key="opt.value" :value="opt.value" :disabled="opt.disabled">{{ opt.label }}{{ opt.hint ? ' — '+opt.hint : '' }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div v-else class="display-rail" role="group" aria-label="Model">
|
||||
<div v-if="form.mode!=='music'" class="rail rail-model" role="group" aria-label="Model">
|
||||
<button
|
||||
v-for="opt in engineOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="display-rail__seg"
|
||||
class="seg"
|
||||
:aria-pressed="form.engine===opt.value"
|
||||
:aria-disabled="opt.disabled || undefined"
|
||||
:disabled="opt.disabled"
|
||||
:title="opt.hint || undefined"
|
||||
@click="!opt.disabled && (form.engine=opt.value)"
|
||||
>
|
||||
<span class="block">{{ opt.label }}</span>
|
||||
<span v-if="opt.hint" class="mt-0.5 block text-[11px] font-normal opacity-80">{{ opt.hint }}</span>
|
||||
{{ opt.label }}<span v-if="opt.hint" class="ml-1 text-[11px] opacity-80">· {{ opt.hint }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="display-body text-zinc-400">Model · YuE2</p>
|
||||
<p v-else class="rail-model text-[var(--type-sm)] text-[var(--muted)]">Model · YuE2</p>
|
||||
|
||||
<div class="classic-selects">
|
||||
<select :value="form.mode" class="capitalize" aria-label="Task" @change="onTaskSelect">
|
||||
<option v-for="task in tasks" :key="task" :value="task" class="capitalize">{{ task }}</option>
|
||||
</select>
|
||||
<select v-if="form.mode!=='music'" v-model="form.engine" aria-label="Model">
|
||||
<option v-for="opt in engineOptions" :key="opt.value" :value="opt.value" :disabled="opt.disabled">{{ opt.label }}{{ opt.hint ? ' — '+opt.hint : '' }}</option>
|
||||
</select>
|
||||
<span v-else class="flex h-[var(--rail-h)] items-center text-[var(--type-sm)] text-[var(--muted)]">YuE2</span>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,3fr)_minmax(24rem,2fr)] 2xl:gap-8">
|
||||
<section class="min-w-0 space-y-4">
|
||||
<div class="display-viewer relative flex min-h-80 items-center justify-center overflow-hidden border border-white/10 bg-black">
|
||||
<button
|
||||
v-if="selected && xaigen && isStudio"
|
||||
type="button"
|
||||
class="absolute right-3 top-3 z-10 rounded-full border border-white/20 bg-zinc-950/80 p-2 text-zinc-200 outline-none ring-amber-300/50 hover:border-amber-300/50 focus-visible:ring-2"
|
||||
:aria-pressed="!isHidden(selected)"
|
||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
||||
@click.stop="toggleReveal(selected.id)"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||
<path v-if="isHidden(selected)" d="M3 3l18 18M10.5 10.6a2.5 2.5 0 0 0 3 3M9.4 5.1A10.4 10.4 0 0 1 12 4.5c5 0 9.3 3.1 11 7.5a11.6 11.6 0 0 1-4.2 5.1M6.2 6.2A11.5 11.5 0 0 0 1 12c1.2 2.7 3.3 4.9 5.9 6.3A10.5 10.5 0 0 0 12 19.5c1.2 0 2.4-.2 3.5-.6" stroke-linecap="round"/>
|
||||
<template v-else>
|
||||
<path d="M2 12s3.5-7.5 10-7.5S22 12 22 12s-3.5 7.5-10 7.5S2 12 2 12Z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</template>
|
||||
</svg>
|
||||
</button>
|
||||
<p v-if="selected && isHidden(selected)" class="text-zinc-400">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
||||
<div v-else-if="selected?.kind==='track'" class="w-full max-w-xl px-6 py-4">
|
||||
<AudioPlayer :src="`/api/library/tracks/${selected.id}/audio`" :download-name="`${selected.name || 'track'}.${selected.ext || 'wav'}`" :label="selected.name || 'Track'" />
|
||||
</div>
|
||||
<video v-else-if="selected?.kind==='clip'" :key="selected.id" :src="`/api/library/clips/${selected.id}/video`" controls class="max-h-[65vh] w-full object-contain" />
|
||||
<img v-else-if="selected" :src="stillUrl(selected.id)" class="max-h-[65vh] max-w-full object-contain" alt="Current output">
|
||||
<p v-else class="text-zinc-500">Select a result to view it here</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selected" class="flex flex-wrap items-start gap-3">
|
||||
<button v-if="selected.kind!=='track'" class="rounded-full border border-white/20 px-4 py-2 text-sm outline-none ring-amber-300/40 focus-visible:ring-2" @click="restore(selected)">Use as input</button>
|
||||
<button v-if="selected.kind==='still'" class="rounded-full border border-white/20 px-4 py-2 text-sm outline-none ring-amber-300/40 focus-visible:ring-2" @click="Object.assign(form,useAsSource(form,selected))">Use as source still</button>
|
||||
<button v-if="selected.kind==='still'" class="rounded-full border border-white/20 px-4 py-2 text-sm outline-none ring-amber-300/40 focus-visible:ring-2" @click="Object.assign(form,useAsHero(form,selected))">Use as hero</button>
|
||||
<button v-if="selected.kind!=='track'" class="rounded-full border border-white/20 px-4 py-2 text-sm outline-none ring-amber-300/40 focus-visible:ring-2" @click="detailsOpen=true">Details</button>
|
||||
<button class="rounded-full border border-red-400/40 px-4 py-2 text-sm text-red-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="deleteOutput">Delete</button>
|
||||
<VideoUpscaleControl v-if="selected.kind==='clip'" :clip-id="selected.id" queue-only @complete="refresh" />
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-0 max-w-full gap-2 overflow-x-auto" aria-label="Current family outputs">
|
||||
<div
|
||||
v-for="o in family"
|
||||
:key="o.id"
|
||||
class="relative shrink-0 rounded-lg border p-1"
|
||||
:class="selected?.id===o.id ? 'border-amber-300' : 'border-white/10'"
|
||||
>
|
||||
<button type="button" class="block outline-none ring-amber-300/40 focus-visible:ring-2" @click="selectOutput(o)">
|
||||
<span v-if="isHidden(o)" class="flex h-20 w-24 items-center justify-center p-1 text-xs text-zinc-400">{{ o.kind==='track' ? 'Audio hidden' : o.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</span>
|
||||
<span v-else-if="o.kind==='track'" class="flex h-20 w-24 items-center justify-center rounded bg-zinc-900 text-xs text-amber-100">Audio</span>
|
||||
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" class="h-20 w-24 rounded object-cover" alt="Family output">
|
||||
</button>
|
||||
<button
|
||||
v-if="xaigen && isStudio"
|
||||
type="button"
|
||||
class="absolute right-1 top-1 rounded-md border border-white/20 bg-zinc-950/80 p-1 text-zinc-200 outline-none ring-amber-300/50 focus-visible:ring-2"
|
||||
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
||||
:aria-pressed="!isHidden(o)"
|
||||
@click.stop="toggleReveal(o.id)"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||
<path v-if="isHidden(o)" d="M3 3l18 18M10.5 10.6a2.5 2.5 0 0 0 3 3M9.4 5.1A10.4 10.4 0 0 1 12 4.5c5 0 9.3 3.1 11 7.5a11.6 11.6 0 0 1-4.2 5.1M6.2 6.2A11.5 11.5 0 0 0 1 12c1.2 2.7 3.3 4.9 5.9 6.3A10.5 10.5 0 0 0 12 19.5c1.2 0 2.4-.2 3.5-.6" stroke-linecap="round"/>
|
||||
<template v-else>
|
||||
<path d="M2 12s3.5-7.5 10-7.5S22 12 22 12s-3.5 7.5-10 7.5S2 12 2 12Z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</template>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="form.mode!=='music'" aria-label="Source" class="display-card">
|
||||
<div class="mb-3 flex flex-wrap items-start justify-between gap-3">
|
||||
<h2 class="text-xs uppercase tracking-wider text-zinc-400">Source</h2>
|
||||
<div v-if="isStudio" class="flex flex-wrap gap-4 text-sm text-zinc-300" aria-label="Identity locks">
|
||||
<label class="flex items-start gap-2"><input v-model="form.lockFace" type="checkbox" class="mt-1 accent-amber-400"><span>Lock face<span class="block text-xs text-zinc-500">Hero encode + lock on Klein / Krea stills</span></span></label>
|
||||
<label class="flex items-start gap-2"><input v-model="form.lockOutfit" type="checkbox" class="mt-1 accent-amber-400"><span>Lock outfit<span class="block text-xs text-zinc-500">Hero encode + lock on Klein / Krea stills</span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="display-grid" :class="form.mode==='music' ? 'display-grid--music' : ''">
|
||||
<aside v-if="form.mode!=='music'" class="display-col space-y-2">
|
||||
<Studio2AssetSlot label="Hero" :asset="stills.find((a:any)=>a.id===form.identityStillId)" :hidden="slotHidden(form.identityStillId)" upload :disabled="uploading" @pick="picker='identityStillId'" @upload="upload($event,'hero')" />
|
||||
<Studio2AssetSlot :label="form.mode==='extend'?'Previous clip':'Start still'" :asset="sourceSlot" :hidden="slotHidden(form.mode==='extend'?form.startClipId:form.imageAId)" upload :disabled="uploading" @pick="picker=form.mode==='extend'?'startClipId':'imageAId'" @upload="upload($event,'source')" />
|
||||
<Studio2AssetSlot v-if="form.mode==='compose'" label="Second still" :asset="stills.find((a:any)=>a.id===form.imageBId)" :hidden="slotHidden(form.imageBId)" @pick="picker='imageBId'" />
|
||||
<Studio2AssetSlot v-if="form.mode==='refine'" label="Mask" :asset="stills.find((a:any)=>a.id===form.maskId)" :hidden="slotHidden(form.maskId)" @pick="picker='maskId'" />
|
||||
<Studio2AssetSlot v-if="video" label="End still" :asset="stills.find((a:any)=>a.id===form.endStillId)" :hidden="slotHidden(form.endStillId)" @pick="picker='endStillId'" />
|
||||
<div class="identity-slots" aria-label="Identity locks">
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-[var(--amber)]">Lock face</label>
|
||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
|
||||
<p class="hint">hero encode + lock text — not a MiniMax face/clothes split on stills</p>
|
||||
</div>
|
||||
<label v-if="form.mode==='refine'" class="mt-3 block text-sm">Mask still (white = change). On-canvas mask comes later.
|
||||
<select v-model="form.maskId" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="">Choose mask still</option><option v-for="a in stills" :key="a.id" :value="a.id">{{ assetLabel(a) }}</option></select>
|
||||
<label v-if="form.mode==='refine'" class="block text-[var(--type-sm)]">Mask still (white = change). On-canvas mask comes later.
|
||||
<select v-model="form.maskId" class="display-field mt-1"><option value="">Choose mask still</option><option v-for="a in stills" :key="a.id" :value="a.id">{{ assetLabel(a) }}</option></select>
|
||||
</label>
|
||||
<p v-if="uploading || uploadMessage" role="status" class="mt-2 text-sm text-amber-200">{{ uploading?'Uploading image…':uploadMessage }}</p>
|
||||
</section>
|
||||
|
||||
<details v-if="video" class="rounded-xl border border-white/10 p-3"><summary>Add Guide stills</summary>
|
||||
<p v-if="uploading || uploadMessage" role="status" class="text-[var(--type-sm)] text-[var(--amber)]">{{ uploading?'Uploading image…':uploadMessage }}</p>
|
||||
<details v-if="video" class="rounded-[var(--radius)] border border-[var(--line)] p-2"><summary class="text-[var(--type-sm)]">Add Guide stills</summary>
|
||||
<div v-for="(g,i) in form.guides" :key="i" class="mt-2 flex gap-2">
|
||||
<Studio2AssetSlot label="Guide" :asset="stills.find((a:any)=>a.id===g.stillId)" :hidden="slotHidden(g.stillId)" @pick="picker='guide:'+i" />
|
||||
<input v-model.number="g.frame" type="number" min="1" aria-label="Guide frame" class="w-20 bg-zinc-800 p-2">
|
||||
<input v-model.number="g.frame" type="number" min="1" aria-label="Guide frame" class="display-field w-20">
|
||||
<button type="button" @click="form.guides.splice(i,1)">Remove</button>
|
||||
</div>
|
||||
<button type="button" class="mt-3 text-amber-200" @click="form.guides.push({stillId:'',frame:24})">Add guide</button>
|
||||
<button type="button" class="mt-3 text-[var(--amber)]" @click="form.guides.push({stillId:'',frame:24})">Add guide</button>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
<section class="display-col min-w-0">
|
||||
<div class="display-viewer">
|
||||
<button
|
||||
v-if="selected && xaigen"
|
||||
type="button"
|
||||
class="eye-viewer"
|
||||
:aria-pressed="!isHidden(selected)"
|
||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
||||
@click.stop="toggleReveal(selected.id)"
|
||||
>
|
||||
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="20" height="20">
|
||||
</button>
|
||||
<p v-if="selected && isHidden(selected)">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
||||
<div v-else-if="selected?.kind==='track'" class="w-full max-w-xl px-6 py-4">
|
||||
<AudioPlayer :src="`/api/library/tracks/${selected.id}/audio`" :download-name="`${selected.name || 'track'}.${selected.ext || 'wav'}`" :label="selected.name || 'Track'" />
|
||||
</div>
|
||||
<video v-else-if="selected?.kind==='clip'" :key="selected.id" :src="`/api/library/clips/${selected.id}/video`" controls class="max-h-[65vh] w-full object-contain" />
|
||||
<img v-else-if="selected" :src="stillUrl(selected.id)" class="max-h-[65vh] max-w-full object-contain" alt="Current output">
|
||||
<p v-else>Select a result to view it here</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selected" class="mt-3 flex flex-wrap items-start gap-2">
|
||||
<button v-if="selected.kind!=='track'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="restore(selected)">Use as input</button>
|
||||
<button v-if="selected.kind==='still'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="Object.assign(form,useAsSource(form,selected))">Use as source still</button>
|
||||
<button v-if="selected.kind==='still'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="Object.assign(form,useAsHero(form,selected))">Use as hero</button>
|
||||
<button v-if="selected.kind!=='track'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="detailsOpen=true">Details</button>
|
||||
<button type="button" class="seg h-8 text-[var(--type-sm)] text-red-200" @click="deleteOutput">Delete</button>
|
||||
<VideoUpscaleControl v-if="selected.kind==='clip'" :clip-id="selected.id" queue-only @complete="refresh" />
|
||||
</div>
|
||||
|
||||
<div class="display-strip" aria-label="Current family outputs">
|
||||
<div
|
||||
v-for="o in family"
|
||||
:key="o.id"
|
||||
class="display-tile"
|
||||
:data-active="selected?.id===o.id ? 'true' : undefined"
|
||||
>
|
||||
<button type="button" class="block h-full w-full outline-none" @click="selectOutput(o)">
|
||||
<span v-if="isHidden(o)" class="flex h-full items-center justify-center p-1 text-[10px] text-[var(--muted)]">{{ o.kind==='track' ? 'Audio hidden' : o.role==='input' ? 'Hidden' : 'Hidden' }}</span>
|
||||
<span v-else-if="o.kind==='track'" class="flex h-full items-center justify-center text-[10px] text-[var(--amber)]">Audio</span>
|
||||
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" class="h-full w-full object-cover" alt="Family output">
|
||||
</button>
|
||||
<button
|
||||
v-if="xaigen"
|
||||
type="button"
|
||||
class="eye-tile"
|
||||
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
||||
:aria-pressed="!isHidden(o)"
|
||||
@click.stop="toggleReveal(o.id)"
|
||||
>
|
||||
<img :src="isHidden(o) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="12" height="12">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="min-w-0 space-y-4">
|
||||
<section class="display-col min-w-0 space-y-3">
|
||||
<template v-if="form.mode==='music'">
|
||||
<p class="text-sm text-zinc-400">YuE2 · lyrics-to-song · plan → synthesize → decode</p>
|
||||
<label class="block text-sm">Genre / style tags
|
||||
<textarea v-model="music.tags" rows="3" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2" :placeholder="tagsPlaceholder" />
|
||||
<p class="text-[var(--type-sm)] text-[var(--muted)]">YuE2 · lyrics-to-song · plan → synthesize → decode</p>
|
||||
<label class="block text-[var(--type-sm)]">Genre / style tags
|
||||
<textarea v-model="music.tags" rows="3" class="display-field mt-1" :placeholder="tagsPlaceholder" />
|
||||
</label>
|
||||
<label class="block text-sm">Lyrics
|
||||
<textarea v-model="music.lyrics" rows="8" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2" :placeholder="lyricsPlaceholder" />
|
||||
<label class="block text-[var(--type-sm)]">Lyrics
|
||||
<textarea v-model="music.lyrics" rows="8" class="display-field mt-1" :placeholder="lyricsPlaceholder" />
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<label class="block text-sm">Target length (sec)
|
||||
<input v-model.number="music.duration" type="number" min="15" max="150" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2">
|
||||
<span class="mt-1 block text-[11px] text-zinc-500">Caps semantic tokens (~25/s). Actual length follows the model end token — not an FFmpeg trim.</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="block text-[var(--type-sm)]">Target length (sec)
|
||||
<input v-model.number="music.duration" type="number" min="15" max="150" class="display-field mt-1">
|
||||
<span class="mt-1 block text-[11px] text-[var(--muted)]">Caps semantic tokens (~25/s). Actual length follows the model end token — not an FFmpeg trim.</span>
|
||||
</label>
|
||||
<label class="block text-sm">Seed
|
||||
<input v-model="music.seed" placeholder="random" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2">
|
||||
<label class="block text-[var(--type-sm)]">Seed
|
||||
<input v-model="music.seed" placeholder="random" class="display-field mt-1">
|
||||
</label>
|
||||
</div>
|
||||
<label class="block text-sm">Track name (optional)
|
||||
<input v-model="music.name" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2" placeholder="Optional track name">
|
||||
<label class="block text-[var(--type-sm)]">Track name (optional)
|
||||
<input v-model="music.name" class="display-field mt-1" placeholder="Optional track name">
|
||||
</label>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="block">Prompt<textarea v-model="form.promptSections.action" rows="4" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2" placeholder="Describe the image or the shot." /></label>
|
||||
<label class="block text-[var(--type-sm)]">Prompt<textarea v-model="form.promptSections.action" rows="5" class="display-field mt-1" placeholder="Describe the image or the shot." /></label>
|
||||
<Studio2PromptControls v-if="video" v-model="form.promptSections" />
|
||||
<Studio2StyleChips v-else v-model="form.imageStyles" />
|
||||
<label v-if="video" class="block">Extra constraints (optional) — do not repeat the Prompt. Example: keep the black glasses, no extra people.
|
||||
<textarea v-model="form.promptSections.locks" rows="2" class="mt-1 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2" />
|
||||
<label v-if="video" class="block text-[var(--type-sm)]">Extra constraints (optional) — do not repeat the Prompt. Example: keep the black glasses, no extra people.
|
||||
<textarea v-model="form.promptSections.locks" rows="2" class="display-field mt-1" />
|
||||
</label>
|
||||
<details v-if="video || form.mode==='edit'" class="rounded-xl border border-white/10 p-3">
|
||||
<summary>{{ video?'Queue more shots':'Queue extra edit passes' }}</summary>
|
||||
<p class="mt-2 text-xs text-zinc-400">Each blank-line-separated block is another job. Hero and settings stay. Press Enter twice between shots. A wrapped sentence is still one shot.{{ video?'':' Each extra pass edits the previous result with Klein.' }}</p>
|
||||
<textarea v-model="batchText" rows="5" class="mt-2 w-full rounded-xl bg-zinc-900 p-3 outline-none ring-amber-300/40 focus:ring-2" placeholder="First additional prompt Next additional prompt" />
|
||||
<details v-if="video || form.mode==='edit'" class="rounded-[var(--radius)] border border-[var(--line)] p-2">
|
||||
<summary class="text-[var(--type-sm)]">{{ video?'Queue more shots':'Queue extra edit passes' }}</summary>
|
||||
<p class="mt-2 text-[11px] text-[var(--muted)]">Each blank-line-separated block is another job. Hero and settings stay. Press Enter twice between shots. A wrapped sentence is still one shot.{{ video?'':' Each extra pass edits the previous result with Klein.' }}</p>
|
||||
<textarea v-model="batchText" rows="5" class="display-field mt-2" placeholder="First additional prompt Next additional prompt" />
|
||||
</details>
|
||||
<button type="button" class="text-sm text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="settingsOpen=true">Generation settings</button>
|
||||
<p v-if="!video && form.engine==='krea'" class="text-sm text-zinc-400">Krea does not attach the hero identity reference.</p>
|
||||
<p v-if="form.mode==='extend'" class="text-sm text-amber-200">{{ form.startClipId ? 'Extend · '+(clips.find((c:any)=>c.id===form.startClipId)?.name || form.startClipId)+' · last frame' : 'Select the clip in the family strip or Previous clip.' }}</p>
|
||||
<button type="button" class="text-[var(--type-sm)] text-[var(--amber)] outline-none focus-visible:underline" @click="settingsOpen=true">Generation settings</button>
|
||||
<p v-if="!video && form.engine==='krea'" class="text-[var(--type-sm)] text-[var(--muted)]">Krea does not attach the hero identity reference.</p>
|
||||
<p v-if="form.mode==='extend'" class="text-[var(--type-sm)] text-[var(--amber)]">{{ form.startClipId ? 'Extend · '+(clips.find((c:any)=>c.id===form.startClipId)?.name || form.startClipId)+' · last frame' : 'Select the clip in the family strip or Previous clip.' }}</p>
|
||||
</template>
|
||||
|
||||
<p v-if="error" role="alert" class="text-sm text-red-300">{{ error }}</p>
|
||||
<button type="button" :disabled="submitting" class="display-cta w-full p-4 outline-none disabled:opacity-50" @click="queue">{{ submitting?'Queuing…':'Queue' }}</button>
|
||||
<p v-if="error" role="alert" class="text-[var(--type-sm)] text-red-300">{{ error }}</p>
|
||||
<button type="button" :disabled="submitting" class="display-cta" @click="queue">{{ submitting?'Queuing…':'Queue' }}</button>
|
||||
|
||||
<h2 class="text-sm font-semibold text-zinc-300">Job queue</h2>
|
||||
<div v-for="job in upscales.filter(j=>j.folderId===form.folderId && ['queued','running'].includes(j.status))" :key="job.id" class="space-y-2 rounded-xl border border-amber-300/25 bg-zinc-900 p-4 text-sm" role="status">
|
||||
<div class="flex justify-between gap-3"><strong>Video upscale</strong><span class="capitalize text-amber-200">{{ job.status }}</span></div>
|
||||
<p class="truncate text-zinc-300">{{ job.name }}</p>
|
||||
<p class="text-xs text-zinc-400">{{ job.width?job.width+' × '+job.height+' · ':'' }}{{ Math.round((job.elapsedMs||0)/1000) }}s elapsed</p>
|
||||
<progress class="h-2 w-full accent-amber-300" max="100" :value="job.progress || 0" />
|
||||
<p class="text-xs text-zinc-400">{{ job.message }}</p>
|
||||
<h2 class="text-[var(--type-sm)] font-semibold text-[var(--muted)]">Job queue</h2>
|
||||
<div v-for="job in upscales.filter(j=>j.folderId===form.folderId && ['queued','running'].includes(j.status))" :key="job.id" class="space-y-2 rounded-[var(--radius)] border border-[color-mix(in_srgb,var(--amber)_25%,transparent)] bg-[var(--bg-slot)] p-3 text-[var(--type-sm)]" role="status">
|
||||
<div class="flex justify-between gap-3"><strong>Video upscale</strong><span class="capitalize text-[var(--amber)]">{{ job.status }}</span></div>
|
||||
<p class="truncate">{{ job.name }}</p>
|
||||
<p class="text-[11px] text-[var(--muted)]">{{ job.width?job.width+' × '+job.height+' · ':'' }}{{ Math.round((job.elapsedMs||0)/1000) }}s elapsed</p>
|
||||
<progress class="h-2 w-full accent-[var(--amber)]" max="100" :value="job.progress || 0" />
|
||||
<p class="text-[11px] text-[var(--muted)]">{{ job.message }}</p>
|
||||
</div>
|
||||
<div v-for="job in musicJobs" :key="job.id" class="space-y-2 rounded-xl border border-amber-300/25 bg-zinc-900 p-3 text-sm" aria-live="polite" role="status">
|
||||
<div v-for="job in musicJobs" :key="job.id" class="space-y-2 rounded-[var(--radius)] border border-[color-mix(in_srgb,var(--amber)_25%,transparent)] bg-[var(--bg-slot)] p-3 text-[var(--type-sm)]" aria-live="polite" role="status">
|
||||
<div class="flex justify-between gap-3">
|
||||
<strong class="truncate">{{ job.name || 'YuE2' }}</strong>
|
||||
<span class="capitalize text-amber-200">{{ job.live?.status || job.status }}</span>
|
||||
<span class="capitalize text-[var(--amber)]">{{ job.live?.status || job.status }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-400">YuE2 · target {{ job.duration || '—' }}s<span v-if="job.live?.message"> · {{ job.live.message }}</span></p>
|
||||
<progress v-if="job.live?.progress" class="h-2 w-full accent-amber-300" max="100" :value="job.live.progress || 0" />
|
||||
<p class="text-[11px] text-[var(--muted)]">YuE2 · target {{ job.duration || '—' }}s<span v-if="job.live?.message"> · {{ job.live.message }}</span></p>
|
||||
<progress v-if="job.live?.progress" class="h-2 w-full accent-[var(--amber)]" max="100" :value="job.live.progress || 0" />
|
||||
<p v-if="job.lastError || job.live?.error" role="alert" class="text-red-300">{{ job.lastError || job.live?.error }}</p>
|
||||
</div>
|
||||
<Studio2JobProgress v-for="job in benchJobs" :key="job.id" :job="job" :now="now" @select="focusJob(job)" @cancel="cancel(job)" />
|
||||
<button v-if="folderJobs.some(j=>['complete','failed','cancelled'].includes(j.state)) || musicJobs.some(j=>['complete','done','error','cancelled'].includes(j.status))" type="button" class="text-sm text-zinc-400 outline-none ring-amber-300/40 focus-visible:ring-2" @click="clearFinished">Clear finished</button>
|
||||
<button v-if="folderJobs.some(j=>['complete','failed','cancelled'].includes(j.state)) || musicJobs.some(j=>['complete','done','error','cancelled'].includes(j.status))" type="button" class="text-[var(--type-sm)] text-[var(--muted)] outline-none focus-visible:underline" @click="clearFinished">Clear finished</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-if="settingsOpen || detailsOpen" class="fixed inset-0 z-50 bg-black/70" @click.self="settingsOpen=false;detailsOpen=false">
|
||||
<aside class="absolute inset-y-0 right-0 w-full max-w-md overflow-auto border-l border-white/15 bg-zinc-950 p-6">
|
||||
<button type="button" class="float-right outline-none ring-amber-300/40 focus-visible:ring-2" @click="settingsOpen=false;detailsOpen=false">Close</button>
|
||||
<aside class="absolute inset-y-0 right-0 w-full max-w-md overflow-auto border-l border-[var(--line)] bg-[var(--bg)] p-6">
|
||||
<button type="button" class="float-right outline-none focus-visible:underline" @click="settingsOpen=false;detailsOpen=false">Close</button>
|
||||
<template v-if="settingsOpen">
|
||||
<h2 class="mb-6 text-xl font-semibold">Generation settings</h2>
|
||||
<AspectRatioPick v-model="form.settings.aspect" heading="Aspect" :options="aspectOptions" />
|
||||
<p class="my-3 text-sm text-zinc-400">{{ resolvedSize.width }} × {{ resolvedSize.height }} · fit with padding, never stretch</p>
|
||||
<p class="my-3 text-[var(--type-sm)] text-[var(--muted)]">{{ resolvedSize.width }} × {{ resolvedSize.height }} · fit with padding, never stretch</p>
|
||||
<label v-for="key in ['steps','cfg',...(video?['duration','fps']:[])]" :key="key" class="mb-4 block capitalize">{{ key }}
|
||||
<input v-model.number="form.settings[key]" type="number" class="mt-1 w-full rounded bg-zinc-800 p-2 outline-none ring-amber-300/40 focus:ring-2">
|
||||
<input v-model.number="form.settings[key]" type="number" class="display-field mt-1">
|
||||
</label>
|
||||
<label class="mb-4 block">Seed · {{ form.settings.seedMode==='locked'?'Locked':'Random each job' }}
|
||||
<input :value="form.settings.seed" type="number" min="0" max="2147483647" class="mt-1 w-full rounded bg-zinc-800 p-2 outline-none ring-amber-300/40 focus:ring-2" @input="form.settings.seed=Number(($event.target as HTMLInputElement).value);form.settings.seedMode='locked'">
|
||||
<input :value="form.settings.seed" type="number" min="0" max="2147483647" class="display-field mt-1" @input="form.settings.seed=Number(($event.target as HTMLInputElement).value);form.settings.seedMode='locked'">
|
||||
</label>
|
||||
<button type="button" class="mb-4 text-sm text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="form.settings.seedMode='random';form.settings.seed=randomSeed()">Random</button>
|
||||
<label v-if="video"><input v-model="form.settings.turbo" type="checkbox" class="accent-amber-400"> Turbo</label>
|
||||
<button type="button" class="mb-4 text-[var(--type-sm)] text-[var(--amber)] outline-none focus-visible:underline" @click="form.settings.seedMode='random';form.settings.seed=randomSeed()">Random</button>
|
||||
<label v-if="video"><input v-model="form.settings.turbo" type="checkbox" class="accent-[var(--amber)]"> Turbo</label>
|
||||
<LoraSelect v-model="form.settings.loraStack" class="mt-5" :names="pickerLoras" :use="currentLoraUse" :clip-strength="true" filename-subtitle :aliases="{...loraNames.aliases,...library.loraAliases}" :triggers="{...loraNames.triggers,...library.loraTriggers}" missing-label="No cached LoRAs yet — Refresh to check Comfy" @insert-trigger="form.promptSections.action += ' '+$event" />
|
||||
<button type="button" class="mt-2 text-sm text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="loadLoras(true)">Refresh LoRAs</button>
|
||||
<button type="button" class="mt-2 text-[var(--type-sm)] text-[var(--amber)] outline-none focus-visible:underline" @click="loadLoras(true)">Refresh LoRAs</button>
|
||||
<details class="mt-6"><summary>Compiled prompt</summary>
|
||||
<pre class="whitespace-pre-wrap text-xs text-zinc-400">{{ compiled }}</pre>
|
||||
<p v-if="!video" class="mt-3 text-xs text-zinc-400">Negative: {{ stylePrompt(form.imageStyles,true) || 'None' }}</p>
|
||||
<pre class="whitespace-pre-wrap text-xs text-[var(--muted)]">{{ compiled }}</pre>
|
||||
<p v-if="!video" class="mt-3 text-xs text-[var(--muted)]">Negative: {{ stylePrompt(form.imageStyles,true) || 'None' }}</p>
|
||||
</details>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -264,12 +238,12 @@
|
||||
</div>
|
||||
|
||||
<div v-if="picker" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6" @click.self="picker=''">
|
||||
<section role="dialog" aria-modal="true" aria-label="Choose source asset" class="max-h-[80vh] w-full max-w-4xl overflow-auto rounded-2xl border border-white/15 bg-zinc-950 p-5">
|
||||
<section role="dialog" aria-modal="true" aria-label="Choose source asset" class="max-h-[80vh] w-full max-w-4xl overflow-auto rounded-[var(--radius)] border border-[var(--line)] bg-[var(--bg)] p-5">
|
||||
<div class="mb-4 flex justify-between"><h2>Choose {{ picker==='identityStillId'?'hero':'source' }}</h2><button type="button" @click="picker=''">Close</button></div>
|
||||
<button type="button" class="mb-4 text-sm text-zinc-400" @click="pickAsset(null)">Clear slot</button>
|
||||
<button type="button" class="mb-4 text-[var(--type-sm)] text-[var(--muted)]" @click="pickAsset(null)">Clear slot</button>
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-5">
|
||||
<button v-for="a in pickerAssets" :key="a.id" type="button" class="rounded-xl border border-white/15 p-2 text-left outline-none ring-amber-300/40 focus-visible:ring-2" @click="pickAsset(a)">
|
||||
<span v-if="isHidden(a)" class="flex h-24 items-center text-xs text-zinc-400">Still loaded · preview hidden</span>
|
||||
<button v-for="a in pickerAssets" :key="a.id" type="button" class="rounded-[var(--radius)] border border-[var(--line)] p-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--amber)_50%,transparent)]" @click="pickAsset(a)">
|
||||
<span v-if="isHidden(a)" class="flex h-24 items-center text-xs text-[var(--muted)]">Still loaded · preview hidden</span>
|
||||
<img v-else :src="a.kind==='clip'?'/api/library/clips/'+a.id+'/thumb':stillUrl(a.id)" class="h-24 w-full object-contain" alt="Source option">
|
||||
<span class="mt-2 block truncate text-xs">{{ assetLabel(a) }}</span>
|
||||
</button>
|
||||
@@ -293,7 +267,8 @@ import {randomSeed} from '~/shared/studio2/seed.mjs'
|
||||
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
|
||||
|
||||
const xaigen=useRuntimeConfig().public.studio==='xaigen'
|
||||
const {displayMode,isStudio,isClassic}=useDisplayMode()
|
||||
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
||||
const {displayMode,isClassic}=useDisplayMode()
|
||||
const classicHideInput=ref(true)
|
||||
const classicHideOutput=ref(true)
|
||||
const picker=ref('')
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f3f1ea" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 3l18 18"/>
|
||||
<path d="M10.6 10.6A3 3 0 0 0 12 15a3 3 0 0 0 2.4-4.4"/>
|
||||
<path d="M9.9 5.1A11 11 0 0 1 12 5c6.5 0 10 7 10 7a18 18 0 0 1-3.2 3.8"/>
|
||||
<path d="M6.1 6.1C3.7 7.8 2 12 2 12s3.5 7 10 7a10.5 10.5 0 0 0 4.2-.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 441 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f3f1ea" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 303 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f3f1ea" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="4" y="11" width="16" height="10" rx="2"/>
|
||||
<path d="M8 11V8a4 4 0 0 1 8 0v3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 297 B |
Reference in New Issue
Block a user