Exclusivebeta

Understood As <mv-understood-as>

Shows how a free-text request was understood, right next to the answer, so nobody trusts a result computed for the wrong question: “Showing results for [Net revenue] · [EU countries] · [Q2 2026 ?] · [Excluding refunds]”. Give it an interpretation (the query, parts with a kind, value, confidence, alternatives and the words they came from, plus the assumptions the system made on its own) as a property or a JSON script, and put your result in a [data-answer] child. Every part is a chip button with its kind icon (metric, scope, time range, entity, filter, sort) that opens a listbox of alternatives with hints (“Q2 2026 · calendar, Apr 1-Jun 30” vs “Q2 FY2027 · fiscal”), optionally with a Remove action; hovering or focusing a chip underlines the exact words of the query it came from. Assumptions are listed apart (“Assumed: “EU customers” means [billed to an EU address] ✓ Looks right”) and can be confirmed or changed. Low-confidence parts are drawn dashed with a “?” mark, and the least certain one can ask a single clarifying question upfront instead of guessing, either above the best-guess answer (clarify="inline") or holding the answer back until it is answered (clarify="block"). Changing anything fires the cancelable mv-reinterpret with the next interpretation and a flat params map; the answer is then dimmed under an “Updating… Results below are for the previous interpretation” bar with Undo until the app settles it, and detail.waitUntil(promise) settles automatically, adopts a refined interpretation the promise resolves with, or rolls back if it rejects. A short history of earlier interpretations can be restored in one click.

CategoryFeedback
TypeWeb Component (<mv-understood-as>)
Statusbeta
KitAI you can check
Keywordsexclusive, 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

Install

node scripts/add.mjs understood-as --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["understood-as"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): 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.

Usage

Quick start, the smallest working 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>

Canonical markup, to start from and customize with attributes, data-* and CSS variables:

<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>

Cultural reference

The Hitchhiker's Guide to the Galaxy, Douglas Adams (1979, book). A vast computer spends ages producing the ultimate answer, which turns out to be useless because nobody knew exactly what the question was. In the UI, every answer is shown with the question as the system understood it, each interpreted part and assumption can be corrected before the result is trusted, and the answer is visibly marked as belonging to the old question until it is recomputed.

API

Attributes

NameTypeDefaultDescription
clarify"inline" | "block" | "off"inlineWhen 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).
lownumber (0..1)0.6Confidence 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.
labelstringShowing results forLead words of the interpretation sentence (visible and read by screen readers).
historynumber5How many earlier interpretations to keep (0 disables the history button). Every applied change and every new query pushes the previous interpretation.
stalebooleanSet 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-stateready | stale | held | emptySet by the component on itself (styleable). held = a blocking question is waiting; empty = no interpretation.
data-answerattribute on a direct childMarks 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

NameTypeDescription
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.
summarystringThe interpretation read as one sentence, the text screen readers get (read-only).
past[{ interpretation, summary, at }]Earlier interpretations, newest first (read-only).
stringsobjectUI 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 / stalereflectedMirror the attributes.

Methods

NameDescription
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

NameDescription
mv-reinterpretThe 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-errorA waitUntil promise rejected: the previous interpretation is back. detail: { reason, part, error, interpretation }.
mv-clarifyThe 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-acceptAn 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

NameDescription
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

NameDescription
mv-understood-as-headEverything 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 / -chipThe interpretation sentence and its chips (data-part, data-kind, data-confidence="low", data-edited, data-linked, aria-expanded).
mv-understood-as-assumed / -assumption / -acceptAssumptions row, one item per assumption (data-accepted), and its Looks right button.
mv-understood-as-clarify / -choice / -skipThe clarifying question, its answers (data-current on the guess) and the skip button.
mv-understood-as-stale / -undoStatus bar shown while the answer is stale.
mv-understood-as-heldPlaceholder shown instead of the answer while a blocking question waits.
mv-understood-as-menu / -list / -option / -removePopover with the listbox of alternatives (data-kind="part" | "history").

CSS variables

NameDefaultDescription
--mv-understood-as-accentvar(--mv-accent)Tint of edited chips, linked words, the open chip and the spinner.
--mv-understood-as-doubtvar(--mv-warning)Tint of uncertain parts, assumptions and the clarifying question (always paired with a dashed line or a “?” mark).
--mv-understood-as-stale-opacity0.42Opacity of the answer while it belongs to the previous interpretation.
--mv-understood-as-gapvar(--mv-space-4)Space between the interpretation and the answer.
--mv-understood-as-list-height16remMaximum 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.