| <!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> |
| |
| |
| |
| |
| |
| |
| |
| |
| :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); |
| } |
| |
| |
| [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; |
| } |
| |
| |
| [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; } |
| |
| |
| #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 { 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 { 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}} |
| |
| |
| .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 { 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%)} |
| |
| |
| .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-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-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} |
| |
| |
| .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} |
| |
| |
| .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} |
| |
| |
| @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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");} |
| 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> |
|
|