Skip to content

Portal Components ​

The building blocks of the portal experience: the terms gate, announcements, the legal footer, the empty state with starters, the message toolbar, the feedback form, the message editor, the export menu and the notifications menu. Chat composes them from its props; use them directly when you build your own layout.

New in 0.4.0

All of these are new. Every one of them is opt-in.

Import ​

tsx
import {
  TermsGate, AnnouncementBanner, LegalFooter, ChatEmptyState, InfoMarkdown,
  MessageToolbar, FeedbackSheet, MessageEditor, ExportMenu, NotificationsMenu,
} from "@cognipeer/chat-ui";

TermsGate ​

Wraps the chat; nothing inside is mounted until the person accepts. The host decides whether acceptance is required and records it.

The terms gate dialog

PropTypeDefaultDescription
requiredbooleanrequiredfalse shows the children at once
title, bodystringrequiredHeading and the terms (Markdown)
versionstring | number-Changes whenever the terms do: a new version asks again
onAccept() => void | Promise<void>requiredRecords the acceptance; if it throws, the dialog stays and says it could not be saved (or the localized message of a known code)
onDecline() => void-The person pressed Decline
acceptLabel, declineLabelstringlocalizedButton texts
declinedTitle, declinedBodystringlocalizedThe screen after declining
allowedImageHostsstring[]any https hostHosts a picture of the terms may come from
className, children

The dialog is role="dialog" with aria-modal, takes focus when it appears, and traps Tab.

AnnouncementBanner ​

tsx
<AnnouncementBanner
  announcements={[{ id: "maint-1", severity: "warning", text: "Maintenance **tonight** at 22:00." }]}
  onDismiss={(a) => saveDismissal(a.id)}
/>

Shows the announcements due now (checked every 30 seconds), most severe first, at most five, with inline Markdown. A person can close the ones that allow it; the choice is kept in localStorage (cp:announcement:dismissed:<id>). A critical notice is role="alert", the others role="status".

Announcements above the chat

LegalFooter ​

tsx
<LegalFooter links={{ privacy: "https://example.com/privacy", terms: "/terms", support: "mailto:help@example.com" }} text="Answers may be wrong." />

A short text and the links in a fixed order (privacy, terms, support, "How this AI works"), in a nav labelled "Legal". A link that is not a plain web address, a page path (or mailto: for support) is left out. Chat puts it under the composer.

ChatEmptyState and InfoMarkdown ​

tsx
<ChatEmptyState
  welcome={"## Welcome back\n\nAsk me about orders."}
  starters={[{ text: "Where is my order?", label: "Track an order" }]}
  onPick={(starter) => composer.setValue(starter.text)}
/>
PropDescription
customThe host's own empty state (replaces the welcome and the default text)
welcomeMarkdown shown before the first message
allowedImageHostsHosts a picture of the welcome may come from
startersConversationStarter[], shown as chips (a list labelled "Suggested messages")
onPickA chip was chosen
disabledThe chips are not offered now (the composer is closed)

InfoMarkdown is the safe Markdown renderer behind the welcome text and announcements: no raw HTML, links open in a new tab, pictures follow the rules above; inline allows only text, emphasis, code and links.

The welcome text and two starters

MessageToolbar ​

The row of buttons under a message: copy, edit, regenerate, thumbs.

tsx
<MessageToolbar
  text={messageText}
  copy
  onRegenerate={() => chat.regenerate()}
  feedback={{ comments: true, reasons: ["inaccurate", "other"], current: message.feedback, onSubmit: (input) => chat.submitFeedback(message.id, input) }}
  persistent
/>
PropDescription
textWhat copy puts on the clipboard
copyShow the copy button
onRegenerate, onEditShow the regenerate / edit button
feedback{ comments, reasons, current?, onSubmit }: thumbs up and down; a thumbs-down opens the form
persistentKeep the row visible (otherwise it shows on hover and focus)

The row is a role="group" labelled "Message actions"; pressed thumbs use aria-pressed, and copy and rating results are announced.

Under an answer: copy, regenerate and thumbs

FeedbackSheet ​

The form that opens with a thumbs-down.

tsx
<FeedbackSheet reasons={["inaccurate", "incomplete", "other"]} comments onSubmit={({ reasons, comment }) => send(reasons, comment)} onCancel={close} />
PropDescription
reasonsReason ids (each has a chat.feedback.reason.<id> string; others show as written)
commentsOffer the comment box (1000 characters at most)
initialWhat the person said before, when they come back to change it
busyThe rating is being sent
onSubmit, onCancel

It is not modal: it takes focus when it opens, closes on Escape and returns focus to the thumb.

The feedback form

MessageEditor ​

Edits one of the person's messages in place. Saving sends it again and replaces everything the conversation said after it, which the editor says first.

PropDescription
initialThe text being edited
onSave(content)Resolve false when the new text was refused (the editor stays open)
onCancel

Ctrl or Cmd + Enter saves, Escape cancels, and the box grows up to 320 px.

ExportMenu ​

tsx
<ExportMenu formats={["md", "json", "pdf"]} onExport={(format) => download(format)} disabled={chat.isStreaming} />

A download button that opens a menu (role="menu") of formats: "Markdown (.md)", "JSON (.json)" and "PDF (print)". Arrow keys, Home, End and Escape work; focus returns to the button. The button is busy until onExport settles.

NotificationsMenu ​

tsx
const alerts = useBackgroundAlerts({ config, busy, waiting, heading, doneText, waitingText });
{alerts.available && <NotificationsMenu alerts={alerts} offer={{ sound: true, desktop: true }} />}

A bell button that opens a dialog with two switches (role="switch"): "Play a sound" and "Desktop notifications". The browser's permission prompt appears only when the person turns desktop notifications on, and a blocked permission says so. offer is what the host allows.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub