Skip to content

ChatPanel

A comprehensive chat interface system designed for AI assistant integration in editor applications. ChatPanel is the layout container, and the module exports a full suite of components for building rich conversational UIs: message lists, chat bubbles, input with attachments, typing indicators, tool call displays, code blocks, and empty states. Also includes hooks for managing message state, input behavior, and auto-scroll.

Live Preview

Import

import {
ChatPanel,
ChatMessageList,
ChatMessage,
ChatBubble,
ChatInput,
ChatTypingIndicator,
ChatToolCall,
ChatCodeBlock,
ChatMarkdownRenderer,
ChatAttachmentChip,
ChatContextChip,
ChatEmptyState,
ChatActionBar,
ChatInputToolbar,
useChatMessages,
useChatInput,
useChatScroll,
} from 'entangle-ui';

For consumers driving scroll imperatively, also import the API type:

import type { ChatMessageListScrollApi } from 'entangle-ui';

Usage

const { messages, appendMessage, updateMessage } = useChatMessages();
<ChatPanel density="comfortable">
<ChatMessageList
messages={messages}
emptyState={
<ChatEmptyState
title="How can I help?"
description="Ask me anything about your scene."
suggestions={['Create a cube', 'Change material', 'Add lighting']}
onSuggestionClick={s => handleSend(s)}
/>
}
/>
<ChatInput
onSubmit={value => handleSend(value)}
placeholder="Ask the assistant..."
streaming={isStreaming}
onStop={handleStop}
/>
</ChatPanel>;

Components

ChatPanel

The root layout container that controls spacing density. Wrap all other chat components inside it.

<ChatPanel density="comfortable">
{/* ChatMessageList, ChatInput, etc. */}
</ChatPanel>
DensityDescription
comfortableMore spacing and larger bubbles. Use for side panels and fullscreen.
compactTight spacing. Use for bottom panels and constrained areas.

ChatMessageList

Renders an array of messages with auto-scroll behavior. Accepts a custom renderMessage function for full control over individual message rendering.

<ChatMessageList
messages={messages}
autoScroll={true}
emptyState={<ChatEmptyState title="No messages yet" />}
renderMessage={(message, index) => (
<ChatMessage key={message.id} message={message} showTimestamp showAvatar />
)}
/>

ChatMessage

Renders a single message with avatar, timestamp, and action buttons. Supports a custom content renderer for markdown, LaTeX, or other rich formats.

<ChatMessage
message={message}
showTimestamp
showAvatar
actions={
<ChatActionBar>
<button onClick={handleCopy}>Copy</button>
<button onClick={handleRetry}>Retry</button>
</ChatActionBar>
}
renderContent={content => <MarkdownRenderer>{content}</MarkdownRenderer>}
/>

ChatBubble

Low-level bubble component with role-based alignment and coloring. Use for custom message layouts.

<ChatBubble role="user">Hello, can you help?</ChatBubble>
<ChatBubble role="assistant">Of course! What do you need?</ChatBubble>
<ChatBubble role="system">Session started</ChatBubble>

ChatInput

Multi-line input with submit/stop button, attachment chips, and configurable submit key. Auto-resizes from 1 line up to maxLines.

<ChatInput
value={input}
onChange={setInput}
onSubmit={handleSubmit}
onStop={handleStop}
streaming={isStreaming}
placeholder="Type a message..."
submitKey="enter"
maxLines={6}
attachments={attachments}
onRemoveAttachment={handleRemoveAttachment}
toolbar={
<ChatInputToolbar>
<button onClick={handleUpload}>Attach File</button>
</ChatInputToolbar>
}
/>

Attachments-only submit

When the textarea is empty, submit is allowed if at least one attachment is queued. The send button stays enabled and onSubmit fires with ('', attachments). Both controlled and uncontrolled modes honor this — no extra prop needed.

ChatTypingIndicator

Animated indicator shown while the assistant is generating a response.

<ChatTypingIndicator visible={isStreaming} label="Thinking..." variant="dots" />

ChatToolCall

Displays a tool/function invocation with expandable input/output details.

<ChatToolCall
toolCall={{
id: '1',
name: 'create_cube',
status: 'completed',
input: { size: 2 },
output: { nodeId: 'cube_01' },
durationMs: 120,
}}
collapsible
defaultExpanded={false}
/>

ChatCodeBlock

Code display with optional syntax highlighting, line numbers, copy button, and custom action buttons.

<ChatCodeBlock
code={`const mesh = new THREE.Mesh(geometry, material);`}
language="typescript"
copyable
lineNumbers
maxHeight={400}
actions={<button onClick={handleInsert}>Insert</button>}
/>

ChatMarkdownRenderer

Lightweight built-in markdown renderer designed to be passed to ChatMessage.renderContent. Covers what assistant LLMs typically produce: paragraphs, headings, lists, blockquotes, horizontal rules, inline emphasis / code, links (rendered with rel="noopener noreferrer" by default), fenced code blocks, and GFM pipe tables.

