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.
| Message | Shape | Drawn 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. |
working | string[] | 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
| Behaviour | Detail |
|---|---|
| Starting width | 440px |
| Limits | At least 340px, at most 760px. |
| Room for the canvas | The 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. |
| Drag | Pointer 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. |
| Keyboard | Focus the edge with Tab. Left and Right arrows change the width by 24px; Home resets it. |
| Reset | Double-click the edge to go back to 440px. |
| Memory | Every 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
| Part | Class | Style |
|---|---|---|
| Column | .lib-chat | White, a 1px --color-grey-100 line on the right, its width set inline. |
| Thread | .lib-chat__thread | Scrolls. 24px padding at the top, 16px on the sides and bottom, a 16px gap between messages. |
| Picked part | .lib-chat__target | 12px medium --color-primary-700 on --color-primary-50, 8px corners, with a 20px clear button. |
| Composer | .lib-chat__composer | A 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__send | A 40px circle in --color-primary-500, --color-primary-600 on hover, grey when disabled. |
| Footer | .lib-chat__footer | 16px margin at the sides, 20px below, the same place as the composer. |
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
greeting | string | Markdown. The first assistant message. | |
thread | Message[] | Required. See Messages. | |
busy | boolean | Shows "Thinking…" and disables sending. | |
placeholder | string | For the text box. Change it with the state: "Describe the page you want…", "What should change here?". | |
onSend | (text: string) => void | Called 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 } | null | The part picked on the canvas. Hidden while footer is set. | |
onClearTarget | () => void | The cross on the picked part. | |
footer | ReactNode | Replaces 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-valueminandaria-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__bodyrow, with the canvas second, so the resize rules work. - Do set
busywhile a reply is on its way, so people cannot send twice. - Do use
footerfor 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
| File | Greeting, target and footer |
|---|---|
src/pages/library/PageBuilderPage.jsx | BUILD_GREETING. The picked section or part of the page. AskCard or DirectionCard while it asks before building. |
src/pages/library/CreativeEditorPage.jsx | CREATIVE_GREETING. The picked layer of the creative. AskCard or DirectionCard. |
src/pages/library/ComponentEditorPage.jsx | A greeting for the component. No target or footer. |
Page settings dialog
Everything about a landing page apart from where it is published: its name, access, search and sharing previews, tracking and forms.
Code view
Shows a landing page as the project it is published from: a searchable file tree, the open file with line numbers, copy and download as a zip.