Files
ComfyAssets-ComfyUI_PromptM…/docs/concepts/prompt-variant-timeline.html
Vito caa9649f32 feat: reuse positive and negative prompt combinations (#195)
* feat(db): add pair schema and stage restore migrations for #193

* feat(database): persist reusable ordered prompt pairs (#193)

* feat(api): expose reusable pairs and atomic metadata enrichment (#193)

* feat(metadata): resolve canonical prompt pairs with bounded provenance (#193)

* feat(scan): enrich image pairs without guessing or duplicate links (#193)

* fix(database): preserve pair tags during global maintenance (#193)

* feat(tracking): enrich live image pairs and cancel scans safely (#193)

* feat(nodes): encode paired prompts and count cached recipe runs (#193)

* fix(database): validate restored pair ownership and provenance (#193)

* feat(admin): report pair scan outcomes and cancellation (#193)

* feat(ui): add paired prompt editor and explicit recipe library for #193

* fix(autocomplete): include reusable pair metadata vocabulary (#193)

* fix(pairs): honor recent sorting and conservative scan outcomes (#193)

* fix(ui): protect paired draft saves and clarify negative variants (#193)

* fix(metadata): preserve bounded paired execution evidence (#193)

* fix(tracking): follow actual pair output roles and prefer execution evidence (#193)

* docs(pairs): document live validation and retain renderer screenshots (#193)

* test(api): inject disposable databases before API startup (#194)

* test(api): canonicalize portable temporary gallery roots (#194)

* docs(ui): prototype branching negative variant history (#193)

* docs(ui): show selected negative beneath positive in timeline concept (#193)

* docs: retain approved timeline for later gallery redesign (#193)

* test(db): close restore fixture connections before cleanup (#194)

* test(db): reuse existing handle guards in all pair fixtures (#194)

* chore: clarify rollback test and intentional exception handling (#193)
2026-10-17 08:00:42 -07:00

117 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="referrer" content="no-referrer">
<title>Prompt history · ComfyAssets prototype</title>
<link rel="stylesheet" href="/toolkit/controls.css">
<style>
*{box-sizing:border-box}body{margin:0;--bg-color:#18191c}body.ca-tools{--page:#191a1d;--green:#58d594;--green-ink:#71e6a8;--red:#f05763;--red-ink:#ff9ba3;background:var(--page);font-size:14px;line-height:1.55}body[data-ca-theme=light]{--page:#f1f2f5;--green:#168553;--green-ink:#126d44;--red:#d52430;--red-ink:#a91c29}button,a,input,select,textarea{touch-action:manipulation}button:focus-visible,a:focus-visible{outline:3px solid var(--ca-focus);outline-offset:3px}a{color:var(--ca-focus)}[hidden]{display:none!important}.skip{position:fixed;top:8px;left:12px;z-index:5;clip-path:inset(50%);width:1px;height:1px;overflow:hidden}.skip:focus{clip-path:none;width:auto;height:auto;padding:8px;background:var(--ca-surface)}.topbar{border-bottom:1px solid var(--ca-line);background:var(--ca-surface)}.topbar-inner{max-width:1440px;margin:auto;padding:16px 30px;display:flex;align-items:center;gap:12px}.brand{font-size:15px;font-weight:650}.brand-icon{color:var(--ca-focus);font-size:22px}.muted{color:var(--ca-muted)}.badge{font-size:11px;border:1px solid var(--ca-line);border-radius:5px;padding:3px 8px;display:inline-flex;gap:6px;align-items:center;white-space:nowrap}.badge.blue{color:var(--ca-focus);border-color:var(--ca-accent)}.topbar .demo{margin-inline-start:auto}.theme-label{display:flex;align-items:center;gap:8px;font-size:12px}main{max-width:1440px;margin:auto;padding:28px 30px 42px}.page-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:20px}h1{font-size:27px;line-height:1.25;font-weight:600;letter-spacing:-.6px;margin:0 0 6px}h2{font-size:16px;font-weight:600;margin:0}h3{font-size:14px;font-weight:600;margin:0 0 10px}p{margin:0 0 10px}.page-heading p{margin:0}.sample-note{max-width:350px;font-size:12px;text-align:right;color:var(--ca-muted)}.card{border:1px solid var(--ca-line);border-radius:9px;background:var(--ca-surface);overflow:hidden}.positive-summary{padding:20px 24px;border-bottom:1px solid var(--ca-line)}.prompt-summary-row{display:flex;gap:16px;align-items:flex-start}.negative-summary{margin:8px 0 16px}.negative-summary .prompt-summary-row{padding-top:14px;border-top:1px solid var(--ca-line)}.negative-summary .prompt-summary-row+.prompt-summary-row{margin-top:12px}.negative-symbol{color:var(--red-ink);background:color-mix(in srgb,var(--red) 10%,transparent);border:1px solid var(--red)}.negative-summary .eyebrow{color:var(--red-ink)}.negative-summary .negative-text{white-space:pre-wrap;overflow-wrap:anywhere;font-size:16px;line-height:1.7;margin:0}.positive-summary>.stats{margin-inline-start:46px}.role-symbol{display:grid;place-items:center;width:30px;height:30px;border-radius:50%;font-size:20px;font-weight:600;flex-shrink:0}.positive-symbol{color:var(--green-ink);background:color-mix(in srgb,var(--green) 13%,transparent);border:1px solid var(--green)}.positive-summary .content{flex:1;min-width:0}.eyebrow{font-size:11px;letter-spacing:.7px;text-transform:uppercase;color:var(--green-ink);margin-bottom:5px;font-weight:650}.positive-text{font-size:16px;line-height:1.7;max-width:1050px;margin:0 0 10px}.stats{display:flex;align-items:center;gap:18px;flex-wrap:wrap;color:var(--ca-muted);font-size:12px}.stats strong{font-size:14px;color:var(--ca-text);font-weight:600}.history-toolbar{padding:16px 24px;display:flex;justify-content:space-between;gap:14px;align-items:center}.toolbar-actions{display:flex;gap:8px;flex-wrap:wrap}.ca-tools .toolbar-actions button[aria-pressed=true]{background:var(--ca-accent);color:white;border-color:var(--ca-accent)}.history-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;border-top:1px solid var(--ca-line)}.graph-pane{min-width:0;padding:14px 18px 10px}.graph-scroll{overflow-x:auto;border:1px solid var(--ca-line);border-radius:6px;background:var(--ca-field)}.graph{position:relative;min-width:710px;isolation:isolate}.graph svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.axis-label{position:absolute;top:8px;transform:translateX(-50%);font-size:10px;color:var(--ca-muted);font-variant-numeric:tabular-nums}.lane-label{position:absolute;inset-inline-start:18px;font-size:11px;color:var(--ca-muted);max-width:185px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none}.lane-label.positive{color:var(--green-ink)}.graph .node{position:absolute;display:grid;place-items:center;min-width:40px;min-height:40px;width:40px;height:40px;padding:0;transform:translate(-50%,-50%);border:0;background:transparent;border-radius:50%;z-index:1}.node .circle{width:23px;height:23px;display:grid;place-items:center;border:2px solid var(--red);border-radius:50%;background:var(--red);font-size:16px;line-height:1;color:#fff;font-weight:650;box-shadow:0 0 0 5px var(--ca-field)}.node.positive-node .circle{background:var(--green);border-color:var(--green);color:#102b20}.node.saved .circle{background:var(--ca-field);color:var(--red-ink);border-style:dashed}.node.run-node .circle{width:13px;height:13px;font-size:0;border-width:2px;background:var(--ca-field);box-shadow:0 0 0 4px var(--ca-field)}.graph .node:hover{background:color-mix(in srgb,var(--ca-focus) 10%,transparent)}.node[aria-pressed=true] .circle{outline:3px solid var(--ca-focus);outline-offset:5px}.node.dim{opacity:.34}.dot-caption{position:absolute;top:25px;color:var(--ca-muted);font-size:10px;white-space:nowrap;pointer-events:none}.legend{display:flex;gap:15px;align-items:center;flex-wrap:wrap;font-size:11px;color:var(--ca-muted);padding:12px 4px 4px}.legend-item{display:inline-flex;gap:6px;align-items:center}.mini-dot{width:10px;height:10px;border-radius:50%;display:inline-block;background:var(--red)}.mini-dot.green{background:var(--green)}.mini-dot.hollow{background:transparent;border:1.5px dashed var(--red)}.mini-dot.run{width:7px;height:7px;background:transparent;border:1.5px solid var(--red)}.inspector{border-inline-start:1px solid var(--ca-line);padding:20px;background:color-mix(in srgb,var(--ca-field) 40%,var(--ca-surface));min-width:0}.inspector .eyebrow{color:var(--red-ink)}.inspector .eyebrow.overview{color:var(--ca-muted)}.prompt-block{white-space:pre-wrap;overflow-wrap:anywhere;padding:12px;background:var(--ca-field);border:1px solid var(--ca-line);border-radius:5px;font-size:13px;margin:12px 0;max-height:180px;overflow:auto}.inspector p{font-size:12px;color:var(--ca-muted)}.run-list{display:grid;gap:7px;margin:12px 0;padding:0;list-style:none}.run-list button{width:100%;justify-content:space-between;white-space:normal;text-align:start;padding:8px 10px;border-color:var(--ca-line)}.run-list button[aria-pressed=true]{border-color:var(--ca-focus);color:var(--ca-focus)}.run-detail{font-size:11px;color:var(--ca-muted)}.hint{font-size:12px;color:var(--ca-muted)}.compare-pane{display:grid;gap:8px}.diff-terms{font-size:12px;line-height:2;overflow-wrap:anywhere}.diff-terms ins{background:color-mix(in srgb,var(--ca-accent) 18%,transparent);color:var(--ca-focus);text-decoration:none;padding:2px 4px;border-radius:3px}.diff-terms del{color:var(--red-ink);background:color-mix(in srgb,var(--red) 12%,transparent);padding:2px 4px;border-radius:3px}.gallery{border-top:1px solid var(--ca-line);padding:20px 24px 24px}.gallery-heading{display:flex;justify-content:space-between;align-items:center;gap:15px;flex-wrap:wrap;margin-bottom:14px}.gallery-heading h2{margin-bottom:4px}.gallery-heading p{font-size:12px;margin:0;color:var(--ca-muted)}.filters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px}.filters button{font-size:12px;border-color:var(--ca-line);min-height:34px}.filters button[aria-pressed=true]{color:white;background:var(--ca-accent);border-color:var(--ca-accent)}.image-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.ca-tools .image-card{display:block;white-space:normal;min-width:0;padding:0;text-align:start;border-color:var(--ca-line);overflow:hidden;background:var(--ca-field)}.image-card img{display:block;width:100%;aspect-ratio:1.6;object-fit:cover}.image-info{display:block;padding:10px 12px}.image-title{display:block;font-size:12px;margin-bottom:3px}.image-meta{display:flex;gap:6px;justify-content:space-between;color:var(--ca-muted);font-size:10px}.negative-badge{color:var(--red-ink)}.empty{padding:36px;text-align:center;border:1px dashed var(--ca-line);border-radius:6px;color:var(--ca-muted);grid-column:1/-1}.status-line{font-size:12px;min-height:20px;padding:12px 0 0;color:var(--ca-muted)}.footer{display:flex;justify-content:space-between;gap:16px;font-size:11px;color:var(--ca-muted);padding:16px 0}.ca-tools dialog{color:var(--ca-text);background:var(--ca-surface);border:1px solid var(--ca-border);border-radius:9px;padding:24px;width:min(610px,calc(100vw - 32px));max-height:90dvh;overflow:auto}.ca-tools dialog::backdrop{background:#0009}.dialog-header{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:18px}.dialog-header h2{font-size:18px}.form-field{display:grid;gap:6px;margin-bottom:16px;font-size:12px}.form-field input,.form-field textarea{width:100%}.form-field textarea{min-height:140px;resize:vertical}.dialog-actions{display:flex;justify-content:flex-end;gap:8px}.preview-image{width:100%;display:block;border-radius:5px;margin-bottom:15px}.dialog-description{font-size:12px;color:var(--ca-muted)}.ca-tools .mobile-summary{display:none}.ca-tools button.expand{font-size:11px;padding:3px 8px;border-color:var(--ca-line)}
@media(min-width:1500px){.image-grid{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(max-width:1050px){.history-layout{grid-template-columns:minmax(0,1fr) 290px}.image-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.topbar-inner,main{padding-inline:20px}.graph-pane{padding-inline:12px}.inspector{padding:16px}}@media(max-width:780px){.history-layout{grid-template-columns:minmax(0,1fr)}.inspector{border-inline-start:0;border-top:1px solid var(--ca-line)}.history-toolbar{align-items:flex-start;flex-direction:column}.sample-note{display:none}.positive-summary{padding:16px}.history-toolbar,.gallery{padding:16px}.topbar-inner,main{padding-inline:12px}.topbar .crumb{display:none}.image-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.positive-text,.negative-summary .negative-text{font-size:14px}.graph-pane{padding:10px}.footer{flex-direction:column;gap:4px}.theme-label>span{display:none}.page-heading{margin-bottom:16px}.topbar .demo{font-size:10px}h1{font-size:23px}.stats{gap:12px}}@media(pointer:coarse){.graph .node{min-width:44px;min-height:44px}.ca-tools button{min-height:40px}}@media(prefers-reduced-motion:no-preference){button{transition:background-color .12s,border-color .12s}}@media(forced-colors:active){.node .circle{border-color:ButtonText}.node[aria-pressed=true] .circle{outline-color:Highlight}.graph path{stroke:CanvasText!important}}
</style>
</head>
<body class="ca-tools">
<a class="skip" href="#content">Skip to prompt history</a>
<header class="topbar"><div class="topbar-inner"><span class="brand-icon" aria-hidden="true">◈</span><span class="brand">ComfyAssets</span><span class="muted crumb">/ Prompt Manager / Admin</span><span class="badge blue demo">Interactive concept</span><label class="theme-label" for="theme"><span>Theme</span><select id="theme"><option value="dark">Dark</option><option value="light">Light</option><option value="system">System</option></select></label></div></header>
<main id="content" tabindex="-1">
<div class="page-heading"><div><h1>Prompt history</h1><p class="muted">One positive prompt. Every negative experiment.</p></div><p class="sample-note">Sample data and illustrative previews.<br>Everything here stays in this browser tab.</p></div>
<article class="card">
<header class="positive-summary"><div class="prompt-summary-row"><span class="role-symbol positive-symbol" aria-hidden="true">+</span><div class="content"><div class="eyebrow">Positive prompt · unchanged</div><p class="positive-text" id="positive-text">A remote alpine observatory at blue hour, warm light through glass, dramatic mountain peaks, cinematic composition, atmospheric mist, fine detail</p></div><span class="badge">Landscapes</span></div><div class="negative-summary" id="selected-negative"></div><div class="stats" id="stats"></div></header>
<div class="history-toolbar"><div><h2>Negative variants</h2><span class="hint" id="graph-help">Click a red dot to inspect a negative. Small dots are repeat runs. Select a variant for its full run history.</span></div><div class="toolbar-actions"><button id="compare" type="button" aria-pressed="false">Compare negatives</button><button id="new-variant" class="ca-primary" type="button">+ Add negative</button></div></div>
<div class="history-layout"><div class="graph-pane"><div class="graph-scroll"><div class="graph" id="graph" role="group" aria-label="Positive prompt and negative variant timeline" aria-describedby="graph-help"></div></div><div class="legend"><span class="legend-item"><i class="mini-dot green" aria-hidden="true"></i>+ Positive</span><span class="legend-item"><i class="mini-dot" aria-hidden="true"></i>− Negative variant</span><span class="legend-item"><i class="mini-dot run" aria-hidden="true"></i>Run</span><span class="legend-item"><i class="mini-dot hollow" aria-hidden="true"></i>Saved, not run</span></div></div><aside class="inspector" id="inspector" aria-label="Selection details"></aside></div>
<section class="gallery"><div class="gallery-heading"><div><h2>Shared gallery <span class="muted" id="image-count"></span></h2><p id="gallery-description">All images from this positive prompt, together.</p></div><span class="badge">1 positive · 1 gallery</span></div><div class="filters" id="filters" role="group" aria-label="Filter gallery by negative variant"></div><div class="image-grid" id="images"></div></section>
</article>
<div class="status-line" role="status" aria-atomic="true" id="status">Select a negative or a run to explore its images.</div>
<footer class="footer"><span>Concept only · No ComfyUI or database connection</span><span>Time flows left to right · 3 images per sample run</span></footer>
</main>
<dialog id="add-dialog" aria-labelledby="add-title"><div class="dialog-header"><h2 id="add-title">Add a negative variant</h2><button type="button" id="close-add" aria-label="Close add negative dialog">×</button></div><p class="dialog-description">The positive stays unchanged. A saved variant starts with a hollow dot until its first run.</p><form id="add-form"><label class="form-field" for="variant-name">Label (optional)<input id="variant-name" name="name" maxlength="60" placeholder="e.g. Cleaner architecture"></label><label class="form-field" for="variant-text">Negative prompt<textarea id="variant-text" name="negative" maxlength="2000" placeholder="blurry, low quality, watermark…" aria-describedby="negative-help"></textarea></label><p class="hint" id="negative-help">Leave empty to explicitly use no negative prompt. Existing text is reused.</p><div class="dialog-actions"><button type="button" id="cancel-add">Cancel</button><button type="submit" class="ca-primary">Save variant</button></div></form></dialog>
<dialog id="image-dialog" aria-labelledby="image-title"><div class="dialog-header"><h2 id="image-title">Generated image</h2><button type="button" id="close-image" aria-label="Close image preview">×</button></div><div id="image-detail"></div></dialog>
<script type="module">
import {createButton,createScope,mountTheme} from '/toolkit/core.js';
const scope=createScope();mountTheme(document.body,null,scope);
const $=id=>document.getElementById(id);
const variants=[
{id:'n1',name:'Quality baseline',text:'blurry, low quality, low resolution',created:4},
{id:'n2',name:'Clean architecture',text:'blurry, low quality, low resolution, distorted architecture, watermark, text',created:12},
{id:'n3',name:'No negative',text:'',created:24},
{id:'n4',name:'Atmosphere experiment',text:'blurry, low quality, low resolution, flat lighting, oversaturated',created:32},
];
const runs=[{id:'r1',variant:'n1',minute:4,seed:42001},{id:'r2',variant:'n2',minute:12,seed:42004},{id:'r3',variant:'n1',minute:16,seed:42007},{id:'r4',variant:'n3',minute:24,seed:42010}];
let selected=null,selectedRun=null,comparing=false,comparison=[],clock=36,expanded=false;
const time=m=>new Date(Date.UTC(2026,9,17,12,m)).toISOString().slice(11,16);
const getVariant=id=>variants.find(v=>v.id===id);
const variantRuns=id=>runs.filter(r=>r.variant===id);
const sampleImages=()=>runs.flatMap(run=>Array.from({length:3},(_,index)=>({id:`${run.id}-${index}`,run,seed:run.seed+index,index})));
function el(tag,className,text){const node=document.createElement(tag);if(className)node.className=className;if(text!==undefined)node.textContent=text;return node;}
function announce(text){$('status').textContent=text;}
function setTheme(){const dark=$('theme').value==='system'?matchMedia('(prefers-color-scheme: dark)').matches:$('theme').value==='dark';document.body.style.setProperty('--bg-color',dark?'#18191c':'#f1f2f5');}
scope.listen($('theme'),'change',setTheme);scope.listen(matchMedia('(prefers-color-scheme: dark)'),'change',setTheme);setTheme();
function choose(id,run=null){
if(comparing&&id){selectedRun=null;if(comparison.includes(id))comparison=comparison.filter(item=>item!==id);else comparison=comparison.length===2?[comparison[0],id]:[...comparison,id];selected=id;}
else{selected=id;selectedRun=run;if(!id){comparing=false;comparison=[];}}
render();announce(comparing?`${comparison.length} of 2 negatives selected for comparison.`:id?`${getVariant(id).name}${run?`, ${run}`:''}. ${visibleImages().length} images shown.`:'All 12 sample images shown.'.replace('12',String(sampleImages().length)));
}
function visibleImages(){return sampleImages().filter(image=>comparing?comparison.length===0||comparison.includes(image.run.variant):selectedRun?image.run.id===selectedRun:selected?image.run.variant===selected:true);}
function renderGraph(){
const graph=$('graph');const focusKey=graph.contains(document.activeElement)?document.activeElement.dataset.key:null;
graph.replaceChildren();const shown=expanded?variants:variants.slice(0,5);const width=900,height=100+shown.length*66,end=Math.max(40,clock+4);graph.style.height=`${height}px`;
const x=minute=>65+minute/end*770;
const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox',`0 0 ${width} ${height}`);svg.setAttribute('preserveAspectRatio','none');svg.setAttribute('aria-hidden','true');graph.append(svg);
function path(d,color,dashed=false,opacity=1){const p=document.createElementNS(svg.namespaceURI,'path');p.setAttribute('d',d);p.setAttribute('stroke',color);p.setAttribute('fill','none');p.setAttribute('stroke-width','2');p.setAttribute('opacity',opacity);if(dashed)p.setAttribute('stroke-dasharray','5 5');svg.append(p);}
for(let minute=0;minute<=end;minute+=8){const xx=x(minute);path(`M${xx} 30V${height-20}`,'var(--ca-line)',true,.35);const label=el('span','axis-label',time(minute));label.style.left=`${xx/9}%`;graph.append(label);}
path(`M${x(0)} 64H${x(end)}`,'var(--green)',false,.75);
function node({xx,y,label,key,cls,symbol,active,click,caption}){const b=createButton('','node '+cls);b.dataset.key=key;b.style.left=`${xx/9}%`;b.style.top=`${y}px`;b.setAttribute('aria-label',label);b.setAttribute('title',label);b.setAttribute('aria-pressed',String(active));const circle=el('span','circle',symbol);circle.setAttribute('aria-hidden','true');b.append(circle);if(caption){const text=el('span','dot-caption',caption);text.setAttribute('aria-hidden','true');b.append(text);}b.addEventListener('click',click);graph.append(b);}
node({xx:x(0),y:64,label:'Positive prompt, unchanged. Show all images',key:'positive',cls:'positive-node',symbol:'+',active:!selected&&!comparing,click:()=>choose(null)});
const title=el('span','lane-label positive','Positive prompt · unchanged');title.style.top='38px';title.style.left=`${x(0)/9+3}%`;graph.append(title);
shown.forEach((variant,index)=>{
const y=130+index*66,vruns=variantRuns(variant.id),first=variant.created,active=comparing?comparison.includes(variant.id):selected===variant.id;
const dim=selected&&!active? .28:.85;
path(`M${x(Math.max(0,first-6))} 64 C${x(first-2)} 64 ${x(first-4)} ${y} ${x(first)} ${y}H${x(end)}`,'var(--red)',vruns.length===0,dim);
const label=el('span','lane-label',`${variant.name} · ${vruns.length?`${vruns.length} ${vruns.length===1?'run':'runs'}`:'saved'}`);label.style.top=`${y-25}px`;label.style.left=`${x(first)/9+3}%`;label.style.maxWidth=`${Math.max(55,900-x(first)-40)/9}%`;label.title=variant.name;graph.append(label);
node({xx:x(first),y,label:`Negative: ${variant.name}. ${variant.text||'Explicitly empty'}. ${vruns.length} runs, ${vruns.length*3} images`,key:variant.id,cls:`${vruns.length?'':'saved'}${selected&&!active?' dim':''}`,symbol:'−',active,click:()=>choose(variant.id)});
vruns.slice(1).forEach(run=>node({xx:x(run.minute),y,label:`Run ${run.id.slice(1)}: ${variant.name}, ${time(run.minute)}, 3 images, seed ${run.seed}`,key:run.id,cls:`run-node${selected&&!active?' dim':''}`,symbol:'',active:selectedRun===run.id,click:()=>{if(comparing)choose(variant.id);else choose(variant.id,run.id);},caption:`#${run.id.slice(1)}`}));
});
if(focusKey)graph.querySelector(`[data-key="${focusKey}"]`)?.focus({preventScroll:true});
}
function termDiff(a,b){const before=a.split(',').map(s=>s.trim()).filter(Boolean),after=b.split(',').map(s=>s.trim()).filter(Boolean);const root=el('div','diff-terms');for(const term of before){const node=el(after.includes(term)?'span':'del','',after.includes(term)?term:`− ${term}`);root.append(node,document.createTextNode(', '));}for(const term of after.filter(term=>!before.includes(term)))root.append(el('ins','',`+ ${term}`),document.createTextNode(', '));if(!before.length&&!after.length)root.textContent='Both negatives are explicitly empty.';return root;}
function simulate(id){const minute=clock;clock+=4;runs.push({id:`r${runs.length+1}`,variant:id,minute,seed:42001+runs.length*3});const variant=getVariant(id);if(variantRuns(id).length===1)variant.created=minute;selected=id;selectedRun=null;render();announce(`Simulated one run of ${variant.name}: 3 images added. The positive prompt is unchanged.`);}
function renderSelectedNegative(){
const target=$('selected-negative');target.replaceChildren();
const ids=comparing?comparison:selected?[selected]:[];
function row(label,text){const item=el('div','prompt-summary-row'),symbol=el('span','role-symbol negative-symbol','−');symbol.setAttribute('aria-hidden','true');const content=el('div','content');content.append(el('div','eyebrow',label),el('p','negative-text',text));item.append(symbol,content);target.append(item);}
if(!ids.length){row(comparing?'Negative prompts · comparison':'Negative prompts · all variants',comparing?'Select two red dots to compare their negative prompts.':'Multiple negative variants are included. Select a red dot to see its negative prompt here.');return;}
ids.forEach((id,index)=>{const variant=getVariant(id);row(`Negative prompt · ${comparing?(index===0?'From':'To')+' · ':''}${variant.name}${selectedRun?` · Run #${selectedRun.slice(1)}`:''}`,variant.text||'No negative prompt (explicitly empty)');});
if(comparing&&ids.length===1)target.append(el('p','hint','Select another red dot to show the second negative below.'));
}
function renderInspector(){
const target=$('inspector');target.replaceChildren();
if(comparing){target.append(el('div','eyebrow overview','Compare negatives'),el('h3','',comparison.length===2?'What changed?':'Pick two red dots'),el('p','',comparison.length===2?'Changes are shown by comma-separated term. Blue is added; struck-through red is removed.':'Select two different negative variants in the timeline or filters.'));
comparison.forEach((id,index)=>{const v=getVariant(id);target.append(el('span','badge',`${index===0?'From':'To'} · ${v.name}`),el('div','prompt-block',v.text||'No negative prompt (explicitly empty)'));});
if(comparison.length===2)target.append(termDiff(getVariant(comparison[0]).text,getVariant(comparison[1]).text));return;
}
if(!selected){target.append(el('div','eyebrow overview','One positive · one gallery'),el('h3','','Explore the branches'),el('p','','Each red branch is a reusable negative. Small dots are repeat runs; each sample run produced three images.'),el('div','prompt-block','Click a red dot → inspect its negative\nClick a small dot → see that run\nCompare negatives → see changed terms'),el('p','','The hollow red dot is a saved idea that has not been run. Saving it does not create another positive prompt.'));return;}
const variant=getVariant(selected),vruns=variantRuns(selected);target.append(el('div','eyebrow',`− ${vruns.length?'Negative variant':'Saved · not run yet'}`),el('h3','',variant.name),el('div','prompt-block',variant.text||'No negative prompt (explicitly empty)'),el('p','',`${vruns.length} ${vruns.length===1?'run':'runs'} · ${vruns.length*3} images · Positive unchanged`));
if(vruns.length){target.append(el('h3','','Run history'));const list=el('ul','run-list');vruns.forEach(run=>{const li=el('li'),b=createButton('');b.append(el('span','',`Run #${run.id.slice(1)} · ${time(run.minute)}`),el('span','run-detail','3 images'));b.setAttribute('aria-label',`Show run ${run.id.slice(1)} images for ${variant.name}`);b.setAttribute('aria-pressed',String(selectedRun===run.id));b.addEventListener('click',()=>choose(selected,run.id));b.dataset.focusKey=`history-${run.id}`;li.append(b);list.append(li);});target.append(list);}
const simulateButton=createButton('Simulate run · 3 images','ca-primary');simulateButton.id='simulate-run';simulateButton.addEventListener('click',()=>simulate(variant.id));target.append(simulateButton,el('p','hint','Prototype action only. No inference is started.'));
}
function scene(image){const n=image.seed%9;const skies=[['#15283d','#688ab0'],['#26344b','#b1a4bb'],['#152e40','#75a3ab']][n%3];const shifted=n*6;const svg=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 300"><defs><linearGradient id="sky" x2="0" y2="1"><stop stop-color="${skies[0]}"/><stop offset="1" stop-color="${skies[1]}"/></linearGradient><linearGradient id="snow" x2=".5" y2="1"><stop stop-color="#b7c6d8"/><stop offset="1" stop-color="#2d4b66"/></linearGradient></defs><rect width="480" height="300" fill="url(#sky)"/><circle cx="${355-shifted}" cy="${48+n*2}" r="15" fill="#d9e4ef" opacity=".7"/><path d="M0 190 90 55 145 130 250 36 356 156 426 70 480 144V300H0" fill="url(#snow)"/><path d="m90 55-28 62 29-12 18 24 36 1Zm160-19-39 84 33-17 18 22 24-18Z" fill="#dae1eb" opacity=".7"/><path d="M0 228 98 182 180 220 290 169 480 233V300H0" fill="#21394a"/><path d="M${181+shifted} 200h110v57H${181+shifted}Z" fill="#182731"/><path d="m${174+shifted} 200 62-38 63 38Z" fill="#4c6576"/><path d="M${193+shifted} 205h84v32h-84Z" fill="#e5ae67"/><path d="M${220+shifted} 204v34m27-34v34" stroke="#273945" stroke-width="4"/><path d="M0 270 Q140 236 300 264 T480 256V300H0" fill="#90a8b7" opacity=".32"/><path d="M0 285 Q140 256 300 280 T480 274V300H0" fill="#adc0cb" opacity=".2"/></svg>`;return`data:image/svg+xml,${encodeURIComponent(svg)}`;}
function openImage(image){const variant=getVariant(image.run.variant);$('image-title').textContent=`Run #${image.run.id.slice(1)} · Seed ${image.seed}`;const detail=$('image-detail');detail.replaceChildren();const img=el('img','preview-image');img.src=scene(image);img.alt='Illustrative alpine observatory preview';detail.append(img,el('div','eyebrow','+ Positive · unchanged'),el('p','dialog-description',$('positive-text').textContent),el('h3','',`− ${variant.name}`),el('div','prompt-block',variant.text||'No negative prompt (explicitly empty)'),el('p','hint',`Run at ${time(image.run.minute)} · 3 images in this batch · Illustrative sample`));$('image-dialog').showModal();}
function renderGallery(){
const all=sampleImages(),shown=visibleImages();$('image-count').textContent=`· ${shown.length}${shown.length!==all.length?` of ${all.length}`:''} images`;
$('gallery-description').textContent=comparing?'Comparing variants inside the same gallery.':selectedRun?`Run #${selectedRun.slice(1)} · ${getVariant(selected).name} · Same positive prompt`:selected?`${getVariant(selected).name} · Same positive prompt`:'All images from this positive prompt, together.';
const filters=$('filters');filters.replaceChildren();const allButton=createButton(`All images · ${all.length}`);allButton.dataset.focusKey='filter-all';allButton.setAttribute('aria-pressed',String(!selected&&!comparing));allButton.addEventListener('click',()=>choose(null));filters.append(allButton);
variants.forEach(variant=>{const b=createButton(`− ${variant.name} · ${variantRuns(variant.id).length*3}`);b.dataset.focusKey=`filter-${variant.id}`;b.setAttribute('aria-pressed',String(comparing?comparison.includes(variant.id):selected===variant.id));b.addEventListener('click',()=>choose(variant.id));filters.append(b);});
const grid=$('images');grid.replaceChildren();if(!shown.length)grid.append(el('div','empty','Saved for later. Simulate a run to add images to this gallery.'));
shown.forEach(image=>{const v=getVariant(image.run.variant),b=createButton('','image-card');b.setAttribute('aria-label',`Open ${v.name}, run ${image.run.id.slice(1)}, seed ${image.seed}`);const img=el('img');img.src=scene(image);img.alt='';img.loading='lazy';const info=el('span','image-info');info.append(el('span','image-title',`Observatory · ${image.seed}`));const meta=el('span','image-meta');meta.append(el('span','negative-badge',`− ${v.name}`),el('span','',`Run #${image.run.id.slice(1)}`));info.append(meta);b.append(img,info);b.addEventListener('click',()=>openImage(image));grid.append(b);});
}
function render(){const focusKey=document.activeElement?.dataset.focusKey,focusId=document.activeElement?.id;const used=variants.filter(v=>variantRuns(v.id).length).length;const stats=$('stats');stats.replaceChildren();for(const [count,label]of[[runs.length,'tracked runs'],[sampleImages().length,'images'],[used,'used negatives'],[variants.length-used,'saved idea'+(variants.length-used===1?'':'s')]]){const span=el('span');span.append(el('strong','',String(count)),document.createTextNode(` ${label}`));stats.append(span);}const compare=$('compare');compare.setAttribute('aria-pressed',String(comparing));compare.textContent=comparing?'Exit comparison':'Compare negatives';renderSelectedNegative();renderGraph();renderInspector();renderGallery();if(focusKey)document.querySelector(`[data-focus-key="${focusKey}"]`)?.focus({preventScroll:true});else if(focusId)$(focusId)?.focus({preventScroll:true});}
scope.listen($('compare'),'click',()=>{comparing=!comparing;comparison=comparing&&selected?[selected]:[];selectedRun=null;if(!comparing)selected=null;render();announce(comparing?'Choose two negative variants to compare.':'Showing all variants.');});
scope.listen($('new-variant'),'click',()=>{$('add-form').reset();$('add-dialog').showModal();});
for(const id of['close-add','cancel-add'])scope.listen($(id),'click',()=>$('add-dialog').close());scope.listen($('close-image'),'click',()=>$('image-dialog').close());
scope.listen($('add-form'),'submit',event=>{event.preventDefault();const text=$('variant-text').value.trim(),existing=variants.find(v=>v.text.trim().toLowerCase()===text.toLowerCase());if(existing){$('add-dialog').close();comparing=false;choose(existing.id);announce('This negative already exists. Selected its saved variant.');return;}const id=`n${variants.length+1}`;variants.push({id,name:$('variant-name').value.trim()||`Negative ${variants.length+1}`,text,created:clock});clock+=4;expanded=true;$('add-dialog').close();comparing=false;choose(id);announce('Negative saved as a hollow dot. No run or images added.');});
scope.listen(window,'pagehide',()=>scope.dispose());render();
</script>
</body>
</html>