refactor(core): reorganize chat-panel components (#10935)

### TL;DR
Split complex chat message component into smaller, reusable units with ​finer granularity

> CLOSE AF-2323 AF-2326

### What Changed
* Split messages into components:
  * `<chat-message-assistant />`
  * `<chat-message-user />`
  * `<chat-message-action />`
* Split message content into types:
  * `<chat-content-images />`
  * `<chat-content-rich-text />`
  * `<chat-content-pure-text />`
This commit is contained in:
yoyoyohamapi
2025-03-20 06:39:27 +00:00
parent 1f2caca3f5
commit 99491eb3c5
15 changed files with 404 additions and 276 deletions

View File

@@ -24,7 +24,6 @@ import { property, state } from 'lit/decorators.js';
import { createTextRenderer } from '../../components/text-renderer';
import type { ChatAction } from '../chat-context';
import { renderImages } from '../components/images';
import { HISTORY_IMAGE_ACTIONS } from '../const';
const icons: Record<string, TemplateResult<1>> = {
@@ -141,7 +140,10 @@ export class ActionWrapper extends WithDisposable(LitElement) {
<div class="answer-prompt">
<div class="subtitle">Answer</div>
${HISTORY_IMAGE_ACTIONS.includes(item.action)
? images && renderImages(images)
? images &&
html`<chat-content-images
.images=${images}
></chat-content-images>`
: nothing}
${answer
? createTextRenderer(this.host, { customHeading: true })(answer)

View File

@@ -1,4 +1,5 @@
import './action-wrapper';
import '../content/images';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
@@ -8,7 +9,6 @@ import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { ChatAction } from '../chat-context';
import { renderImages } from '../components/images';
export class ActionImageToText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
@@ -21,8 +21,14 @@ export class ActionImageToText extends WithDisposable(ShadowlessElement) {
const answer = this.item.messages[1].attachments;
return html`<action-wrapper .host=${this.host} .item=${this.item}>
<div style=${styleMap({ marginBottom: '12px' })}>
${answer ? renderImages(answer) : nothing}
<div
style=${styleMap({
marginBottom: '12px',
})}
>
${answer
? html`<chat-content-images .images=${answer}></chat-content-images>`
: nothing}
</div>
</action-wrapper>`;
}

View File

@@ -1,4 +1,5 @@
import './action-wrapper';
import '../content/images';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
@@ -8,7 +9,6 @@ import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { ChatAction } from '../chat-context';
import { renderImages } from '../components/images';
export class ActionImage extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
@@ -18,11 +18,13 @@ export class ActionImage extends WithDisposable(ShadowlessElement) {
accessor host!: EditorHost;
protected override render() {
const answer = this.item.messages[0].attachments;
const images = this.item.messages[0].attachments;
return html`<action-wrapper .host=${this.host} .item=${this.item}>
<div style=${styleMap({ marginBottom: '12px' })}>
${answer ? renderImages(answer) : nothing}
${images
? html`<chat-content-images .images=${images}></chat-content-images>`
: nothing}
</div>
</action-wrapper>`;
}

View File

@@ -19,6 +19,7 @@ import { AIProvider } from '../provider';
import {
type ChatContextValue,
type ChatMessage,
isChatAction,
isChatMessage,
} from './chat-context';
import { HISTORY_IMAGE_ACTIONS } from './const';
@@ -245,20 +246,28 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
(_, index) => index,
(item, index) => {
const isLast = index === filteredItems.length - 1;
return isChatMessage(item) && item.role === 'user'
? html`<chat-panel-user-message
.item=${item}
></chat-panel-user-message>`
: html`<chat-panel-assistant-message
.host=${this.host}
.item=${item}
.isLast=${isLast}
.status=${status}
.error=${error}
.previewSpecBuilder=${this.previewSpecBuilder}
.getSessionId=${this.getSessionId}
.retry=${() => this.retry()}
></chat-panel-assistant-message>`;
if (isChatMessage(item) && item.role === 'user') {
return html`<chat-message-user
.item=${item}
></chat-message-user>`;
} else if (isChatMessage(item) && item.role === 'assistant') {
return html`<chat-message-assistant
.host=${this.host}
.item=${item}
.isLast=${isLast}
.status=${isLast ? status : 'idle'}
.error=${isLast ? error : null}
.previewSpecBuilder=${this.previewSpecBuilder}
.getSessionId=${this.getSessionId}
.retry=${() => this.retry()}
></chat-message-assistant>`;
} else if (isChatAction(item)) {
return html`<chat-message-action
.host=${this.host}
.item=${item}
></chat-message-action>`;
}
return nothing;
}
)}
</div>

View File

@@ -1,105 +0,0 @@
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { type ChatMessage } from './chat-context';
export class ChatPanelUserMessage extends WithDisposable(ShadowlessElement) {
static override styles = css`
.chat-user-message {
display: flex;
flex-direction: column;
align-items: flex-end;
.images {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 8px;
margin-bottom: 8px;
max-width: 100%;
overflow-x: auto;
padding: 4px;
scrollbar-width: auto;
}
.images::-webkit-scrollbar {
height: 4px;
}
.images::-webkit-scrollbar-thumb {
background-color: var(--affine-border-color);
border-radius: 4px;
}
.images::-webkit-scrollbar-track {
background: transparent;
}
img {
max-width: 180px;
max-height: 264px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
.text-content {
display: inline-block;
text-align: left;
max-width: 800px;
max-height: 500px;
overflow-y: auto;
background: var(--affine-v2-aI-userTextBackground);
border-radius: 8px;
padding: 12px;
white-space: pre-wrap;
word-wrap: break-word;
}
}
`;
@property({ attribute: false })
accessor item!: ChatMessage;
renderImages(images: string[]) {
return images.length > 0
? html`<div class="images">
${repeat(
images,
image => image,
image => {
return html`<img src="${image}" />`;
}
)}
</div>`
: nothing;
}
renderText(text: string) {
return text.length > 0
? html`<div class="text-content">${text}</div>`
: nothing;
}
renderContent() {
const { item } = this;
const imagesRendered = item.attachments
? this.renderImages(item.attachments)
: nothing;
return html` ${imagesRendered} ${this.renderText(item.content)} `;
}
protected override render() {
return html` <div class="chat-user-message">${this.renderContent()}</div> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-panel-user-message': ChatPanelUserMessage;
}
}

View File

@@ -1,41 +0,0 @@
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
export const renderImages = (images: string[]) => {
return html`<style>
.images-container {
display: flex;
gap: 12px;
flex-direction: column;
margin-bottom: 8px;
}
.image-container {
border-radius: 4px;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 70%;
max-width: 320px;
img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
}
</style>
<div class="images-container">
${repeat(
images,
image => image,
image => {
return html`<div class="image-container">
<img src="${image}" />
</div>`;
}
)}
</div>`;
};

View File

@@ -0,0 +1,30 @@
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { AiIcon } from '@blocksuite/icons/lit';
import { css, html } from 'lit';
const AffineAvatarIcon = AiIcon({
width: '20px',
height: '20px',
style: 'color: var(--affine-primary-color)',
});
export class AssistantAvatar extends ShadowlessElement {
static override styles = css`
.assistant-avatar {
display: inline-flex;
align-items: center;
gap: 8px;
}
`;
protected override render() {
return html`<span class="assistant-avatar"
>${AffineAvatarIcon} AFFiNE AI</span
>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-assistant-avatar': AssistantAvatar;
}
}

View File

@@ -0,0 +1,106 @@
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
export class ChatContentImages extends WithDisposable(ShadowlessElement) {
static override styles = css`
.chat-content-images-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 8px;
margin-bottom: 8px;
max-width: 100%;
overflow-x: auto;
padding: 4px;
scrollbar-width: auto;
}
.chat-content-images-row::-webkit-scrollbar {
height: 4px;
}
.chat-content-images-row::-webkit-scrollbar-thumb {
background-color: var(--affine-border-color);
border-radius: 4px;
}
.chat-content-images-row::-webkit-scrollbar-track {
background: transparent;
}
.chat-content-images-row img {
max-width: 180px;
max-height: 264px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
.chat-content-images-column {
display: flex;
gap: 12px;
flex-direction: column;
margin-bottom: 8px;
}
.chat-content-images-column .image-container {
border-radius: 4px;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 70%;
max-width: 320px;
}
.chat-content-images-column .image-container img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
`;
@property({ attribute: false })
accessor images: string[] = [];
@property({ attribute: false })
accessor layout: 'row' | 'column' = 'row';
protected override render() {
if (this.images.length === 0) {
return nothing;
}
if (this.layout === 'row') {
return html`<div class="chat-content-images-row">
${repeat(
this.images,
image => image,
image => html`<img src="${image}" />`
)}
</div>`;
} else {
return html`<div class="chat-content-images-column">
${repeat(
this.images,
image => image,
image =>
html`<div class="image-container">
<img src="${image}" />
</div>`
)}
</div>`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-content-images': ChatContentImages;
}
}

View File

@@ -0,0 +1,35 @@
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
export class ChatContentPureText extends ShadowlessElement {
static override styles = css`
.chat-content-pure-text {
display: inline-block;
text-align: left;
max-width: 800px;
max-height: 500px;
overflow-y: auto;
background: var(--affine-v2-aI-userTextBackground);
border-radius: 8px;
padding: 12px;
white-space: pre-wrap;
word-wrap: break-word;
}
`;
@property({ attribute: false })
accessor text: string = '';
protected override render() {
return this.text.length > 0
? html`<div class="chat-content-pure-text">${this.text}</div>`
: nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-content-pure-text': ChatContentPureText;
}
}

View File

@@ -1,22 +1,16 @@
import './action-wrapper';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
import { html, nothing } from 'lit';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { createTextRenderer } from '../../components/text-renderer';
import { renderImages } from '../components/images';
export class ChatText extends WithDisposable(ShadowlessElement) {
export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor attachments: string[] | undefined = undefined;
@property({ attribute: false })
accessor text!: string;
@@ -27,18 +21,16 @@ export class ChatText extends WithDisposable(ShadowlessElement) {
accessor previewSpecBuilder!: SpecBuilder;
protected override render() {
const { attachments, text, host } = this;
return html`${attachments && attachments.length > 0
? renderImages(attachments)
: nothing}${createTextRenderer(host, {
const { text, host } = this;
return html`${createTextRenderer(host, {
customHeading: true,
extensions: this.previewSpecBuilder.value,
})(text, this.state)} `;
})(text, this.state)}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-text': ChatText;
'chat-content-rich-text': ChatContentRichText;
}
}

View File

@@ -0,0 +1,78 @@
import '../content/assistant-avatar';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { type ChatAction } from '../chat-context';
import { HISTORY_IMAGE_ACTIONS } from '../const';
export class ChatMessageAction extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor item!: ChatAction;
renderHeader() {
return html`
<div class="user-info">
<chat-assistant-avatar></chat-assistant-avatar>
</div>
`;
}
renderContent() {
const { host, item } = this;
switch (item.action) {
case 'Create a presentation':
return html`<action-slides
.host=${host}
.item=${item}
></action-slides>`;
case 'Make it real':
return html`<action-make-real
.host=${host}
.item=${item}
></action-make-real>`;
case 'Brainstorm mindmap':
return html`<action-mindmap
.host=${host}
.item=${item}
></action-mindmap>`;
case 'Explain this image':
case 'Generate a caption':
return html`<action-image-to-text
.host=${host}
.item=${item}
></action-image-to-text>`;
default:
if (HISTORY_IMAGE_ACTIONS.includes(item.action)) {
return html`<action-image
.host=${host}
.item=${item}
></action-image>`;
}
return html`<action-text
.item=${item}
.host=${host}
.isCode=${item.action === 'Explain this code' ||
item.action === 'Check code error'}
></action-text>`;
}
}
protected override render() {
return html` ${this.renderHeader()} ${this.renderContent()} `;
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-message-action': ChatMessageAction;
}
}

View File

@@ -1,30 +1,22 @@
import '../content/assistant-avatar';
import '../content/rich-text';
import type { EditorHost } from '@blocksuite/affine/block-std';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
import { AiIcon } from '@blocksuite/icons/lit';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import {
EdgelessEditorActions,
PageEditorActions,
} from '../_common/chat-actions-handle';
import { type AIError } from '../components/ai-item/types';
import { AIChatErrorRenderer } from '../messages/error';
import { isChatMessage } from './chat-context';
import { type ChatItem } from './chat-context';
import { HISTORY_IMAGE_ACTIONS } from './const';
} from '../../_common/chat-actions-handle';
import { type AIError } from '../../components/ai-item/types';
import { AIChatErrorRenderer } from '../../messages/error';
import { type ChatMessage, isChatMessage } from '../chat-context';
const AffineAvatarIcon = AiIcon({
width: '20px',
height: '20px',
style: 'color: var(--affine-primary-color)',
});
export class ChatPanelAssistantMessage extends WithDisposable(
ShadowlessElement
) {
export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
static override styles = css`
.message-info {
color: var(--affine-placeholder-color);
@@ -37,7 +29,7 @@ export class ChatPanelAssistantMessage extends WithDisposable(
accessor host!: EditorHost;
@property({ attribute: false })
accessor item!: ChatItem;
accessor item!: ChatMessage;
@property({ attribute: false })
accessor isLast: boolean = false;
@@ -57,18 +49,15 @@ export class ChatPanelAssistantMessage extends WithDisposable(
@property({ attribute: false })
accessor retry!: () => void;
renderAvatar() {
const isAssistant =
isChatMessage(this.item) && this.item.role === 'assistant';
renderHeader() {
const isWithDocs =
isAssistant &&
'content' in this.item &&
this.item.content &&
this.item.content.includes('[^') &&
/\[\^\d+\]:{"type":"doc","docId":"[^"]+"}/.test(this.item.content);
return html`<div class="user-info">
${AffineAvatarIcon} AFFiNE AI
<chat-assistant-avatar></chat-assistant-avatar>
${isWithDocs
? html`<span class="message-info">with your docs</span>`
: nothing}
@@ -82,61 +71,28 @@ export class ChatPanelAssistantMessage extends WithDisposable(
return html`<ai-loading></ai-loading>`;
}
if (isChatMessage(item)) {
const state = isLast
? status !== 'loading' && status !== 'transmitting'
? 'finished'
: 'generating'
: 'finished';
const shouldRenderError = isLast && status === 'error' && !!error;
return html`<chat-text
.host=${host}
.attachments=${item.attachments}
.text=${item.content}
.state=${state}
.previewSpecBuilder=${this.previewSpecBuilder}
></chat-text>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${this.renderEditorActions()}`;
} else {
switch (item.action) {
case 'Create a presentation':
return html`<action-slides
.host=${host}
.item=${item}
></action-slides>`;
case 'Make it real':
return html`<action-make-real
.host=${host}
.item=${item}
></action-make-real>`;
case 'Brainstorm mindmap':
return html`<action-mindmap
.host=${host}
.item=${item}
></action-mindmap>`;
case 'Explain this image':
case 'Generate a caption':
return html`<action-image-to-text
.host=${host}
.item=${item}
></action-image-to-text>`;
default:
if (HISTORY_IMAGE_ACTIONS.includes(item.action)) {
return html`<action-image
.host=${host}
.item=${item}
></action-image>`;
}
const state = isLast
? status !== 'loading' && status !== 'transmitting'
? 'finished'
: 'generating'
: 'finished';
const shouldRenderError = isLast && status === 'error' && !!error;
return html`<action-text
.item=${item}
.host=${host}
.isCode=${item.action === 'Explain this code' ||
item.action === 'Check code error'}
></action-text>`;
}
}
return html`
${item.attachments
? html`<chat-content-images
.images=${item.attachments}
></chat-content-images>`
: nothing}
<chat-content-rich-text
.host=${host}
.text=${item.content}
.state=${state}
.previewSpecBuilder=${this.previewSpecBuilder}
></chat-content-rich-text>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${this.renderEditorActions()}
`;
}
renderEditorActions() {
@@ -185,7 +141,7 @@ export class ChatPanelAssistantMessage extends WithDisposable(
protected override render() {
return html`
${this.renderAvatar()}
${this.renderHeader()}
<div class="item-wrapper">${this.renderContent()}</div>
`;
}
@@ -193,6 +149,6 @@ export class ChatPanelAssistantMessage extends WithDisposable(
declare global {
interface HTMLElementTagNameMap {
'chat-panel-assistant-message': ChatPanelAssistantMessage;
'chat-message-assistant': ChatMessageAssistant;
}
}

View File

@@ -0,0 +1,45 @@
import '../content/images';
import '../content/pure-text';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { type ChatMessage } from '../chat-context';
export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
static override styles = css`
.chat-message-user {
display: flex;
flex-direction: column;
align-items: flex-end;
}
`;
@property({ attribute: false })
accessor item!: ChatMessage;
renderContent() {
const { item } = this;
return html`
${item.attachments
? html`<chat-content-images
.images=${item.attachments}
></chat-content-images>`
: nothing}
<chat-content-pure-text .text=${item.content}></chat-content-pure-text>
`;
}
protected override render() {
return html` <div class="chat-message-user">${this.renderContent()}</div> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-message-user': ChatMessageUser;
}
}

View File

@@ -15,7 +15,6 @@ import { UserInfo } from './blocks/ai-chat-block/components/user-info';
import { AIChatBlockSchemaExtension } from './blocks/ai-chat-block/model';
import { ChatPanel } from './chat-panel';
import { ActionWrapper } from './chat-panel/actions/action-wrapper';
import { ChatText } from './chat-panel/actions/chat-text';
import { ActionImage } from './chat-panel/actions/image';
import { ActionImageToText } from './chat-panel/actions/image-to-text';
import { ActionMakeReal } from './chat-panel/actions/make-real';
@@ -23,17 +22,22 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
import { ActionSlides } from './chat-panel/actions/slides';
import { ActionText } from './chat-panel/actions/text';
import { AILoading } from './chat-panel/ai-loading';
import { ChatPanelAssistantMessage } from './chat-panel/chat-panel-assistant-message';
import { ChatPanelChips } from './chat-panel/chat-panel-chips';
import { ChatPanelInput } from './chat-panel/chat-panel-input';
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
import { ChatPanelUserMessage } from './chat-panel/chat-panel-user-message';
import { ChatPanelAddPopover } from './chat-panel/components/add-popover';
import { ChatPanelChip } from './chat-panel/components/chip';
import { ChatPanelCollectionChip } from './chat-panel/components/collection-chip';
import { ChatPanelDocChip } from './chat-panel/components/doc-chip';
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
import { ChatPanelTagChip } from './chat-panel/components/tag-chip';
import { AssistantAvatar } from './chat-panel/content/assistant-avatar';
import { ChatContentImages } from './chat-panel/content/images';
import { ChatContentPureText } from './chat-panel/content/pure-text';
import { ChatContentRichText } from './chat-panel/content/rich-text';
import { ChatMessageAction } from './chat-panel/message/action';
import { ChatMessageAssistant } from './chat-panel/message/assistant';
import { ChatMessageUser } from './chat-panel/message/user';
import { effects as componentAiItemEffects } from './components/ai-item';
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
import { AskAIButton } from './components/ask-ai-button';
@@ -83,7 +87,6 @@ export function registerAIEffects() {
customElements.define('chat-copy-more', ChatCopyMore);
customElements.define('image-preview-grid', ImagePreviewGrid);
customElements.define('action-wrapper', ActionWrapper);
customElements.define('chat-text', ChatText);
customElements.define('action-image-to-text', ActionImageToText);
customElements.define('action-image', ActionImage);
customElements.define('action-make-real', ActionMakeReal);
@@ -91,11 +94,6 @@ export function registerAIEffects() {
customElements.define('action-slides', ActionSlides);
customElements.define('action-text', ActionText);
customElements.define('ai-loading', AILoading);
customElements.define(
'chat-panel-assistant-message',
ChatPanelAssistantMessage
);
customElements.define('chat-panel-user-message', ChatPanelUserMessage);
customElements.define('chat-panel-input', ChatPanelInput);
customElements.define('chat-panel-messages', ChatPanelMessages);
customElements.define('chat-panel', ChatPanel);
@@ -136,6 +134,13 @@ export function registerAIEffects() {
customElements.define('ai-panel-input', AIPanelInput);
customElements.define('ai-panel-generating', AIPanelGenerating);
customElements.define('ai-panel-error', AIPanelError);
customElements.define('chat-assistant-avatar', AssistantAvatar);
customElements.define('chat-content-images', ChatContentImages);
customElements.define('chat-content-pure-text', ChatContentPureText);
customElements.define('chat-content-rich-text', ChatContentRichText);
customElements.define('chat-message-action', ChatMessageAction);
customElements.define('chat-message-assistant', ChatMessageAssistant);
customElements.define('chat-message-user', ChatMessageUser);
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);