greentic-gui 1.1.0

Greentic GUI runtime (Axum-based) that serves tenant packs, enforces auth, and exposes worker/session APIs plus a browser SDK.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Greentic GUI · Chat</title>
  <link rel="icon" href="data:," />
  <style>
    :root { --brand:#16a34a; --bg:#0b1220; --panel:#0f172a; --line:#1e293b; }
    html,body { height:100%; margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); color:#e2e8f0; }
    #app { display:flex; flex-direction:column; height:100%; max-width:900px; margin:0 auto; }
    header { display:flex; align-items:center; gap:10px; padding:12px 16px; background:var(--panel); border-bottom:1px solid var(--line); }
    header .dot { width:10px; height:10px; border-radius:50%; background:var(--brand); box-shadow:0 0 8px var(--brand); }
    header h1 { font-size:15px; font-weight:600; margin:0; }
    header .meta { margin-left:auto; font-size:12px; color:#94a3b8; }
    #log { flex:1; overflow-y:auto; padding:18px; display:flex; flex-direction:column; gap:12px; }
    .row { display:flex; }
    .row.user { justify-content:flex-end; }
    .bubble { max-width:80%; padding:9px 13px; border-radius:14px; line-height:1.4; white-space:pre-wrap; word-break:break-word; }
    .user .bubble { background:var(--brand); color:#04140a; border-bottom-right-radius:4px; }
    .bot  .bubble { background:#fff; color:#0b1220; border-bottom-left-radius:4px; }
    .card { background:#fff; color:#0b1220; border-radius:12px; padding:6px 10px; max-width:560px; }
    .note .bubble { background:#1e293b; color:#94a3b8; font-size:13px; }
    .err .bubble { background:#7f1d1d; color:#fee2e2; }
    form { display:flex; gap:8px; padding:12px 16px; border-top:1px solid var(--line); background:var(--panel); }
    input { flex:1; padding:11px 14px; border-radius:10px; border:1px solid var(--line); background:#0b1220; color:#e2e8f0; font-size:14px; }
    button { padding:0 18px; border:0; border-radius:10px; background:var(--brand); color:#04140a; font-weight:600; cursor:pointer; }
  </style>
</head>
<body>
  <div id="app">
    <header>
      <span class="dot"></span>
      <h1>Greentic GUI · Chat</h1>
      <span class="meta">new deployment model · via greentic-gui worker gateway</span>
    </header>
    <div id="log"></div>
    <form id="composer" autocomplete="off">
      <input id="input" placeholder="Type a message…" />
      <button type="submit">Send</button>
    </form>
  </div>

  <!-- Posts straight to greentic-gui's worker gateway. No /api/gui/config fetch
       (the chat needs no tenant GUI pack), so a pack-less env stays quiet. -->
  <script src="/adaptivecards.min.js"></script>
  <script>
    const WORKER_URL = '/api/gui/worker/message';
    // A stable session id per page load keys the runtime's flow pause/resume,
    // so a paused menu node resumes on the next click and card buttons navigate.
    // A page refresh mints a new session → a fresh menu.
    const SESSION_ID = 'web-' + (self.crypto && crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36));
    const USER_ID = 'web-user';
    const log = document.getElementById('log');
    const input = document.getElementById('input');

    function row(kind) { const r = document.createElement('div'); r.className = 'row ' + kind; log.appendChild(r); return r; }
    function bubble(kind, text) { const r = row(kind); const b = document.createElement('div'); b.className = 'bubble'; b.textContent = text; r.appendChild(b); scroll(); }
    function scroll() { log.scrollTop = log.scrollHeight; }

    function renderCard(cardJson) {
      const r = row('bot');
      const host = document.createElement('div'); host.className = 'card'; r.appendChild(host);
      try {
        const ac = new AdaptiveCards.AdaptiveCard();
        ac.onExecuteAction = (action) => {
          const name = action.getJsonTypeName ? action.getJsonTypeName() : '';
          if (name === 'Action.OpenUrl' && action.url) { window.open(action.url, '_blank'); return; }
          const data = action.data || {};
          const label = data.text || action.title || JSON.stringify(data);
          bubble('user', String(label));
          send(data);
        };
        ac.parse(cardJson);
        host.appendChild(ac.render());
      } catch (e) {
        host.textContent = 'Adaptive Card render error: ' + e.message;
      }
      scroll();
    }

    function renderMessages(resp) {
      const messages = (resp && resp.messages) || [];
      if (!messages.length) { bubble('bot', '(no reply)'); return; }
      for (const m of messages) {
        if (m.kind === 'adaptive-card') renderCard(m.payload);
        else if (m.kind === 'text') bubble('bot', (m.payload && m.payload.text) || '');
        else bubble('bot', JSON.stringify(m.payload, null, 2));
      }
    }

    // Post payload to the gui worker gateway (forwarded to the runtime's
    // /workers/invoke). A `text` field becomes a chat message; a card
    // Action.Submit's data (e.g. {action:"about_card"}) routes the flow. The
    // session_id keys flow pause/resume so buttons navigate across turns.
    async function send(payload) {
      try {
        const res = await fetch(WORKER_URL, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({
            worker_id: 'chat',
            payload,
            context: { session_id: SESSION_ID, user_id: USER_ID },
          }),
        });
        if (!res.ok) {
          const body = await res.text();
          const hint = res.status === 500 ? ' — is a bundle deployed? Try: gtc op deploy --bundle <file> --bundle-id quickstart' : '';
          const r = row('err'); const b = document.createElement('div'); b.className = 'bubble';
          b.textContent = ` HTTP ${res.status}: ${body.slice(0, 200)}${hint}`; r.appendChild(b); scroll();
          return;
        }
        renderMessages(await res.json());
      } catch (e) {
        const r = row('err'); const b = document.createElement('div'); b.className = 'bubble';
        b.textContent = '⚠️ ' + e.message; r.appendChild(b); scroll();
      }
    }

    document.getElementById('composer').addEventListener('submit', (e) => {
      e.preventDefault();
      const text = input.value.trim();
      if (!text) return;
      bubble('user', text);
      input.value = '';
      send({ text });
    });

    // Render the entry flow's first card (the welcome menu) on load, with no
    // user bubble — an empty payload starts the flow at its entry node.
    send({});
    input.focus();
  </script>
</body>
</html>