ExklusivBeta

Whose Words <mv-whose-words>

Offenlegung der Urheberschaft für Text gemischter Herkunft: KI-gestützte Entwürfe, gemeinsam geschriebene Dokumente, zitierte Passagen, E-Mails aus Vorlagen und Übersetzungen. Jeder Abschnitt weiß, wessen Worte er enthält (data-origin auf bestehendem Markup, ein spans-Array oder Live-Tracking): die des Autors oder eines namentlich genannten Mitautors, ein unverändert übernommener KI-Vorschlag, ein von einer Person bearbeiteter KI-Vorschlag, ein aus einer Quelle eingefügtes Zitat, Textbausteine aus einer Vorlage oder eine Übersetzung. Ein Schalter „Show whose words“ gibt jeder Herkunft ihr eigenes Unterstreichungsmuster (durchgezogen mit zarter Tönung für unbearbeiteten KI-Text, gestrichelt für bearbeiteten KI-Text, doppelt für Zitate, gepunktet für Vorlagen, gewellt für Übersetzungen, dünn für Mitautoren), eine Zusammenfassung am Rand als ganzer Satz („62% written by Lena · 30% AI-assisted, 21% of it edited · 8% quoted“) mit gemustertem Balken und einer Legende, die eine Herkunft im Text hervorhebt, sowie eine Karte bei Hover oder Fokus, die sagt, woher und von wann ein Abschnitt stammt, das Original einer Übersetzung zeigt und bei bearbeitetem KI-Text einen Diff auf Wortebene mit „Lena kept 16 of 23 suggested words“. Drei Ansichten: Leser, Badge (eine kompakte veröffentlichte Offenlegung wie „Written with AI assistance · Details“, aus den Anteilen abgeleitet oder überschrieben) und Autor, die einen contenteditable-Bereich oder ein <textarea> live verfolgt (Eingefügtes wird zum Zitat, Tippen in KI-Text macht ihn zu bearbeitetem KI-Text und Rückgängigmachen der Änderung stellt ihn wieder her, Tippen an seinem Rand bleibt Text des Autors, insert() und markOrigin() erfassen, was die App eingefügt hat) und vor dem Veröffentlichen einen Hinweis gibt, wenn zu viel des Entwurfs KI-Text ist, den niemand bearbeitet hat, mit einer schrittweisen Prüfung.

KategorieDatenanzeige
TypWeb Component (<mv-whose-words>)
StatusBeta
KitKI, die du überprüfen kannst
Installiert auchbutton, switch, checkbox
Keywordsexclusive, culture, authorship, disclosure, ai, ai-assisted, provenance, attribution, transparency, editor, co-writing, quote, translation, diff, publishing

When to use

  • A publishing tool, CMS or newsletter editor must disclose which parts of an article were suggeste T by AI and how muc Std a person edited the Min
  • A support or sales composer should warn agents before they send a reply that is mostly unedited AI text
  • A co-written doc, report or proposal should show who wrote which passage, and what was quoted, templated or translated
  • A published page needs a compact AI-assistance badge that readers can expand into a per-passage breakdown

Avoid when

  • The question is how much of a document changed across revisions since a signed or approved baseline → use Theseus instead
  • Readers need per-passage reliability of generated text rather than who wrote it → use Certainty instead
  • The origin of each passage is not actually recorded: guessed or detector-based attributions would mislead readers

Installation

node scripts/add.mjs whose-words --out ./src/marvelous

KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["whose-words"], 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/observe.js, core/position.js, components/whose-words/whose-words.js, components/whose-words/whose-words.css, components/button/button.css, components/switch/switch.css, components/checkbox/checkbox.css.

Verwendung

Schnellstart mit dem kleinsten funktionierenden Markup:

<mv-whose-words author="Lena" show>
  <p>We cut checkout from 94 to 58 seconds. <span data-origin="ai">Every extra field is a question the customer must answer.</span> <span data-origin="quoted" data-source="Customer interview">“I just want to pay.”</span></p>
</mv-whose-words>

Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:

