@aphrody/m3-ai
Material Design 3 components for agent interfaces: tool-call cards, markdown, chat bubbles, the
chat composer, model selector, worker and branch cards, and the task board. They are the SpaceUI
@spacedrive/ai set, rebuilt on @aphrody/m3-primitives, the M3 color roles, typescale, shapes
and motion of Aphrody OS, and Material Symbols icons.
Install
bun add @aphrody/m3-ai @aphrody/m3-primitives @aphrody/m3-motion
Peers: react and react-dom 18 or 19, tailwindcss 4.1+.
CSS setup
The components ship Tailwind class names that use the M3 utilities of @aphrody/m3-theme. Import
the tokens, a palette sheet and the M3 utilities, then let Tailwind scan the package output. The
full recipe is in docs/design/SPACEUI-M3-FUSION.md,
section "Integration":
@import "tailwindcss";
@import "@aphrody/m3-tokens/m3-tokens.css";
@import "@aphrody/m3-theme/spaceui.css";
@import "@aphrody/m3-theme/tailwind.css";
@source "../node_modules/@aphrody/m3-primitives/dist";
@source "../node_modules/@aphrody/m3-ai/dist";
Register the Material Symbols font once, for example with ensureMaterialSymbols() from
@aphrody/material-web/icon/material-symbols.js.
Usage
import { ChatComposer, MessageBubble, ToolCall } from "@aphrody/m3-ai";
import type { ToolCallPair } from "@aphrody/m3-ai";
const pair: ToolCallPair = {
id: "1",
name: "file_read",
argsRaw: '{"path":"README.md"}',
args: { path: "README.md" },
resultRaw: '{"text":"# Aphrody"}',
result: { text: "# Aphrody" },
status: "completed",
};
export function Conversation({ draft, setDraft, send }: Props) {
return (
<>
<MessageBubble content="List the files" isUser />
<ToolCall pair={pair} />
<MessageBubble content="There is one file: `README.md`." isUser={false} />
<ChatComposer draft={draft} onDraftChange={setDraft} onSend={send} />
</>
);
}
Components
| Component | M3 anatomy |
|---|---|
ToolCall |
Outlined card (surface-container-low, medium shape): status icon or circular indicator, text-label-large tool name, expandable arguments and result blocks in font-code on surface-container-highest, error results on error-container. |
Markdown |
Prose on the M3 typescale (headline / title / body), font-code code blocks, primary links, outline-variant quotes and tables. size picks the body scale; allowHtml enables raw HTML for trusted content. |
MessageBubble |
User turn as a secondary-container bubble (large shape), assistant turn as markdown on the surface with a standard copy icon button. |
ChatComposer |
Search-bar anatomy: surface-container-high, extra-large shape, text-body-large input, project pill, model chip, standard mic icon button, filled send / stop icon button. |
ModelSelector |
Pill trigger opening an M3 menu (surface-container, extra-small shape, level 2): search field, provider section headers, 48 dp items, selection on secondary-container, capability chips. ModelOptionList and groupModelsByProvider expose the list alone. |
InlineWorkerCard |
Filled card (surface-container, medium shape), text-title-small header, status chip, circular indicator while running, expandable transcript of ToolCalls and markdown. |
InlineBranchCard |
Filled card for a thinking branch: indicator or branch icon, status chip, expandable conclusion. |
TaskStatusIcon |
Material Symbol per status: done success, in progress and ready primary, pending approval warning, backlog outline. |
TaskPriorityIcon |
Material Symbol per priority: critical error, high warning, medium primary, low outline. |
TaskRow |
Dense M3 list item (48 dp): text-body-large headline, text-body-small supporting columns, 8 % hover state layer, secondary-container selection, overflow menu. |
TaskList |
Tasks grouped by status: collapsible text-title-small section headers, outline-variant dividers. |
TaskDetail |
Task on an M3 surface: text-headline-small title, property grid, subtasks as a checkbox list, markdown description, filled and text buttons. |
TaskCreateForm |
Title and description text fields, priority select, filled Create and text Cancel buttons. |
Helpers from types.ts: pairTranscriptSteps, tryParseJson, isErrorResult,
TASK_STATUS_ORDER, TASK_STATUS_LABEL, TASK_PRIORITY_LABEL, and the transcript, task and
model types.
Local chat
Everything a chat over an OpenAI-compatible server (llama-server, vLLM, the Aphrody gateway) needs, with no visible string hard-coded:
| Export | Role |
|---|---|
ChatLabels, chatLabelsFr / chatLabelsEn / chatLabelsJa, resolveChatLabels, formatLabel |
One dictionary for every visible string and aria-label. Every chat component takes labels?: Partial<ChatLabels>; missing keys fall back on English. |
streamChatCompletion, createSseParser, parseChatCompletionChunk, chatReducer, ChatStreamError |
Pure streaming client of POST <baseUrl>/chat/completions (stream: true): SSE framing, content / reasoning_content / tool-call deltas, finish_reason, top-level citations and refusal. Network failures and 404/502/503/504 are unavailable. |
useOpenAIChat({ baseUrl, model, fetch?, headers?, initialMessages?, preamble?, body? }) |
React hook: { messages, send, stop, status, error, reset, setMessages }, status is idle, streaming, error or unavailable. |
ConversationList |
Navigation-drawer list: new conversation, optional search, rename / delete callbacks, empty state, localized dates. |
ModelUnavailable |
Downloading / loading / unavailable state with a determinate linear indicator and a retry button. |
CitedMarkdown, CitationChip, CitationList, RefusalNotice |
Grounded answers: [n] markers as chips, the numbered sources, the "not established in the sources" notice. MessageBubble takes citations and refusal directly. |
MemoryPanel |
Saved memories with per-item deletion and a confirmed "clear all" (confirmClear injects your own dialog). |
ChatIcon |
The chat icons as inline Material Symbols SVG: no icon font is needed by the chat components. |
Markdown (and MessageBubble, CitedMarkdown) open external links with
target="_blank" rel="noopener noreferrer" and take onLinkClick(href, event) (call
event.preventDefault() to open the system browser from a Tauri webview) or renderLink.
ChatComposer takes disabled (the model is unavailable: input, send, voice and selectors are
inert), distinct from isSending.
Tests
bun test
test/preload.ts resolves @aphrody/m3-primitives to its sources, so the tests do not need a
built dist.
Credits
Derived from SpaceUI @spacedrive/ai (MIT, see
LICENSE.spaceui), rebuilt on Material Design 3. New code is Apache-2.0.