Design system

Chat pane

The resizable chat column beside the canvas in the page builder, the creative editor and the component editor.

import ChatPane from "@/pages/library/ChatPane";
import "@/pages/library/library.css";

ChatPane is the left column of every library editor: the conversation that builds or changes what is on the canvas beside it. Its bubbles use the session chat's classes (s-msg-*), so it reads as the same chat as the rest of Petavue. An assistant reply can show the steps it took and what it was built from. A part picked on the canvas shows above the text box, and the next message applies to it. When the agent is asking a question, footer takes the place of the text box.

The column's right edge can be dragged. Its width is remembered in localStorage and shared by every editor.

When it mounts, ChatPane focuses its text box (autoFocus) and scrolls the end of its thread into view with scrollIntoView. Inside a page that scrolls, that moves the whole page. That is why the examples here open in a frame the size of the editor, instead of rendering in place.

Building a page

A thread with a first build and a change to a picked part. The reply shows its steps, then the template and components it used. "Show the description the agent followed for each" opens the description of every component. A part is picked ("Button · Demo request form"), so the placeholder asks what should change there; clear it with the cross.

<div className="lib-editor__body">
  <ChatPane
    greeting={BUILD_GREETING}
    thread={page.thread}
    busy={busy}
    placeholder={target ? "What should change here?" : "Describe the change you want…"}
    onSend={send}
    target={target ? { label: targetLabel(page, target) } : null}
    onClearTarget={() => setPicked(null)}
  />
  <section className="lib-stage" aria-label="Page">…</section>
</div>

Asking before it builds

Before the first build, the page builder asks up to three questions. The question card goes in footer, which hides the text box and the picked part until it is answered.

<ChatPane
  greeting={BUILD_GREETING}
  thread={thread}
  busy={busy}
  placeholder="Describe the page you want…"
  onSend={send}
  footer={asking && !busy && (
    <AskCard step={step} total={questions.length} question={questions[step].question} options={questions[step].options} onAnswer={answer} onSkip={() => answer(null)} onBack={back} />
  )}
/>

Messages

thread is an array. The greeting is drawn first as an assistant message and is not part of it.

MessageShapeDrawn as
From the person{ role: "user", text, target? }A right-aligned blue bubble (--bg-user-msg, #3661ED). target adds a small line above the text with a cursor icon.
From the agent{ role: "assistant", text, working?, used? }The Petavue logo at 20px beside Markdown text.
workingstring[]A list of steps above the text, each with a green filled tick (--color-green). 13px secondary text.
used{ title?, template?, components: string[], reasons?: { name, why }[] }A grey box under the text: the template as a blue chip, then one chip per component. With reasons, a toggle swaps the chips for a numbered list of the descriptions the agent followed. title defaults to "Built from your workspace".

While busy is true, a "Thinking…" reply in the muted text colour is added at the end, and the send button is disabled.

Resizing

BehaviourDetail
Starting width440px
LimitsAt least 340px, at most 760px.
Room for the canvasThe canvas beside it keeps at least 600px, so the chat cannot be dragged past its parent's width minus 600px, and gives way when the window narrows. With a parent under 940px the chat stays at 340px.
DragPointer down on the 8px strip along the right edge (.lib-chat__resizer). A 2px --color-primary-300 line shows on hover, on focus and while dragging. While dragging, text selection and pointer events are turned off on the chat and everything after it.
KeyboardFocus the edge with Tab. Left and Right arrows change the width by 24px; Home resets it.
ResetDouble-click the edge to go back to 440px.
MemoryEvery change is saved as lib-chat-width in localStorage and read by the next editor that opens. If storage is blocked, the width still applies for the visit.

The parent has to be a row the chat can size against. The editors use .lib-editor__body, a grid of auto minmax(600px, 1fr) that scrolls sideways when the window is too narrow.

Anatomy

PartClassStyle
Column.lib-chatWhite, a 1px --color-grey-100 line on the right, its width set inline.
Thread.lib-chat__threadScrolls. 24px padding at the top, 16px on the sides and bottom, a 16px gap between messages.
Picked part.lib-chat__target12px medium --color-primary-700 on --color-primary-50, 8px corners, with a 20px clear button.
Composer.lib-chat__composerA pill with 20px corners and a --color-grey-200 border that turns --color-primary-300 on hover and --color-primary-500 on focus. 14px text.
Send.lib-chat__sendA 40px circle in --color-primary-500, --color-primary-600 on hover, grey when disabled.
Footer.lib-chat__footer16px margin at the sides, 20px below, the same place as the composer.

Props

PropTypeDefaultNotes
greetingstringMarkdown. The first assistant message.
threadMessage[]Required. See Messages.
busybooleanShows "Thinking…" and disables sending.
placeholderstringFor the text box. Change it with the state: "Describe the page you want…", "What should change here?".
onSend(text: string) => voidCalled with the trimmed text on Enter or the send button. Empty text and sends while busy are ignored. The box is cleared before it is called.
target{ label: string } | nullThe part picked on the canvas. Hidden while footer is set.
onClearTarget() => voidThe cross on the picked part.
footerReactNodeReplaces the text box and the picked part. A falsy value shows the text box again.

Accessibility

  • The column is a <section> labelled "Chat".
  • The resize edge has role="separator", aria-orientation="vertical", aria-valuenow, aria-valuemin and aria-valuemax, and the label "Resize the chat. Double-click to reset." It is focusable, and works with the arrow keys and Home. Double-click has no keyboard equivalent other than Home.
  • The text box is a single-line <input> labelled "Message". Enter sends; there is no way to type a line break.
  • The send button and the clear button have aria-labels ("Send", "Clear selection").
  • New messages are not announced: the thread has no live region, so a screen reader user has to move to the reply to hear it.
  • The avatar image has an empty alt, so it is skipped.

Do and don't

  • Do put it first in a .lib-editor__body row, with the canvas second, so the resize rules work.
  • Do set busy while a reply is on its way, so people cannot send twice.
  • Do use footer for a question the agent is asking, instead of a dialog. The conversation stays the one place a request is shaped.
  • Don't mount it inside a scrolling page or a small panel. It takes focus and scrolls itself into view.
  • Don't use it for the main Petavue chat. That is the session chat, which has its own composer; see Chat composer.

When to use it

Use ChatPane for an editor where chat changes something drawn beside it. For a conversation on its own page, use the session chat and its composer, described in Chat composer.

Where it is used

FileGreeting, target and footer
src/pages/library/PageBuilderPage.jsxBUILD_GREETING. The picked section or part of the page. AskCard or DirectionCard while it asks before building.
src/pages/library/CreativeEditorPage.jsxCREATIVE_GREETING. The picked layer of the creative. AskCard or DirectionCard.
src/pages/library/ComponentEditorPage.jsxA greeting for the component. No target or footer.

On this page