<ChatMessage
message={msg}
renderContent={content => <ChatMarkdownRenderer content={content} />}
/>
PropTypeDefaultDescription
contentstringRaw markdown content.
gfmbooleantrueEnable GitHub-flavored markdown extensions (tables).
linkTarget'_self' | '_blank''_blank'Link target. _blank adds rel="noopener noreferrer".
componentspartial overridesReplace code / codeBlock / link renderers.

Inline code uses the Code primitive; fenced code blocks use ChatCodeBlock. Tables and headings pick up theme tokens automatically.

For full CommonMark / advanced GFM support, swap in your own renderer via renderContent — the API stays the same.

ChatAttachmentChip

Displays an attached file, image, code snippet, or selection as a chip.

<ChatAttachmentChip
attachment={{ id: '1', name: 'scene.glb', type: 'file', size: 1024000 }}
removable
onRemove={id => handleRemove(id)}
onClick={attachment => handlePreview(attachment)}
/>

ChatContextChip

Displays editor context (e.g., selected objects, active file) as a labeled chip.

<ChatContextChip
label="Selected"
items={['Cube', 'Sphere', 'Light']}
icon={<SelectionIcon />}
onDismiss={handleDismiss}
/>

ChatEmptyState

Shown when the message list is empty. Includes optional suggestion chips.

<ChatEmptyState
title="Start a conversation"
description="Ask me to modify your scene, generate code, or explain concepts."
suggestions={['Add a spotlight', 'Optimize materials', 'Export scene']}
onSuggestionClick={suggestion => handleSend(suggestion)}
/>

ChatActionBar

Horizontal row of action buttons shown below a message.

<ChatActionBar>
<button>Copy</button>
<button>Retry</button>
<button>Apply to Scene</button>
</ChatActionBar>

ChatInputToolbar

Horizontal row of action buttons rendered below the chat input area.

<ChatInputToolbar>
<button>Attach File</button>
<button>Add Context</button>
</ChatInputToolbar>

Customizing the message bubble width

Message bubbles default to max-width: 85% of the message row. You can change this at three levels:

// Global default — set the public CSS variable in your own stylesheet:
:root { --etui-chat-message-max-width: 720px; }
// Panel-level cascade:
<ChatPanel messageMaxWidth="100%"></ChatPanel>
// Per-message override:
<ChatMessage maxWidth={500} message={msg} />

Numbers are interpreted as pixels (500500px); strings are passed through verbatim ("60ch", "70%", …). Per-message wins over the panel cascade, which wins over the global CSS variable, which wins over the library default.

Hooks

useChatMessages

Manages the chat message array state with convenience methods.

const {
messages,
setMessages,
appendMessage,
updateMessage,
removeMessage,
clearMessages,
getMessage,
} = useChatMessages({
initialMessages: [],
maxMessages: 200,
});

useChatInput

Manages textarea input state with auto-resize and submit key handling.

const { value, setValue, clear, textareaRef, handleKeyDown, handleChange } =
useChatInput({
submitKey: 'enter',
maxLines: 6,
onSubmit: value => handleSend(value),
attachmentsCount: attachments.length, // allows attachments-only submit
});

The optional attachmentsCount lets the submit gate fire onSubmit('') when the textarea is empty but attachments are queued. Defaults to 0.

useChatScroll

Manages auto-scroll behavior for the message list. Pauses when the user scrolls up and resumes when they scroll back to the bottom. Streaming-aware: an internal ResizeObserver watches the content element so the list stays pinned while a single message grows token-by-token (no messages.length change required).

const {
scrollContainerRef,
scrollContentRef,
isAtBottom,
hasNewMessages,
scrollToBottom,
scrollTo,
scrollToElement,
} = useChatScroll({
messages,
enabled: true,
threshold: 100,
});
Returned refWhere to attach
scrollContainerRefThe scroll container (the element with overflow: auto).
scrollContentRefThe inner content element whose height grows as content streams.

ChatMessageList wires both refs internally — you only need this hook directly if you’re building a custom message list shell.

Imperative scroll API on ChatMessageList

Drive scroll position from outside the list by passing scrollApiRef:

import type { ChatMessageListScrollApi } from 'entangle-ui';
const scrollApi = useRef<ChatMessageListScrollApi>(null);
<ChatMessageList scrollApiRef={scrollApi} messages={messages} />;
// Later — e.g., from a search panel:
scrollApi.current?.scrollToBottom('smooth');
scrollApi.current?.scrollToElement(messageEl, { block: 'center' });
const atBottom = scrollApi.current?.isAtBottom();

The DOM ref on ChatMessageList still points to the scroll container element — scrollApiRef is intentionally separate so methods don’t overload the DOM ref.

Props

ChatPanel

Prop Type Default Description
density 'comfortable' | 'compact' 'comfortable' Visual density of the chat layout. Comfortable has more spacing for side panels; compact is for constrained areas.
messageMaxWidth number | string Maximum width of message bubbles inside this panel. Cascades to every ChatMessage via the --etui-chat-message-max-width CSS variable. Numbers are pixels; strings are used verbatim.
children * ReactNode Panel content, typically ChatMessageList and ChatInput.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.
ref Ref<HTMLDivElement> Ref to the root element.

