From 0da5ef28d60bf9b9e3b909bc5fb422c8ec23f738 Mon Sep 17 00:00:00 2001 From: akumatus Date: Fri, 25 Apr 2025 02:51:20 +0000 Subject: [PATCH] feat(core): add button supports uploading images (#11943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [AI-81](https://linear.app/affine-design/issue/AI-81). ![截屏2025-04-24 16.09.19.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/sJGviKxfE3Ap685cl5bj/ace2f896-b652-450d-be86-fb0365d5b2bc.png) ## Summary by CodeRabbit - **New Features** - Added support for uploading and handling multiple images in the AI chat interface, with a maximum image limit enforced. - Chat assistant avatar now visually indicates different statuses, including a transmitting animation. - **Improvements** - Enhanced file upload experience by allowing batch image uploads and clearer notifications for files exceeding size limits. - Stronger status handling for chat assistant messages, improving reliability and user feedback. - Unified image count limit across chat input components using a shared constant. - **Bug Fixes** - Improved consistency in how image limits are enforced across chat components. --- .../ai/chat-panel/content/assistant-avatar.ts | 13 +++++- .../ai/chat-panel/message/assistant.ts | 5 +- .../components/ai-chat-chips/add-popover.ts | 46 ++++++++++++------- .../ai-chat-chips/chat-panel-chips.ts | 4 ++ .../ai/components/ai-chat-chips/doc-chip.ts | 1 - .../ai-chat-composer/ai-chat-composer.ts | 10 ++++ .../components/ai-chat-input/ai-chat-input.ts | 19 +++----- .../ai/components/ai-chat-input/const.ts | 1 + .../ai/components/ai-chat-input/index.ts | 1 + 9 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/const.ts 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 index fdedaa7784..4f62e7e972 100644 --- 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 @@ -1,6 +1,10 @@ +import { AIStarIconWithAnimation } from '@blocksuite/affine/components/icons'; import { ShadowlessElement } from '@blocksuite/affine/std'; import { AiIcon } from '@blocksuite/icons/lit'; import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { ChatStatus } from '../../components/ai-chat-messages'; const AffineAvatarIcon = AiIcon({ width: '20px', @@ -9,6 +13,9 @@ const AffineAvatarIcon = AiIcon({ }); export class AssistantAvatar extends ShadowlessElement { + @property({ attribute: 'data-status', reflect: true }) + accessor status: ChatStatus = 'idle'; + static override styles = css` chat-assistant-avatar { display: inline-flex; @@ -16,8 +23,12 @@ export class AssistantAvatar extends ShadowlessElement { gap: 8px; } `; + protected override render() { - return html`${AffineAvatarIcon} AFFiNE AI`; + return html`${this.status === 'transmitting' + ? AIStarIconWithAnimation + : AffineAvatarIcon} + AFFiNE AI`; } } diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts index 3140d63864..4b67b58b77 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts @@ -15,6 +15,7 @@ import { } from '../../_common/chat-actions-handle'; import { type ChatMessage, + type ChatStatus, isChatMessage, } from '../../components/ai-chat-messages'; import { AIChatErrorRenderer } from '../../messages/error'; @@ -39,7 +40,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { accessor isLast: boolean = false; @property({ attribute: 'data-status', reflect: true }) - accessor status: string = 'idle'; + accessor status: ChatStatus = 'idle'; @property({ attribute: false }) accessor error: AIError | null = null; @@ -64,7 +65,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { /\[\^\d+\]:{"type":"doc","docId":"[^"]+"}/.test(this.item.content); return html`
- + ${isWithDocs ? html`with your docs` : nothing} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts index bf6ba3a3b4..c7a82ea08f 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts @@ -161,16 +161,27 @@ export class ChatPanelAddPopover extends SignalWatcher( }; private readonly _addFileChip = async () => { - const file = await openFileOrFiles(); - if (!file) return; - if (file.size > 50 * 1024 * 1024) { - toast('You can only upload files less than 50MB'); - return; - } - this.addChip({ - file, - state: 'processing', + const files = await openFileOrFiles({ + multiple: true, }); + if (!files || files.length === 0) return; + + const images = files.filter(file => file.type.startsWith('image/')); + if (images.length > 0) { + this.addImages(images); + } + + const others = files.filter(file => !file.type.startsWith('image/')); + for (const file of others) { + if (file.size > 50 * 1024 * 1024) { + toast(`${file.name} is too large, please upload a file less than 50MB`); + } else { + await this.addChip({ + file, + state: 'processing', + }); + } + } this._track('file'); this.abortController.abort(); }; @@ -249,7 +260,10 @@ export class ChatPanelAddPopover extends SignalWatcher( accessor docDisplayConfig!: DocDisplayConfig; @property({ attribute: false }) - accessor addChip!: (chip: ChatChip) => void; + accessor addChip!: (chip: ChatChip) => Promise; + + @property({ attribute: false }) + accessor addImages!: (images: File[]) => void; @property({ attribute: false }) accessor abortController!: AbortController; @@ -459,8 +473,8 @@ export class ChatPanelAddPopover extends SignalWatcher( } } - private readonly _addDocChip = (meta: DocMeta) => { - this.addChip({ + private readonly _addDocChip = async (meta: DocMeta) => { + await this.addChip({ docId: meta.id, state: 'processing', }); @@ -469,8 +483,8 @@ export class ChatPanelAddPopover extends SignalWatcher( this.abortController.abort(); }; - private readonly _addTagChip = (tag: TagMeta) => { - this.addChip({ + private readonly _addTagChip = async (tag: TagMeta) => { + await this.addChip({ tagId: tag.id, state: 'processing', }); @@ -478,8 +492,8 @@ export class ChatPanelAddPopover extends SignalWatcher( this.abortController.abort(); }; - private readonly _addCollectionChip = (collection: CollectionMeta) => { - this.addChip({ + private readonly _addCollectionChip = async (collection: CollectionMeta) => { + await this.addChip({ collectionId: collection.id, state: 'processing', }); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts index 9ce2e1ac47..a0cdc9d7db 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts @@ -91,6 +91,9 @@ export class ChatPanelChips extends SignalWatcher( @property({ attribute: false }) accessor updateChips!: (chips: ChatChip[]) => void; + @property({ attribute: false }) + accessor addImages!: (images: File[]) => void; + @property({ attribute: false }) accessor pollContextDocsAndFiles!: () => void; @@ -262,6 +265,7 @@ export class ChatPanelChips extends SignalWatcher( template: html` (''); markdown.value = value; this.updateChip(this.chip, { - state: 'finished', markdown, tokenCount, }); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index a8690e3c2b..5eda4a204a 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -29,6 +29,7 @@ import type { AINetworkSearchConfig, AIReasoningConfig, } from '../ai-chat-input'; +import { MAX_IMAGE_COUNT } from '../ai-chat-input/const'; export class AIChatComposer extends SignalWatcher( WithDisposable(ShadowlessElement) @@ -118,6 +119,7 @@ export class AIChatComposer extends SignalWatcher( .docDisplayConfig=${this.docDisplayConfig} .searchMenuConfig=${this.searchMenuConfig} .portalContainer=${this.portalContainer} + .addImages=${this.addImages} >