# Chat Launcher

> Site-wide chrome for customer-facing apps: ChatLauncher (floating assistant with page context), CookieConsent and AnnouncementBar.

Component · Overlays · Pro kit · [Live page](https://ui.twentyfirst.media/pro/components/ChatLauncher/)

Site-wide chrome for customer-facing apps: `ChatLauncher` (floating assistant with page context), `CookieConsent` and `AnnouncementBar`.

ChatLauncher: `title`, `context` (what the assistant is looking at), `tint`, `open` / `onOpenChange`, `unread`, `headerAction`, children (the thread), `composer`, `inline` (for previews; fixed bottom-right by default).
CookieConsent: `title`, `text`, `policy` (link), `categories` ({id, label, text, required, default}), `onSave(choices)`, `open`, `inline`. "Accept all" and "Only necessary" carry equal weight.
AnnouncementBar: children, `tint`, `icon`, `action`, `onDismiss` (false hides the close button).

## Example

```tsx
import * as React from "react";
import {
  AnnouncementBar,
  ChatLauncher,
  ChatMessage,
  Composer,
  CookieConsent,
  PromptStarters,
} from "@twentyfirstmedia/pro";

function App() {
  const o = React.useState(true);
  return (
    <div
      className="tf-col"
      style={{
        gap: 16,
      }}
    >
      <AnnouncementBar tint="butter" action={<a href="#">See plans</a>}>
        Free plan: 3 stagings left this month.
      </AnnouncementBar>
      <div
        className="tf-grid"
        style={
          {
            "--cols": "1fr 1fr",
            gap: 16,
            alignItems: "end",
          } as React.CSSProperties
        }
      >
        <CookieConsent
          inline
          policy={<a href="#">Privacy policy</a>}
          categories={[
            {
              id: "necessary",
              label: "Necessary",
              text: "Sign-in, security, cart",
              required: true,
            },
            {
              id: "stats",
              label: "Statistics",
              text: "Anonymous usage numbers",
            },
            {
              id: "marketing",
              label: "Marketing",
              text: "Ads and remarketing",
            },
          ]}
          onSave={() => {}}
        />
        <div
          style={{
            display: "flex",
            justifyContent: "flex-end",
          }}
        >
          <ChatLauncher
            inline
            open={o[0]}
            onOpenChange={o[1]}
            title="Homefield assistant"
            context="Looking at: Loft in HafenCity"
            composer={<Composer placeholder="Ask about this listing…" />}
          >
            <ChatMessage
              role="assistant"
              name="Assistant"
              text="This loft is about **4% above** the estimated value. Want me to draft a first message to the agent?"
            />
            <PromptStarters
              items={[
                {
                  title: "Draft a message",
                  icon: "mail",
                },
                {
                  title: "Compare with similar",
                  icon: "chart",
                },
              ]}
            />
          </ChatLauncher>
        </div>
      </div>
    </div>
  );
}
export default App;
```

## Props

### AnnouncementBar

| Prop | Type | Notes |
| --- | --- | --- |
| `children` | `ReactNode` | Required |
| `tint?` | `Tint` |  |
| `icon?` | `IconName \| false` |  |
| `action?` | `ReactNode` |  |
| `onDismiss?` | `(() => void) \| false` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ChatLauncher

| Prop | Type | Notes |
| --- | --- | --- |
| `title?` | `string` |  |
| `context?` | `ReactNode` |  |
| `tint?` | `Tint` |  |
| `open?` | `boolean` |  |
| `onOpenChange?` | `(open: boolean) => void` |  |
| `unread?` | `number` |  |
| `headerAction?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `composer?` | `ReactNode` |  |
| `inline?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ChatMessage

| Prop | Type | Notes |
| --- | --- | --- |
| `role` | `"user" \| "assistant" \| "system"` | Required |
| `name?` | `string` |  |
| `time?` | `string` |  |
| `model?` | `string` |  |
| `text?` | `string` |  |
| `streaming?` | `boolean` |  |
| `tint?` | `Tint` |  |
| `sources?` | `Source[]` |  |
| `attachments?` | `{ name: string; size?: string; kind?: string }[]` |  |
| `usage?` | `string` |  |
| `onRetry?` | `() => void` |  |
| `onFeedback?` | `(v: "up" \| "down") => void` |  |
| `actions?` | `boolean` |  |
| `before?` | `ReactNode` |  |
| `after?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `feedback?` | `"up" \| "down" \| null` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Composer

| Prop | Type | Notes |
| --- | --- | --- |
| `value?` | `string` |  |
| `onChange?` | `(v: string) => void` |  |
| `onSend?` | `(text: string, files: (File \| { name: string })[]) => void` |  |
| `busy?` | `boolean` |  |
| `onStop?` | `() => void` |  |
| `placeholder?` | `string` |  |
| `label?` | `string` |  |
| `models?` | `ModelOption[]` |  |
| `model?` | `string` |  |
| `onModel?` | `(id: string) => void` |  |
| `commands?` | `ComposerCommand[]` |  |
| `onCommand?` | `(c: ComposerCommand) => void` |  |
| `attachments?` | `(File \| { name: string })[]` |  |
| `onAttach?` | `(files: FileList) => void` |  |
| `onRemoveAttachment?` | `(i: number) => void` |  |
| `hint?` | `string` |  |
| `left?` | `ReactNode` |  |
| `disabled?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### CookieConsent

| Prop | Type | Notes |
| --- | --- | --- |
| `title?` | `string` |  |
| `text?` | `ReactNode` |  |
| `policy?` | `ReactNode` |  |
| `categories?` | `{ id: string; label: string; /** What the category covers; may hold a link to the details. */ text?: ReactNode; required?: boolean; default?: boolean; }[]` |  |
| `onSave?` | `(choices: Record<string, boolean>) => void` |  |
| `open?` | `boolean` |  |
| `inline?` | `boolean` |  |
| `equalChoice?` | `boolean` | "Only necessary" and "Accept all" with the same weight (both secondary buttons), as German data protection authorities expect on the first layer. |
| `id?` | `string` | Id of the banner (its text is referenced by `aria-describedby`). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### PromptStarters

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `{ icon?: IconName; tint?: Tint; title: string; text?: string; prompt?: string }[]` | Required |
| `onPick?` | `(prompt: string) => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
