Portal Setup
A workplace assistant with the opt-in experience switched on: copy, regenerate, edit, feedback, history tools, starters, a welcome, an AI label with legal links, announcements, a terms gate, export and background alerts, plus Turkish strings.
The page
import { Chat, TermsGate } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";
import { useSearchParams } from "react-router-dom"; // any router: the chat only needs the values
export default function SupportPortal({ terms, session }: { terms: Terms; session: { getToken(): Promise<string> } }) {
const [params] = useSearchParams();
const chat = (
<Chat
baseUrl="/api/portal/support"
getHeaders={async () => ({ Authorization: `Bearer ${await session.getToken()}` })}
// deep links: /support?agent=billing&q=Where%20is%20my%20invoice&send=1
initialAgentId={params.get("agent") ?? undefined}
initialInput={params.get("q") ?? undefined}
autoSend={params.get("send") === "1"}
// message toolbar and feedback
messageActions={{ copy: true, regenerate: true, edit: true }}
feedback={{ enabled: true, comments: true, reasons: ["inaccurate", "incomplete", "other"] }}
onFeedback={({ messageId, feedback }) => analytics.track("rating", { messageId, rating: feedback?.rating ?? null })}
// history, export, background alerts
historyActions={{ rename: true, pin: true, bulkDelete: true }}
exportMenu={{ enabled: true, formats: ["md", "json", "pdf"], includeTools: false }}
notifications={{ tabBadge: true, sound: false, desktop: "opt_in" }}
// transparency
disclosure={{
aiLabel: true,
links: { privacy: "https://example.com/privacy", terms: "/terms", support: "mailto:help@example.com", aboutAi: "/about-ai" },
}}
announcements={[
{ id: "maint-oct", severity: "warning", text: "Maintenance on **12 October** at 22:00.", endsAt: "2026-10-13T00:00:00Z" },
]}
// the empty chat
welcome={"## Welcome back\n\nAsk me about orders, invoices and shipping."}
starters={[
{ text: "Where is my latest order?", label: "Track an order" },
{ text: "Show my invoices", autoSend: true },
]}
// language and wording
locale={params.get("lang") ?? undefined}
customMessages={{ en: { "chat.input.placeholder": "Ask the support assistant..." } }}
/>
);
return (
<div className="h-screen">
<TermsGate
required={terms.required}
title={terms.title}
body={terms.markdown}
version={terms.version}
onAccept={() =>
fetch("/api/portal/support/terms/accept", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ version: terms.version }),
}).then((r) => { if (!r.ok) throw new Error("not saved"); })
}
>
{chat}
</TermsGate>
</div>
);
}
What the agent decides
The host switched everything on, but each agent can take features away in its metadata.experience, and publishes content that wins over your props:
{
"id": "billing",
"name": "Billing",
"metadata": {
"welcome": "## Hello\n\nI can explain an invoice.",
"starters": [{ "text": "Explain my last invoice" }],
"experience": {
"messageActions": { "copy": true, "regenerate": true, "edit": false },
"feedback": { "enabled": true, "reasons": ["inaccurate", "other"] },
"history": { "rename": true, "pin": true, "bulkDelete": false },
"export": { "enabled": true, "formats": ["md", "json", "html"] },
"notifications": { "desktop": "off" }
}
},
"capabilities": { "attachments": { "enabled": true, "maxFiles": 3, "retentionDays": 30 } }
}With this agent the chat shows copy and regenerate (not edit), rename and pin (not bulk delete), the Markdown and JSON exports plus the PDF item (the server's html print view), no desktop notifications, and "Files are kept for 30 days" under the composer. The agent's welcome and starters replace yours.
Controlling the chat from the page
const controller = useRef<ChatController>(null);
<Chat controllerRef={controller} /* ... */ />
<button onClick={() => controller.current?.prefill("Where is my order?", { send: true })}>Ask</button>
<button onClick={() => controller.current?.newChat()}>New chat</button>prefill(text, { send }) sends only while the composer is open; otherwise it just fills the box.

