Skip to content

Customize the chat

Change more than colours and sizes — which part of the chat each seam changes, when a change shows on the chats already on screen, and how two chats get different setups.

On this page
Open
The same message twice: core's message, then a frame of your own, given to the second one by its own configuration.
You want toUsePage
change colours, sizes and shapesCSS variablesTheme
replace the error card, an attachment chip, the status linea render hookRender hooks
redraw the whole messagea frame of your own, or an element of your ownThe message
turn on retry and edit, add a button to a message or to the composerthe actionsButtons
put a mode picker or a model selector under the composerthe composer’s toolbarThe composer toolbar
show a chart, a map or a form in a replya segment rendererYour own content
show “Thought for 8s”the timing a segment carriesDurations
swap the icons, add avatarsthe icon and avatar providersIcons and avatars
render Markdown and colour codethe Markdown and highlight pluginsMarkdown and code
ts
import { aparteGlobalConfig } from '@aparte/core';

aparteGlobalConfig.setErrorRenderer(({ message }) => {
  const el = document.createElement('div');
  el.className = 'my-error';
  el.textContent = message;
  return el;
});

aparteGlobalConfig.setErrorRenderer(null);   // the built-in one again

The render hooks, the providers and the button settings are methods on aparteGlobalConfig, the configuration every chat on the page reads; a render hook or the avatar provider takes null to put the built-in back. A change to the buttons, the icons, the avatars or the language reaches the messages already on screen, and a new status-line renderer reaches the line already shown. So do the Markdown and highlight providers: a message drawn before them is drawn again with them. The message frame, the error card, the attachment chips and a segment renderer apply to what is drawn after them, so set those before the chat renders.

ts
import { registerSegmentRenderer } from '@aparte/core';

registerSegmentRenderer({
  type: 'chart',
  render(segment) {
    const el = document.createElement('div');
    el.className = 'my-chart';
    return el;
  },
});

A reply is made of typed parts — text, reasoning, code, a tool call — called segments, and a renderer registered for a type of your own draws it: a chart, a map, a form, generative UI. A segment whose type no renderer claims shows its fallback text, or [Unknown segment type: …] with a warning in the console when it has none: register a renderer for the type, or give the segment a fallback. Your own content shows how such a segment reaches a message, and the rest of a renderer’s life.

ts
import { AparteConfig, attachConfig } from '@aparte/core';

const support = new AparteConfig();
support.setBubbleActions({ retry: true, edit: true });

const chat = document.querySelector<HTMLElement>('#support-chat');
if (chat) attachConfig(chat, support);

Attach a configuration of its own to a chat, or to an element around it, and every part inside reads that one; detachConfig takes it off. The framework packages take it as their config prop. A new configuration starts from the built-in defaults: set up on it what that chat needs — its Markdown and highlighting (setupMarkedProvider(undefined, support)), its icons, its language. It inherits one thing from the page-wide one, the segment renderers registered there, and cannot remove one of those; registerSegmentRenderer(renderer, support) registers one for that chat alone, which wins over the page-wide one.