← back to The Ai Factory

public/index.html

153 lines

<!doctype html>
<html lang="en">
<head>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>The AI Factory</title>
<style>
  :root {
    --bg: #0e0e10;
    --panel: #16161a;
    --line: #2a2a30;
    --text: #e8e8ea;
    --muted: #8a8a92;
    --accent: #d4af37;
  }
  html, body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif; }
  header { padding: 24px 32px; border-bottom: 1px solid var(--line); display: flex; align-items: baseline; gap: 16px; }
  header h1 { margin: 0; font-size: 20px; letter-spacing: 0.02em; }
  header .sub { color: var(--muted); font-size: 12px; }
  main { padding: 24px 32px; max-width: 1100px; }
  form { display: flex; gap: 8px; margin-bottom: 24px; }
  input[type=text] { flex: 1; background: var(--panel); border: 1px solid var(--line); color: var(--text); padding: 10px 12px; border-radius: 6px; font: inherit; }
  button { background: var(--accent); color: #1b1b1b; border: 0; padding: 10px 16px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer; }
  button:disabled { opacity: 0.4; cursor: not-allowed; }
  .activate-btn { padding: 4px 10px; font-size: 12px; }
  tr.run-row { cursor: pointer; }
  tr.run-row:hover { background: #1a1a1f; }
  tr.events-row td { background: #0a0a0c; padding: 0; border-bottom: 1px solid var(--line); }
  .events-pane { padding: 12px 16px; }
  .events-pane h4 { margin: 0 0 8px; font-size: 11px; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; }
  .event { display: grid; grid-template-columns: 60px 110px 50px 1fr; gap: 12px; padding: 4px 0; border-bottom: 1px dotted #222; font-size: 12px; }
  .event:last-child { border-bottom: 0; }
  .event .stg { color: var(--muted); }
  .event .nm  { color: var(--accent); }
  .event .lv  { font-size: 10px; text-transform: uppercase; }
  .event .lv.warn  { color: #f4b400; }
  .event .lv.error { color: #ff6b6b; }
  .event .lv.info  { color: #5fb878; }
  .event .ms  { color: var(--text); }
  .event details { margin-top: 4px; }
  .event details summary { color: var(--muted); cursor: pointer; font-size: 10px; }
  .event pre { background: #050507; color: #d0d0d4; padding: 8px; margin: 4px 0 0; border-radius: 4px; font-size: 11px; overflow-x: auto; max-height: 200px; }
  table { width: 100%; border-collapse: collapse; }
  th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line); font-size: 13px; }
  th { color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
  td.muted { color: var(--muted); }
  td.status { font-weight: 600; }
  .empty { color: var(--muted); padding: 24px 0; text-align: center; }
  .err { color: #ff6b6b; font-size: 12px; margin-top: 8px; }
</style>
</head>
<body>
<header>
  <h1>The AI Factory</h1>
  <span class="sub">prompt → shipped AI artifact · :9890 orchestrator · :9891 viewer</span>
</header>
<main>
  <form id="newRun">
    <input type="text" id="prompt" placeholder="Describe the AI artifact to build (subagent, skill, agent, script)…" />
    <button type="submit">Build</button>
  </form>
  <div id="err" class="err"></div>
  <table>
    <thead><tr><th>#</th><th>Prompt</th><th>Type</th><th>Name</th><th>Stage</th><th>Status</th><th>Created</th><th></th></tr></thead>
    <tbody id="runs"><tr><td class="empty" colspan="8">No runs yet.</td></tr></tbody>
  </table>
</main>
<script>
const ORCH = 'http://127.0.0.1:9890';
async function loadRuns() {
  const tbody = document.getElementById('runs');
  const errEl = document.getElementById('err');
  try {
    const res = await fetch(ORCH + '/runs');
    const data = await res.json();
    if (data.error) { errEl.textContent = data.error + (data.hint ? ' — ' + data.hint : ''); return; }
    errEl.textContent = '';
    if (!data.runs || !data.runs.length) {
      tbody.innerHTML = '<tr><td class="empty" colspan="8">No runs yet.</td></tr>';
      return;
    }
    tbody.innerHTML = data.runs.map(r => {
      const canActivate = String(r.status).startsWith('awaiting_activation');
      const isActivated = r.status === 'activated';
      const action = canActivate
        ? `<button data-id="${r.id}" class="activate-btn">Activate</button>`
        : (isActivated ? '<span class="muted">activated</span>' : '');
      return `
      <tr class="run-row" data-id="${r.id}">
        <td class="muted">${r.id}</td>
        <td>${escapeHtml(r.prompt).slice(0, 100)}</td>
        <td class="muted">${r.artifact_type || '—'}</td>
        <td class="muted">${escapeHtml(r.artifact_name || '—')}</td>
        <td class="muted">${r.current_stage}</td>
        <td class="status">${r.status}</td>
        <td class="muted">${new Date(r.created_at).toLocaleString()}</td>
        <td>${action}</td>
      </tr>
      <tr class="events-row" id="events-${r.id}" style="display:none;"><td colspan="8"><div class="events-pane" id="events-pane-${r.id}">loading…</div></td></tr>`;
    }).join('');
    document.querySelectorAll('.activate-btn').forEach(btn => {
      btn.addEventListener('click', async (e) => {
        e.stopPropagation();
        const id = btn.getAttribute('data-id');
        if (!confirm(`Activate run ${id}? This copies the file to ~/.claude/.`)) return;
        btn.disabled = true;
        const res = await fetch(`${ORCH}/runs/${id}/activate`, { method: 'POST' });
        const data = await res.json();
        if (data.error) alert(`Activate failed: ${data.error}\n\n${data.hint || ''}`);
        else alert(`Activated → ${data.dest}`);
        loadRuns();
      });
    });
    document.querySelectorAll('.run-row').forEach(row => {
      row.addEventListener('click', async () => {
        const id = row.getAttribute('data-id');
        const evRow = document.getElementById(`events-${id}`);
        const evPane = document.getElementById(`events-pane-${id}`);
        if (evRow.style.display === 'none') {
          evRow.style.display = '';
          const r = await fetch(`${ORCH}/runs/${id}/events`);
          const d = await r.json();
          if (d.error) { evPane.textContent = d.error; return; }
          evPane.innerHTML = `<h4>Events for run ${id}</h4>` + (d.events || []).map(ev => {
            const payloadHtml = ev.payload ? `<details><summary>payload</summary><pre>${escapeHtml(JSON.stringify(ev.payload, null, 2))}</pre></details>` : '';
            return `<div class="event"><span class="stg">stage ${ev.stage}</span><span class="nm">${ev.stage_name}</span><span class="lv ${ev.level}">${ev.level}</span><span class="ms">${escapeHtml(ev.message || '')}${payloadHtml}</span></div>`;
          }).join('') || '<div class="muted">no events</div>';
        } else {
          evRow.style.display = 'none';
        }
      });
    });
  } catch (e) {
    errEl.textContent = 'Orchestrator unreachable on :9890 — is it running?';
  }
}
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
document.getElementById('newRun').addEventListener('submit', async (e) => {
  e.preventDefault();
  const prompt = document.getElementById('prompt').value.trim();
  if (!prompt) return;
  await fetch(ORCH + '/runs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) });
  document.getElementById('prompt').value = '';
  loadRuns();
});
loadRuns();
setInterval(loadRuns, 4000);
</script>
</body>
</html>