Skip to content

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 ​

tsx
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>
  );
}

The empty chat with a Markdown welcome and two starters

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:

json
{
  "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 ​

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

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