익스클루시브

Certainty <mv-certainty>

구절마다 신뢰도가 표시된 텍스트(AI 답변, 요약, 보고서)를 차분하게 렌더링해, 경고문으로 도배하지 않고도 독자가 신뢰 수준을 가늠할 수 있게 합니다. 세 단계(표시 없는 Verified, 점선 밑줄의 Likely, 의심이 클수록 진폭이 커지는 손글씨 물결의 Uncertain)가 있고, 신뢰도가 낮을수록 잉크가 옅어집니다.

작동 방식

구절마다 신뢰도 수준이 있는 텍스트(AI 답변, 요약, 보고서)를 차분하게 렌더링해, 경고로 도배하지 않고도 읽는 사람이 신뢰 정도를 가늠할 수 있게 합니다. 세 단계(표시 없는 Verified, 점선 밑줄의 Likely, 의심이 클수록 진폭이 커지는 손으로 그린 물결선의 Uncertain)가 있고, 신뢰도가 낮을수록 글자색이 옅어집니다. 구절에 호버하거나 포커스하면 렌즈(백분율, 단계, 게이지, 이유, 출처, "Flag")가 열리고 나머지는 살짝 어두워집니다. "Only show what's at least X% reliable" 슬라이더는 기준 미만의 구절을 흐리게 하거나 접고, 요약 띠가 전체 신뢰도를 보여 줍니다. 기존 마크업에도, 스트림이 도착하는 대로 추가되는 세그먼트에도 사용할 수 있습니다.

카테고리애니메이션 텍스트
유형Web Component (<mv-certainty>)
상태안정
키트검증할 수 있는 AI
Keywordsexclusive, text, ai, llm, confidence, uncertainty, trust, calibration, citations, sources, hallucination, streaming, annotation, threshold

When to use

  • An AI answer, summary or report must show per-passage confidence so readers can calibrate trust
  • Low-confidence passages should be dimmed or collapsed below a reliability threshold
  • Streamed generated text needs a reason, sources and a flag action on each passage

Avoid when

  • A full chat interface with composer and streaming replies is needed → use AI Chat instead
  • No real confidence scores are available; made-up levels would mislead readers

설치

