Skip to content
ResearchCanvasMenu

Canvas

A document canvas for writing with an agent: select to ask, suggested edits that stream in place, accept or reject each one, and version history.

The agent tightens a draft with streamed suggested edits. Accept, reject, or select text to ask about it.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/canvas.json

Usage

tsx
import { Canvas } from "@/components/ui/chatcn/canvas"
import { useChatcnCanvasSuggestions } from "@/hooks/use-chatcn-canvas-suggestions"

const doc = useChatcnCanvasSuggestions(initialBlocks)

<Canvas
  title="Reading width for long answers"
  blocks={doc.blocks}
  onBlocksChange={doc.setBlocks}
  suggestions={doc.suggestions}
  onAccept={doc.accept}
  onReject={doc.reject}
  onAcceptAll={doc.acceptAll}
  onRejectAll={doc.rejectAll}
  versions={doc.versions}
  currentVersionId={doc.currentVersionId}
  onRestoreVersion={doc.restoreVersion}
  onAsk={(question, selection) => askAgent(question, selection)}
/>

Stream an edit with the hook: suggest({ blockId, from, to, insert: "", status: "streaming" }) returns an id, append(id, chunk) grows the insertion and finish(id) makes it reviewable. Offsets are characters in the block's text.

The editor is plain contentEditable with title, heading and paragraph blocks: no editor dependency. Paste becomes plain text, formatting shortcuts are ignored, IME composition is native, and React never writes into the text while you type, so the caret and undo stay intact.

Props

PropTypeNotes
blocksCanvasBlock[]{ id, type, text } with title, heading or paragraph
onBlocksChange(blocks) => voidOmit for read-only
suggestionsCanvasSuggestion[]{ id, blockId, from, to, insert, status?, reason? }
onAccept, onReject(id) => void
onAcceptAll, onRejectAll() => voidStreaming edits stay
onStop, writingLabelStop while edits stream, and the status text
versions, currentVersionId, onRestoreVersionThe History menu
onAsk(question, selection) => voidAsk field in the selection toolbar
onSelectionAction(action, selection) => voidRewrite, Explain and Copy
noticeReactNodeA line above the document, such as agent status
title, actions, placeholder, readOnly

A selection arrives as { text, blockIds, start, end, range }, where start and end are { blockId, offset }.

Keyboard

KeyAction
Tab with text selectedMove into the selection toolbar
Enter on a suggested editAccept it, then focus the next one
Delete, Backspace on a suggested editReject it
ArrowDown, ArrowUpNext and previous edit
Enter at the end of a headingStart a paragraph
Cmd+ZNative undo while editing

Accessibility

  • The editor is a multiline textbox. While edits are pending it renders read-only, so suggestions and the text never fight over the caret.
  • Each edit is a group named Replace, Delete or Insert with the exact text, and a single tab stop with aria-keyshortcuts.
  • Insertions are underlined and deletions struck through as well as dimmed, so the difference never depends on color.
  • Streaming status is announced politely; tokens are not.

Motion

  • Insertions stream in place with the caret and no per-word effects.
  • An edit accepted with a pointer keeps a fill for 600 ms, then fades over 300 ms. Keyboard accepts and reduced motion skip it.
  • The Accept and Reject bar moves with the active edit instantly. Hover fills take 150 ms.