Skip to content

Data display

Chat messages

The conversation log for copilots: attributed messages, streaming, tool calls.

import { ChatMessages, ChatMessage, ChatToolCall } from '@jsonresume/ui'Source(opens in a new tab)

Examples

Resume copilot
Y
YouAdd Rust to my skills.
C
Copilot
update_resume· Done· skills +1
{ "skills.backend": ["Rust"] }

Done. I added Rust under Backend. Want me to mention it in your summary too?

Show code
chat/copilot.tsx
'use client';

import {
  ChatComposer,
  ChatMessage,
  ChatMessages,
  ChatToolCall,
  Markdown,
  Stack,
} from '@jsonresume/ui';
import { useState } from 'react';
import * as styles from './copilot.css';

interface Turn {
  role: 'user' | 'assistant';
  text: string;
}

export default function ChatCopilot() {
  const [turns, setTurns] = useState<Turn[]>([
    { role: 'user', text: 'Add Rust to my skills.' },
    {
      role: 'assistant',
      text: 'Done. I added **Rust** under *Backend*. Want me to mention it in your summary too?',
    },
  ]);
  const [pending, setPending] = useState(false);
  return (
    <Stack gap={3} className={styles.root}>
      <ChatMessages
        label="Copilot conversation"
        scrollKey={turns.length + Number(pending)}
        className={styles.log}
      >
        {turns.map((t, i) =>
          t.role === 'user' ? (
            <ChatMessage key={i} role="user">
              {t.text}
            </ChatMessage>
          ) : (
            <ChatMessage key={i} role="assistant" author="Copilot">
              {i === 1 ? (
                <ChatToolCall
                  name="update_resume"
                  status="done"
                  summary="skills +1"
                  details={'{ "skills.backend": ["Rust"] }'}
                />
              ) : null}
              <Markdown>{t.text}</Markdown>
            </ChatMessage>
          )
        )}
        {pending ? (
          <ChatMessage role="assistant" author="Copilot" pending />
        ) : null}
      </ChatMessages>
      <ChatComposer
        busy={pending}
        onSubmit={(text) => {
          setTurns((all) => [...all, { role: 'user', text }]);
          setPending(true);
          setTimeout(() => {
            setPending(false);
            setTurns((all) => [
              ...all,
              {
                role: 'assistant',
                text: 'Good idea. I can draft that for you.',
              },
            ]);
          }, 1200);
        }}
      />
    </Stack>
  );
}
chat/copilot.css.ts
import { style } from '@vanilla-extract/css';
import { vars } from '@jsonresume/ui/tokens';

export const root = style({ width: 'min(100%, 40rem)' });

export const log = style({
  height: '20rem',
  border: `1px solid ${vars.color.border.subtle}`,
  borderRadius: vars.radius.sm,
});

Accessibility

  • A labelled log region announcing new messages politely.
  • Each message is an article named by its author; the typing state is spoken.
  • The view follows new messages only while the reader is at the bottom.

Props

<ChatMessages>

Also accepts the attributes of the element ChatMessages renders.
PropTypeDefaultDescription
labelstringConversation
scrollKeyunknown–Change this (e.g. message count or streamed length) to keep the view pinned to the newest message.

<ChatMessage>

Also accepts the attributes of the element ChatMessage renders.
PropTypeDefaultDescription
role requireduserassistantsystem–
authorstring–Shown above the message and to screen readers ("You", "Copilot").
avatarUrlstring | null–
pendingbooleanfalseShow a typing indicator (no content yet).

<ChatToolCall>

Also accepts the attributes of the element ChatToolCall renders.
PropTypeDefaultDescription
name requiredstring–The tool's name, e.g. update_resume.
status requireddonerunningerror–
detailsstring–Input or output, shown when expanded.
summaryReactNode–A one-line summary of what it did.