/ widget_test / public / index.html
index.html
  1  <!DOCTYPE html>
  2  <html lang="en">
  3  <head>
  4    <meta charset="UTF-8" />
  5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6    <title>Widget Context Injection Test</title>
  7    <meta http-equiv="Permissions-Policy" content="clipboard-read=*, clipboard-write=*" />
  8    <style>
  9      *, *::before, *::after { box-sizing: border-box; }
 10      body, h1, h2, p, hr { margin: 0; padding: 0; }
 11      body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
 12      .container { max-width: 800px; margin: 40px auto; padding: 0 20px; }
 13      h1 { font-size: 1.5rem; margin-bottom: 4px; }
 14      .subtitle { color: #666; margin-bottom: 24px; font-size: 0.9rem; }
 15      .card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 24px; margin-bottom: 20px; }
 16      .card h2 { font-size: 1.1rem; margin-bottom: 16px; }
 17      .field { margin-bottom: 14px; }
 18      .field label { display: block; font-weight: 600; font-size: 0.82rem; margin-bottom: 4px; color: #555; }
 19      .field input, .field textarea { width: 100%; padding: 8px 10px; border: 1px solid #d0d0d0; border-radius: 6px; font-size: 0.9rem; font-family: inherit; }
 20      .field textarea { font-family: "SF Mono", "Fira Code", monospace; font-size: 0.82rem; resize: vertical; }
 21      .field .hint { font-size: 0.75rem; color: #888; margin-top: 3px; }
 22      button { padding: 8px 20px; border: none; border-radius: 6px; font-size: 0.88rem; font-weight: 600; cursor: pointer; }
 23      .btn-primary { background: #6366f1; color: #fff; }
 24      .btn-primary:hover { background: #4f46e5; }
 25      .btn-primary:disabled { background: #aaa; cursor: not-allowed; }
 26      .btn-danger { background: #ef4444; color: #fff; margin-left: 8px; }
 27      .btn-danger:hover { background: #dc2626; }
 28      .actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
 29      #status { margin-top: 12px; font-size: 0.85rem; }
 30      #status.ok { color: #16a34a; }
 31      #status.err { color: #ef4444; }
 32      .token-box { background: #f8f8f8; border: 1px solid #e0e0e0; border-radius: 6px; padding: 10px; font-family: "SF Mono", monospace; font-size: 0.75rem; word-break: break-all; max-height: 120px; overflow: auto; margin-top: 8px; }
 33      .payload-box { background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; padding: 10px; font-family: "SF Mono", monospace; font-size: 0.78rem; white-space: pre-wrap; margin-top: 8px; }
 34      .divider { border: none; border-top: 1px solid #e5e5e5; margin: 20px 0; }
 35      .info { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px; padding: 14px; font-size: 0.82rem; color: #1e40af; line-height: 1.6; }
 36      .info code { background: #dbeafe; padding: 1px 5px; border-radius: 3px; font-size: 0.8rem; }
 37      #widget-host { margin-top: 20px; padding: 20px; border: 2px dashed #d0d0d0; border-radius: 8px; min-height: 60px; display: flex; align-items: center; justify-content: center; color: #999; }
 38    </style>
 39  </head>
 40  <body>
 41    <div class="container">
 42      <h1>Widget Context Injection Test</h1>
 43      <p class="subtitle">Generate signed JWT tokens and test the RESTai widget context feature</p>
 44  
 45      <div class="info">
 46        <strong>How it works:</strong><br>
 47        1. Enter your <code>Widget Key</code> and <code>Context Secret</code> (from the Widget tab in your project)<br>
 48        2. Define context claims (e.g. user name, plan, account ID) as JSON<br>
 49        3. Click <strong>Generate Token &amp; Load Widget</strong> to sign a JWT and inject the widget<br>
 50        4. The widget sends <code>X-Widget-Context</code> header &mdash; the server verifies it and injects claims into the system prompt<br>
 51        5. Use <code>{{context.key}}</code> template variables in your project's system prompt to reference the claims
 52      </div>
 53  
 54      <form class="card" style="margin-top: 20px;" onsubmit="event.preventDefault(); generateAndLoad();">
 55        <h2>Configuration</h2>
 56  
 57        <div class="field">
 58          <label>RESTai Server URL</label>
 59          <input type="text" id="serverUrl" value="http://127.0.0.1:9000" />
 60          <div class="hint">The base URL of your RESTai instance</div>
 61        </div>
 62  
 63        <div class="field">
 64          <label>Widget Key</label>
 65          <input type="text" id="widgetKey" placeholder="wk_..." autocomplete="off" />
 66          <div class="hint">From the Widget tab in your project settings</div>
 67        </div>
 68  
 69        <div class="field">
 70          <label>Context Secret</label>
 71          <input type="text" id="contextSecret" placeholder="The secret shown when you generated it" autocomplete="off" />
 72          <div class="hint">Shown once when you click "Generate Context Secret" in the Widget tab</div>
 73        </div>
 74  
 75        <hr class="divider" />
 76  
 77        <div class="field">
 78          <label>Context Claims (JSON)</label>
 79          <textarea id="claims" rows="6">{
 80    "user_name": "John Doe",
 81    "user_email": "john@example.com",
 82    "account_id": "acct_12345",
 83    "plan": "pro",
 84    "role": "admin"
 85  }</textarea>
 86          <div class="hint">These claims will be available as <code>{{context.user_name}}</code>, <code>{{context.plan}}</code>, etc. in the system prompt</div>
 87        </div>
 88  
 89        <div class="field">
 90          <label>Token TTL (seconds)</label>
 91          <input type="number" id="ttl" value="3600" min="60" max="86400" />
 92          <div class="hint">How long the token is valid (max 86400 = 24h)</div>
 93        </div>
 94  
 95        <div class="field">
 96          <label>Enable Streaming</label>
 97          <input type="checkbox" id="enableStream" style="width: auto;" />
 98          <span style="font-size: 0.82rem; color: #666; margin-left: 6px;">Send <code>data-stream="true"</code> to the widget</span>
 99        </div>
100  
101        <div class="actions">
102          <button class="btn-primary" id="generateBtn" onclick="generateAndLoad()">Generate Token &amp; Load Widget</button>
103          <button class="btn-danger" id="removeBtn" onclick="removeWidget()" style="display:none;">Remove Widget</button>
104        </div>
105  
106        <div id="status"></div>
107      </form>
108  
109      <div id="token-result" style="display: none;">
110        <div class="card">
111          <h2>Generated Token</h2>
112          <div class="token-box" id="tokenValue"></div>
113          <div style="margin-top: 12px; font-size: 0.82rem; color: #555;">Decoded payload:</div>
114          <div class="payload-box" id="payloadValue"></div>
115        </div>
116      </div>
117  
118      <div id="widget-host">Widget will appear here (bottom-right corner) after you generate a token</div>
119    </div>
120  
121    <script>
122      let currentScript = null;
123  
124      async function generateAndLoad() {
125        const serverUrl = document.getElementById("serverUrl").value.trim();
126        const widgetKey = document.getElementById("widgetKey").value.trim();
127        const secret = document.getElementById("contextSecret").value.trim();
128        const ttl = document.getElementById("ttl").value;
129        const stream = document.getElementById("enableStream").checked;
130        const status = document.getElementById("status");
131        let claims;
132  
133        // Validate
134        if (!widgetKey) { showStatus("Widget Key is required", false); return; }
135        if (!secret) { showStatus("Context Secret is required", false); return; }
136  
137        try {
138          claims = JSON.parse(document.getElementById("claims").value);
139        } catch (e) {
140          showStatus("Invalid JSON in claims: " + e.message, false);
141          return;
142        }
143  
144        // Generate token via our local backend
145        showStatus("Generating token...", true);
146        try {
147          const resp = await fetch("/token", {
148            method: "POST",
149            headers: { "Content-Type": "application/json" },
150            body: JSON.stringify({ secret, claims, ttl }),
151          });
152          const data = await resp.json();
153          if (!resp.ok) { showStatus("Token error: " + data.error, false); return; }
154  
155          // Show token
156          document.getElementById("tokenValue").textContent = data.token;
157          document.getElementById("payloadValue").textContent = JSON.stringify(data.payload, null, 2);
158          document.getElementById("token-result").style.display = "";
159  
160          // Remove old widget if any
161          removeWidget();
162  
163          // Inject widget script
164          const script = document.createElement("script");
165          script.src = serverUrl + "/widget/chat.js";
166          script.setAttribute("data-widget-key", widgetKey);
167          script.setAttribute("data-server", serverUrl);
168          script.setAttribute("data-context-token", data.token);
169          if (stream) script.setAttribute("data-stream", "true");
170          document.body.appendChild(script);
171          currentScript = script;
172  
173          document.getElementById("removeBtn").style.display = "";
174          document.getElementById("widget-host").textContent = "Widget loaded! Check the bottom-right corner of the page.";
175          showStatus("Token generated and widget loaded successfully", true);
176        } catch (e) {
177          showStatus("Request failed: " + e.message, false);
178        }
179      }
180  
181      function removeWidget() {
182        // Remove the script tag
183        if (currentScript) {
184          currentScript.remove();
185          currentScript = null;
186        }
187        // Remove any shadow host the widget created
188        document.querySelectorAll("[id^='restai-widget']").forEach(el => el.remove());
189        // Also try generic shadow hosts the widget might create
190        document.querySelectorAll("div").forEach(el => {
191          if (el.shadowRoot && el.shadowRoot.querySelector(".restai-chat-container, .chat-toggle")) {
192            el.remove();
193          }
194        });
195        document.getElementById("removeBtn").style.display = "none";
196        document.getElementById("widget-host").textContent = "Widget removed. Generate a new token to reload.";
197      }
198  
199      function showStatus(msg, ok) {
200        const el = document.getElementById("status");
201        el.textContent = msg;
202        el.className = ok ? "ok" : "err";
203      }
204    </script>
205  </body>
206  </html>