Download widget/adaptive.html from joduor/adaptive-model: direct link, hf CLI and curl.
- Browser
- Download file 29.1 kB
-
https://huggingface.co/joduor/adaptive-model/resolve/main/widget/adaptive.html
- Command line
-
hf download hf://joduor/adaptive-model/widget/adaptive.html
-
curl -L -o adaptive.html https://huggingface.co/joduor/adaptive-model/resolve/main/widget/adaptive.html
29.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"> | |
| <title>Adaptive UI</title> | |
| <style> | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| /* base */ | |
| --bg:#f0f4ff;--surface:#fff;--border:#e2e8f0;--text:#1e1b4b;--muted:#6b7280; | |
| --radius:12px;--shadow:0 4px 20px rgba(99,102,241,.10); | |
| /* vivid palette */ | |
| --indigo:#6366f1;--indigo-d:#4338ca; | |
| --sky:#0ea5e9; --sky-d:#0284c7; | |
| --emerald:#10b981;--emerald-d:#059669; | |
| --amber:#f59e0b; --amber-d:#d97706; | |
| --rose:#f43f5e; --rose-d:#e11d48; | |
| --violet:#8b5cf6;--violet-d:#7c3aed; | |
| --cyan:#06b6d4; --cyan-d:#0891b2; | |
| --lime:#84cc16; --lime-d:#65a30d; | |
| --orange:#f97316;--orange-d:#ea580c; | |
| --pink:#ec4899; --pink-d:#db2777; | |
| /* per-component accent */ | |
| --form-a:#7c3aed; --form-b:#4f46e5; | |
| --chart-a:#0ea5e9; --chart-b:#6366f1; | |
| --card-a:#10b981; --card-b:#0284c7; | |
| --table-a:#f59e0b; --table-b:#ea580c; | |
| --fig-a:#f43f5e; --fig-b:#8b5cf6; | |
| } | |
| /* ββ body ββ */ | |
| body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; | |
| font-size:14px;line-height:1.5;color:var(--text); | |
| background:var(--bg);padding:12px 14px 110px; | |
| background-image:radial-gradient(circle at 20% 0%,rgba(99,102,241,.08) 0%,transparent 50%), | |
| radial-gradient(circle at 80% 100%,rgba(16,185,129,.07) 0%,transparent 50%)} | |
| /* ββ section header ββ */ | |
| .comp-header{border-radius:var(--radius) var(--radius) 0 0;padding:12px 16px; | |
| display:flex;align-items:center;justify-content:space-between;margin-bottom:0} | |
| .comp-title{font-size:15px;font-weight:700;color:#fff;letter-spacing:-.01em} | |
| .comp-badge{font-size:10px;font-weight:600;background:rgba(255,255,255,.25); | |
| color:#fff;padding:2px 8px;border-radius:99px;letter-spacing:.04em;text-transform:uppercase} | |
| /* ββ reply bar ββ */ | |
| #reply-bar{background:linear-gradient(135deg,var(--indigo),var(--sky)); | |
| border-radius:var(--radius);padding:10px 14px;margin-bottom:10px; | |
| font-size:13px;color:#fff;display:none;box-shadow:0 2px 12px rgba(99,102,241,.3)} | |
| #reply-bar.thinking{opacity:.7;font-style:italic} | |
| /* ββ chat bar ββ */ | |
| #chat-bar{position:fixed;bottom:0;left:0;right:0; | |
| background:rgba(255,255,255,.92);backdrop-filter:blur(12px); | |
| border-top:1px solid var(--border);padding:10px 14px; | |
| display:flex;gap:8px;z-index:100; | |
| box-shadow:0 -4px 20px rgba(99,102,241,.08)} | |
| #chat-input{flex:1;padding:9px 14px;border:2px solid var(--border); | |
| border-radius:99px;background:#fff;color:var(--text);font-size:13px;outline:none; | |
| transition:border-color .15s} | |
| #chat-input:focus{border-color:var(--indigo)} | |
| #chat-send{width:38px;height:38px;border:none;border-radius:50%; | |
| background:linear-gradient(135deg,var(--indigo),var(--violet)); | |
| color:#fff;font-size:16px;cursor:pointer;transition:transform .12s,opacity .12s; | |
| display:flex;align-items:center;justify-content:center;flex-shrink:0} | |
| #chat-send:hover{transform:scale(1.08)} | |
| /* ββ card wrapper ββ */ | |
| .ui-card{background:var(--surface);border-radius:var(--radius); | |
| box-shadow:var(--shadow);overflow:hidden;margin-bottom:4px} | |
| /* ββ error ββ */ | |
| .error{color:#be123c;padding:10px 14px;background:#fff1f2; | |
| border-left:4px solid var(--rose);border-radius:var(--radius)} | |
| .loading{padding:32px;text-align:center;color:var(--muted)} | |
| /* ββ buttons ββ */ | |
| .btn{display:inline-flex;align-items:center;gap:5px;padding:8px 16px;border:none; | |
| border-radius:99px;font-size:13px;font-weight:600;cursor:pointer; | |
| transition:transform .12s,opacity .12s;white-space:nowrap} | |
| .btn:hover{transform:translateY(-1px);opacity:.9} | |
| .btn:active{transform:translateY(0)} | |
| .btn:disabled{opacity:.45;cursor:not-allowed;transform:none} | |
| .btn-outline{background:transparent;border:2px solid currentColor} | |
| /* ββββββββββββββββββββββββββββββββ | |
| FORM β violet / indigo theme | |
| ββββββββββββββββββββββββββββββββ */ | |
| .form-header{background:linear-gradient(135deg,var(--form-a),var(--form-b))} | |
| .form-body{padding:14px} | |
| .form-field{margin-bottom:14px} | |
| .form-field label{display:flex;align-items:center;gap:4px; | |
| font-weight:600;font-size:13px;margin-bottom:6px;color:var(--text)} | |
| .req{color:var(--rose);font-size:13px} | |
| .form-field input:not([type=range]), | |
| .form-field select, | |
| .form-field textarea{ | |
| width:100%;padding:9px 12px; | |
| border:2px solid #e0e7ff;border-radius:10px; | |
| background:#f8f7ff;color:var(--text);font-size:14px;outline:none; | |
| transition:border-color .15s,background .15s} | |
| .form-field input:focus,.form-field select:focus,.form-field textarea:focus{ | |
| border-color:var(--form-a);background:#fff} | |
| .form-field textarea{resize:vertical;min-height:72px} | |
| .number-row{display:flex;align-items:center;gap:6px} | |
| .step-btn{width:34px;height:38px;border:2px solid #e0e7ff;background:#f8f7ff; | |
| color:var(--form-a);border-radius:10px;font-size:18px;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;flex-shrink:0; | |
| transition:background .12s,border-color .12s} | |
| .step-btn:hover{background:#ede9fe;border-color:var(--form-a)} | |
| .number-row input{text-align:center} | |
| .range-row{display:flex;align-items:center;gap:10px} | |
| .range-row input[type=range]{flex:1;accent-color:var(--form-a)} | |
| .range-display{min-width:46px;text-align:right;font-weight:700; | |
| color:var(--form-a);font-size:15px} | |
| .form-submit{ | |
| background:linear-gradient(135deg,var(--form-a),var(--form-b)); | |
| color:#fff;width:100%;justify-content:center;font-size:14px;padding:10px} | |
| .form-status{margin-top:10px;font-size:13px;padding:8px 12px; | |
| border-radius:10px;display:none;font-weight:500} | |
| .form-status.ok{display:block;background:#f0fdf4;color:#166534; | |
| border:1px solid #bbf7d0} | |
| .form-status.err{display:block;background:#fff1f2;color:#be123c; | |
| border:1px solid #fecdd3} | |
| /* ββββββββββββββββββββββββββββββββ | |
| CHART β sky / indigo theme | |
| ββββββββββββββββββββββββββββββββ */ | |
| .chart-header-wrap{background:linear-gradient(135deg,var(--chart-a),var(--chart-b))} | |
| .chart-body{padding:14px} | |
| .chart-bars{display:flex;align-items:flex-end;gap:8px; | |
| height:170px;padding-bottom:0;margin-bottom:8px} | |
| .bar-col{display:flex;flex-direction:column;align-items:center; | |
| gap:4px;flex:1;min-width:36px;position:relative} | |
| .bar-fill{width:100%;border-radius:6px 6px 0 0;cursor:pointer; | |
| transition:filter .15s,height .3s cubic-bezier(.34,1.56,.64,1); | |
| position:relative;min-height:6px} | |
| .bar-fill:hover{filter:brightness(1.12)} | |
| .bar-fill.selected{outline:3px solid #fff;outline-offset:2px} | |
| .bar-label{font-size:10px;color:var(--muted);text-align:center; | |
| max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .bar-val{font-size:10px;font-weight:700;color:var(--text)} | |
| .bar-edit{display:none;width:100%;padding:4px;border:2px solid var(--sky); | |
| border-radius:6px;font-size:12px;text-align:center;background:#fff;color:var(--text)} | |
| .bar-col.editing .bar-fill{display:none} | |
| .bar-col.editing .bar-edit{display:block} | |
| .bar-tooltip{position:absolute;top:-32px;left:50%;transform:translateX(-50%); | |
| background:#1e1b4b;color:#fff;font-size:11px;font-weight:600; | |
| padding:4px 8px;border-radius:6px;white-space:nowrap; | |
| pointer-events:none;opacity:0;transition:opacity .15s} | |
| .bar-fill:hover + .bar-tooltip{opacity:1} | |
| .chart-hint{font-size:11px;color:var(--muted);text-align:center;margin-top:4px} | |
| .analyze-btn{ | |
| background:linear-gradient(135deg,var(--chart-a),var(--chart-b)); | |
| color:#fff;font-size:12px;padding:6px 14px} | |
| /* bar colors β cycling rainbow */ | |
| .bar-c0{background:linear-gradient(180deg,#6366f1,#4338ca)} | |
| .bar-c1{background:linear-gradient(180deg,#0ea5e9,#0284c7)} | |
| .bar-c2{background:linear-gradient(180deg,#10b981,#059669)} | |
| .bar-c3{background:linear-gradient(180deg,#f59e0b,#d97706)} | |
| .bar-c4{background:linear-gradient(180deg,#f43f5e,#e11d48)} | |
| .bar-c5{background:linear-gradient(180deg,#8b5cf6,#7c3aed)} | |
| .bar-c6{background:linear-gradient(180deg,#06b6d4,#0891b2)} | |
| .bar-c7{background:linear-gradient(180deg,#84cc16,#65a30d)} | |
| /* ββββββββββββββββββββββββββββββββ | |
| CARD β emerald / sky theme | |
| ββββββββββββββββββββββββββββββββ */ | |
| .card-header-wrap{background:linear-gradient(135deg,var(--card-a),var(--card-b))} | |
| .card-body-wrap{padding:0} | |
| /* left accent colors cycling */ | |
| .card-item{display:flex;justify-content:space-between;align-items:center; | |
| padding:10px 14px;border-bottom:1px solid #f0fdf4;cursor:pointer; | |
| transition:background .12s;position:relative;overflow:hidden} | |
| .card-item::before{content:"";position:absolute;left:0;top:0;bottom:0; | |
| width:4px;border-radius:0 2px 2px 0} | |
| .ci-c0::before{background:var(--indigo)} | |
| .ci-c1::before{background:var(--sky)} | |
| .ci-c2::before{background:var(--emerald)} | |
| .ci-c3::before{background:var(--amber)} | |
| .ci-c4::before{background:var(--rose)} | |
| .ci-c5::before{background:var(--violet)} | |
| .ci-c6::before{background:var(--cyan)} | |
| .ci-c7::before{background:var(--lime)} | |
| .card-item:last-child{border-bottom:none} | |
| .card-item:hover{background:#f0fdf4} | |
| .card-item .key{color:var(--muted);font-size:13px;padding-left:8px} | |
| .card-item .val{font-weight:700;color:var(--text);display:flex;align-items:center;gap:6px} | |
| .ask-icon{opacity:0;font-size:11px;color:var(--card-a); | |
| background:#ecfdf5;padding:2px 7px;border-radius:99px;transition:opacity .12s} | |
| .card-item:hover .ask-icon{opacity:1} | |
| /* ββββββββββββββββββββββββββββββββ | |
| TABLE β amber / orange theme | |
| ββββββββββββββββββββββββββββββββ */ | |
| .table-header-wrap{background:linear-gradient(135deg,var(--table-a),var(--table-b))} | |
| .table-body{padding:0;overflow-x:auto} | |
| table{width:100%;border-collapse:collapse} | |
| th{background:linear-gradient(135deg,#fffbeb,#fff7ed); | |
| font-weight:700;font-size:12px;text-align:left; | |
| padding:9px 12px;border-bottom:2px solid #fed7aa; | |
| cursor:pointer;user-select:none;white-space:nowrap; | |
| color:#9a3412;text-transform:uppercase;letter-spacing:.04em} | |
| th:hover{color:var(--table-b)} | |
| th .sort-arrow{font-size:10px;margin-left:4px;opacity:.5} | |
| th.asc .sort-arrow::after{content:"β²"} | |
| th.desc .sort-arrow::after{content:"βΌ"} | |
| th:not(.asc):not(.desc) .sort-arrow::after{content:"β "} | |
| td{padding:8px 12px;border-bottom:1px solid #fef3c7;font-size:13px} | |
| tr:last-child td{border-bottom:none} | |
| tbody tr:nth-child(even) td{background:#fffbeb} | |
| tbody tr{cursor:pointer;transition:background .12s} | |
| tbody tr:hover td{background:#fed7aa } | |
| tbody tr.selected td{background:#fde68a ;font-weight:600} | |
| /* ββββββββββββββββββββββββββββββββ | |
| FIGURES β rose / violet theme | |
| ββββββββββββββββββββββββββββββββ */ | |
| .fig-header-wrap{background:linear-gradient(135deg,var(--fig-a),var(--fig-b))} | |
| .fig-body{padding:14px} | |
| .fig-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr)); | |
| gap:10px;margin-bottom:14px} | |
| .fig-cell{background:#fff;border:2px solid #fce7f3;border-radius:10px; | |
| padding:10px;text-align:center;transition:border-color .15s,box-shadow .15s} | |
| .fig-cell:focus-within{border-color:var(--fig-a);box-shadow:0 0 0 3px rgba(244,63,94,.12)} | |
| .fig-cell label{display:block;font-size:11px;font-weight:600; | |
| color:var(--fig-a);margin-bottom:6px;text-transform:uppercase;letter-spacing:.04em} | |
| .fig-cell .fig-unit{font-size:10px;color:var(--muted);margin-top:4px} | |
| .fig-cell input{width:100%;padding:6px 4px;border:none;border-bottom:2px solid #fce7f3; | |
| background:transparent;color:var(--text);font-size:18px;font-weight:700; | |
| text-align:center;outline:none;transition:border-color .15s} | |
| .fig-cell input:focus{border-color:var(--fig-a)} | |
| .fig-cta{background:linear-gradient(135deg,var(--fig-a),var(--fig-b)); | |
| color:#fff;width:100%;justify-content:center;font-size:14px;padding:10px} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="reply-bar"></div> | |
| <div id="root"><p class="loading">Initialisingβ¦</p></div> | |
| <div id="chat-bar"> | |
| <input id="chat-input" placeholder="Ask about this dataβ¦" /> | |
| <button id="chat-send">β</button> | |
| </div> | |
| <script> | |
| // ββ state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let _spec = null; | |
| // ββ bridge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function getSpec() { | |
| if (window.openai?.ready) { | |
| return new Promise(res => { | |
| window.openai.ready(async api => { | |
| const s = await api.state.get(); | |
| res(s?.structuredContent ?? null); | |
| api.state.onChanged(ns => render(ns?.structuredContent ?? null)); | |
| }); | |
| }); | |
| } | |
| return new Promise(res => { | |
| let done = false; | |
| window.addEventListener("message", ev => { | |
| const d = ev.data; | |
| if (d?.type === "mcp:structuredContent") { | |
| if (!done) { done = true; res(d.payload); } else render(d.payload); | |
| } | |
| if (d?.type === "mcp:toolResult") showReply(d.text, d.spec); | |
| }); | |
| window.parent?.postMessage({ type: "mcp:ready" }, "*"); | |
| setTimeout(() => { if (!done) { done = true; res(null); } }, 4000); | |
| }); | |
| } | |
| async function sendToChat(userText) { | |
| showThinking(); | |
| try { | |
| if (window.openai?.callTool) { | |
| const res = await window.openai.callTool("respond", { | |
| messages: [{ role: "user", content: userText }] | |
| }); | |
| showReply(res?.content?.[0]?.text ?? "", res?.structuredContent); | |
| } else { | |
| window.parent?.postMessage({ type: "mcp:toolCall", tool: "respond", | |
| args: { messages: [{ role: "user", content: userText }] } }, "*"); | |
| showReply("Request sent β awaiting responseβ¦"); | |
| } | |
| } catch(e) { showReply("β " + e.message); } | |
| } | |
| function showThinking() { | |
| const b = document.getElementById("reply-bar"); | |
| b.className = "thinking"; b.textContent = "Thinkingβ¦"; b.style.display = "block"; | |
| } | |
| function showReply(text, spec) { | |
| const b = document.getElementById("reply-bar"); | |
| b.className = ""; b.style.display = text ? "block" : "none"; | |
| b.textContent = text || ""; | |
| if (spec) render(spec); | |
| } | |
| // ββ render dispatch βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function render(spec) { | |
| _spec = spec; | |
| const root = document.getElementById("root"); | |
| if (!spec) { root.innerHTML = `<p style="color:var(--muted);padding:24px;text-align:center">No UI data.</p>`; return; } | |
| const { component, props = {}, _widget, ...rest } = spec; | |
| const p = Object.keys(props).length ? props : rest; | |
| try { | |
| const map = { form: renderForm, chart: renderChart, | |
| card: renderCard, table: renderTable, figures: renderFigures }; | |
| root.innerHTML = (map[component] ?? (() => `<p class="error">[Unknown component: ${esc(component)}]</p>`))(p); | |
| if (component === "form") attachForm(p); | |
| if (component === "chart") attachChart(p); | |
| if (component === "card") attachCard(); | |
| if (component === "table") attachTable(p); | |
| if (component === "figures") attachFigures(p); | |
| } catch(e) { root.innerHTML = `<p class="error">${e.message}</p>`; } | |
| } | |
| // ββββββββββββββββββββββββββββββββ | |
| // FORM | |
| // ββββββββββββββββββββββββββββββββ | |
| function renderForm(p) { | |
| const fields = (p.fields || []).map(f => { | |
| const req = f.required ? `<span class="req">*</span>` : ""; | |
| return `<div class="form-field"> | |
| <label for="f_${esc(f.name)}">${esc(f.label || f.name)}${req}</label> | |
| ${buildInput(f)} | |
| </div>`; | |
| }).join(""); | |
| return `<div class="ui-card"> | |
| <div class="comp-header form-header"> | |
| <span class="comp-title">${esc(p.title || "Form")}</span> | |
| <span class="comp-badge">β Input</span> | |
| </div> | |
| <div class="form-body"> | |
| <form id="adaptiveForm">${fields} | |
| <button type="submit" class="btn form-submit" id="submitBtn"> | |
| ${esc(p.submitLabel || "Submit β")} | |
| </button> | |
| <div class="form-status" id="formStatus"></div> | |
| </form> | |
| </div> | |
| </div>`; | |
| } | |
| function buildInput(f) { | |
| const id = `f_${esc(f.name)}`, name = esc(f.name), req = f.required ? "required" : ""; | |
| const type = f.type || "text"; | |
| if (type === "number" || type === "currency" || type === "integer") { | |
| const step = f.step ?? (type === "integer" ? 1 : type === "currency" ? 0.01 : 1); | |
| const prefix = type === "currency" ? `<span style="color:var(--form-a);font-weight:700;padding:0 4px">$</span>` : ""; | |
| return `<div class="number-row"> | |
| <button type="button" class="step-btn" data-for="${id}" data-d="-${step}">β</button> | |
| ${prefix}<input type="number" id="${id}" name="${name}" ${req} | |
| value="${f.default ?? 0}" min="${f.min ?? ""}" max="${f.max ?? ""}" step="${step}" style="flex:1"> | |
| <button type="button" class="step-btn" data-for="${id}" data-d="${step}">+</button> | |
| </div>`; | |
| } | |
| if (type === "range") { | |
| return `<div class="range-row"> | |
| <input type="range" id="${id}" name="${name}" ${req} | |
| min="${f.min ?? 0}" max="${f.max ?? 100}" step="${f.step ?? 1}" value="${f.default ?? 0}" | |
| oninput="document.getElementById('rv_${esc(f.name)}').textContent=this.value"> | |
| <span class="range-display" id="rv_${esc(f.name)}">${f.default ?? 0}</span> | |
| </div>`; | |
| } | |
| if (type === "select") { | |
| const opts = (f.options || []).map(o => | |
| `<option value="${esc(o.value ?? o)}">${esc(o.label ?? o)}</option>`).join(""); | |
| return `<select id="${id}" name="${name}" ${req}>${opts}</select>`; | |
| } | |
| if (type === "textarea") | |
| return `<textarea id="${id}" name="${name}" ${req} rows="3" placeholder="${esc(f.placeholder || "")}"></textarea>`; | |
| return `<input type="${esc(type)}" id="${id}" name="${name}" ${req} | |
| placeholder="${esc(f.placeholder || "")}" value="${esc(f.default || "")}">`; | |
| } | |
| function attachForm(p) { | |
| document.querySelectorAll(".step-btn").forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| const el = document.getElementById(btn.dataset.for); | |
| if (el) { el.value = +(el.value || 0) + +btn.dataset.d; } | |
| }); | |
| }); | |
| document.getElementById("adaptiveForm")?.addEventListener("submit", async ev => { | |
| ev.preventDefault(); | |
| const btn = document.getElementById("submitBtn"); | |
| const status = document.getElementById("formStatus"); | |
| btn.disabled = true; | |
| const data = Object.fromEntries(new FormData(ev.target).entries()); | |
| const summary = Object.entries(data).map(([k, v]) => `${k}: ${v}`).join(", "); | |
| status.className = "form-status"; | |
| try { | |
| await sendToChat(`Form submitted β ${p.title || "response"}: ${summary}`); | |
| status.className = "form-status ok"; status.textContent = "β " + (p.successMessage || "Submitted!"); | |
| } catch(e) { | |
| status.className = "form-status err"; status.textContent = "β " + e.message; | |
| } finally { btn.disabled = false; } | |
| }); | |
| } | |
| // ββββββββββββββββββββββββββββββββ | |
| // CHART | |
| // ββββββββββββββββββββββββββββββββ | |
| function renderChart(p) { | |
| const data = p.data || {}; | |
| const labels = data.labels || p.labels || []; | |
| const ds = data.datasets || p.datasets || []; | |
| const vals = ds[0]?.data || data.values || []; | |
| const max = Math.max(...vals, 1); | |
| const bars = labels.map((lbl, i) => { | |
| const v = vals[i] ?? 0; | |
| const h = Math.max(Math.round((v / max) * 150), 6); | |
| const ci = i % 8; | |
| return `<div class="bar-col" data-idx="${i}"> | |
| <span class="bar-val">${fmtNum(v)}</span> | |
| <div class="bar-fill bar-c${ci}" style="height:${h}px"></div> | |
| <div class="bar-tooltip">${esc(String(lbl))}: ${fmtNum(v)} β click to edit</div> | |
| <input class="bar-edit" type="number" value="${v}" data-idx="${i}"> | |
| <span class="bar-label">${esc(String(lbl))}</span> | |
| </div>`; | |
| }).join(""); | |
| return `<div class="ui-card"> | |
| <div class="comp-header chart-header-wrap"> | |
| <span class="comp-title">${esc(p.title || "Chart")}</span> | |
| <button class="btn analyze-btn" id="analyzeBtn">Analyze β</button> | |
| </div> | |
| <div class="chart-body"> | |
| <div class="chart-bars" id="chartBars">${bars}</div> | |
| <p class="chart-hint">Click a bar to edit Β· Enter to confirm Β· Analyze to discuss</p> | |
| </div> | |
| </div>`; | |
| } | |
| function attachChart(p) { | |
| const data = p.data || {}; | |
| const labels = data.labels || p.labels || []; | |
| const ds = data.datasets || p.datasets || []; | |
| const vals = [...(ds[0]?.data || data.values || [])]; | |
| document.querySelectorAll(".bar-col").forEach(col => { | |
| const idx = +col.dataset.idx; | |
| col.querySelector(".bar-fill")?.addEventListener("click", () => { | |
| document.querySelectorAll(".bar-col").forEach(c => c.classList.remove("editing")); | |
| col.classList.add("editing"); | |
| col.querySelector(".bar-edit")?.focus(); | |
| }); | |
| col.querySelector(".bar-edit")?.addEventListener("keydown", ev => { | |
| if (ev.key === "Enter") { | |
| const newVal = +ev.target.value; | |
| vals[idx] = newVal; | |
| col.classList.remove("editing"); | |
| col.querySelector(".bar-val").textContent = fmtNum(newVal); | |
| const max2 = Math.max(...vals, 1); | |
| col.querySelector(".bar-fill").style.height = Math.max(Math.round((newVal / max2) * 150), 6) + "px"; | |
| sendToChat(`Updated "${esc(String(labels[idx]))}" to ${newVal} in chart "${esc(p.title || "data")}". Analyse this change.`); | |
| } | |
| if (ev.key === "Escape") col.classList.remove("editing"); | |
| }); | |
| }); | |
| document.getElementById("analyzeBtn")?.addEventListener("click", () => { | |
| const pairs = labels.map((l, i) => `${l}: ${vals[i] ?? 0}`).join(", "); | |
| sendToChat(`Analyse chart "${esc(p.title || "data")}": ${pairs}`); | |
| }); | |
| } | |
| // ββββββββββββββββββββββββββββββββ | |
| // CARD | |
| // ββββββββββββββββββββββββββββββββ | |
| function renderCard(p) { | |
| const items = (p.items || []).map((it, i) => { | |
| const [k, v] = typeof it === "object" | |
| ? [it.label ?? it.key ?? "", it.value ?? ""] | |
| : ["", it]; | |
| return `<div class="card-item ci-c${i % 8}" data-key="${esc(String(k))}" data-val="${esc(String(v))}"> | |
| <span class="key">${esc(String(k))}</span> | |
| <span class="val">${esc(String(v))} <span class="ask-icon">β ask</span></span> | |
| </div>`; | |
| }).join(""); | |
| const head = (p.title || p.body) ? ` | |
| <div class="comp-header card-header-wrap"> | |
| <span class="comp-title">${esc(p.title || "Summary")}</span> | |
| <span class="comp-badge">π Card</span> | |
| </div> | |
| ${p.body ? `<div style="padding:10px 14px;background:#f0fdf4;font-size:13px;color:#166534;border-bottom:1px solid #d1fae5">${esc(p.body)}</div>` : ""} | |
| ` : ""; | |
| return `<div class="ui-card">${head}<div class="card-body-wrap">${items}</div></div>`; | |
| } | |
| function attachCard() { | |
| document.querySelectorAll(".card-item").forEach(el => | |
| el.addEventListener("click", () => | |
| sendToChat(`Tell me more about "${el.dataset.key}": ${el.dataset.val}`))); | |
| } | |
| // ββββββββββββββββββββββββββββββββ | |
| // TABLE | |
| // ββββββββββββββββββββββββββββββββ | |
| function renderTable(p) { | |
| const cols = p.columns || [], rows = p.rows || []; | |
| const hdr = cols.map((c, i) => | |
| `<th data-col="${i}"><span>${esc(c)}</span><span class="sort-arrow"></span></th>`).join(""); | |
| const body = rows.map((row, ri) => { | |
| const cells = Array.isArray(row) | |
| ? row.map(c => `<td>${esc(String(c))}</td>`).join("") | |
| : cols.map(c => `<td>${esc(String(row[c] ?? ""))}</td>`).join(""); | |
| return `<tr data-ri="${ri}">${cells}</tr>`; | |
| }).join(""); | |
| return `<div class="ui-card"> | |
| <div class="comp-header table-header-wrap"> | |
| <span class="comp-title">${esc(p.title || "Table")}</span> | |
| <span class="comp-badge">π Data</span> | |
| </div> | |
| <div class="table-body"> | |
| <table><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table> | |
| </div> | |
| </div>`; | |
| } | |
| function attachTable(p) { | |
| const cols = p.columns || [], rows = p.rows || []; | |
| let sortCol = -1, sortDir = 1; | |
| document.querySelectorAll("th[data-col]").forEach(th => { | |
| th.addEventListener("click", () => { | |
| const ci = +th.dataset.col; | |
| sortDir = sortCol === ci ? -sortDir : 1; sortCol = ci; | |
| document.querySelectorAll("th").forEach(t => t.classList.remove("asc","desc")); | |
| th.classList.add(sortDir > 0 ? "asc" : "desc"); | |
| const tbody = document.querySelector("tbody"); | |
| [...tbody.querySelectorAll("tr")].sort((a, b) => { | |
| const av = a.cells[ci]?.textContent ?? "", bv = b.cells[ci]?.textContent ?? ""; | |
| return isNaN(+av) ? av.localeCompare(bv) * sortDir : (+av - +bv) * sortDir; | |
| }).forEach(tr => tbody.appendChild(tr)); | |
| }); | |
| }); | |
| document.querySelectorAll("tbody tr").forEach(tr => { | |
| tr.addEventListener("click", () => { | |
| document.querySelectorAll("tbody tr").forEach(t => t.classList.remove("selected")); | |
| tr.classList.add("selected"); | |
| const ri = +tr.dataset.ri, row = rows[ri]; | |
| const summary = Array.isArray(row) | |
| ? cols.map((c, i) => `${c}: ${row[i]}`).join(", ") | |
| : cols.map(c => `${c}: ${row[c] ?? ""}`).join(", "); | |
| sendToChat(`Analyse this row β ${summary}`); | |
| }); | |
| }); | |
| } | |
| // ββββββββββββββββββββββββββββββββ | |
| // FIGURES | |
| // ββββββββββββββββββββββββββββββββ | |
| function renderFigures(p) { | |
| const cells = (p.fields || []).map((f, i) => | |
| `<div class="fig-cell"> | |
| <label>${esc(f.label || f.name)}</label> | |
| <input type="number" id="fig_${i}" value="${f.value ?? 0}" step="${f.step ?? 1}" placeholder="0"> | |
| ${f.unit ? `<div class="fig-unit">${esc(f.unit)}</div>` : ""} | |
| </div>`).join(""); | |
| return `<div class="ui-card"> | |
| <div class="comp-header fig-header-wrap"> | |
| <span class="comp-title">${esc(p.title || "Enter Figures")}</span> | |
| <span class="comp-badge">π’ Figures</span> | |
| </div> | |
| <div class="fig-body"> | |
| <div class="fig-grid">${cells}</div> | |
| <button class="btn fig-cta" id="figAnalyze">${esc(p.cta || "Analyse my figures β")}</button> | |
| </div> | |
| </div>`; | |
| } | |
| function attachFigures(p) { | |
| document.getElementById("figAnalyze")?.addEventListener("click", () => { | |
| const pairs = (p.fields || []).map((f, i) => { | |
| const v = document.getElementById(`fig_${i}`)?.value ?? 0; | |
| return `${f.label || f.name}: ${v}${f.unit ? " " + f.unit : ""}`; | |
| }).join(", "); | |
| sendToChat(`${p.cta || "Analyse"}: ${pairs}`); | |
| }); | |
| } | |
| // ββ utils ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function esc(s) { | |
| return String(s ?? "").replace(/&/g,"&").replace(/</g,"<") | |
| .replace(/>/g,">").replace(/"/g,"""); | |
| } | |
| function fmtNum(n) { | |
| if (Math.abs(n) >= 1_000_000) return (n/1_000_000).toFixed(1).replace(/\.0$/,"") + "M"; | |
| if (Math.abs(n) >= 1_000) return (n/1_000).toFixed(1).replace(/\.0$/,"") + "K"; | |
| return String(n); | |
| } | |
| // ββ chat bar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| document.getElementById("chat-send").addEventListener("click", () => { | |
| const inp = document.getElementById("chat-input"); | |
| const t = inp.value.trim(); | |
| if (t) { inp.value = ""; sendToChat(t); } | |
| }); | |
| document.getElementById("chat-input").addEventListener("keydown", ev => { | |
| if (ev.key === "Enter") document.getElementById("chat-send").click(); | |
| }); | |
| // ββ boot ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| getSpec().then(render).catch(e => { | |
| document.getElementById("root").innerHTML = `<p class="error">Bridge error: ${e.message}</p>`; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |