adaptive-model-gateway / adaptive_widget.html
joduor's picture
feat: generative UI with cognitive load adaptation (FAST tokens + ComputationalMR)
e08b5fe verified
Raw
History Blame Contribute Delete
36.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Adaptive UI</title>
<style>
/* ══════════════════════════════════════════════════════════════
FAST-INSPIRED DESIGN TOKEN SYSTEM
Tokens cascade from [data-density] on the root provider.
All components self-adapt by reading var(--*) tokens only.
Transitions on concrete CSS properties animate the morph.
══════════════════════════════════════════════════════════════ */
/* Provider β€” comfortable (default) */
:root,[data-density="comfortable"] {
--spacing-v: 11px;
--spacing-h: 14px;
--border-radius: 10px;
--border-radius-sm: 6px;
--ctrl-height: 36px;
--font-body: 14px;
--font-label: 12px;
--font-title: 15px;
--font-hero: 22px;
--bar-max-h: 160px;
--capacity: 8;
--accent-h: 239;
--accent-s: 84%;
--accent-l: 63%;
--surface: #ffffff;
--bg: #f0f4ff;
--muted: #64748b;
--border-c: #e2e8f0;
--morph-dur: 0.38s;
--morph-ease: cubic-bezier(0.34,1.4,0.64,1);
}
/* Spacious β€” high cognitive load, calming */
[data-density="spacious"] {
--spacing-v: 16px;
--spacing-h: 20px;
--border-radius: 16px;
--border-radius-sm: 10px;
--ctrl-height: 46px;
--font-body: 15.5px;
--font-label: 13px;
--font-title: 17px;
--font-hero: 28px;
--bar-max-h: 140px;
--capacity: 4;
--accent-h: 158;
--accent-s: 72%;
--accent-l: 45%;
--surface: #f0fdf4;
--bg: #ecfdf5;
--border-c: #bbf7d0;
}
/* Dense β€” low cognitive load, expert */
[data-density="dense"] {
--spacing-v: 7px;
--spacing-h: 10px;
--border-radius: 7px;
--border-radius-sm: 4px;
--ctrl-height: 28px;
--font-body: 12.5px;
--font-label: 10.5px;
--font-title: 13px;
--font-hero: 18px;
--bar-max-h: 190px;
--capacity: 20;
--accent-h: 262;
--accent-s: 80%;
--accent-l: 58%;
--surface: #faf5ff;
--bg: #f5f3ff;
--border-c: #ddd6fe;
}
*,*::before,*::after {
box-sizing: border-box; margin: 0; padding: 0;
transition:
padding var(--morph-dur) var(--morph-ease),
gap var(--morph-dur) var(--morph-ease),
font-size var(--morph-dur) var(--morph-ease),
border-radius var(--morph-dur) var(--morph-ease),
min-height var(--morph-dur) var(--morph-ease),
background var(--morph-dur) ease,
color var(--morph-dur) ease,
border-color var(--morph-dur) ease,
opacity 0.25s ease;
}
html,body {
height: 100%;
background: var(--bg);
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
font-size: var(--font-body);
color: #1e1b4b;
line-height: 1.5;
overflow-y: auto;
}
body { padding: 8px; }
/* ── COGNITIVE LOAD BAR ──────────────────────────────────────── */
#cog-bar {
background: var(--surface);
border: 1px solid var(--border-c);
border-radius: var(--border-radius);
padding: var(--spacing-v) var(--spacing-h);
margin-bottom: 8px;
display: none;
gap: 10px; align-items: center;
}
#cog-bar.visible { display: flex; }
.cog-label { font-size: var(--font-label); font-weight: 700; white-space: nowrap;
color: hsl(var(--accent-h),var(--accent-s),calc(var(--accent-l) - 10%)); }
.cog-track { flex:1; height:6px; background:var(--border-c); border-radius:99px; overflow:hidden; }
.cog-fill { height:100%; border-radius:99px;
background: hsl(var(--accent-h),var(--accent-s),var(--accent-l));
transition: width 0.6s var(--morph-ease), background 0.5s ease; }
.cog-mode { font-size:var(--font-label); color:var(--muted); white-space:nowrap; }
.cog-sig { font-size:10px; padding:2px 7px; border-radius:99px;
background: hsl(var(--accent-h),80%,94%);
color: hsl(var(--accent-h),60%,35%); font-weight:600; display:none; }
/* ── LOADING ─────────────────────────────────────────────────── */
.loading { display:flex;flex-direction:column;align-items:center;justify-content:center;
height:100px;gap:10px;color:var(--muted);font-size:var(--font-label); }
.dots span { display:inline-block;width:7px;height:7px;border-radius:50%;
background:hsl(var(--accent-h),var(--accent-s),var(--accent-l));margin:0 2px;
animation:bounce 0.8s infinite; }
.dots span:nth-child(2){animation-delay:.15s}.dots span:nth-child(3){animation-delay:.3s}
@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-6px)}}
/* ── REPLY BAR ───────────────────────────────────────────────── */
#reply-bar { border-radius:var(--border-radius);padding:var(--spacing-v) var(--spacing-h);
margin-bottom:8px;font-size:var(--font-label);display:none;animation:fadeUp 0.2s ease;
background:hsl(var(--accent-h),var(--accent-s),var(--accent-l));color:#fff; }
#reply-bar.visible{display:block}
@keyframes fadeUp{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
/* ── CARD SHELL ──────────────────────────────────────────────── */
.ui-card { background:var(--surface);border:1px solid var(--border-c);
border-radius:var(--border-radius);overflow:hidden;margin-bottom:4px;
animation:fadeUp 0.22s ease;box-shadow:0 2px 12px rgba(0,0,0,.06); }
.comp-header { padding:var(--spacing-v) var(--spacing-h);
display:flex;align-items:center;justify-content:space-between;
background:linear-gradient(135deg,
hsl(var(--accent-h),var(--accent-s),var(--accent-l)),
hsl(calc(var(--accent-h) - 20),var(--accent-s),calc(var(--accent-l) - 10%))); }
.comp-title { font-size:var(--font-title);font-weight:700;color:#fff; }
.comp-body { padding:var(--spacing-v) var(--spacing-h); }
.density-chip { font-size:10px;font-weight:700;padding:2px 8px;border-radius:99px;
background:rgba(255,255,255,.22);color:#fff;text-transform:uppercase;letter-spacing:.04em; }
/* ── BTN ─────────────────────────────────────────────────────── */
.btn { display:inline-flex;align-items:center;gap:4px;
padding:calc(var(--spacing-v)*.7) var(--spacing-h);
min-height:var(--ctrl-height);border:none;border-radius:var(--border-radius-sm);
font-size:var(--font-label);font-weight:600;cursor:pointer;
background:hsl(var(--accent-h),var(--accent-s),var(--accent-l));color:#fff; }
.btn:hover{opacity:.88;transform:translateY(-1px)} .btn:disabled{opacity:.4;cursor:not-allowed}
.btn-ghost { background:transparent;
border:1.5px solid hsl(var(--accent-h),var(--accent-s),var(--accent-l));
color:hsl(var(--accent-h),var(--accent-s),calc(var(--accent-l) - 10%)); }
.show-more { width:100%;padding:var(--spacing-v);
background:hsl(var(--accent-h),20%,96%);border:none;
border-top:1px solid var(--border-c);
color:hsl(var(--accent-h),var(--accent-s),calc(var(--accent-l) - 5%));
font-size:var(--font-label);font-weight:600;cursor:pointer;text-align:center;
border-radius:0 0 var(--border-radius) var(--border-radius); }
.show-more:hover{background:hsl(var(--accent-h),30%,92%)}
/* ── CHART ───────────────────────────────────────────────────── */
.bars { display:flex;align-items:flex-end;gap:calc(var(--spacing-h)*.4);
height:var(--bar-max-h);margin:var(--spacing-v) 0 6px;overflow:hidden; }
.bar-col { display:flex;flex-direction:column;align-items:center;
gap:3px;flex:1;min-width:0;position:relative; }
.bar-col[data-hidden="1"]{display:none}
.bar-fill { width:100%;border-radius:var(--border-radius-sm) var(--border-radius-sm) 0 0;
cursor:pointer;min-height:4px;transition:height .4s var(--morph-ease),filter .15s; }
.bar-fill:hover{filter:brightness(1.12)}
.bar-val { font-size:calc(var(--font-label) - 1px);font-weight:700;text-align:center; }
.bar-lbl { font-size:calc(var(--font-label) - 1px);color:var(--muted);
text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%; }
.chart-hint { font-size:calc(var(--font-label) - 2px);color:var(--muted);text-align:center;margin-top:2px; }
.bar-edit { width:100%;padding:2px;border:2px solid hsl(var(--accent-h),var(--accent-s),var(--accent-l));
border-radius:var(--border-radius-sm);font-size:var(--font-label);text-align:center;
background:#fff;display:none; }
.bar-col.editing .bar-fill,.bar-col.editing .bar-val{display:none}
.bar-col.editing .bar-edit{display:block}
.bc0{background:linear-gradient(180deg,#6366f1,#4f46e5)}
.bc1{background:linear-gradient(180deg,#0ea5e9,#0284c7)}
.bc2{background:linear-gradient(180deg,#10b981,#059669)}
.bc3{background:linear-gradient(180deg,#f59e0b,#d97706)}
.bc4{background:linear-gradient(180deg,#f43f5e,#e11d48)}
.bc5{background:linear-gradient(180deg,#8b5cf6,#7c3aed)}
.bc6{background:linear-gradient(180deg,#06b6d4,#0891b2)}
.bc7{background:linear-gradient(180deg,#84cc16,#65a30d)}
/* ── FORM ────────────────────────────────────────────────────── */
.form-field{margin-bottom:var(--spacing-v)}
.form-field[data-hidden="1"]{display:none}
.form-field label{display:block;font-weight:700;font-size:var(--font-label);margin-bottom:5px}
.req{color:#f43f5e;margin-left:2px}
.form-field input:not([type=range]),.form-field select,.form-field textarea {
width:100%;padding:calc(var(--spacing-v)*.75) var(--spacing-h);
border:2px solid var(--border-c);border-radius:var(--border-radius-sm);
background:var(--bg);color:#1e1b4b;font-size:var(--font-body);outline:none;
min-height:var(--ctrl-height); }
.form-field input:focus,.form-field select:focus,.form-field textarea:focus {
border-color:hsl(var(--accent-h),var(--accent-s),var(--accent-l));background:#fff; }
.form-field textarea{resize:vertical;min-height:calc(var(--ctrl-height)*2)}
.wizard-prog{font-size:var(--font-label);color:var(--muted);margin-bottom:var(--spacing-v);display:none}
[data-density="spacious"] .wizard-prog{display:block}
.form-submit{width:100%;justify-content:center;font-size:var(--font-body)}
.form-status{margin-top:var(--spacing-v);font-size:var(--font-label);
padding:var(--spacing-v) var(--spacing-h);border-radius:var(--border-radius-sm);display:none}
.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}
/* ── CARD ITEMS ──────────────────────────────────────────────── */
.card-item { display:flex;justify-content:space-between;align-items:center;
padding:var(--spacing-v) var(--spacing-h);border-bottom:1px solid var(--border-c);
cursor:pointer;position:relative;overflow:hidden; }
.card-item[data-hidden="1"]{display:none}
.card-item:last-child{border-bottom:none}
.card-item::before { content:"";position:absolute;left:0;top:0;bottom:0;width:4px;
border-radius:0 2px 2px 0;
background:hsl(var(--accent-h),var(--accent-s),var(--accent-l)); }
.card-item:hover{background:hsl(var(--accent-h),40%,97%)}
.card-key{color:var(--muted);font-size:var(--font-label);padding-left:8px}
.card-val{font-weight:700;font-size:var(--font-body);display:flex;align-items:center;gap:5px}
.ask-arr{opacity:0;font-size:10px;color:hsl(var(--accent-h),var(--accent-s),var(--accent-l));transition:opacity .15s}
.card-item:hover .ask-arr{opacity:1}
.card-nav{display:none;gap:var(--spacing-h);padding:var(--spacing-v) var(--spacing-h);
border-top:1px solid var(--border-c);align-items:center;justify-content:space-between}
[data-density="spacious"] .card-nav{display:flex}
.card-ctr{font-size:var(--font-label);color:var(--muted)}
[data-density="dense"] .card-items{display:grid;grid-template-columns:1fr 1fr}
[data-density="dense"] .card-item{border-right:1px solid var(--border-c)}
[data-density="dense"] .card-item:nth-child(even){border-right:none}
/* ── TABLE ───────────────────────────────────────────────────── */
.tbl-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse}
th { background:hsl(var(--accent-h),30%,97%);font-weight:700;font-size:var(--font-label);
text-align:left;padding:var(--spacing-v) var(--spacing-h);
border-bottom:2px solid hsl(var(--accent-h),50%,86%);
cursor:pointer;user-select:none;
color:hsl(var(--accent-h),60%,30%);text-transform:uppercase;letter-spacing:.04em; }
th.asc::after{content:" β–²";opacity:.6}th.desc::after{content:" β–Ό";opacity:.6}
td{padding:var(--spacing-v) var(--spacing-h);border-bottom:1px solid var(--border-c);font-size:var(--font-body)}
tr:last-child td{border-bottom:none}
tbody tr:nth-child(even) td{background:hsl(var(--accent-h),20%,98%)}
tbody tr:hover td{background:hsl(var(--accent-h),40%,95%)!important;cursor:pointer}
tbody tr.selected td{background:hsl(var(--accent-h),50%,92%)!important;font-weight:600}
th[data-col-hidden="1"],td[data-col-hidden="1"]{display:none}
tr[data-row-hidden="1"]{display:none}
/* ── FIGURES ─────────────────────────────────────────────────── */
.fig-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));
gap:var(--spacing-v);margin-bottom:var(--spacing-v)}
.fig-cell{background:var(--bg);border:2px solid var(--border-c);
border-radius:var(--border-radius);padding:var(--spacing-v);text-align:center}
.fig-cell[data-hidden="1"]{display:none}
.fig-cell:focus-within{border-color:hsl(var(--accent-h),var(--accent-s),var(--accent-l))}
.fig-lbl{font-size:calc(var(--font-label) - 1px);font-weight:700;
color:hsl(var(--accent-h),var(--accent-s),calc(var(--accent-l) - 12%));
margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em}
.fig-cell input[type=number]{width:100%;border:none;
border-bottom:2px solid var(--border-c);background:transparent;
text-align:center;font-size:var(--font-hero);font-weight:700;color:#1e1b4b;outline:none}
.fig-cell input[type=number]:focus{border-color:hsl(var(--accent-h),var(--accent-s),var(--accent-l))}
.fig-unit{font-size:calc(var(--font-label) - 1px);color:var(--muted);margin-top:3px}
.fig-wiz-nav{display:none;gap:var(--spacing-h);margin-bottom:var(--spacing-v);
align-items:center;justify-content:space-between}
[data-density="spacious"] .fig-wiz-nav{display:flex}
[data-density="spacious"] .fig-grid{grid-template-columns:1fr;max-width:280px;margin:0 auto}
/* ── MORPH FLASH ─────────────────────────────────────────────── */
@keyframes morphFlash{
0% {outline:3px solid hsl(var(--accent-h),var(--accent-s),var(--accent-l));outline-offset:2px}
100%{outline:3px solid transparent;outline-offset:12px}
}
.morph-flash{animation:morphFlash 0.55s ease-out}
</style>
</head>
<body>
<div id="provider" data-density="comfortable">
<div id="cog-bar">
<span class="cog-label">🧠</span>
<div class="cog-track"><div class="cog-fill" id="cog-fill" style="width:0%"></div></div>
<span class="cog-mode" id="cog-mode">β€”</span>
<span class="cog-sig" id="cog-sig"></span>
</div>
<div id="reply-bar"></div>
<div id="root">
<div class="loading">
<div class="dots"><span></span><span></span><span></span></div>
<p>Connecting…</p>
</div>
</div>
</div>
<script>
// ══════════════════════════════════════════════════════════════════
// FAST-STYLE DESIGN SYSTEM PROVIDER
// #provider[data-density] broadcasts tokens to all children.
// ComputationalMR-style: capacity budget limits item count per density.
// ══════════════════════════════════════════════════════════════════
const provider = document.getElementById("provider");
let _density = "comfortable";
let _lastDensity = "";
let _msgId = 1;
function applyDensity(d) {
if (!d || d === _lastDensity) return;
_lastDensity = _density = d;
provider.dataset.density = d;
const root = document.getElementById("root");
root.classList.remove("morph-flash");
void root.offsetWidth;
root.classList.add("morph-flash");
}
function getCapacity() {
// Read --capacity token (integer) from computed style
const raw = getComputedStyle(provider).getPropertyValue("--capacity").trim();
return parseInt(raw) || 8;
}
// ── Cognitive Load Provider Update ─────────────────────────────
function updateCogBar(cog) {
if (!cog) return;
document.getElementById("cog-bar").classList.add("visible");
document.getElementById("cog-fill").style.width = `${cog.level}%`;
const labels = { spacious:"Guided Mode", comfortable:"Balanced", dense:"Expert Mode" };
document.getElementById("cog-mode").textContent = labels[cog.density] || "β€”";
const sig = document.getElementById("cog-sig");
if (cog.signals?.length) { sig.style.display="inline"; sig.textContent=cog.signals[0]; }
else { sig.style.display="none"; }
applyDensity(cog.density);
}
// ── Reply bar ───────────────────────────────────────────────────
function showReply(text) {
const b = document.getElementById("reply-bar");
b.className = text ? "visible" : "";
b.textContent = text || "";
}
// ── Follow-up sender ────────────────────────────────────────────
function sendFollowup(text) {
showReply(`βŒ› Sending…`);
const oa = window.openai;
if (oa?.sendFollowupMessage) { oa.sendFollowupMessage(text); return; }
window.parent?.postMessage({
jsonrpc:"2.0", id:_msgId++,
method:"ui/message",
params:{ content:[{type:"text",text}] }
}, "*");
}
// ── Incoming structured content ─────────────────────────────────
function handleSC(sc) {
if (!sc) return;
const ui = sc.ui ?? sc.ui_spec ?? (sc.component ? sc : null);
const cog = sc.cognitive_load;
if (cog) updateCogBar(cog);
if (ui?.component) render(ui);
if (sc.text) showReply(sc.text);
}
window.addEventListener("message", (e) => {
const m = e.data;
if (!m || typeof m !== "object") return;
if (m.jsonrpc==="2.0" && m.method==="ui/notifications/tool-result") { handleSC(m.params?.structuredContent); return; }
if (m.type==="tool-result"||m.type==="structured-content") { handleSC(m.data??m.structuredContent); return; }
if (m.type==="mcp:structuredContent") { handleSC(m.payload); return; }
});
function initBridge() {
const oa = window.openai;
if (oa) {
const sc = oa.toolOutput?.structuredContent ?? oa.toolInput?.structuredContent;
if (sc) { handleSC(sc); return; }
}
window.parent?.postMessage({jsonrpc:"2.0",method:"ui/ready",params:{}}, "*");
window.parent?.postMessage({type:"mcp:ready"}, "*");
}
// ══════════════════════════════════════════════════════════════════
// GENERATIVE UI β€” SELF-ASSEMBLING COMPONENTS
// Each renderer reads the CSS --capacity token (set by density)
// to decide how much information to surface (ComputationalMR budget).
// ══════════════════════════════════════════════════════════════════
function render(spec) {
const root = document.getElementById("root");
const { component, props={}, ...rest } = spec;
const p = Object.keys(props).length ? props : rest;
try {
const map = { chart:renderChart, form:renderForm, card:renderCard, table:renderTable, figures:renderFigures };
root.innerHTML = (map[component]?.(p)) ??
`<p style="padding:12px;color:var(--muted)">[Unknown component: ${esc(component)}]</p>`;
root.classList.remove("morph-flash"); void root.offsetWidth; root.classList.add("morph-flash");
if (component==="chart") attachChart(p);
if (component==="form") attachForm(p);
if (component==="card") attachCard(p);
if (component==="table") attachTable(p);
if (component==="figures") attachFigures(p);
} catch(e) {
root.innerHTML = `<p style="padding:12px;color:#f43f5e">Render error: ${esc(e.message)}</p>`;
}
}
function chip() {
const icon = {dense:"πŸ”¬",comfortable:"πŸ“Š",spacious:"🎯"}[_density]||"πŸ“Š";
const name = {dense:"Expert",comfortable:"Standard",spacious:"Guided"}[_density]||"Standard";
return `<span class="density-chip">${icon} ${name}</span>`;
}
// ── CHART ──────────────────────────────────────────────────────
function renderChart(p) {
const cap = getCapacity();
const data=p.data||{}, ds=data.datasets||p.datasets||[];
const labels=data.labels||p.labels||[], vals=ds[0]?.data||data.values||[];
const max=Math.max(...vals.filter(Number.isFinite),1);
// Spacious: rank by value, show only top-cap
const ranked=vals.map((v,i)=>({v,i})).sort((a,b)=>b.v-a.v);
const vis=new Set((_density==="spacious"?ranked.slice(0,cap):ranked).map(x=>x.i));
const bars=labels.map((lbl,i)=>{
const v=vals[i]??0, h=Math.max(Math.round((v/max)*148),4);
return `<div class="bar-col" data-idx="${i}" ${!vis.has(i)?'data-hidden="1"':""}>
<span class="bar-val">${fmt(v)}</span>
<div class="bar-fill bc${i%8}" style="height:${h}px"></div>
<input class="bar-edit" type="number" value="${v}" data-idx="${i}">
<span class="bar-lbl">${esc(String(lbl))}</span></div>`;
}).join("");
const hint=_density==="spacious"?`Top ${Math.min(cap,labels.length)} of ${labels.length} shown`:"Click bar to edit Β· Enter to confirm";
const actions=_density==="dense"
?`<button class="btn" id="aBtn">Deep Analysis β†—</button>
<button class="btn btn-ghost" id="cmpBtn">Compare ↕</button>`
:`<button class="btn" id="aBtn">Analyse β†—</button>`;
return `<div class="ui-card"><div class="comp-header">
<span class="comp-title">${esc(p.title||"Chart")}</span>${chip()}</div>
<div class="comp-body"><div class="bars">${bars}</div>
<p class="chart-hint">${hint}</p>
<div style="display:flex;gap:8px;margin-top:var(--spacing-v)">${actions}</div>
</div></div>`;
}
function attachChart(p) {
const data=p.data||{},ds=data.datasets||p.datasets||[];
const labels=data.labels||p.labels||[], 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 nv=+ev.target.value; vals[idx]=nv;
col.classList.remove("editing");
col.querySelector(".bar-val").textContent=fmt(nv);
const mx=Math.max(...vals,1);
col.querySelector(".bar-fill").style.height=Math.max(Math.round((nv/mx)*148),4)+"px";
sendFollowup(`Update chart "${p.title||"data"}": "${labels[idx]}" is now ${nv}. Re-analyse.`);
}
if(ev.key==="Escape") col.classList.remove("editing");});
});
document.getElementById("aBtn")?.addEventListener("click",()=>{
const pairs=labels.map((l,i)=>`${l}:${vals[i]??0}`).join(", ");
sendFollowup(_density==="dense"
?`Deep analysis of "${p.title}": ${pairs}. Include trends, anomalies, recommendations.`
:`Analyse "${p.title||"chart"}": ${pairs}`);
});
document.getElementById("cmpBtn")?.addEventListener("click",()=>
sendFollowup(`Compare "${p.title||"chart"}" data against benchmarks or historical values.`));
}
// ── FORM ───────────────────────────────────────────────────────
function renderForm(p) {
const fields=p.fields||[], isWiz=_density==="spacious";
const flds=fields.map((f,i)=>{
const req=f.required?`<span class="req">*</span>`:"";
return `<div class="form-field" data-fi="${i}" ${isWiz&&i>0?'data-hidden="1"':""}>
<label for="ff_${i}">${esc(f.label||f.name)}${req}</label>
${buildInput(f,i)}</div>`;
}).join("");
return `<div class="ui-card"><div class="comp-header">
<span class="comp-title">${esc(p.title||"Form")}</span>${chip()}</div>
<div class="comp-body">
<div class="wizard-prog" id="wiz-prog">Step 1 of ${fields.length}</div>
<form id="aForm">${flds}
<button type="submit" class="btn form-submit" id="subBtn">
${esc(isWiz&&fields.length>1?`Next β†’ (${fields[1]?.label||""})`:p.submitLabel||"Submit β†’")}
</button>
<div class="form-status" id="fStatus"></div>
</form></div></div>`;
}
function buildInput(f,i){
const id=`ff_${i}`,nm=esc(f.name),rq=f.required?"required":"",t=f.type||"text";
if(t==="select"){const opts=(f.options||[]).map(o=>`<option value="${esc(o.value??o)}">${esc(o.label??o)}</option>`).join("");return `<select id="${id}" name="${nm}" ${rq}>${opts}</select>`;}
if(t==="textarea") return `<textarea id="${id}" name="${nm}" ${rq} placeholder="${esc(f.placeholder||"")}"></textarea>`;
return `<input type="${esc(t==="currency"||t==="integer"?"number":t)}" id="${id}" name="${nm}" ${rq}
placeholder="${esc(f.placeholder||"")}" value="${esc(f.default||"")}"
min="${f.min??""}" max="${f.max??""}" step="${f.step||((t==="integer"||t==="currency")?"1":"")}">`;
}
function attachForm(p){
const fields=p.fields||[], isWiz=_density==="spacious"; let step=0;
function gotoStep(n){
step=Math.max(0,Math.min(n,fields.length-1));
document.querySelectorAll(".form-field[data-fi]").forEach(el=>{el.dataset.hidden=(+el.dataset.fi===step)?"0":"1";});
const prog=document.getElementById("wiz-prog");
if(prog) prog.textContent=`Step ${step+1} of ${fields.length}`;
const btn=document.getElementById("subBtn");
if(btn) btn.textContent=step<fields.length-1
?`Next β†’ (${fields[step+1]?.label||""})`:p.submitLabel||"Submit β†’";
}
if(isWiz) gotoStep(0);
document.getElementById("aForm")?.addEventListener("submit",async ev=>{
ev.preventDefault();
if(isWiz&&step<fields.length-1){gotoStep(step+1);return;}
const btn=document.getElementById("subBtn"),st=document.getElementById("fStatus");
btn.disabled=true;
const data=Object.fromEntries(new FormData(ev.target).entries());
sendFollowup(`Form "${p.title||"submission"}": ${Object.entries(data).map(([k,v])=>`${k}: ${v}`).join(", ")}`);
st.className="form-status ok"; st.textContent="βœ“ "+(p.successMessage||"Submitted!");
btn.disabled=false;});
}
// ── CARD ───────────────────────────────────────────────────────
function renderCard(p){
const items=p.items||[], cap=getCapacity(), isWiz=_density==="spacious";
const rows=items.map((it,i)=>{
const [k,v]=typeof it==="object"?[it.label??it.key??"",it.value??""]:[it,it];
const hide=isWiz&&i>0?'data-hidden="1"':(!isWiz&&_density!=="dense"&&i>=cap?'data-hidden="1"':"");
return `<div class="card-item" data-ci="${i}" data-key="${esc(String(k))}" data-val="${esc(String(v))}" ${hide}>
<span class="card-key">${esc(String(k))}</span>
<span class="card-val">${esc(String(v))}<span class="ask-arr">β†’</span></span></div>`;
}).join("");
const nav=isWiz?`<div class="card-nav">
<button class="btn btn-ghost" id="cvPrev">← Prev</button>
<span class="card-ctr" id="cvctr">1 / ${items.length}</span>
<button class="btn" id="cvNext">Next β†’</button></div>`:"";
const more=(!isWiz&&_density!=="dense"&&items.length>cap)?
`<button class="show-more" id="cardMore">Show all ${items.length} items β–Ύ</button>`:"";
return `<div class="ui-card"><div class="comp-header">
<span class="comp-title">${esc(p.title||"Summary")}</span>${chip()}</div>
${p.body?`<div style="padding:var(--spacing-v) var(--spacing-h);background:hsl(var(--accent-h),20%,97%);font-size:var(--font-label)">${esc(p.body)}</div>`:""}
<div class="card-items">${rows}</div>${nav}${more}</div>`;
}
function attachCard(p){
const items=p.items||[], isWiz=_density==="spacious"; let cidx=0;
function showCard(n){
cidx=Math.max(0,Math.min(n,items.length-1));
document.querySelectorAll(".card-item[data-ci]").forEach(el=>{el.dataset.hidden=(+el.dataset.ci===cidx)?"0":"1";});
const ctr=document.getElementById("cvctr"); if(ctr) ctr.textContent=`${cidx+1} / ${items.length}`;
}
if(isWiz) showCard(0);
document.getElementById("cvPrev")?.addEventListener("click",()=>showCard(cidx-1));
document.getElementById("cvNext")?.addEventListener("click",()=>{
if(cidx<items.length-1) showCard(cidx+1);
else{const it=items[cidx];if(typeof it==="object") sendFollowup(`Tell me more about "${it.label}": ${it.value}`);}});
document.querySelectorAll(".card-item").forEach(el=>
el.addEventListener("click",()=>sendFollowup(`Tell me more about "${el.dataset.key}": ${el.dataset.val}`)));
document.getElementById("cardMore")?.addEventListener("click",()=>{
document.querySelectorAll(".card-item[data-ci]").forEach(el=>{el.dataset.hidden="0";});
document.getElementById("cardMore")?.remove();});
}
// ── TABLE ──────────────────────────────────────────────────────
function renderTable(p){
const cols=p.columns||[], rows=p.rows||[], cap=getCapacity();
const maxCols=_density==="spacious"?Math.min(3,cols.length):cols.length;
const maxRows=_density==="spacious"?Math.min(cap,rows.length):rows.length;
const hdr=cols.map((c,i)=>`<th data-col="${i}" ${i>=maxCols?'data-col-hidden="1"':""}>${esc(c)}</th>`).join("");
const body=rows.map((row,ri)=>{
const rh=ri>=maxRows?'data-row-hidden="1"':"";
const cells=Array.isArray(row)
?row.map((c,ci)=>`<td ${ci>=maxCols?'data-col-hidden="1"':''}>${esc(String(c))}</td>`).join("")
:cols.map((c,ci)=>`<td ${ci>=maxCols?'data-col-hidden="1"':''}>${esc(String(row[c]??"))}</td>`).join("");
return `<tr data-ri="${ri}" ${rh}>${cells}</tr>`;}).join("");
const more=rows.length>maxRows?`<button class="show-more" id="tblMore">Show all ${rows.length} rows β–Ύ</button>`:"";
return `<div class="ui-card"><div class="comp-header">
<span class="comp-title">${esc(p.title||"Table")}</span>${chip()}</div>
<div class="tbl-wrap"><table><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table></div>
${more}</div>`;
}
function attachTable(p){
const cols=p.columns||[], rows=p.rows||[]; let sc=-1,sd=1;
document.querySelectorAll("th[data-col]").forEach(th=>th.addEventListener("click",()=>{
const ci=+th.dataset.col; sd=sc===ci?-sd:1; sc=ci;
document.querySelectorAll("th").forEach(t=>t.classList.remove("asc","desc"));
th.classList.add(sd>0?"asc":"desc");
const tb=document.querySelector("tbody");
[...tb.querySelectorAll("tr:not([data-row-hidden='1'])")].sort((a,b)=>{
const av=a.cells[ci]?.textContent??"",bv=b.cells[ci]?.textContent??"";
return isNaN(+av)?av.localeCompare(bv)*sd:(+av-+bv)*sd;
}).forEach(tr=>tb.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 s=Array.isArray(row)?cols.map((c,i)=>`${c}:${row[i]}`).join(", "):cols.map(c=>`${c}:${row[c]??""}`).join(", ");
sendFollowup(`Analyse this: ${s}`);}));
document.getElementById("tblMore")?.addEventListener("click",()=>{
document.querySelectorAll("tr[data-row-hidden]").forEach(tr=>{tr.removeAttribute("data-row-hidden");});
document.getElementById("tblMore")?.remove();});
}
// ── FIGURES ────────────────────────────────────────────────────
function renderFigures(p){
const fields=p.fields||[], isWiz=_density==="spacious";
const cells=fields.map((f,i)=>`<div class="fig-cell" data-fi="${i}" ${isWiz&&i>0?'data-hidden="1"':""}>
<div class="fig-lbl">${esc(f.label||f.name)}</div>
<input type="number" id="fig_${i}" value="${f.value??0}" step="${f.step??1}">
${f.unit?`<div class="fig-unit">${esc(f.unit)}</div>`:""}</div>`).join("");
const nav=isWiz?`<div class="fig-wiz-nav">
<button class="btn btn-ghost" id="fwPrev">← Back</button>
<span id="fwctr" style="font-size:var(--font-label);color:var(--muted)">1 / ${fields.length}</span>
<button class="btn" id="fwNext">Next β†’</button></div>`:"";
return `<div class="ui-card"><div class="comp-header">
<span class="comp-title">${esc(p.title||"Figures")}</span>${chip()}</div>
<div class="comp-body">${nav}<div class="fig-grid">${cells}</div>
<button class="btn" id="figBtn" style="width:100%;justify-content:center">
${esc(p.cta||"Analyse my figures β†’")}</button></div></div>`;
}
function attachFigures(p){
const fields=p.fields||[], isWiz=_density==="spacious"; let fwStep=0;
function showFig(n){
fwStep=Math.max(0,Math.min(n,fields.length-1));
document.querySelectorAll(".fig-cell[data-fi]").forEach(el=>{el.dataset.hidden=(+el.dataset.fi===fwStep)?"0":"1";});
const ctr=document.getElementById("fwctr"); if(ctr) ctr.textContent=`${fwStep+1} / ${fields.length}`;
const btn=document.getElementById("figBtn");
if(btn) btn.textContent=fwStep<fields.length-1?`Next: ${fields[fwStep+1]?.label||""} β†’`:p.cta||"Analyse β†’";
}
if(isWiz) showFig(0);
document.getElementById("fwPrev")?.addEventListener("click",()=>showFig(fwStep-1));
document.getElementById("fwNext")?.addEventListener("click",()=>{
if(fwStep<fields.length-1) showFig(fwStep+1); else document.getElementById("figBtn")?.click();});
document.getElementById("figBtn")?.addEventListener("click",()=>{
const pairs=fields.map((f,i)=>`${f.label||f.name}: ${document.getElementById(`fig_${i}`)?.value??0}${f.unit?" "+f.unit:""}`).join(", ");
sendFollowup(`${p.cta||"Analyse"}: ${pairs}`);});
}
// ── utils ──────────────────────────────────────────────────────
function esc(s){return String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
function fmt(n){if(Math.abs(n)>=1e6)return(n/1e6).toFixed(1)+"M";if(Math.abs(n)>=1e3)return(n/1e3).toFixed(1)+"K";return String(n);}
initBridge();
</script>
</body>
</html>