node scripts/add.mjs certainty --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["certainty"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/position.js, components/certainty/certainty.js, components/certainty/certainty.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<mv-certainty summary>
  <p><span data-confidence="0.93">Heat pumps work efficiently in most climates</span>, and <span data-confidence="0.34" data-reason="Based on 2023 contractor quotes.">a ducted system costs about $14,000 installed</span>.</p>
</mv-certainty>

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="ct-demo" style="width:min(100%,46rem);margin-inline:auto">
  <style>
    #ct-demo { display:grid; gap:2.25rem }
    #ct-demo .ct-card { display:grid; gap:1rem; padding:1.25rem 1.375rem 1.375rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #ct-demo .ct-ask { justify-self:end; max-width:85%; margin:0; padding:.55rem .9rem; border-radius:1rem 1rem .25rem 1rem; background:var(--mv-bg-muted); font-size:.9375rem; line-height:1.45 }
    #ct-demo .ct-who { display:flex; align-items:center; gap:.5rem; margin:0; color:var(--mv-fg-muted); font-size:.75rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #ct-demo .ct-who::before { content:""; width:.5rem; height:.5rem; border-radius:50%; background:var(--mv-accent) }
    #ct-demo mv-certainty { font-size:1rem }
    #ct-demo .ct-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding-top:.25rem; color:var(--mv-fg-muted); font-size:.8125rem }
    #ct-demo .ct-foot .mv-choice { font-size:.8125rem }
    #ct-demo .ct-head { display:flex; align-items:baseline; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin:0 0 -.25rem }
    #ct-demo .ct-head h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
    #ct-demo .ct-head span { color:var(--mv-fg-muted); font-size:.8125rem }
    #ct-demo .ct-flagged { font-variant-numeric:tabular-nums }
  </style>

  <!-- 1 · Annotated markup: summary strip + threshold -->
  <section class="ct-card" aria-label="Annotated answer">
    <p class="ct-ask">Is a heat pump worth it for my house built in 1975?</p>
    <p class="ct-who">Assistant</p>
    <mv-certainty id="ct-answer" summary filter mode="dim" label="Answer reliability">
      <p>For a house built in 1975, <span data-confidence="0.93" data-reason="Standard retrofit for forced-air homes of that era." data-sources="Home Energy Handbook · Heat pump systems|https://energy.example.com/heat-pumps">an air-source heat pump can usually replace both the furnace and the central air conditioner</span>. <span data-confidence="0.66" data-reason="Inferred from the construction year; only an energy audit will confirm it.">Homes from that period often have thin wall insulation and leaky ductwork</span>.</p>
      <p><span data-confidence="0.95" data-reason="Well established, confirmed by two official sources." data-sources="Home Energy Handbook · Heat pump systems|https://energy.example.com/heat-pumps;Efficiency Label Program · Heat pumps|https://label.example.com/heat-pumps">Modern heat pumps work efficiently in most climates, and cold-climate models keep heating well below freezing</span>. <span data-confidence="0.34" data-reason="Based on 2023 contractor quotes; current regional prices were not found.">A ducted whole-house system would cost about $14,000 to $20,000 installed</span>.</p>
      <p><span data-confidence="0.9" data-reason="Certification criteria are published and stable." data-sources="Efficiency Label Program · Heat pumps|https://label.example.com/heat-pumps">Efficiency-certified models use noticeably less electricity than standard ones</span>, and <span data-confidence="0.97" data-reason="Basic physics of heat transfer, explained by the handbook." data-sources="Home Energy Handbook · Heat pump systems|https://energy.example.com/heat-pumps">they move heat rather than generate it, which is why they can deliver more energy than they consume</span>. <span data-confidence="0.58" data-reason="Depends on your utility and local program.">Many utilities offer rebates for qualifying installations</span>, <span data-confidence="0.18" data-reason="Federal credit rules changed recently; no current source confirms this.">and these would still stack with federal tax credits</span>.</p>
      <p><span data-confidence="0.72" data-reason="Common in many states, not everywhere." data-sources="Incentive Finder · State programs|https://incentives.example.com/states">Your state or city may add its own incentive</span>: <span data-confidence="0.41" data-reason="Generalized from a handful of programs only.">it often takes the form of a $500 to $1,500 rebate</span>. <span data-confidence="0.88" data-sources="Home Energy Handbook · Energy audits|https://energy.example.com/audits">The best first step is a professional home energy audit</span>, which will confirm current prices and programs.</p>
    </mv-certainty>
    <div class="ct-foot">
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="ct-collapse"><span class="mv-choice-text"><span class="mv-choice-title">Collapse hidden passages</span></span></label>
      <span class="ct-flagged" id="ct-flagged" aria-live="polite">Hover or tab to a passage to see where it comes from.</span>
    </div>
  </section>

  <!-- 2 · Streaming: segments appended as they arrive -->
  <section class="ct-card" aria-label="Streamed answer">
    <div class="ct-head"><h3>Live stream</h3><span>segments added one by one with appendSegments()</span></div>
    <p class="ct-ask">How far away is the Moon?</p>
    <mv-certainty id="ct-stream" summary label="Reliability of the answer in progress"></mv-certainty>
    <div class="ct-foot">
      <span id="ct-stream-state">Generating…</span>
      <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ct-regen">Regenerate</button>
    </div>
  </section>

  <script type="module">
    await customElements.whenDefined("mv-certainty");
    const answer = document.getElementById("ct-answer");
    const flagged = document.getElementById("ct-flagged");
    document.getElementById("ct-collapse").addEventListener("change", (e) => { answer.mode = e.target.checked ? "collapse" : "dim"; });
    answer.addEventListener("mv-flag", () => {
      const n = answer.stats.flagged;
      flagged.textContent = n ? `${n} passage${n > 1 ? "s" : ""} flagged for review` : "No passages flagged";
    });

    // Streaming: two alternative answers, each segment arrives on its own.
    const atlas = "Sky Atlas · The Moon|https://atlas.example.com/moon";
    const takes = [
      [
        { text: "The Moon orbits Earth at an average distance of " },
        { text: "about 384,400 km (238,900 miles)", confidence: 0.95, reason: "Standard value in astronomy references.", sources: atlas },
        { text: ", a figure measured to within centimeters " },
        { text: "by bouncing lasers off reflectors left on its surface", confidence: 0.9, reason: "Several sources agree.", sources: atlas },
        { text: ". " },
        { text: "Its orbit is slightly elliptical, so the distance varies by about 42,000 km", confidence: 0.74, reason: "Rounded; the closest and farthest points change from one orbit to the next." },
        { text: ", and " },
        { text: "total solar eclipses will reportedly end in about 600 million years", confidence: 0.38, reason: "Estimate quoted in popular articles; the underlying model is not cited." },
        { text: "." },
      ],
      [
        { text: "Light from the Moon reaches us in " },
        { text: "about 1.3 seconds", confidence: 0.93, reason: "Average distance divided by the speed of light.", sources: atlas },
        { text: ", so " },
        { text: "a radio signal needs roughly 2.6 seconds for the round trip", confidence: 0.88, sources: atlas },
        { text: ".\n\n" },
        { text: "The Moon drifts away from Earth by about 3.8 cm a year", confidence: 0.69, reason: "Current rate; it has changed over geological time." },
        { text: ", and " },
        { text: "it may once have orbited only about 25,000 km away", confidence: 0.34, reason: "Early-history models disagree widely." },
        { text: "." },
      ],
    ];
    const stream = document.getElementById("ct-stream");
    const state = document.getElementById("ct-stream-state");
    let run = 0;
    let take = 0;
    async function play() {
      const id = ++run;
      stream.segments = [];
      stream.setAttribute("aria-busy", "true");
      state.textContent = "Generating…";
      for (const seg of takes[take]) {
        await new Promise((r) => setTimeout(r, seg.confidence === undefined ? 160 : 520));
        if (id !== run || !stream.isConnected) return;
        stream.appendSegments(seg);
      }
      stream.setAttribute("aria-busy", "false");
      state.textContent = `Done · ${stream.stats.count} annotated passages`;
    }
    document.getElementById("ct-regen").addEventListener("click", () => { take = (take + 1) % takes.length; play(); });
    play();
  </script>
</div>

API

Attributes

Name유형DefaultDescription
summarybooleanShows the "Overall reliability" strip: mean confidence weighted by passage length, a segmented bar of tier proportions and a legend of the three underline styles.
filterbooleanShows the "Only show what's at least X% reliable" threshold slider and the count of passages below the threshold.
thresholdnumber (0..1)0Confidence threshold: passages below it recede (0 = show everything). Also accepts 0..100.
modedim | collapsedimdim: passages below the threshold stay in place, blurred and faded (stable layout; hover or focus to read them). collapse: they leave the flow and are replaced by a "… uncertain passage hidden" pill that reveals them on click.
tiersstring0.5,0.8Tier boundaries: below the first = Uncertain, from the second up = Verified, in between = Likely.
labelstringText reliabilityAccessible name of the strip.
aria-busy"true"While streaming: shows a subtle typing caret at the end of the generated text.
data-confidencenumber (0..1 or %)On a child element (span…): makes it an annotated passage. Passages added or changed later are picked up.
data-reasonstringOn a passage: short justification shown in the lens ("single source, dated 2023").
data-sourcesstringOn a passage: "Title|https://…;Title 2|https://…". Only http(s) URLs become links, opened in a new tab.

Properties

Name유형Description
segments{ text, confidence?, reason?, sources? }[]Text rendered with text nodes only (never HTML). Without confidence: neutral text; a blank line (\n\n) starts a paragraph. sources: an array of { title, url } or a string in the data-sources format. Assigning replaces all generated content.
statsobjectRead-only: { count, confidence (weighted mean), threshold, hidden, flagged, tiers: { verified | likely | uncertain: { label, count, share } }, passages: [...] }.

Methods

NameDescription
appendSegments(segment | segment[])Appends one or more segments at the end (streamed LLM output) with a fade-in; returns the created passages. The native append() is untouched.
setThreshold(value)Sets the threshold (0..1 or 0..100); same as the threshold attribute.
inspect(index) / close()Opens the lens on passage number index / closes it.

Events

NameDescription
mv-thresholdThe threshold changed. detail: { threshold, hidden, count }.
mv-inspectThe lens opens on a passage. detail: { index, text, confidence, tier, label, reason, sources, below, flagged, element }.
mv-flag"Flag" (toggle). detail: the passage data, including flagged. The passage gets data-flagged.

Content structure

NameDescription
(content)Any text (paragraphs, lists…) containing data-confidence elements. With segments, text is rendered into a [data-certainty-body] container (provided or created).

CSS classes

NameDescription
mv-certainty-passageAnnotated passage (data-tier = verified | likely | uncertain, data-below under the threshold, data-active under the lens).
mv-certainty-bar / -segments / -legend / -rangeSummary strip, segmented bar, legend and threshold slider.
mv-certainty-lensLens (popover): tier, percentage, gauge, reason, sources, .mv-certainty-flag button.
mv-certainty-stubPill standing in for a collapsed passage (collapse mode).

CSS variables

NameDefaultDescription
--mv-certainty-huevar(--mv-accent)Single hue for the whole scale (underlines, bar, gauge), stepped light to dark.
--mv-certainty-inkvar(--mv-fg)Ink of verified text.
--mv-certainty-ink-likely / -ink-uncertain84% / 68%Ink density of likely / uncertain passages.
--mv-certainty-leading1.8Line height (leaves room for the waves).
--mv-certainty-bar-bgSummary strip background.

Accessibility

Confidence is never conveyed by color alone: ink density + underline style (none / dotted / wavy) + a text label (lens, legend, description). Every passage is focusable (Tab) and described via aria-describedby ("42% confidence, uncertain: reason. 2 sources."); on keyboard focus the lens opens without stealing focus, Enter or Space moves into it (links, "Flag"), Tab out of it resumes reading right after the passage, Shift+Tab returns to the passage, Escape closes it and restores focus. The lens is a non-modal role="dialog" (manual popover), also closed by an outside click; on touch, a tap opens or closes it. The threshold slider is a labelled native range input (aria-valuetext "60% reliable"); the number of passages below the threshold is announced politely after a pause (role=status). Documented choice for hidden passages: in dim mode they stay in the accessibility tree with "Below the threshold" in their description; in collapse mode they leave it (display: none) and the pill, a real button, announces them ("Hidden uncertain passage, 32% confidence. Show"). prefers-reduced-motion and [data-motion="reduce"]: instant transitions, no fade-in or blinking caret. Forced colors: CanvasText dotted and native wavy underlines, solid slider track and lens gauge. Known limits: the component rewrites the inside of each annotated passage (its content moves into a .mv-certainty-text span and a hidden stub button is appended), so with a framework that renders the children, prefer the segments property; a data-confidence element nested in another one is ignored; segments are rendered as plain text only (no links or formatting inside a passage).

이 페이지는 AI로 번역되었습니다. 번역 문제 신고