エクスクルーシブ
Certainty <mv-certainty>
各箇所に確信度を持つテキスト(AI の回答、要約、レポート)を落ち着いた表現で描画し、警告の壁なしに読み手が信頼度を調整できるようにします。3 段階(印なしの Verified、点線の下線の Likely、疑わしさに応じて振幅が大きくなる手書き風の波線の Uncertain)に加え、確信度が下がるほどインクが薄くなります。
仕組み
パッセージごとに信頼度が付いたテキスト(AI の回答、要約、レポート)を穏やかに表示し、読み手が警告だらけにならずに信頼の度合いを見極められるようにします。3 段階の区分(Verified はマークなし、Likely は点線の下線、Uncertain は疑わしさに応じて振幅が大きくなる手描き風の波線)と、信頼度が下がるほど薄くなる文字色で表現します。パッセージにホバーまたはフォーカスするとレンズ(パーセンテージ、区分、ゲージ、理由、出典、「Flag」)が開き、それ以外の部分が少し暗くなります。「Only show what's at least X% reliable」スライダーはしきい値未満のパッセージをぼかすか折りたたみ、ストリップが全体の信頼性をまとめて表示します。既存のマークアップでも、ストリームの到着に合わせて追加されるセグメントでも動作します。
| カテゴリー | アニメーションテキスト |
|---|---|
| タイプ | Web Component(<mv-certainty>) |
| ステータス | 安定版 |
| キット | 検証できる AI |
| Keywords | exclusive, 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/marvelousMarvelous 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 | タイプ | Default | Description |
|---|---|---|---|
summary | boolean | Shows the "Overall reliability" strip: mean confidence weighted by passage length, a segmented bar of tier proportions and a legend of the three underline styles. | |
filter | boolean | Shows the "Only show what's at least X% reliable" threshold slider and the count of passages below the threshold. | |
threshold | number (0..1) | 0 | Confidence threshold: passages below it recede (0 = show everything). Also accepts 0..100. |
mode | dim | collapse | dim | dim: 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. |
tiers | string | 0.5,0.8 | Tier boundaries: below the first = Uncertain, from the second up = Verified, in between = Likely. |
label | string | Text reliability | Accessible name of the strip. |
aria-busy | "true" | While streaming: shows a subtle typing caret at the end of the generated text. | |
data-confidence | number (0..1 or %) | On a child element (span…): makes it an annotated passage. Passages added or changed later are picked up. | |
data-reason | string | On a passage: short justification shown in the lens ("single source, dated 2023"). | |
data-sources | string | On 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. |
stats | object | Read-only: { count, confidence (weighted mean), threshold, hidden, flagged, tiers: { verified | likely | uncertain: { label, count, share } }, passages: [...] }. |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-threshold | The threshold changed. detail: { threshold, hidden, count }. |
mv-inspect | The 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
| Name | Description |
|---|---|
(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
| Name | Description |
|---|---|
mv-certainty-passage | Annotated passage (data-tier = verified | likely | uncertain, data-below under the threshold, data-active under the lens). |
mv-certainty-bar / -segments / -legend / -range | Summary strip, segmented bar, legend and threshold slider. |
mv-certainty-lens | Lens (popover): tier, percentage, gauge, reason, sources, .mv-certainty-flag button. |
mv-certainty-stub | Pill standing in for a collapsed passage (collapse mode). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-certainty-hue | var(--mv-accent) | Single hue for the whole scale (underlines, bar, gauge), stepped light to dark. |
--mv-certainty-ink | var(--mv-fg) | Ink of verified text. |
--mv-certainty-ink-likely / -ink-uncertain | 84% / 68% | Ink density of likely / uncertain passages. |
--mv-certainty-leading | 1.8 | Line height (leaves room for the waves). |
--mv-certainty-bar-bg | Summary 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).