Skip to content

Chat

The Chat components give an application a topic-based chat that opens in a sidebar next to the view — a topics list, the conversation for the picked topic, @-mentions of people and agents, emoji, and per-message actions — without the library holding any opinion about where the data lives or what a backend looks like.

Everything about data stays with your application:

  • Topics and messages come in as plain arrays. Hand the components the data a live (observable) query delivers and they re-render as it changes.
  • Everything going out is a callback. Sending a message, starting a topic, naming a topic, resolving an author, offering actions — the components raise intents; your application decides what they mean.
  • Messages are an interface, not a class. ChatMessage declares the minimum (id, topicId, authorId, body, timestamp, mentions, metadata), the components are generic over your own extended type, and every action callback receives your full message back.
  • Sidebar (PrimeReact Drawer) opening to the right of the view, with a topics list and per-topic conversations
  • Host-side topic auto-naming contract with a pending placeholder until the name arrives
  • @-mentions from a list you hold or a provider callback you resolve, rendered distinctly in message bodies
  • Emoji picker in the composer, with a quick row of recently used emoji
  • Host-supplied render callbacks for avatars and display names — messages carry only the author id
  • Extensible per-message actions shown on hover
  • Every color from the --cratis-* token seam, so the chat follows your theme
ComponentWhat it is
ChatSidebarThe whole thing in a Drawer: topics list ⇄ conversation, back navigation, naming contract
ChatTopicListJust the topics — pick one, start a new one
ChatConversationJust one conversation — messages plus composer
ChatSidebarForObservableQueriesOptional: ChatSidebar bound to two Cratis Arc observable queries
import { ChatSidebar, ChatAuthorKind, type ChatAuthor, type ChatIdentifier } from '@cratis/components/Chat';
const authorOf = (authorId: ChatIdentifier): ChatAuthor =>
teamMembers.get(String(authorId)) ?? { name: String(authorId), kind: ChatAuthorKind.User };
export const Workspace = () => {
const [chatOpen, setChatOpen] = useState(false);
const { topics, messages } = useMyChatData(); // however your app gets its live data
return (
<>
<button onClick={() => setChatOpen(true)}>Chat</button>
<ChatSidebar
open={chatOpen}
onClose={() => setChatOpen(false)}
topics={topics}
messages={messages}
authorOf={authorOf}
onStartTopic={() => startTopic()} // create it, answer its id
onSendMessage={(topicId, body, mentions) => send(topicId, body, mentions)}
onRequestTopicName={(topic, firstMessage) => nameTopic(topic, firstMessage)}
mentionCandidates={candidates}
/>
</>
);
};

messages may be the whole chat or just the open topic’s — the sidebar shows the open topic’s messages by matching on topicId either way.

interface ChatMessage {
id: ChatIdentifier;
topicId: ChatIdentifier;
authorId: ChatIdentifier;
body: string; // mentions appear in it as plain `@Name`
timestamp: Date;
mentions?: ChatMention[]; // who the body mentions
metadata?: Record<string, unknown>; // whatever else your shape carries
}

ChatIdentifier accepts a string or anything that renders itself as one (a Guid, a number, a custom id type) — the components never look past its string form, so query results plug in without mapping.

Extend the interface for what your actions need; the components are generic over it:

interface ProjectMessage extends ChatMessage {
projectId: string;
}
<ChatSidebar<ProjectMessage>
...
actions={[{
id: 'open-project',
label: 'Open the project',
icon: 'pi pi-folder-open',
onInvoke: message => navigate(`/projects/${message.projectId}`),
}]}
/>

A message deliberately carries only authorId. The authorOf callback resolves it to a ChatAuthor (name, kind, hasAvatar, avatarVersion) at render time — so a rename or a new picture shows up everywhere at once. For full control, renderAvatar and renderAuthorName replace the built-in avatar circle and name text entirely:

<ChatConversation
messages={messages}
onSendMessage={send}
authorOf={authorOf}
renderAvatar={(authorId, author) => <MyAvatar id={authorId} title={author.name} />}
renderAuthorName={(authorId, author) => <MyProfileLink id={authorId}>{author.name}</MyProfileLink>}
/>

Every color comes from the --cratis-* token layer, so the chat follows the PrimeReact theme the application runs — light, dark, or its own overrides. The Drawer itself accepts pt, ptOptions, and unstyled for full control.