Migrate from chatscope to aparté
Move an AI chat built on @chatscope/chat-ui-kit-react to aparté — what each component becomes, how the message model maps, and how to keep your own backend or stream a model.
@chatscope/chat-ui-kit-react is a kit for chats between people: you assemble the components, and
the messages and the sending stay in your code. If you built an AI chat on it, <AparteChat> from
@aparte/react is the whole chat in one component, and it draws what a model’s reply needs: tokens
as they stream, tool calls, reasoning.
You can move in two steps: change the view and keep your own backend call, then stream the reply.
Swap the packages
Section titled “Swap the packages”npm uninstall @chatscope/chat-ui-kit-react @chatscope/chat-ui-kit-styles
npm install @aparte/react @aparte/coreChange the view, keep your backend call
Section titled “Change the view, keep your backend call”A typical chatscope chat holds the messages and the typing flag itself:
import { useState } from 'react';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import { MainContainer, ChatContainer, MessageList, Message, MessageInput, TypingIndicator } from '@chatscope/chat-ui-kit-react';
export function Chat() {
const [messages, setMessages] = useState([]);
const [typing, setTyping] = useState(false);
async function send(_html, text) {
setMessages((m) => [...m, { message: text, direction: 'outgoing', position: 'single' }]);
setTyping(true);
const answer = await askMyServer(text);
setMessages((m) => [...m, { message: answer, direction: 'incoming', position: 'single' }]);
setTyping(false);
}
return (
<div style={{ position: 'relative', height: 500 }}>
<MainContainer>
<ChatContainer>
<MessageList typingIndicator={typing ? <TypingIndicator content="Typing…" /> : null}>
{messages.map((m, i) => <Message key={i} model={m} />)}
</MessageList>
<MessageInput placeholder="Type message here" onSend={send} />
</ChatContainer>
</MainContainer>
</div>
);
}With aparté, the person’s message is added for you and the waiting dots show until the first word:
import { useCallback } from 'react';
import { uuid } from '@aparte/core';
import { AparteChat, useAparteChat } from '@aparte/react';
import '@aparte/core/styles.css';
async function* reply(text: string) {
yield await askMyServer(text);
}
export function Chat() {
const chat = useAparteChat();
const onMessageSent = useCallback(async (event: { content: string }) => {
const id = uuid();
chat.ref.current?.appendMessage({ id, role: 'assistant', content: '', timestamp: Date.now() });
await chat.ref.current?.injectTokenStream(id, reply(event.content));
}, [chat.ref]);
return (
<div style={{ height: 500 }}>
<AparteChat
ref={chat.ref}
messages={chat.messages}
onMessagesChange={chat.setMessages}
onMessageSent={onMessageSent}
placeholder="Type message here"
/>
</div>
);
}What each part becomes
Section titled “What each part becomes”| chatscope | aparté |
|---|---|
MainContainer, ChatContainer, MessageList, MessageInput | <AparteChat>, in a parent with a height |
Message and its model | an AparteMessage in messages |
direction: 'outgoing', 'incoming' | role: 'user', 'assistant' |
message | content, drawn as Markdown once a renderer is set up |
sentTime, a string | timestamp, a number such as Date.now() |
position | not needed |
type: 'custom' and payload | renderBubble(message) |
TypingIndicator | built in, while a reply has no words yet |
MessageInput’s onSend | onMessageSent; it receives event.content |
MessageInput’s attachButton | the attachments prop (Attachments) |
InputToolbox | the toolbar prop |
Avatar on a message | setAvatarProvider() |
ConversationHeader | the header |
Sidebar | <aparte-sidebar> |
ConversationList, Conversation | <aparte-conversation-list> |
@chatscope/use-chat | useAparteChat, and conversation persistence |
MessageSeparator | no equivalent in the transcript |
onYReachStart, loadingMore | no equivalent: you give the messages up front |
Styles
Section titled “Styles”@chatscope/chat-ui-kit-styles is replaced by @aparte/core/styles.css. Where you set Sass variables,
set CSS variables instead (Theme):
:root {
--aparte-primary: #b45309;
--aparte-bg: #fbf7f0;
--aparte-text: #241a12;
}Stream the reply
Section titled “Stream the reply”injectTokenStream takes any async iterable, so a server that streams text only changes reply:
async function* reply(text: string) {
const res = await fetch('/api/chat', { method: 'POST', body: text });
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
}Or let aparté call the model
Section titled “Or let aparté call the model”If you have no loop worth keeping, remove onMessageSent and start a client. Register a provider
(Connect a model), or keep the key on your server
(Key on your server):
import { AparteChat, useAparteClient } from '@aparte/react';
import '@aparte/core/styles.css';
export function Chat() {
useAparteClient();
return <AparteChat />;
}When to stay on chatscope
Section titled “When to stay on chatscope”aparté draws one person talking to an assistant: a message is user or assistant, and has no
sender. For a chat between several people, with a name on each message, chatscope fits better.