ChatMessageList

Prop Type Default Description
messages * ChatMessageData[] Array of messages to render.
renderMessage (message: ChatMessageData, index: number) => ReactNode Custom renderer for individual messages.
emptyState ReactNode Content shown when the messages array is empty.
autoScroll boolean true Auto-scroll to bottom on new messages and during streaming. Pauses when user scrolls up, resumes at bottom.
scrollApiRef Ref<ChatMessageListScrollApi> Imperative handle exposing scrollToBottom, scrollTo, scrollToElement, and isAtBottom. Separate from the DOM ref.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatMessage

Prop Type Default Description
message * ChatMessageData Message data object.
showTimestamp boolean Show timestamp below the message content.
showAvatar boolean Show avatar next to the message.
actions ReactNode Action buttons rendered below the message (typically a ChatActionBar).
renderContent (content: string) => ReactNode Custom content renderer for markdown, LaTeX, or other rich formats. Pair with ChatMarkdownRenderer for built-in markdown support.
maxWidth number | string Per-message bubble max width. Overrides ChatPanel.messageMaxWidth and the 85% default. Numbers are pixels; strings are used verbatim.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatInput

Prop Type Default Description
value string Current input value (controlled).
onChange (value: string) => void Change handler for controlled usage.
onSubmit (value: string, attachments: ChatAttachmentData[]) => void Called when the user submits the message.
onStop () => void Called when the user clicks the stop generation button.
placeholder string Placeholder text for the input.
streaming boolean false Whether the assistant is currently streaming. Shows stop button instead of send.
disabled boolean Disable the input.
submitKey 'enter' | 'ctrl+enter' 'enter' Key combination that submits the message. The other combination inserts a newline.
maxLines number 6 Maximum visible lines before the input scrolls.
attachments ChatAttachmentData[] Currently attached items shown as chips above the input.
onRemoveAttachment (attachmentId: string) => void Called when user removes an attachment chip.
prefix ReactNode Content rendered before the textarea (e.g., context chips).
suffix ReactNode Content rendered after the textarea (e.g., additional action buttons).
toolbar ReactNode Toolbar rendered below the input area (use ChatInputToolbar as the wrapper).
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatTypingIndicator

Prop Type Default Description
label string 'Thinking...' Label shown alongside the animation.
variant 'dots' | 'pulse' 'dots' Animation style: three animated dots or a pulsing bar.
visible boolean Whether the indicator is visible.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatToolCall

Prop Type Default Description
toolCall * ChatToolCallData Tool call data object with id, name, status, input, output, error, and duration.
collapsible boolean true Whether the input/output details can be expanded.
defaultExpanded boolean false Whether details are initially expanded.
icon ReactNode Custom icon for the tool (defaults to a wrench icon).
renderOutput (output: Record<string, unknown>) => ReactNode Custom renderer for the tool output.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatCodeBlock

Prop Type Default Description
code * string Code content to display.
language string Programming language for syntax highlighting.
copyable boolean true Show a copy-to-clipboard button.
lineNumbers boolean false Show line numbers.
maxHeight number 400 Maximum visible height in pixels before the block scrolls.
actions ReactNode Actions rendered in the code block header alongside the copy button.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatAttachmentChip

Prop Type Default Description
attachment * ChatAttachmentData Attachment data object.
onRemove (id: string) => void Called when the remove button is clicked.
onClick (attachment: ChatAttachmentData) => void Called when the chip itself is clicked (e.g., to preview).
removable boolean false Whether to show the remove button.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatContextChip

Prop Type Default Description
label * string Label describing the context (e.g., "Selected", "Active file").
items * string[] Items in this context group.
icon ReactNode Icon shown before the label.
onDismiss () => void Called when the chip is dismissed.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

ChatEmptyState

Prop Type Default Description
title string Main heading text.
description string Supporting description text.
icon ReactNode Icon or illustration.
suggestions string[] Quick-start suggestion chips that populate the input when clicked.
onSuggestionClick (suggestion: string) => void Called when a suggestion chip is clicked.
className string Additional CSS class names.
style CSSProperties Inline styles.
testId string Test identifier for automated testing.

Data Types

ChatMessageData

interface ChatMessageData {
id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
status: 'complete' | 'streaming' | 'error' | 'pending';
timestamp: string; // ISO 8601
attachments?: ChatAttachmentData[];
toolCalls?: ChatToolCallData[];
avatar?: string;
displayName?: string;
}

ChatAttachmentData

interface ChatAttachmentData {
id: string;
name: string;
type: 'file' | 'image' | 'code' | 'selection';
mimeType?: string;
size?: number;
thumbnailUrl?: string;
content?: string;
meta?: Record<string, unknown>;
}

ChatToolCallData

interface ChatToolCallData {
id: string;
name: string;
status: 'pending' | 'running' | 'completed' | 'error';
input?: Record<string, unknown>;
output?: Record<string, unknown>;
error?: string;
durationMs?: number;
}