<div id="ww-demo" style="width:min(100%,66rem);margin-inline:auto">
  <style>
    #ww-demo { display:grid; gap:1.5rem }
    #ww-demo .ww-card { padding:1.5rem 1.75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #ww-demo .ww-eyebrow { display:flex; align-items:center; gap:.5rem; margin:0 0 1rem; color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
    #ww-demo article h3 { margin:0 0 .25rem; font-size:1.375rem; letter-spacing:-.015em; line-height:1.25 }
    #ww-demo article .ww-by { margin:0 0 1rem; color:var(--mv-fg-muted); font-size:.8125rem }
    #ww-demo article p { margin:0 0 .875rem; font-size:.9375rem; line-height:1.7; color:var(--mv-fg) }
    #ww-demo article p:last-child { margin-bottom:0 }
    #ww-demo article .ww-foot { padding-top:.75rem; border-top:1px dashed var(--mv-border); color:var(--mv-fg-muted); font-size:.8125rem }
    #ww-demo .ww-row { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1.5rem; align-items:start }
    #ww-demo .ww-help h4 { margin:0 0 .5rem; font-size:1rem; letter-spacing:-.01em }
    #ww-demo .ww-help article p { margin:.75rem 0 0; font-size:.875rem; line-height:1.65 }
    #ww-demo .ww-draft-head { display:flex; justify-content:space-between; gap:.75rem; margin:0 0 1rem; font-size:.8125rem; color:var(--mv-fg-muted) }
    #ww-demo .ww-draft-head b { color:var(--mv-fg); font-weight:600 }
    #ww-demo .ww-editor { min-height:9rem; padding:.875rem 1rem; border:1px solid var(--mv-input); border-radius:var(--mv-radius-lg); background:var(--mv-bg); font-size:.875rem; line-height:1.65; outline:none }
    #ww-demo .ww-editor:focus-visible { box-shadow:var(--mv-focus-ring) }
    #ww-demo .ww-editor p { margin:0 0 .625rem } #ww-demo .ww-editor p:last-child { margin:0 }
    #ww-demo .ww-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin-top:1rem }
    #ww-demo .ww-actions .ww-status { margin-left:auto; color:var(--mv-fg-muted); font-size:.75rem }
    #ww-demo .ww-hint { margin:0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
    @media (max-width:56rem) { #ww-demo .ww-row { grid-template-columns:minmax(0,1fr) } }
  </style>

  <section class="ww-card" aria-label="Published article, reader view">
    <p class="ww-eyebrow"><span class="mv-badge" data-variant="secondary" data-shape="pill">Reader view</span> Field notes · Payments</p>
    <mv-whose-words id="ww-post" author="Lena Fischer" show>
      <article>
        <h3>How we cut checkout time by 38%</h3>
        <p class="ww-by">Lena Fischer, with Omar Haddad · September 18, 2026</p>
        <p>For two quarters, our checkout asked for more than it needed. <span data-origin="ai" data-model="Draft assistant" data-at="2026-09-12T10:04:00">Every extra field is a question the customer has to answer before they can pay.</span> We started by counting them: fourteen, on a phone screen, before the Pay button.</p>
        <p>The customer interviews said it more bluntly. <span data-origin="quoted" data-source="Customer interview #14, Aug 28" data-href="#interviews" data-at="2026-09-12T10:11:00">“I just want to pay and get back to my day.”</span> <span data-origin="ai-edited" data-model="Draft assistant" data-at="2026-09-12T10:26:00" data-original="The redesigned flow reduced the median checkout time from 94 seconds to 58 seconds, which represents a significant 38% improvement for our customers.">The new flow brought median checkout time down from 94 to 58 seconds, 38% faster, with no drop in average order value.</span></p>
        <p><span data-origin="author" data-by="Omar Haddad" data-at="2026-09-13T16:40:00">On the engineering side, we moved card tokenization ahead of the review step, so the Pay button no longer waits on a network round trip.</span> <span data-origin="translated" data-lang="de" data-model="Machine translation" data-at="2026-09-14T09:02:00" data-original="Die Adressprüfung läuft jetzt im Hintergrund und blockiert den Bezahlknopf nicht mehr.">Address validation now runs in the background and no longer blocks the Pay button.</span></p>
        <p>We shipped it to 10% of traffic first and watched refunds and support tickets for a week. <span data-origin="ai" data-model="Draft assistant" data-at="2026-09-12T10:31:00">Next, we will test saved payment methods for returning customers and one-tap wallets in six more markets.</span></p>
        <p class="ww-foot"><span data-origin="template" data-source="Release note footer" data-at="2026-09-12T09:58:00">Questions about this release? Reach the Payments team in #payments-help.</span></p>
      </article>
    </mv-whose-words>
  </section>

  <div class="ww-row">
    <section class="ww-card ww-help" aria-label="Help center article, badge view">
      <p class="ww-eyebrow"><span class="mv-badge" data-variant="secondary" data-shape="pill">Badge view</span> Help center</p>
      <mv-whose-words id="ww-help" view="badge" author="Priya Raman">
        <article>
          <h4>Reset two-factor authentication</h4>
          <p><span data-origin="ai-edited" data-model="Support assistant" data-at="2026-09-02T14:12:00" data-original="If you lost access to your authenticator app, you can reset two-factor authentication from the sign-in page.">Lost your phone or authenticator app? You can reset two-factor authentication yourself from the sign-in page.</span> Choose <b>Use a recovery code</b>, enter one of the codes you saved when you turned it on, then scan the new QR code.</p>
          <p><span data-origin="ai" data-model="Support assistant" data-at="2026-09-02T14:15:00">If you no longer have your recovery codes, an admin of your workspace can reset it for you from Members.</span></p>
        </article>
      </mv-whose-words>
    </section>

    <section class="ww-card" aria-label="Reply draft, author view">
      <p class="ww-eyebrow"><span class="mv-badge" data-variant="secondary" data-shape="pill">Author view</span> Live tracking</p>
      <div class="ww-draft-head"><span>Reply to <b>Kenji Watanabe</b></span><span>Ticket #48213 · Refund request</span></div>
      <mv-whose-words id="ww-draft" view="author" author="Sofia Marino" summary="none" show>
        <div class="ww-editor" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Reply to Kenji Watanabe">
          <p>Hi Kenji,</p>
          <p><span data-origin="ai" data-model="Reply assistant" data-at="2026-09-24T09:41:00">Thank you for reaching out, and I'm sorry the order arrived damaged. I've issued a full refund of $86.40 to your original payment method; it should appear within 5 to 7 business days.</span> I've also flagged the carrier so it doesn't happen again.</p>
        </div>
      </mv-whose-words>
      <div class="ww-actions">
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ww-suggest">Suggest a closing line</button>
        <button class="mv-button" data-size="sm" type="button" id="ww-send">Send reply</button>
        <span class="ww-status" id="ww-status" role="status">Type inside the highlighted text to edit it</span>
      </div>
    </section>
  </div>
  <p class="ww-hint">Hover or Tab through the underlined passages · open the badge’s Details · click a legend row to highlight one origin</p>

  <script type="module">
    await customElements.whenDefined("mv-whose-words");
    const draft = document.getElementById("ww-draft");
    const status = document.getElementById("ww-status");
    const closings = [
      " Please let me know if there is anything else I can help with.",
      " If the refund hasn't arrived by next Friday, just reply to this email and I'll follow up with our payments team.",
    ];
    let n = 0;
    document.getElementById("ww-suggest").addEventListener("click", () => {
      const last = draft.querySelector(".ww-editor p:last-child");
      const r = document.createRange();
      r.selectNodeContents(last);
      r.collapse(false);
      const sel = getSelection();
      sel.removeAllRanges();
      sel.addRange(r);
      draft.insert(closings[n++ % closings.length], "ai", { model: "Reply assistant" });
    });
    document.getElementById("ww-send").addEventListener("click", () => {
      const res = draft.checkBeforePublish();
      status.textContent = res.ok ? `Sent · disclosed as “${res.label}”` : `Held: ${Math.round(res.unedited * 100)}% unedited AI text`;
    });
    draft.addEventListener("mv-origin-change", (e) => {
      if (e.detail.source === "edit") status.textContent = e.detail.origin === "ai-edited" ? "Edited: now counted as AI-assisted, edited" : "Edit undone: back to AI, as-is";
    });
    document.querySelector("#ww-post").addEventListener("click", (e) => { if (e.target.closest("a[href^='#']")) e.preventDefault(); });
  </script>
</div>

Kulturelle Referenz

Cyrano de Bergerac, Edmond Rostand (1897, Theaterstück). Ein junger Mann umwirbt die Frau, die er liebt, mit Briefen und Versen, die heimlich ein anderer schreibt, und das ganze Drama beruht darauf, dass sie nicht weiß, wessen Worte sie liest, bis die Wahrheit, endlich enthüllt, alles verändert. Die UI macht diese Enthüllung jederzeit möglich: Jede Passage weiß, wessen Worte sie wirklich enthält (die des Autors, die einer KI, bearbeitet oder nicht, ein Zitat, eine Vorlage, eine Übersetzung), und jeder kann den Text bitten, es zu zeigen.

API

Attributes

NameTypDefaultDescription
authorstringName of the primary author. Unmarked text is theirs; it is used in the summary (“written by Lena”), in the badge (“Written by Lena”) and as data-by of spans the component creates.
showbooleanfalseShows whose words: underline patterns in the text, the margin summary (reader view), hover cards and spoken markers. Reflects the switch.
view"reader" | "badge" | "author"readerreader: switch + disclosure chip, summary in the margin while shown. badge: a compact published disclosure with a Details button that expands the summary and the switch. author: summary always visible, “Readers see: …” preview of the disclosure and the publish nudge.
summary"margin" | "top" | "none"marginWhere the summary goes. margin needs about 680px of width and falls back to top below that; the margin column is kept while hidden, so toggling never reflows the text.
nudge-thresholdnumber (0..1)0.4Author view: share of the text that is unedited AI suggestion above which the nudge shows and checkBeforePublish() returns ok: false.
paste-originorigin | "none"quotedLive tracking: origin given to pasted text (as plain text). "none" leaves pastes to the browser, counted as the author’s words.
badge-labelstringOverrides the disclosure text (otherwise “Written by {author}”, “Written with AI assistance” or “Mostly AI-generated, reviewed by {author}” when unedited AI text is 50% or more).
labelstringWhose wordsTitle of the summary (and its group label).
localeBCP 47 tagen-USDates, numbers and language names.
data-origin"author" | "ai" | "ai-edited" | "quoted" | "template" | "translated"On any inline element of the text: its origin. Aliases accepted: human, ai-accepted, edited, quote, pasted, translation. Nested marks: the innermost wins.
data-by / data-at / data-modelstring / ISO date / stringOn a mark: the person responsible (author, co-author, who accepted or edited the suggestion), when it was inserted or last edited, and the AI tool or translation engine.
data-originalstringOn ai-edited: the suggestion as the AI proposed it (the diff is computed against it). On translated: the source text. Set automatically by live tracking.
data-source / data-href / data-lang / data-notestringOn quoted: source title and link; on template: template name; on translated: source language (BCP 47, shown as “From German”); any: a free note shown in the card.
data-origin-idstringGroups several elements into one passage (set automatically). A passage split across paragraphs keeps one id, so its diff and counts stay whole.
data-whose-words-docbooleanMarks the content element when the host has several children (otherwise the only child, or a wrapper created around them). A <textarea> child enables textarea tracking; a contenteditable child enables live tracking.

Properties

NameTypDescription
spansArray<{ text, origin, by?, at?, model?, original?, source?, href?, lang?, note?, id? }>Get: the whole text as ordered segments (paragraphs separated by blank lines). Set: renders them (blank lines start a new paragraph) or fills the textarea. Can also come from a <script type="application/json" data-spans> child, or be set before the element is defined.
stats{ total, words, shares: { human, ai, aiEdited, aiAssisted, quoted, template, translated }, editedOfAI, persons: [{ name, share, primary }], level, label, sentence }Read-only. Shares are 0..1 by letters (whitespace ignored). level: "empty" | "human" | "assisted" | "generated"; label is the disclosure text; sentence is the summary.
disclosure{ level, label }Read-only: what the badge says, for use elsewhere (RSS, meta tags, an export).
stringsobjectOverrides any built-in English text (keys as in the source, e.g. { toggle: "Show origins" }).
author / show / view / summary / nudgeThreshold / pasteOrigin / badgeLabel / label / localereflectedMirror the attributes.

Methods

NameDescription
markOrigin(range, origin, meta?)Records the origin of text the app inserted or knows about. range: a DOM Range inside the text, { start, end } character offsets (the only form for a textarea), or an element. meta: { by, at, model, original, source, href, lang, note }. AI text marked "ai" is snapshotted, so a later human edit is detected. Returns the first marked element (or the offsets).
insert(text, origin = "ai", meta?)Inserts text at the caret (or at the end of the last paragraph) with its origin, e.g. an accepted AI suggestion, and fires an input event so frameworks stay in sync. Returns the new span (or the offsets).
toggle(force?)Shows or hides whose words (through the same cancelable event as the switch). Returns false if canceled.
reviewNext()Author view: shows whose words and selects the next AI passage nobody edited (the review button calls it). Returns it, or null when none is left.
checkBeforePublish()Returns { ok, unedited, level, label }. When not ok, the nudge flashes, focus moves to its review button and the reason is announced: call it from your Publish or Send button.
refresh()Re-reads the markup (changes inside the text are already observed).

Events

NameDescription
mv-origin-changeA passage got or changed its origin. detail: { origin, previous, text, source: "api" | "edit" | "paste", target (element or null), range ({ start, end } for a textarea), stats }. Cancelable only for source "paste" (fired before the paste): preventDefault() lets the paste count as the author’s words.
mv-disclosure-toggleCancelable. detail: { part: "text", show, source: "switch" | "api" | "review" | "legend" } when the marks are shown or hidden, or { part: "details", expanded } when the badge’s details open or close.
mv-nudgeAuthor view: unedited AI text just crossed nudge-threshold. detail: { unedited, threshold, passages, level }.

Content structure

NameDescription
(content)One element holding the text (an <article>, a contenteditable <div> or a <textarea>). Marks are data-origin attributes on its inline elements; the component only adds data-origin-id, data-person, data-spot, data-ww-active and, while shown in read mode, tabindex, aria-describedby and a visually hidden marker, all removed when hidden.

CSS classes

NameDescription
mv-whose-words-bar / -toggle / -chip / -detailsHeader: the switch (input.mv-switch role=switch), the disclosure chip (data-level), and in badge view the Details button.
mv-whose-words-nudgeAuthor view notice before publishing; data-flash while checkBeforePublish() draws attention to it.
mv-whose-words-aside / -sentence / -meter / -seg / -legend / -key / -swatchSummary: sentence, patterned meter (data-key per origin, data-person per co-author) and legend buttons (aria-pressed) that highlight one origin.
mv-whose-words-cardHover, focus or caret card (top layer). data-pinned after a click or Enter: links become clickable and it closes with Escape.
mv-whose-words-field / -mirrorTextarea tracking: the wrapper and the aligned mirror that draws the marks under the textarea’s own text.

CSS variables

NameDefaultDescription
--mv-whose-words-aiviolet (light-dark)AI suggestions, as-is and edited.
--mv-whose-words-quoted / -template / -translatedblue / gray / greenOther origins.
--mv-whose-words-author / --mv-whose-words-person-2…5neutral / amber, pink, teal, olivePrimary author (meter) and co-authors.
--mv-whose-words-margin15.5remWidth of the margin column.
--mv-whose-words-offset0.26emUnderline offset of the marks.

Accessibility

Nothing is added to the reading experience until someone asks: with the marks hidden, the text is untouched for screen readers. The toggle is a native checkbox with role="switch" and a visible label; showing or hiding announces the summary sentence in a polite live region. The summary is always real text (“62% written by Lena · 30% AI-assisted, 21% of it edited · 8% quoted”), in a group labeled by its title; the meter is decorative and aria-hidden, and the legend is a list of toggle buttons (aria-pressed) whose labels carry the same underline pattern as the text. Origins never rely on color: each has its own line style (solid with a wash, dashed, double, dotted, wavy, thin), repeated in the meter as fill patterns. While shown in read mode, every marked passage gets a visually hidden start marker (“AI suggestion, edited:”) so continuous reading hears where each origin begins, becomes a tab stop and is described (aria-describedby) by a full sentence: origin, who, when, how many suggested words were kept, the original suggestion, source or source-language text. Focus or hover opens the card; Enter, Space or a click pins it as a labeled dialog with focus moved inside (Escape closes it and returns focus); all of this is removed when hidden. In live editing (contenteditable or textarea) passages are not tab stops: moving the caret into one opens the card and announces its origin once, and the review button selects each unedited AI passage in turn. The badge’s Details button is a disclosure (aria-expanded, aria-controls). Reduced motion (OS or data-motion="reduce"): no card, panel or nudge animation. Forced colors: marks use CanvasText (Highlight for unedited AI text), meter patterns are redrawn in system colors, borders use CanvasText.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden