ExklusivBeta
Understood As <mv-understood-as>
Zeigt direkt neben der Antwort, wie eine Freitextanfrage verstanden wurde, damit niemand einem Ergebnis vertraut, das für die falsche Frage berechnet wurde: „Showing results for [Net revenue] · [EU countries] · [Q2 2026 ?] · [Excluding refunds]“. Gib ihm eine Interpretation (die Anfrage, Teile mit Art, Wert, Konfidenz, Alternativen und den Wörtern, aus denen sie stammen, sowie die Annahmen, die das System selbst getroffen hat) als Eigenschaft oder JSON-Script, und setze dein Ergebnis in ein Kind [data-answer]. Jeder Teil ist ein Chip-Button mit dem Icon seiner Art (Metrik, Umfang, Zeitraum, Entität, Filter, Sortierung), der eine Listbox mit Alternativen samt Hinweisen öffnet („Q2 2026 · calendar, Apr 1-Jun 30“ vs. „Q2 FY2027 · fiscal“), optional mit einer Remove-Aktion; Hover oder Fokus auf einem Chip unterstreicht genau die Wörter der Anfrage, aus denen er stammt. Annahmen werden separat aufgeführt („Assumed: ‚EU customers‘ means [billed to an EU address] ✓ Looks right“) und lassen sich bestätigen oder ändern. Teile mit niedriger Konfidenz werden gestrichelt mit einem „?“ gezeichnet, und der unsicherste kann vorab eine einzige Rückfrage stellen, statt zu raten, entweder über der bestmöglich geschätzten Antwort (clarify="inline") oder indem er die Antwort zurückhält, bis sie beantwortet ist (clarify="block"). Jede Änderung löst das abbrechbare mv-reinterpret mit der nächsten Interpretation und einer flachen params-Map aus; die Antwort wird dann unter einer Leiste „Updating… Results below are for the previous interpretation“ mit Undo abgedunkelt, bis die App sie abschließt, und detail.waitUntil(promise) schließt automatisch ab, übernimmt eine verfeinerte Interpretation, mit der das Promise aufgelöst wird, oder rollt zurück, wenn es abgelehnt wird. Ein kurzer Verlauf früherer Interpretationen lässt sich mit einem Klick wiederherstellen.
| Kategorie | Feedback |
|---|---|
| Typ | Web Component (<mv-understood-as>) |
| Status | Beta |
| Kit | KI, die du überprüfen kannst |
| Keywords | exclusive, culture, search, natural-language, nlq, query, interpretation, ai, assistant, analytics, bi, filters, chips, disambiguation, clarifying-question, assumptions, confidence, stale, rerun, history, explainability |
When to use
- A natural-language analytics or BI query returns a number and the user must see which metric, scope and period were actually used
- An AI assistant turns a request into a search, report or filter and should expose its assumptions before the result is trusted
- Free-text search maps words to filters or entities that can be ambiguous (fiscal vs calendar, a brand vs an animal)
- The app can rerun quickly and wants users to fix one misunderstood detail instead of rephrasing the whole request
Avoid when
- Users build the query themselves from structured filters; nothing was interpreted, so show the active filters → use Facets instead
- The request is still being written and its outcome and constraints should be structured before it is sent → use Intent Composer instead
- The goal is to show how reliable each sentence of an AI answer is, not how the question was read → use Certainty instead
Installation
node scripts/add.mjs understood-as --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["understood-as"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/understood-as/understood-as.js, components/understood-as/understood-as.css.
Verwendung
Schnellstart mit dem kleinsten funktionierenden Markup:
<mv-understood-as>
<script type="application/json">{ "query": "revenue last quarter", "parts": [
{ "id": "metric", "kind": "metric", "value": "net", "match": "revenue", "options": [{ "value": "net", "label": "Net revenue" }, { "value": "gross", "label": "Gross revenue" }] },
{ "id": "period", "kind": "time", "value": "q2", "confidence": 0.4, "match": "last quarter", "options": [{ "value": "q2", "label": "Q2 2026" }, { "value": "last90", "label": "Last 90 days" }] }
] }</script>
<div data-answer>$1.24M</div>
</mv-understood-as>Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="ua-demo" style="width:min(100%,64rem);margin-inline:auto">
<style>
#ua-demo { display:grid; gap:1.25rem }
#ua-demo .ua-card { padding:1.25rem 1.25rem 1.375rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#ua-demo .ua-top { display:flex; align-items:center; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap; margin:0 0 .875rem }
#ua-demo .ua-top h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
#ua-demo .ua-top p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#ua-demo .ua-ask { display:flex; gap:.5rem; margin:0 0 .625rem }
#ua-demo .ua-ask .mv-input { flex:1; min-width:0 }
#ua-demo .ua-presets { display:flex; align-items:center; flex-wrap:wrap; gap:.375rem; margin:0 0 1.125rem; color:var(--mv-fg-muted); font-size:.75rem }
#ua-demo .ua-presets button { cursor:pointer }
#ua-demo .ua-sep { height:1px; margin:0 0 1rem; background:var(--mv-border) }
#ua-demo .ua-kpi { display:flex; align-items:flex-end; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin:0 0 1rem }
#ua-demo .ua-kpi-label { color:var(--mv-fg-muted); font-size:.75rem }
#ua-demo .ua-kpi-value { font-size:2rem; font-weight:650; letter-spacing:-.03em; line-height:1.1; font-variant-numeric:tabular-nums }
#ua-demo .ua-kpi-delta { font-size:.8125rem; color:var(--mv-fg-muted); font-variant-numeric:tabular-nums }
#ua-demo .ua-kpi-delta b { color:var(--mv-success); font-weight:600 }
#ua-demo .ua-kpi-delta b[data-down] { color:var(--mv-danger) }
#ua-demo .ua-bars { display:grid; gap:.5rem; margin:0; padding:0; list-style:none }
#ua-demo .ua-bars li { display:grid; grid-template-columns:8.5rem minmax(0,1fr) 4.5rem; align-items:center; gap:.75rem; font-size:.8125rem }
#ua-demo .ua-bars .ua-track { height:.5rem; border-radius:999px; background:var(--mv-bg-muted); overflow:hidden }
#ua-demo .ua-bars .ua-fill { display:block; height:100%; border-radius:inherit; background:var(--mv-accent); transform-origin:left; scale:var(--v) 1; transition:scale var(--mv-duration-slow) var(--mv-ease-emphasized) }
#ua-demo .ua-bars .ua-num { text-align:right; font-variant-numeric:tabular-nums; color:var(--mv-fg-muted) }
#ua-demo .ua-foot { margin:.875rem 0 0; color:var(--mv-fg-subtle); font-size:.75rem }
#ua-demo .ua-row { display:grid; grid-template-columns:minmax(0,1.35fr) minmax(0,1fr); gap:1.25rem; align-items:start }
#ua-demo .ua-photos { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.625rem; margin:0; padding:0; list-style:none }
#ua-demo .ua-photo { display:grid; gap:.375rem; font-size:.75rem }
#ua-demo .ua-thumb { aspect-ratio:3/2; border-radius:var(--mv-radius-lg); background:linear-gradient(160deg, color-mix(in oklab, var(--mv-accent) var(--a,30%), var(--mv-gray-900)), var(--mv-gray-950)); box-shadow:inset 0 0 0 1px color-mix(in oklab, var(--mv-fg) 8%, transparent) }
#ua-demo .ua-photo b { font-size:.8125rem; font-weight:600 }
#ua-demo .ua-photo span { color:var(--mv-fg-muted) }
#ua-demo .ua-panel { display:grid; gap:.875rem; padding:1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface-raised); box-shadow:var(--mv-shadow-sm); font-size:.8125rem }
#ua-demo .ua-panel h4 { margin:0; font-size:.8125rem; font-weight:600 }
#ua-demo .ua-k { color:var(--mv-fg-muted); font-size:.6875rem; letter-spacing:.04em; text-transform:uppercase; font-weight:600 }
#ua-demo .ua-log { display:grid; gap:.5rem; margin:0; padding:0; list-style:none }
#ua-demo .ua-log li { display:grid; gap:.125rem; padding:.5rem .625rem; border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); font-size:.75rem }
#ua-demo .ua-log code { font-family:var(--mv-font-mono); font-size:.6875rem; color:var(--mv-fg) }
#ua-demo .ua-log span { color:var(--mv-fg-muted); overflow-wrap:anywhere }
#ua-demo .ua-log .ua-empty { background:none; padding:0; color:var(--mv-fg-subtle) }
#ua-demo .ua-controls { display:grid; gap:.625rem }
@media (max-width:52rem) { #ua-demo .ua-row { grid-template-columns:minmax(0,1fr) } }
@media (max-width:36rem) { #ua-demo .ua-bars li { grid-template-columns:6rem minmax(0,1fr) 3.75rem } }
</style>
<!-- 1. Natural-language analytics: inline clarifying question, assumptions, history -->
<section class="ua-card" aria-labelledby="ua-main-title">
<div class="ua-top">
<div>
<h3 id="ua-main-title">Ask your revenue data</h3>
<p>Northwind Labs · Finance workspace</p>
</div>
<span class="mv-badge" data-variant="secondary" data-shape="pill">Data as of Sep 23, 2026</span>
</div>
<form class="ua-ask" id="ua-form">
<input class="mv-input" id="ua-q" type="text" aria-label="Question" value="revenue from EU customers last quarter without refunds" autocomplete="off">
<button class="mv-button" type="submit">Ask</button>
</form>
<div class="ua-presets">
<span>Try</span>
<button type="button" class="mv-badge" data-variant="outline" data-shape="pill" data-preset="revenue">revenue from EU customers last quarter…</button>
<button type="button" class="mv-badge" data-variant="outline" data-shape="pill" data-preset="signups">new signups in Germany this month vs last</button>
</div>
<div class="ua-sep"></div>
<mv-understood-as id="ua-main" clarify="inline">
<script type="application/json">
{
"query": "revenue from EU customers last quarter without refunds",
"parts": [
{ "id": "metric", "kind": "metric", "value": "net", "confidence": 0.9, "match": "revenue",
"options": [
{ "value": "net", "label": "Net revenue", "hint": "After discounts and taxes" },
{ "value": "gross", "label": "Gross revenue", "hint": "Before discounts and taxes" },
{ "value": "recurring", "label": "Recurring revenue", "hint": "Subscriptions only" }
] },
{ "id": "scope", "kind": "scope", "value": "eu", "confidence": 0.82, "match": "EU",
"options": [
{ "value": "eu", "label": "EU countries", "hint": "27 member states" },
{ "value": "europe", "label": "Europe", "hint": "EU + UK, Switzerland, Norway" },
{ "value": "emea", "label": "EMEA", "hint": "Europe, Middle East and Africa" }
] },
{ "id": "period", "kind": "time", "value": "cal-q2", "confidence": 0.42, "match": "last quarter",
"question": "By “last quarter”, did you mean the calendar or the fiscal quarter?",
"options": [
{ "value": "cal-q2", "label": "Q2 2026 · calendar", "short": "Q2 2026", "hint": "Apr 1-Jun 30, 2026" },
{ "value": "fy-q2", "label": "Q2 FY2027 · fiscal", "short": "Q2 FY2027", "hint": "May 1-Jul 31, 2026" },
{ "value": "last90", "label": "Last 90 days", "hint": "Jun 25-Sep 23, 2026" }
] },
{ "id": "refunds", "kind": "filter", "value": "refunds", "confidence": 0.95, "match": "without refunds", "removable": true,
"options": [
{ "value": "refunds", "label": "Excluding refunds" },
{ "value": "refunds-chargebacks", "label": "Excluding refunds and chargebacks" }
] }
],
"assumptions": [
{ "id": "customers", "text": "“EU customers” means", "value": "billing", "confidence": 0.7, "match": "customers",
"options": [
{ "value": "billing", "label": "billed to an EU address", "hint": "Billing country on the invoice" },
{ "value": "hq", "label": "headquartered in the EU", "hint": "Company address in the CRM" }
] },
{ "id": "currency", "text": "Amounts in", "value": "usd-monthly", "confidence": 0.8,
"options": [
{ "value": "usd-monthly", "label": "USD at monthly rates", "hint": "Each invoice at its month’s average rate" },
{ "value": "usd-today", "label": "USD at today’s rate" },
{ "value": "eur", "label": "EUR (no conversion)" }
] }
]
}
</script>
<div data-answer id="ua-answer"></div>
</mv-understood-as>
</section>
<div class="ua-row">
<!-- 2. Search: ask before guessing, results held until answered -->
<section class="ua-card" aria-labelledby="ua-photo-title">
<div class="ua-top">
<div>
<h3 id="ua-photo-title">Stock photo search</h3>
<p>clarify="block": results wait for the answer</p>
</div>
</div>
<mv-understood-as id="ua-photo" clarify="block" label="Searching for">
<script type="application/json">
{
"query": "jaguar at night",
"parts": [
{ "id": "subject", "kind": "entity", "value": "cat", "confidence": 0.35, "match": "jaguar",
"question": "“Jaguar”: the animal or the car brand?",
"options": [
{ "value": "cat", "label": "Jaguar, the big cat", "short": "Jaguar (animal)", "hint": "Wildlife, 12,400 photos" },
{ "value": "car", "label": "Jaguar, the car brand", "short": "Jaguar (cars)", "hint": "Automotive, 3,150 photos" }
] },
{ "id": "light", "kind": "filter", "value": "night", "confidence": 0.85, "match": "at night", "removable": true,
"options": [
{ "value": "night", "label": "Night shots" },
{ "value": "dusk", "label": "Dusk and night" },
{ "value": "low", "label": "Any low light" }
] },
{ "id": "type", "kind": "other", "kindLabel": "Media type", "value": "photos",
"options": [
{ "value": "photos", "label": "Photos" },
{ "value": "all", "label": "Photos and videos" }
] }
]
}
</script>
<div data-answer id="ua-photo-answer"></div>
</mv-understood-as>
</section>
<aside class="ua-panel" aria-label="Events and options">
<h4>What the app receives</h4>
<div>
<span class="ua-k">Events</span>
<ol class="ua-log" id="ua-log"><li class="ua-empty">Change a chip, confirm an assumption or answer the question.</li></ol>
</div>
<div class="ua-controls">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="ua-ask-first" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Ask before guessing</span></span>
</label>
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="ua-fail">
<span class="mv-choice-text"><span class="mv-choice-title">Make the next rerun fail</span></span>
</label>
</div>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ua-reset">Reset demo</button>
</aside>
</div>
<script type="module">
const root = document.getElementById("ua-demo");
const $ = (id) => document.getElementById(id);
const main = $("ua-main");
const photo = $("ua-photo");
await customElements.whenDefined("mv-understood-as");
const el = (tag, props = {}, ...kids) => { const n = Object.assign(document.createElement(tag), props); n.append(...kids); return n; };
const initial = { main: main.interpretation, photo: photo.interpretation };
/* ── A fake analytics backend: deterministic numbers from the interpretation ── */
const COUNTRIES = {
eu: [["Germany", 1.31], ["France", 0.98], ["Netherlands", 0.61], ["Spain", 0.52], ["Italy", 0.47]],
europe: [["United Kingdom", 1.42], ["Germany", 1.31], ["France", 0.98], ["Switzerland", 0.66], ["Netherlands", 0.61]],
emea: [["United Kingdom", 1.42], ["Germany", 1.31], ["France", 0.98], ["United Arab Emirates", 0.74], ["Switzerland", 0.66]],
};
const F = {
metric: { net: 1, gross: 1.14, recurring: 0.71 },
scope: { eu: 1, europe: 1.38, emea: 1.61 },
period: { "cal-q2": 1, "fy-q2": 1.05, last90: 1.09 },
refunds: { refunds: 1, "refunds-chargebacks": 0.991, none: 1.034 },
customers: { billing: 1, hq: 0.93 },
};
const PERIOD = { "cal-q2": "Apr 1-Jun 30, 2026", "fy-q2": "May 1-Jul 31, 2026", last90: "Jun 25-Sep 23, 2026" };
const METRIC = { net: "Net revenue", gross: "Gross revenue", recurring: "Recurring revenue" };
function revenueAnswer(p) {
const k = F.metric[p.metric] * F.scope[p.scope] * F.period[p.period] * F.refunds[p.refunds ?? "none"] * F.customers[p.customers];
const eur = p.currency === "eur";
const rate = eur ? 0.92 : p.currency === "usd-today" ? 0.987 : 1;
const fmt = new Intl.NumberFormat(eur ? "de-DE" : "en-US", { style: "currency", currency: eur ? "EUR" : "USD", notation: "compact", maximumSignificantDigits: 3 });
const total = 4.82e6 * k * rate;
const delta = ({ "cal-q2": 6.4, "fy-q2": 4.9, last90: 7.8 })[p.period] - (p.metric === "recurring" ? 2.1 : 0);
const rows = COUNTRIES[p.scope];
const max = rows[0][1];
const frag = document.createDocumentFragment();
const kpi = el("div", { className: "ua-kpi" },
el("div", {}, el("div", { className: "ua-kpi-label", textContent: `${METRIC[p.metric]} · ${PERIOD[p.period]}` }), el("div", { className: "ua-kpi-value", textContent: fmt.format(total) })),
el("div", { className: "ua-kpi-delta" }, el("b", { textContent: `+${delta.toFixed(1)}%` }), " vs the previous period"));
const list = el("ol", { className: "ua-bars", ariaLabel: "Top 5 countries" });
for (const [name, v] of rows) {
const fill = el("span", { className: "ua-fill" });
fill.style.setProperty("--v", (v / max).toFixed(3));
list.append(el("li", {}, el("span", { textContent: name }), el("span", { className: "ua-track" }, fill), el("span", { className: "ua-num", textContent: fmt.format((total * v) / 5.2) })));
}
const foot = el("p", { className: "ua-foot", textContent: `Top 5 of ${p.scope === "eu" ? 27 : p.scope === "europe" ? 30 : 71} countries · ${p.refunds ? "refunds excluded" : "refunds included"} · computed in 0.8 s` });
frag.append(kpi, list, foot);
return frag;
}
function signupsAnswer(p) {
const n = Math.round(1284 * (p.metric === "activated" ? 0.64 : 1) * (p.accounts === "all" ? 1.18 : 1));
const prev = Math.round(n / (p.compare === "full" ? 0.71 : 1.12));
const d = ((n - prev) / prev) * 100;
const frag = document.createDocumentFragment();
const b = el("b", { textContent: `${d >= 0 ? "+" : ""}${d.toFixed(1)}%` });
if (d < 0) b.dataset.down = "";
frag.append(
el("div", { className: "ua-kpi" },
el("div", {}, el("div", { className: "ua-kpi-label", textContent: `${p.metric === "activated" ? "Activated signups" : "New signups"} · Germany · Sep 1-23, 2026` }), el("div", { className: "ua-kpi-value", textContent: n.toLocaleString("en-US") })),
el("div", { className: "ua-kpi-delta" }, b, ` vs ${prev.toLocaleString("en-US")} ${p.compare === "full" ? "in all of August" : "in Aug 1-23"}`)),
el("p", { className: "ua-foot", textContent: "Berlin 31% · Munich 18% · Hamburg 11% · other cities 40%" }));
return frag;
}
const SIGNUPS = {
query: "new signups in Germany this month vs last",
parts: [
{ id: "metric", kind: "metric", value: "new", confidence: 0.88, match: "new signups", options: [
{ value: "new", label: "New signups", hint: "Accounts created" },
{ value: "activated", label: "Activated signups", hint: "Created and completed onboarding" } ] },
{ id: "scope", kind: "scope", label: "Germany", value: "de", confidence: 0.97, match: "Germany" },
{ id: "period", kind: "time", label: "Sep 1-23, 2026", value: "mtd", confidence: 0.9, match: "this month" },
{ id: "compare", kind: "time", kindLabel: "Compared with", value: "same", confidence: 0.5, match: "vs last",
question: "Compare with the same days of August, or all of August?", options: [
{ value: "same", label: "Same days of August", short: "vs Aug 1-23", hint: "Like-for-like, 23 days each" },
{ value: "full", label: "All of August", short: "vs August", hint: "31 days against 23" } ] },
],
assumptions: [
{ id: "accounts", text: "“Signups” counts", value: "self", confidence: 0.75, options: [
{ value: "self", label: "self-serve accounts only" },
{ value: "all", label: "self-serve and sales-led accounts" } ] },
],
};
const PHOTOS = {
cat: [["Jaguar crossing a river under moonlight", "Camila Ortega", 48], ["Night portrait in the Pantanal", "João Ribeiro", 36], ["Eyes in the dark, camera trap", "Ana Lucía Paz", 22], ["Resting on a branch after dusk", "Mateo Silva", 30]],
car: [["Classic coupe under city lights", "Hannah Becker", 34], ["Light trails on a mountain road", "Kenji Watanabe", 20], ["Showroom at midnight", "Olivia Brooks", 42], ["Vintage roadster, neon reflections", "Luca Romano", 28]],
};
function photoAnswer(p) {
const list = el("ul", { className: "ua-photos" });
const light = { night: "night", dusk: "dusk", low: "low light" }[p.light] ?? "any light";
for (const [title, by, a] of PHOTOS[p.subject]) {
const t = el("div", { className: "ua-thumb" });
t.style.setProperty("--a", `${a}%`);
t.setAttribute("aria-hidden", "true");
list.append(el("li", { className: "ua-photo" }, t, el("b", { textContent: title }), el("span", { textContent: `${by} · ${light} · 6000 × 4000` })));
}
return list;
}
const answer = (host, target, fn) => { target.replaceChildren(fn(host.params)); };
const renderMain = () => answer(main, $("ua-answer"), main.interpretation?.query.includes("signups") ? signupsAnswer : revenueAnswer);
const renderPhoto = () => answer(photo, $("ua-photo-answer"), photoAnswer);
renderMain();
renderPhoto();
/* ── The app reruns on mv-reinterpret, and can veto or fail ── */
const log = (type, text) => {
const list = $("ua-log");
list.querySelector(".ua-empty")?.remove();
list.prepend(el("li", {}, el("code", { textContent: type }), el("span", { textContent: text })));
while (list.children.length > 4) list.lastElementChild.remove();
};
const rerun = (host, render) => (e) => {
const { reason, part, from, to, summary } = e.detail;
log("mv-reinterpret", reason === "history" || reason === "undo" ? `${reason} → ${summary}` : `${reason} · ${part}: ${from} → ${to ?? "removed"}`);
const fail = $("ua-fail").checked;
e.detail.waitUntil(new Promise((resolve, reject) => setTimeout(() => {
if (fail) { $("ua-fail").checked = false; reject(new Error("Warehouse timeout")); return; }
resolve();
}, 900)).then(() => render()));
};
main.addEventListener("mv-reinterpret", rerun(main, renderMain));
photo.addEventListener("mv-reinterpret", rerun(photo, renderPhoto));
for (const host of [main, photo]) {
host.addEventListener("mv-clarify", (e) => log("mv-clarify", e.detail.skipped ? `${e.detail.part}: kept guess “${e.detail.guess}”` : `${e.detail.part} = ${e.detail.value}`));
host.addEventListener("mv-assumption-accept", (e) => log("mv-assumption-accept", `${e.detail.part}: ${e.detail.label}`));
host.addEventListener("mv-reinterpret-error", (e) => log("mv-reinterpret-error", `${e.detail.error.message}: previous interpretation restored`));
}
/* ── New questions: the previous interpretation goes into the history ── */
const ask = (which) => {
const data = which === "signups" ? SIGNUPS : initial.main;
$("ua-q").value = data.query;
main.interpretation = data;
renderMain();
};
$("ua-form").addEventListener("submit", (e) => { e.preventDefault(); ask(/sign\s?-?ups?/i.test($("ua-q").value) ? "signups" : "revenue"); });
root.querySelector(".ua-presets").addEventListener("click", (e) => { const b = e.target.closest("[data-preset]"); if (b) ask(b.dataset.preset); });
$("ua-ask-first").addEventListener("change", (e) => { main.clarify = e.target.checked ? "inline" : "off"; });
$("ua-reset").addEventListener("click", () => {
$("ua-q").value = initial.main.query;
$("ua-ask-first").checked = true; $("ua-fail").checked = false;
main.clarify = "inline";
// null first: a fresh start forgets the questions already answered.
main.interpretation = null; photo.interpretation = null;
main.interpretation = initial.main; photo.interpretation = initial.photo;
renderMain(); renderPhoto();
$("ua-log").replaceChildren(el("li", { className: "ua-empty", textContent: "Change a chip, confirm an assumption or answer the question." }));
});
</script>
</div>Kulturelle Referenz
Per Anhalter durch die Galaxis, Douglas Adams (1979, Buch). Ein riesiger Computer braucht eine Ewigkeit, um die ultimative Antwort zu liefern, die sich als nutzlos erweist, weil niemand genau wusste, wie die Frage lautete. In der UI wird jede Antwort mit der Frage angezeigt, wie das System sie verstanden hat, jeder interpretierte Teil und jede Annahme lässt sich korrigieren, bevor man dem Ergebnis vertraut, und die Antwort bleibt sichtbar als zur alten Frage gehörend markiert, bis sie neu berechnet ist.
API
Attributes
| Name | Typ | Default | Description |
|---|---|---|---|
clarify | "inline" | "block" | "off" | inline | When a part is below the low threshold and has a question, ask it upfront (only one question at a time, the least certain part first). inline shows the question above the best-guess answer; block hides the [data-answer] element and shows a placeholder until the question is answered or skipped (“Use the best guess”); off never asks (the part stays marked as uncertain). |
low | number (0..1) | 0.6 | Confidence under which a part is marked as uncertain (dashed chip with a “?” mark, wavy underline in the query, “uncertain” in its accessible name) and may be asked about. A part the user edited, confirmed or accepted is never uncertain. |
label | string | Showing results for | Lead words of the interpretation sentence (visible and read by screen readers). |
history | number | 5 | How many earlier interpretations to keep (0 disables the history button). Every applied change and every new query pushes the previous interpretation. |
stale | boolean | Set by the component when a change was applied and the answer still reflects the previous interpretation (aria-busy on [data-answer], dimmed answer, status bar with Undo). Remove it, call settle() or set a new interpretation once the rerun finished. | |
data-state | ready | stale | held | empty | Set by the component on itself (styleable). held = a blocking question is waiting; empty = no interpretation. | |
data-answer | attribute on a direct child | Marks the element holding the result. The interpretation is inserted just before it; it gets aria-busy while stale and hidden while a blocking question waits. |
Properties
| Name | Typ | Description |
|---|---|---|
interpretation | { query?, parts: Part[], assumptions?: Part[], …your fields } | The interpretation to display (a normalized copy when read). Part = { id, kind (metric | scope | time | entity | filter | sort | other, or your own with kindLabel), value, label?, options?: [{ value, label, short?, hint? }], confidence? (0..1, default 1), match? (words of the query it came from), question? (clarifying question), removable?, text? (assumption lead, e.g. “Amounts in”) }. A part with assumed: true (or listed in assumptions) is shown as an assumption. Setting it settles a stale answer; a different query pushes the previous one into the history. Can be set before the element is defined. |
params | { [partId]: value } | Flat map of the current values, assumptions included (read-only). Handy to rerun the query. |
summary | string | The interpretation read as one sentence, the text screen readers get (read-only). |
past | [{ interpretation, summary, at }] | Earlier interpretations, newest first (read-only). |
strings | object | UI texts to translate (lead, asked, assumed, looksRight, confirmed, uncertain, remove, stale, updating, undo, earlier, keepGuess, useGuess, held, clarifyLead, announce* templates…). Merge a partial object. |
clarify / low / label / history / stale | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
setPart(id, value) | Changes one part from code through the same path as the UI (cancelable mv-reinterpret with reason "api"). null removes a removable part. Returns false if nothing changed or the event was canceled. |
accept(id) | Confirms an assumption (fires mv-assumption-accept, no rerun). |
settle(interpretation?) | Marks the answer as up to date again; with an argument, adopts that interpretation at the same time. |
undo() | Restores the latest earlier interpretation (mv-reinterpret with reason "undo"). |
Events
| Name | Description |
|---|---|
mv-reinterpret | The user (or setPart) changed the interpretation. detail: { reason: "edit" | "remove" | "clarify" | "history" | "undo" | "api", part (id or null for a full restore), from, to (null when removed), label, interpretation (next), previous, params (next values), summary, waitUntil(promise) }. Cancelable: preventDefault() keeps the current interpretation. With waitUntil the answer settles when the promise resolves (resolve with an interpretation object to adopt the server's refined parse) and rolls back when it rejects. |
mv-reinterpret-error | A waitUntil promise rejected: the previous interpretation is back. detail: { reason, part, error, interpretation }. |
mv-clarify | The clarifying question was answered or skipped. detail: { part, question, value, guess (the value before), skipped, interpretation }. An answer different from the guess is followed by mv-reinterpret with reason "clarify". |
mv-assumption-accept | An assumption was confirmed as right (Looks right, accept(), or picking its current value). detail: { part, value, label, text, interpretation }. No rerun is needed. |
Content structure
| Name | Description |
|---|---|
script[type="application/json"] | Optional direct child holding the initial interpretation (same shape as the property). |
[data-answer] | Direct child holding the result computed for the interpretation (the app owns its content). |
CSS classes
| Name | Description |
|---|---|
mv-understood-as-head | Everything the component renders above the answer: query, sentence, assumptions, question, stale bar. |
mv-understood-as-query / -src | “You asked” line; -src marks the words each part came from (data-part, data-confidence="low", data-linked while its chip is hovered or focused). |
mv-understood-as-line / -chip | The interpretation sentence and its chips (data-part, data-kind, data-confidence="low", data-edited, data-linked, aria-expanded). |
mv-understood-as-assumed / -assumption / -accept | Assumptions row, one item per assumption (data-accepted), and its Looks right button. |
mv-understood-as-clarify / -choice / -skip | The clarifying question, its answers (data-current on the guess) and the skip button. |
mv-understood-as-stale / -undo | Status bar shown while the answer is stale. |
mv-understood-as-held | Placeholder shown instead of the answer while a blocking question waits. |
mv-understood-as-menu / -list / -option / -remove | Popover with the listbox of alternatives (data-kind="part" | "history"). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-understood-as-accent | var(--mv-accent) | Tint of edited chips, linked words, the open chip and the spinner. |
--mv-understood-as-doubt | var(--mv-warning) | Tint of uncertain parts, assumptions and the clarifying question (always paired with a dashed line or a “?” mark). |
--mv-understood-as-stale-opacity | 0.42 | Opacity of the answer while it belongs to the previous interpretation. |
--mv-understood-as-gap | var(--mv-space-4) | Space between the interpretation and the answer. |
--mv-understood-as-list-height | 16rem | Maximum height of the alternatives list. |
Accessibility
The interpretation is a group labelled by a visually hidden sentence (“Showing results for: Net revenue, EU countries, Q2 2026 (uncertain), Excluding refunds. Assumed: “EU customers” means billed to an EU address.”), so screen readers hear it as one statement before reaching the chips. Each editable chip is a native button with aria-haspopup="listbox" and aria-expanded, named with its kind, value and state (“Time range: Q2 2026, uncertain, change”); Enter, Space, ArrowDown or ArrowUp opens a listbox that takes focus and uses aria-activedescendant, with ArrowUp/ArrowDown, Home/End, first-letter typeahead, Enter/Space to choose, Escape to close and return to the chip, and Tab to leave through the Remove action when there is one. Uncertainty is never shown by color alone (dashed outline, “?” mark, wavy underline and the word “uncertain” in the name); edited chips add “set by you”. The clarifying question is a group labelled by the question with one button per answer, the current guess labelled as such. Every change is announced in a polite live region (“Time range changed to Q2 FY2027. Results are updating.”, then “Results updated for: …”, or the rollback message), the answer gets aria-busy while stale, and focus is kept on the edited chip after a change (or moved to it when the question or the assumption button it came from disappears). Nothing is announced on first render. Reduced motion (prefers-reduced-motion or data-motion="reduce"): the spinner becomes a static dotted ring and the menu and chip transitions are instant. Forced colors: chips and answers use system button colors with dashed or thicker borders for uncertain and edited parts, and the active option uses Highlight.