Exclusivobeta

Whose Words <mv-whose-words>

Revelação de autoria para textos de origem mista: rascunhos assistidos por IA, documentos escritos a várias mãos, trechos citados, e-mails com modelo e traduções. Cada trecho sabe de quem são as palavras (data-origin na marcação existente, um array spans ou acompanhamento ao vivo): do autor ou de um coautor nomeado, uma sugestão de IA aceita como estava, uma sugestão de IA editada por uma pessoa, uma citação colada de uma fonte, um texto padrão de modelo ou uma tradução. Um switch “Show whose words” dá a cada origem seu próprio padrão de sublinhado (sólido com um leve fundo para texto de IA não editado, tracejado para texto de IA editado, duplo para citações, pontilhado para modelos, ondulado para traduções, fino para coautores), um resumo na margem escrito como frase (“62% written by Lena · 30% AI-assisted, 21% of it edited · 8% quoted”) com um medidor com padrões e uma legenda que destaca uma origem no texto, e um card ao passar o mouse ou no foco que diz de onde veio um trecho e quando, o original de uma tradução e, para texto de IA editado, um diff por palavra com “Lena kept 16 of 23 suggested words”. Três visualizações: reader, badge (uma revelação publicada compacta como “Written with AI assistance · Details”, derivada das proporções ou definida manualmente) e author, que acompanha ao vivo uma região contenteditable ou um <textarea> (o que é colado vira citação, digitar dentro de um texto de IA o transforma em texto de IA editado e desfazer a edição o faz voltar, digitar na borda dele continua sendo do próprio autor, insert() e markOrigin() registram o que o app inseriu) e avisa antes da publicação quando uma parte grande demais do rascunho é texto de IA que ninguém editou, com uma revisão passo a passo.

CategoriaExibição de dados
TipoWeb Component (<mv-whose-words>)
Statusbeta
KitIA que você pode verificar
Também instalabutton, 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 d by AI and how muc h 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

Instalação

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

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["whose-words"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): 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.

Uso

Início rápido, a menor marcação que funciona:

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

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

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

Referência cultural

Cyrano de Bergerac, Edmond Rostand (1897, peça). Um jovem corteja a mulher que ama com cartas e falas escritas em segredo por outro, e todo o drama depende de ela não saber de quem são as palavras que lê, até que a verdade, revelada enfim, muda tudo. A interface torna essa revelação possível a qualquer momento: cada passagem guarda de quem são realmente as palavras (do autor, de uma IA, editadas ou não, uma citação, um modelo, uma tradução) e qualquer pessoa pode pedir ao texto que mostre isso.

API

Attributes

NameTipoDefaultDescription
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

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

Esta página foi traduzida com IA. Informar um problema de tradução