# AI content label

A label for content that is wholly or partly AI-generated: "AI-generated", "AI-modified", or "AI involvement not recorded" when the app has no record. Its two levels follow the categories of the EU's voluntary Code of Practice on marking and labelling AI-generated content (final, 10 June 2026): fully AI-generated, and partially AI-modified. It uses Candor's own mark; the EU's optional icons aren't bundled. The level comes from the app's records, never guessed. Use `provenanceText`, `withProvenance`, `provenanceMetadata` and `useCopyWithProvenance` to keep the label on copied and downloaded content. Whether a label meets any rule stays the deployer's call.

- Registry name: `ai-content-label` (`@candor/ai-content-label`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/ai-content-label

## Install

```bash
npx candor-ui add ai-content-label
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/ai-content-label`.
It brings in: locale, use-copy, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { Copy } from "lucide-react"
import { AiContentLabel, provenanceMetadata, useCopyWithProvenance, type AiProvenance } from "@/components/ai-content-label"
import { Button } from "@/components/ui/button"

const summary: AiProvenance = { involvement: "generated", by: "Drafting agent", at: "2026-11-05T10:00:00Z" }

export default function Example() {
  const { state, copy } = useCopyWithProvenance(summary)
  return (
    <div className="flex max-w-xl flex-col gap-5">
      <div className="flex flex-col gap-2 rounded-lg border p-3 text-sm">
        <div className="flex items-center justify-between gap-2">
          <AiContentLabel {...summary} />
          <Button type="button" variant="ghost" size="xs" onClick={() => copy("Quarterly support volume fell 12%, mostly in billing questions.")}>
            <Copy aria-hidden />
            {state === "copied" ? "Copied" : state === "failed" ? "Copy failed" : "Copy with label"}
          </Button>
        </div>
        <p>Quarterly support volume fell 12%, mostly in billing questions.</p>
      </div>
      <div className="flex flex-wrap gap-2">
        <AiContentLabel involvement="modified" by="Writing assistant" />
        <AiContentLabel involvement="none" />
        <AiContentLabel involvement="unknown" />
      </div>
      <figure className="flex flex-col gap-2">
        <div aria-hidden className="h-24 rounded-md bg-muted" />
        <figcaption>
          <AiContentLabel size="caption" involvement="generated" by="Image tool" at="2026-11-04T16:30:00Z" note="Illustration only; not a photo of a real place." />
        </figcaption>
      </figure>
      <pre className="overflow-x-auto rounded-md bg-muted p-2 text-2xs">{JSON.stringify(provenanceMetadata(summary), null, 2)}</pre>
    </div>
  )
}
```

## <AiContentLabel>

Props:

- `involvement: AiInvolvement`: How much AI went into it, from the app's records.
- `by?: string | undefined`: The tool or agent that was used, named by the app ("Drafting agent"). Shown for "generated" and "modified" only.
- `at?: Date | string | undefined`: When it was made or changed, as a Date or ISO string. Shown for "generated" and "modified" only.
- `note?: string | undefined`: The app's own plain-text note, e.g. "Figures checked by finance". Shown in the caption; not part of copied text.
- `size?: "inline" | "caption" | undefined` (default `"inline"`): "inline" (the default): a small chip beside a message or file name. "caption": a line under an image or file, with the note.
- `className?: string | undefined`

## Functions

### provenanceText

```ts
provenanceText(label: AiProvenance, options?: ProvenanceOptions)
```

The label as one plain line: "AI-generated with Drafting agent, 5 Nov 2026", or "AI involvement not recorded".

### withProvenance

```ts
withProvenance(text: string, label: AiProvenance, options?: ProvenanceOptions)
```

Text for the clipboard with its provenance line after it, so the label travels with the words.

### provenanceMetadata

```ts
provenanceMetadata(label: AiProvenance)
```

The label as a small JSON-able object for downloads and sidecar files, under keys that don't assume XMP or IPTC: `"ai:involvement"` always; `"ai:by"` (the app's name for the tool) and `"ai:at"` (ISO 8601) when known. Map them to your format's own fields if it has them. Words aren't included: the values are the records, not the UI's English.

### useCopyWithProvenance

```ts
useCopyWithProvenance(label: AiProvenance, resetAfter?: number)
```

useCopy, with the label's provenance line added under whatever is copied. `copy(text)` resolves to "copied" only after the clipboard write does; `provenance` is the line it adds, for showing beside the copy button.

## Strings (`aiContentLabel`)

The fixed words, English by default. Override any of them under `aiContentLabel` in `<LocaleProvider strings={{ aiContentLabel: { … } }}>`; the table is exported as `aiContentLabelStrings`.

- `generated`: AI-generated
- `modified`: AI-modified
- `none`: No AI used
- `unknown`: AI involvement not recorded
- `withBy`: {label} with {by}
- `withAt`: {text}, {at}
- `clipboard`: — {provenance}

## Types

### AiInvolvement

How much AI went into a piece of content, from the app's records, never guessed:
- "generated": made by an AI system, with no substantive human authorship ("AI-generated").
- "modified": a person's work that an AI system changed or helped write ("AI-modified").
- "none": the records say no AI system was used.
- "unknown": the app has no record either way ("AI involvement not recorded"). Use it whenever you can't say.

```ts
"generated" | "modified" | "none" | "unknown"
```

### AiProvenance

What the app knows about one piece of content's AI involvement.

```ts
{ /** How much AI went into it, from the app's records. */ involvement: AiInvolvement /** The tool or agent that was used, named by the app ("Drafting agent"). Shown for "generated" and "modified" only. */ by?: string | undefined /** When it was made or changed, as a Date or ISO string. Shown for "generated" and "modified" only. */ at?: Date | string | undefined /** The app's own plain-text note, 
```

### ProvenanceOptions

Options for the provenance helpers outside React, where LocaleProvider can't reach.

```ts
{ /** Overrides for `aiContentLabelStrings`, e.g. the same table you pass LocaleProvider under `aiContentLabel`. */ strings?: Partial<StringTable> | undefined /** The locale for the date. Default "en-GB" ("5 Nov 2026"). */ locale?: string | undefined /** The time zone for the date. Default "UTC", so server and browser agree. */ timeZone?: string | undefined }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
