← 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>