← back to Dw Design Bridge
public/index.html
122 lines
<!doctype html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="utf-8">
<title>DW Design Bridge — AI Factory × Designer Wallcoverings</title>
<style>
:root { --bg:#0a0a0a; --card:#141414; --line:#222; --paper:#e8e8e8; --muted:#888; --accent:#c9a14b }
* { box-sizing:border-box }
body { font-family:-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--paper); margin:0; padding:32px; max-width:1400px; margin:0 auto }
h1 { font-size:22px; font-weight:300; letter-spacing:0.32em; text-transform:uppercase; margin-bottom:6px }
.sub { color:var(--muted); font-size:11px; letter-spacing:0.18em; text-transform:uppercase; margin-bottom:32px }
.composer { background:var(--card); border:1px solid var(--line); padding:24px; border-radius:6px; margin-bottom:32px }
.composer h2 { font-size:14px; letter-spacing:0.28em; text-transform:uppercase; margin:0 0 12px; color:var(--accent) }
.composer textarea { width:100%; min-height:80px; background:#000; border:1px solid var(--line); color:var(--paper); padding:12px; font-family:inherit; font-size:14px; resize:vertical; outline:none }
.composer textarea:focus { border-color:var(--accent) }
.composer .row { display:flex; align-items:center; gap:12px; margin-top:12px }
.composer label { font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted) }
.composer input[type=number] { width:80px; background:#000; border:1px solid var(--line); color:var(--paper); padding:8px; font-family:inherit }
.composer button { background:var(--accent); color:#000; border:0; padding:10px 24px; font-family:inherit; font-size:11px; letter-spacing:0.18em; text-transform:uppercase; font-weight:700; cursor:pointer; margin-left:auto }
.composer button:disabled { opacity:0.5; cursor:not-allowed }
.jobs { display:grid; gap:16px }
.job { background:var(--card); border:1px solid var(--line); padding:16px 20px; border-radius:6px }
.job .head { display:flex; justify-content:space-between; align-items:baseline; gap:12px; margin-bottom:8px }
.job .id { font-family:monospace; font-size:11px; color:var(--muted) }
.job .status { font-size:10px; letter-spacing:0.28em; text-transform:uppercase; padding:3px 10px; border-radius:2px }
.job .status.queued { background:#222; color:var(--paper) }
.job .status.running { background:#332; color:var(--accent) }
.job .status.done { background:#040; color:#0f8 }
.job .status.error { background:#400; color:#f88 }
.job .brief { font-size:14px; color:var(--paper); margin-bottom:8px }
.job .progress { font-size:11px; color:var(--muted); margin-bottom:12px }
.designs { display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:12px }
.design { background:#0a0a0a; border:1px solid var(--line); padding:12px; border-radius:4px }
.design .name { font-size:14px; font-weight:500; color:var(--accent); margin-bottom:6px }
.design .palette { display:flex; gap:4px; margin-bottom:8px }
.design .palette span { width:24px; height:24px; border-radius:3px; border:1px solid var(--line) }
.design .meta { font-size:11px; color:var(--muted); line-height:1.5 }
.design .meta b { color:var(--paper); font-weight:500 }
.design .copy { font-size:12px; color:#bbb; margin-top:8px; font-style:italic; border-left:2px solid var(--accent); padding-left:8px }
</style>
</head>
<body>
<h1>DW Design Bridge</h1>
<div class="sub">AI Factory × Designer Wallcoverings · idea-loop 29/40 · qwen3:14b@Mac2</div>
<div class="composer">
<h2>Generate design proposals</h2>
<textarea id="briefInput" placeholder="Brief, e.g. — Hotel lobby feature wall · jewel-toned · botanical large-scale · trade grade · evening dramatic"></textarea>
<div class="row">
<label>Count <input type="number" id="countInput" value="5" min="1" max="20"></label>
<button id="submitBtn" onclick="submit()">Queue Job</button>
</div>
</div>
<div class="jobs" id="jobsList"></div>
<script>
async function submit() {
const brief = document.getElementById('briefInput').value.trim();
const count = parseInt(document.getElementById('countInput').value) || 5;
if (!brief) return alert('Need a brief.');
const btn = document.getElementById('submitBtn');
btn.disabled = true; btn.textContent = '…';
const r = await fetch('/api/jobs', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ brief, count }) });
if (r.ok) { document.getElementById('briefInput').value = ''; await refresh(); }
btn.disabled = false; btn.textContent = 'Queue Job';
}
async function refresh() {
const r = await fetch('/api/jobs');
const { jobs } = await r.json();
document.getElementById('jobsList').innerHTML = jobs.map(j => `
<div class="job">
<div class="head">
<div>
<div class="id">${j.id}</div>
</div>
<div class="status ${j.status}">${j.status}</div>
</div>
<div class="brief">${escapeHtml(j.brief)}</div>
<div class="progress">${j.progress || ''} ${j.error ? '· ' + escapeHtml(j.error) : ''}</div>
<div class="designs">
${(j.designs || []).filter(d => d.ok).map(d => renderDesign(d.design, j.id, d.idx)).join('')}
</div>
</div>
`).join('') || '<div class="job"><div class="brief" style="color:var(--muted)">No jobs yet — queue one above.</div></div>';
}
function renderDesign(d, jobId, idx) {
if (!d || typeof d !== 'object') return '';
const palette = (d.palette || []).map(c => `<span style="background:${escapeHtml(c)}"></span>`).join('');
const previewSrc = jobId != null && idx != null ? `/jobs/${jobId}/${idx}.svg` : '';
return `<div class="design">
${previewSrc ? `<img src="${previewSrc}" alt="${escapeHtml(d.name || '')}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px;margin-bottom:8px;display:block">` : ''}
<div class="name">${escapeHtml(d.name || 'Untitled')}</div>
<div class="palette">${palette}</div>
<div class="meta">
<b>Pattern:</b> ${escapeHtml(d.pattern_concept || '')}<br>
<b>Scale:</b> ${escapeHtml(d.scale || '')} · <b>Repeat:</b> ${escapeHtml(d.repeat || '')}<br>
<b>Material:</b> ${escapeHtml(d.material_pairing || '')} · <b>Grade:</b> ${escapeHtml(d.trade_grade || '')}<br>
<b>Use:</b> ${escapeHtml(d.use_case || '')}<br>
<b>Style:</b> ${(d.style_tags || []).join(' · ')}<br>
<b>Price:</b> ${escapeHtml(d.price_band || '')}
</div>
<div class="copy">${escapeHtml(d.marketing_copy || '')}</div>
</div>`;
}
function escapeHtml(s) {
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>