From 99491eb3c5c4a09059b1c4cf43613e45e4f89ec8 Mon Sep 17 00:00:00 2001 From: yoyoyohamapi <8338436+yoyoyohamapi@users.noreply.github.com> Date: Thu, 20 Mar 2025 06:39:27 +0000 Subject: [PATCH] refactor(core): reorganize chat-panel components (#10935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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: * `` * `` * `` * Split message content into types: * `` * `` * `` --- .../ai/chat-panel/actions/action-wrapper.ts | 6 +- .../ai/chat-panel/actions/image-to-text.ts | 12 +- .../blocksuite/ai/chat-panel/actions/image.ts | 8 +- .../ai/chat-panel/chat-panel-messages.ts | 37 +++--- .../ai/chat-panel/chat-panel-user-message.ts | 105 ---------------- .../ai/chat-panel/components/images.ts | 41 ------- .../ai/chat-panel/content/assistant-avatar.ts | 30 +++++ .../ai/chat-panel/content/images.ts | 106 +++++++++++++++++ .../ai/chat-panel/content/pure-text.ts | 35 ++++++ .../chat-text.ts => content/rich-text.ts} | 20 +--- .../ai/chat-panel/message/action.ts | 78 ++++++++++++ .../assistant.ts} | 112 ++++++------------ .../blocksuite/ai/chat-panel/message/user.ts | 45 +++++++ .../core/src/blocksuite/ai/effects.ts | 23 ++-- .../affine-cloud-copilot/e2e/copilot.spec.ts | 22 ++-- 15 files changed, 404 insertions(+), 276 deletions(-) delete mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-user-message.ts delete mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/components/images.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/content/images.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/content/pure-text.ts rename packages/frontend/core/src/blocksuite/ai/chat-panel/{actions/chat-text.ts => content/rich-text.ts} (62%) create mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts rename packages/frontend/core/src/blocksuite/ai/chat-panel/{chat-panel-assistant-message.ts => message/assistant.ts} (53%) create mode 100644 packages/frontend/core/src/blocksuite/ai/chat-panel/message/user.ts diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts index 2b1a562a8d..24b80c1a60 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts @@ -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> = { @@ -141,7 +140,10 @@ export class ActionWrapper extends WithDisposable(LitElement) {
Answer
${HISTORY_IMAGE_ACTIONS.includes(item.action) - ? images && renderImages(images) + ? images && + html`` : nothing} ${answer ? createTextRenderer(this.host, { customHeading: true })(answer) diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image-to-text.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image-to-text.ts index f259797e4e..f69d81b711 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image-to-text.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image-to-text.ts @@ -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` -
- ${answer ? renderImages(answer) : nothing} +
+ ${answer + ? html`` + : nothing}
`; } diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts index 40e73db4d9..de3cd86202 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts @@ -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`
- ${answer ? renderImages(answer) : nothing} + ${images + ? html`` + : nothing}
`; } diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts index 1e642c0bd0..9a09d5383e 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts @@ -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`` - : html` this.retry()} - >`; + if (isChatMessage(item) && item.role === 'user') { + return html``; + } else if (isChatMessage(item) && item.role === 'assistant') { + return html` this.retry()} + >`; + } else if (isChatAction(item)) { + return html``; + } + return nothing; } )}
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-user-message.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-user-message.ts deleted file mode 100644 index 8715bd9041..0000000000 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-user-message.ts +++ /dev/null @@ -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`
- ${repeat( - images, - image => image, - image => { - return html``; - } - )} -
` - : nothing; - } - - renderText(text: string) { - return text.length > 0 - ? html`
${text}
` - : 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`
${this.renderContent()}
`; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'chat-panel-user-message': ChatPanelUserMessage; - } -} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/components/images.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/components/images.ts deleted file mode 100644 index 67117233bc..0000000000 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/components/images.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { html } from 'lit'; -import { repeat } from 'lit/directives/repeat.js'; - -export const renderImages = (images: string[]) => { - return html` -
- ${repeat( - images, - image => image, - image => { - return html`
- -
`; - } - )} -
`; -}; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts new file mode 100644 index 0000000000..8214c21596 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts @@ -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`${AffineAvatarIcon} AFFiNE AI`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'chat-assistant-avatar': AssistantAvatar; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/images.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/images.ts new file mode 100644 index 0000000000..682f0bb80b --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/images.ts @@ -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`
+ ${repeat( + this.images, + image => image, + image => html`` + )} +
`; + } else { + return html`
+ ${repeat( + this.images, + image => image, + image => + html`
+ +
` + )} +
`; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'chat-content-images': ChatContentImages; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/pure-text.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/pure-text.ts new file mode 100644 index 0000000000..756dc39b75 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/pure-text.ts @@ -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`
${this.text}
` + : nothing; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'chat-content-pure-text': ChatContentPureText; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/chat-text.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts similarity index 62% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/actions/chat-text.ts rename to packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts index c46f7b8c03..26a111482e 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/chat-text.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts @@ -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; } } diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts new file mode 100644 index 0000000000..8cd290f2c7 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts @@ -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` + + `; + } + + renderContent() { + const { host, item } = this; + + switch (item.action) { + case 'Create a presentation': + return html``; + case 'Make it real': + return html``; + case 'Brainstorm mindmap': + return html``; + case 'Explain this image': + case 'Generate a caption': + return html``; + default: + if (HISTORY_IMAGE_ACTIONS.includes(item.action)) { + return html``; + } + + return html``; + } + } + + protected override render() { + return html` ${this.renderHeader()} ${this.renderContent()} `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'chat-message-action': ChatMessageAction; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-assistant-message.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts similarity index 53% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-assistant-message.ts rename to packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts index bd1b14ed84..32d7535489 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-assistant-message.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts @@ -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`