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 & Load Widget</strong> to sign a JWT and inject the widget<br> 50 4. The widget sends <code>X-Widget-Context</code> header — 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 & 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>