diff --git a/packages/frontend/core/src/blocksuite/presets/ai/_common/components/ask-ai-toolbar.ts b/packages/frontend/core/src/blocksuite/presets/ai/_common/components/ask-ai-toolbar.ts index 43915fd3ff..bc7579cd18 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/_common/components/ask-ai-toolbar.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/_common/components/ask-ai-toolbar.ts @@ -15,7 +15,7 @@ import { property } from 'lit/decorators.js'; import { AIProvider } from '../../provider'; import { getAIPanelWidget } from '../../utils/ai-widgets'; -import { extractContext } from '../../utils/extract'; +import { extractSelectedContent } from '../../utils/extract'; export class AskAIToolbarButton extends WithDisposable(LitElement) { static override styles = css` @@ -75,8 +75,7 @@ export class AskAIToolbarButton extends WithDisposable(LitElement) { finish('success'); const aiPanel = getAIPanelWidget(this.host); aiPanel.discard(); - AIProvider.slots.requestOpenWithChat.emit({ host: this.host }); - extractContext(this.host) + extractSelectedContent(this.host) .then(context => { AIProvider.slots.requestSendWithChat.emit({ input, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/_common/config.ts b/packages/frontend/core/src/blocksuite/presets/ai/_common/config.ts index 9a9e65ad0d..8f14f76bfb 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/_common/config.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/_common/config.ts @@ -25,7 +25,6 @@ import { AIPresentationIconWithAnimation, AISearchIcon, AIStarIconWithAnimation, - ChatWithAIIcon, CommentIcon, ExplainIcon, ImproveWritingIcon, @@ -336,19 +335,6 @@ const OthersAIGroup: AIItemGroupConfig = { AIProvider.slots.requestOpenWithChat.emit({ host, autoSelect: true, - appendCard: true, - }); - panel.hide(); - }, - }, - { - name: 'Open AI Chat', - icon: ChatWithAIIcon, - handler: host => { - const panel = getAIPanelWidget(host); - AIProvider.slots.requestOpenWithChat.emit({ - host, - appendCard: true, }); panel.hide(); }, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-response.ts b/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-response.ts index 909d9f553e..30a30c2e1b 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-response.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-response.ts @@ -566,10 +566,7 @@ export function actionToResponse( handler: () => { reportResponse('result:continue-in-chat'); const panel = getAIPanelWidget(host); - AIProvider.slots.requestOpenWithChat.emit({ - host, - appendCard: true, - }); + AIProvider.slots.requestOpenWithChat.emit({ host }); panel.hide(); }, }, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/ai-panel.ts b/packages/frontend/core/src/blocksuite/presets/ai/ai-panel.ts index b232b8fe99..0b577289ef 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/ai-panel.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/ai-panel.ts @@ -191,10 +191,7 @@ function buildPageResponseConfig( icon: ChatWithAIIcon, handler: () => { reportResponse('result:continue-in-chat'); - AIProvider.slots.requestOpenWithChat.emit({ - host, - appendCard: true, - }); + AIProvider.slots.requestOpenWithChat.emit({ host }); panel.hide(); }, }, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-cards.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-cards.ts deleted file mode 100644 index a547ed3971..0000000000 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-cards.ts +++ /dev/null @@ -1,560 +0,0 @@ -import type { EditorHost } from '@blocksuite/affine/block-std'; -import { - type ImageBlockModel, - isInsideEdgelessEditor, - type NoteBlockModel, - NoteDisplayMode, -} from '@blocksuite/affine/blocks'; -import { WithDisposable } from '@blocksuite/affine/global/utils'; -import type { BlockModel } from '@blocksuite/affine/store'; -import { captureException } from '@sentry/react'; -import { - css, - html, - LitElement, - nothing, - type PropertyValues, - type TemplateResult, -} from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { repeat } from 'lit/directives/repeat.js'; -import { styleMap } from 'lit/directives/style-map.js'; - -import { - CurrentSelectionIcon, - DocIcon, - SmallImageIcon, -} from '../_common/icons'; -import { type AIChatParams, AIProvider } from '../provider'; -import { - getSelectedImagesAsBlobs, - getSelectedTextContent, - getTextContentFromBlockModels, - selectedToCanvas, -} from '../utils/selection-utils'; -import type { ChatContextValue } from './chat-context'; - -const cardsStyles = css` - .card-wrapper { - width: 90%; - max-height: 76px; - border-radius: 8px; - border: 1px solid var(--affine-border-color); - padding: 4px 12px; - cursor: pointer; - - .card-title { - display: flex; - gap: 4px; - height: 22px; - margin-bottom: 2px; - font-weight: 500; - font-size: 14px; - color: var(--affine-text-primary-color); - } - - .second-text { - font-size: 14px; - font-weight: 400; - color: var(--affine-text-secondary-color); - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - overflow: hidden; - text-overflow: ellipsis; - } - } -`; - -enum CardType { - Text, - Image, - Block, - Doc, -} - -type CardBase = { - id: number; -}; - -type CardText = CardBase & { - type: CardType.Text; - text: string; - markdown: string; -}; - -type CardImage = CardBase & { - type: CardType.Image; - image: File; - caption?: string; -}; - -type CardBlock = CardBase & { - type: CardType.Block | CardType.Doc; - text?: string; - markdown?: string; - images?: File[]; -}; - -type Card = CardText | CardImage | CardBlock; - -const MAX_CARDS = 3; - -export class ChatCards extends WithDisposable(LitElement) { - static override styles = css` - :host { - display: none; - flex-direction: column; - gap: 12px; - } - - :host([data-show]) { - display: flex; - } - - ${cardsStyles} - `; - - @property({ attribute: false }) - accessor host!: EditorHost; - - @property({ attribute: false }) - accessor updateContext!: (context: Partial) => void; - - @property({ attribute: false }) - accessor temporaryParams: AIChatParams | null = null; - - @property({ attribute: false }) - accessor isEmpty!: boolean; - - @state() - accessor cards: Card[] = []; - - private _currentDocId: string | null = null; - private _selectedCardId: number = 0; - - static renderText({ text }: CardText) { - const lines = text.split('\n'); - - return html` -
-
- ${CurrentSelectionIcon} -
Start with current selection
-
-
- ${repeat( - lines.slice(0, 2), - line => line, - line => html` -
- ${line} -
- ` - )} -
-
- `; - } - - static renderImage({ caption, image }: CardImage) { - return html` -
-
-
- ${SmallImageIcon} -
Start with this Image
-
-
${caption ? caption : 'caption'}
-
- -
- `; - } - - static renderDoc({ text, images }: CardBlock) { - let textTpl = html`you've chosen within the doc`; - let imageTpl: TemplateResult<1> | typeof nothing = nothing; - let hasImage = false; - - if (text?.length) { - const lines = text.split('\n'); - textTpl = html`${repeat( - lines.slice(0, 2), - line => line, - line => html` -
- ${line} -
- ` - )}`; - } - - if (images?.length) { - hasImage = true; - imageTpl = html` - - `; - } - - return html` -
-
-
- ${DocIcon} -
Start with this doc
-
-
${textTpl}
-
- ${imageTpl} -
- `; - } - - private _renderCard(card: Card) { - if (card.type === CardType.Text) { - return ChatCards.renderText(card); - } - - if (card.type === CardType.Image) { - return ChatCards.renderImage(card); - } - - if (card.type === CardType.Doc) { - return ChatCards.renderDoc(card); - } - - return nothing; - } - - private _updateCards(card: Card) { - this.cards.unshift(card); - - if (this.cards.length > MAX_CARDS) { - this.cards.pop(); - } - - this.requestUpdate(); - } - - private async _handleDocSelection(card: CardBlock) { - const { text, markdown, images } = await this._extractAll(); - - card.text = text; - card.markdown = markdown; - card.images = images; - } - - private async _selectCard(card: Card) { - AIProvider.slots.toggleChatCards.emit({ visible: false }); - - this._selectedCardId = card.id; - - switch (card.type) { - case CardType.Text: { - this.updateContext({ - quote: card.text, - markdown: card.markdown, - }); - break; - } - case CardType.Image: { - this.updateContext({ - images: [card.image], - }); - break; - } - case CardType.Block: { - this.updateContext({ - quote: card.text, - markdown: card.markdown, - images: card.images, - }); - break; - } - - case CardType.Doc: { - await this._handleDocSelection(card); - this.updateContext({ - quote: card.text, - markdown: card.markdown, - images: card.images, - }); - break; - } - } - } - - private async _extract() { - const text = await getSelectedTextContent(this.host, 'plain-text'); - const images = await getSelectedImagesAsBlobs(this.host); - const hasText = text.length > 0; - const hasImages = images.length > 0; - - if (hasText && !hasImages) { - const markdown = await getSelectedTextContent(this.host, 'markdown'); - - this._updateCards({ - id: Date.now(), - type: CardType.Text, - text, - markdown, - }); - } else if (!hasText && hasImages && images.length === 1) { - const [_, data] = this.host.command - .chain() - .tryAll(chain => [chain.getImageSelections()]) - .getSelectedBlocks({ - types: ['image'], - }) - .run(); - let caption = ''; - - if (data.currentImageSelections?.[0]) { - caption = - ( - this.host.doc.getBlock(data.currentImageSelections[0].blockId) - ?.model as ImageBlockModel - )?.caption ?? ''; - } - - this._updateCards({ - id: Date.now(), - type: CardType.Image, - image: images[0], - caption, - }); - } else { - const markdown = - (await getSelectedTextContent(this.host, 'markdown')).trim() || ''; - this._updateCards({ - id: Date.now(), - type: CardType.Block, - text, - markdown, - images, - }); - } - } - - private async _extractOnEdgeless(host: EditorHost) { - if (!isInsideEdgelessEditor(host)) return; - - const canvas = await selectedToCanvas(host); - if (!canvas) return; - - const blob: Blob | null = await new Promise(resolve => - canvas.toBlob(resolve) - ); - if (!blob) return; - - this._updateCards({ - id: Date.now(), - type: CardType.Image, - image: new File([blob], 'selected.png'), - }); - } - - private async _extractAll() { - const notes = this.host.doc - .getBlocksByFlavour('affine:note') - .filter( - note => - (note.model as NoteBlockModel).displayMode !== - NoteDisplayMode.EdgelessOnly - ) - .map(note => note.model as NoteBlockModel); - const selectedModels = notes.reduce((acc, note) => { - acc.push(...note.children); - return acc; - }, [] as BlockModel[]); - const text = await getTextContentFromBlockModels( - this.host, - selectedModels, - 'plain-text' - ); - const markdown = await getTextContentFromBlockModels( - this.host, - selectedModels, - 'markdown' - ); - const blobs = await Promise.all( - selectedModels.map(async s => { - if (s.flavour !== 'affine:image') return null; - const sourceId = (s as ImageBlockModel)?.sourceId; - if (!sourceId) return null; - const blob = await (sourceId - ? this.host.doc.blobSync.get(sourceId) - : null); - if (!blob) return null; - return new File([blob], sourceId); - }) ?? [] - ); - const images = blobs.filter((blob): blob is File => !!blob); - - return { - text, - markdown, - images, - }; - } - - private async _appendCardWithParams({ - host, - mode, - autoSelect, - }: AIChatParams) { - if (mode === 'edgeless') { - await this._extractOnEdgeless(host); - } else { - await this._extract(); - } - - if (!autoSelect) { - return; - } - - if (this.cards.length > 0) { - const card = this.cards[0]; - if (card.type === CardType.Doc) return; - - await this._selectCard(card); - } - } - - protected override willUpdate(changedProperties: PropertyValues) { - Promise.resolve() - .then(async () => { - if (changedProperties.has('temporaryParams') && this.temporaryParams) { - const params = this.temporaryParams; - await this._appendCardWithParams(params); - this.temporaryParams = null; - } - - if (changedProperties.has('host')) { - if (this._currentDocId === this.host.doc.id) return; - this._currentDocId = this.host.doc.id; - this.cards = []; - - const { text, images } = await this._extractAll(); - const hasText = text.length > 0; - const hasImages = images.length > 0; - - // Currently only supports checking on first load - if (hasText || hasImages) { - const card: CardBlock = { - id: Date.now(), - type: CardType.Doc, - }; - if (hasText) { - card.text = text; - } - if (hasImages) { - card.images = images; - } - - this.cards.push(card); - this.requestUpdate(); - } - } - }) - .catch(err => { - captureException(err, { - level: 'fatal', - }); - }); - } - - override connectedCallback() { - super.connectedCallback(); - - this._disposables.add( - AIProvider.slots.requestOpenWithChat.on(async params => { - if (params.appendCard) { - await this._appendCardWithParams(params); - } - }) - ); - - this._disposables.add( - AIProvider.slots.toggleChatCards.on(({ visible, ok }) => { - if (visible && ok && this._selectedCardId > 0) { - this.cards = this.cards.filter( - card => card.id !== this._selectedCardId - ); - this._selectedCardId = 0; - } - }) - ); - } - - protected override render() { - if (!this.isEmpty) return nothing; - - return repeat( - this.cards, - card => card.id, - card => html` -
this._selectCard(card)}> - ${this._renderCard(card)} -
- ` - ); - } -} - -declare global { - interface HTMLElementTagNameMap { - 'chat-cards': ChatCards; - } -} diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-input.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-input.ts index e4d6ba2da1..38c46bd847 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-input.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-input.ts @@ -313,8 +313,6 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
{ - AIProvider.slots.toggleChatCards.emit({ visible: true }); - if (this.curIndex >= 0 && this.curIndex < images.length) { const newImages = [...images]; newImages.splice(this.curIndex, 1); @@ -356,7 +354,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) { if (this.host === host) { context && this.updateContext(context); await this.updateComplete; - input && (await this.send(input)); + await this.send(input); } } ) @@ -392,7 +390,6 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
{ - AIProvider.slots.toggleChatCards.emit({ visible: true }); this.updateContext({ quote: '', markdown: '' }); }} > diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts index 543c7e4def..9080bd779e 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts @@ -40,12 +40,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { height: 100%; position: relative; overflow-y: auto; - - chat-cards { - position: absolute; - bottom: 0; - width: 100%; - } } .chat-panel-messages-placeholder { @@ -138,9 +132,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { @query('.chat-panel-messages') accessor messagesContainer: HTMLDivElement | null = null; - @state() - accessor showChatCards = true; - private _renderAIOnboarding() { return this.isLoading || !this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding') @@ -251,12 +242,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
`; } )} -
${this.showDownIndicator ? html`
@@ -289,11 +274,6 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) { } }) ); - disposables.add( - AIProvider.slots.toggleChatCards.on(({ visible }) => { - this.showChatCards = visible; - }) - ); disposables.add( this.host.selection.slots.changed.on(() => { this._selectionValue = this.host.selection.value; diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts index ef46910a8d..c9676b15e4 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts @@ -12,6 +12,7 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import { AIHelpIcon, SmallHintIcon } from '../_common/icons'; import { AIProvider } from '../provider'; +import { extractSelectedContent } from '../utils/extract'; import { getSelectedImagesAsBlobs, getSelectedTextContent, @@ -222,30 +223,34 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { super.connectedCallback(); if (!this.doc) throw new Error('doc is required'); - AIProvider.slots.actions.on(({ action, event }) => { - const { status } = this.chatContextValue; - - if ( - action !== 'chat' && - event === 'finished' && - (status === 'idle' || status === 'success') - ) { - this._resetItems(); - } - - if (action === 'chat' && event === 'finished') { - AIProvider.slots.toggleChatCards.emit({ - visible: true, - ok: status === 'success', - }); - } - }); - - AIProvider.slots.userInfo.on(userInfo => { - if (userInfo) { - this._resetItems(); - } - }); + this._disposables.add( + AIProvider.slots.actions.on(({ action, event }) => { + const { status } = this.chatContextValue; + if ( + action !== 'chat' && + event === 'finished' && + (status === 'idle' || status === 'success') + ) { + this._resetItems(); + } + }) + ); + this._disposables.add( + AIProvider.slots.userInfo.on(userInfo => { + if (userInfo) { + this._resetItems(); + } + }) + ); + this._disposables.add( + AIProvider.slots.requestOpenWithChat.on(async ({ host }) => { + if (this.host === host) { + const context = await extractSelectedContent(host); + if (!context) return; + this.updateContext(context); + } + }) + ); } updateContext = (context: Partial) => { diff --git a/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/actions-config.ts b/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/actions-config.ts index da7d03b3eb..609341b799 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/actions-config.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/actions-config.ts @@ -18,7 +18,6 @@ import { AIPresentationIcon, AIPresentationIconWithAnimation, AISearchIcon, - ChatWithAIIcon, CommentIcon, ExplainIcon, ImproveWritingIcon, @@ -110,21 +109,6 @@ const othersGroup: AIItemGroupConfig = { host, mode: 'edgeless', autoSelect: true, - appendCard: true, - }); - panel.hide(); - }, - }, - { - name: 'Open AI Chat', - icon: ChatWithAIIcon, - showWhen: () => true, - handler: host => { - const panel = getAIPanelWidget(host); - AIProvider.slots.requestOpenWithChat.emit({ - host, - mode: 'edgeless', - appendCard: true, }); panel.hide(); }, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/index.ts b/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/index.ts index 7c7840e229..9007e53c60 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/index.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/entries/edgeless/index.ts @@ -12,7 +12,7 @@ import { html } from 'lit'; import { AIProvider } from '../../provider'; import { getAIPanelWidget } from '../../utils/ai-widgets'; import { getEdgelessCopilotWidget } from '../../utils/edgeless'; -import { extractContext } from '../../utils/extract'; +import { extractSelectedContent } from '../../utils/extract'; import { edgelessAIGroups } from './actions-config'; noop(EdgelessCopilotToolbarEntry); @@ -48,8 +48,7 @@ export function setupEdgelessElementToolbarAIEntry( aiPanel.config.generateAnswer = ({ finish, input }) => { finish('success'); aiPanel.discard(); - AIProvider.slots.requestOpenWithChat.emit({ host: edgeless.host }); - extractContext(edgeless.host) + extractSelectedContent(edgeless.host) .then(context => { AIProvider.slots.requestSendWithChat.emit({ input, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/provider.ts b/packages/frontend/core/src/blocksuite/presets/ai/provider.ts index 536e50c995..8e7ca14bab 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/provider.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/provider.ts @@ -20,12 +20,11 @@ export interface AIChatParams { mode?: 'page' | 'edgeless'; // Auto select and append selection to input via `Continue with AI` action. autoSelect?: boolean; - appendCard?: boolean; } export interface AISendParams { host: EditorHost; - input?: string; + input: string; context?: Partial; } @@ -128,11 +127,6 @@ export class AIProvider { }>(), // downstream can emit this slot to notify ai presets that user info has been updated userInfo: new Slot(), - // add more if needed - toggleChatCards: new Slot<{ - visible: boolean; - ok?: boolean; - }>(), }; // track the history of triggered actions (in memory only) diff --git a/packages/frontend/core/src/blocksuite/presets/ai/utils/extract.ts b/packages/frontend/core/src/blocksuite/presets/ai/utils/extract.ts index 8f5e18d116..1108cc334a 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/utils/extract.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/utils/extract.ts @@ -1,29 +1,35 @@ import type { EditorHost } from '@blocksuite/affine/block-std'; import { DocModeProvider, + type ImageBlockModel, isInsideEdgelessEditor, + type NoteBlockModel, + NoteDisplayMode, } from '@blocksuite/affine/blocks'; +import type { BlockModel } from '@blocksuite/affine/store'; import type { ChatContextValue } from '../chat-panel/chat-context'; import { + allToCanvas, getSelectedImagesAsBlobs, getSelectedTextContent, + getTextContentFromBlockModels, selectedToCanvas, } from './selection-utils'; -export async function extractContext( +export async function extractSelectedContent( host: EditorHost ): Promise | null> { const docModeService = host.std.get(DocModeProvider); const mode = docModeService.getEditorMode() || 'page'; if (mode === 'edgeless') { - return await extractOnEdgeless(host); + return await extractEdgelessSelected(host); } else { - return await extractOnPage(host); + return await extractPageSelected(host); } } -export async function extractOnEdgeless( +async function extractEdgelessSelected( host: EditorHost ): Promise | null> { if (!isInsideEdgelessEditor(host)) return null; @@ -41,7 +47,7 @@ export async function extractOnEdgeless( }; } -export async function extractOnPage( +async function extractPageSelected( host: EditorHost ): Promise | null> { const text = await getSelectedTextContent(host, 'plain-text'); @@ -76,3 +82,77 @@ export async function extractOnPage( }; } } + +export async function extractAllContent( + host: EditorHost +): Promise | null> { + const docModeService = host.std.get(DocModeProvider); + const mode = docModeService.getEditorMode() || 'page'; + if (mode === 'edgeless') { + return await extractEdgelessAll(host); + } else { + return await extractPageAll(host); + } +} + +async function extractEdgelessAll( + host: EditorHost +): Promise | null> { + if (!isInsideEdgelessEditor(host)) return null; + + const canvas = await allToCanvas(host); + if (!canvas) return null; + + const blob: Blob | null = await new Promise(resolve => + canvas.toBlob(resolve) + ); + if (!blob) return null; + + return { + images: [new File([blob], `${host.doc.id}.png`)], + }; +} + +async function extractPageAll( + host: EditorHost +): Promise | null> { + const notes = host.doc + .getBlocksByFlavour('affine:note') + .filter( + note => + (note.model as NoteBlockModel).displayMode !== + NoteDisplayMode.EdgelessOnly + ) + .map(note => note.model as NoteBlockModel); + const blockModels = notes.reduce((acc, note) => { + acc.push(...note.children); + return acc; + }, [] as BlockModel[]); + const text = await getTextContentFromBlockModels( + host, + blockModels, + 'plain-text' + ); + const markdown = await getTextContentFromBlockModels( + host, + blockModels, + 'markdown' + ); + const blobs = await Promise.all( + blockModels.map(async s => { + if (s.flavour !== 'affine:image') return null; + const sourceId = (s as ImageBlockModel)?.sourceId; + if (!sourceId) return null; + const blob = await (sourceId ? host.doc.blobSync.get(sourceId) : null); + if (!blob) return null; + return new File([blob], sourceId); + }) ?? [] + ); + const images = blobs.filter((blob): blob is File => !!blob); + + return { + quote: text, + markdown, + images, + }; +} diff --git a/packages/frontend/core/src/blocksuite/presets/ai/utils/selection-utils.ts b/packages/frontend/core/src/blocksuite/presets/ai/utils/selection-utils.ts index 4a227cbb86..660991d95e 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/utils/selection-utils.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/utils/selection-utils.ts @@ -14,6 +14,7 @@ import { Slice, toDraftModel, } from '@blocksuite/affine/store'; +import type { GfxModel } from '@blocksuite/block-std/gfx'; import { getContentFromSlice } from '../../_common'; import { getEdgelessCopilotWidget, getService } from './edgeless'; @@ -39,11 +40,22 @@ export function getEdgelessService(editor: EditorHost) { throw new Error('Please open switch to edgeless mode'); } -export async function selectedToCanvas(editor: EditorHost) { - const edgelessRoot = getEdgelessRootFromEditor(editor); - const { notes, frames, shapes, images } = BlocksUtils.splitElements( +export async function selectedToCanvas(host: EditorHost) { + const edgelessRoot = getEdgelessRootFromEditor(host); + return elementsToCanvas( + host, edgelessRoot.service.selection.selectedElements ); +} + +export async function allToCanvas(host: EditorHost) { + const edgelessRoot = getEdgelessRootFromEditor(host); + return elementsToCanvas(host, edgelessRoot.gfx.gfxElements); +} + +export async function elementsToCanvas(host: EditorHost, elements: GfxModel[]) { + const edgelessRoot = getEdgelessRootFromEditor(host); + const { notes, frames, shapes, images } = BlocksUtils.splitElements(elements); if (notes.length + frames.length + images.length + shapes.length === 0) { return; } diff --git a/packages/frontend/core/src/blocksuite/presets/effects.ts b/packages/frontend/core/src/blocksuite/presets/effects.ts index 31aefc0978..4b35112efb 100644 --- a/packages/frontend/core/src/blocksuite/presets/effects.ts +++ b/packages/frontend/core/src/blocksuite/presets/effects.ts @@ -15,7 +15,6 @@ import { ActionMindmap } from './ai/chat-panel/actions/mindmap'; import { ActionSlides } from './ai/chat-panel/actions/slides'; import { ActionText } from './ai/chat-panel/actions/text'; import { AILoading } from './ai/chat-panel/ai-loading'; -import { ChatCards } from './ai/chat-panel/chat-cards'; import { ChatPanelInput } from './ai/chat-panel/chat-panel-input'; import { ChatPanelMessages } from './ai/chat-panel/chat-panel-messages'; import { AIErrorWrapper } from './ai/messages/error'; @@ -55,7 +54,6 @@ export function registerBlocksuitePresetsCustomComponents() { customElements.define('action-slides', ActionSlides); customElements.define('action-text', ActionText); customElements.define('ai-loading', AILoading); - customElements.define('chat-cards', ChatCards); customElements.define('chat-panel-input', ChatPanelInput); customElements.define('chat-panel-messages', ChatPanelMessages); customElements.define('chat-panel', ChatPanel); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx index 7934f82ceb..f6cb635abf 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx @@ -17,7 +17,10 @@ import { ViewService } from '@affine/core/modules/workbench'; import { WorkspaceService } from '@affine/core/modules/workspace'; import { isNewTabTrigger } from '@affine/core/utils'; import { RefNodeSlotsProvider } from '@blocksuite/affine/blocks'; -import { DisposableGroup } from '@blocksuite/affine/global/utils'; +import { + type Disposable, + DisposableGroup, +} from '@blocksuite/affine/global/utils'; import { type AffineEditorContainer } from '@blocksuite/affine/presets'; import { AiIcon, @@ -111,16 +114,14 @@ const DetailPageImpl = memo(function DetailPageImpl() { }, [editorContainer, isActiveView, setActiveBlockSuiteEditor]); useEffect(() => { - const disposable = AIProvider.slots.requestOpenWithChat.on(params => { + const disposables: Disposable[] = []; + const openHandler = () => { workbench.openSidebar(); view.activeSidebarTab('chat'); - - if (chatPanelRef.current) { - const chatCards = chatPanelRef.current.querySelector('chat-cards'); - if (chatCards) chatCards.temporaryParams = params; - } - }); - return () => disposable.dispose(); + }; + disposables.push(AIProvider.slots.requestOpenWithChat.on(openHandler)); + disposables.push(AIProvider.slots.requestSendWithChat.on(openHandler)); + return () => disposables.forEach(d => d.dispose()); }, [activeSidebarTab, view, workbench]); useEffect(() => { diff --git a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx index b4d23bf32b..596d844fc1 100644 --- a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx @@ -11,7 +11,11 @@ import { type EdgelessRootService, RefNodeSlotsProvider, } from '@blocksuite/affine/blocks'; -import { Bound, DisposableGroup } from '@blocksuite/affine/global/utils'; +import { + Bound, + type Disposable, + DisposableGroup, +} from '@blocksuite/affine/global/utils'; import type { AffineEditorContainer } from '@blocksuite/affine/presets'; import { FrameworkScope, @@ -123,17 +127,17 @@ function DocPeekPreviewEditor({ ); useEffect(() => { - const disposable = AIProvider.slots.requestOpenWithChat.on(() => { + const disposables: Disposable[] = []; + const openHandler = () => { if (doc) { workbench.openDoc(doc.id); peekView.close(); // chat panel open is already handled in } - }); - - return () => { - disposable.dispose(); }; + disposables.push(AIProvider.slots.requestOpenWithChat.on(openHandler)); + disposables.push(AIProvider.slots.requestSendWithChat.on(openHandler)); + return () => disposables.forEach(d => d.dispose()); }, [doc, peekView, workbench, workspace.id]); const openOutlinePanel = useCallback(() => { diff --git a/tests/affine-cloud-copilot/e2e/copilot.spec.ts b/tests/affine-cloud-copilot/e2e/copilot.spec.ts index 2a3402e0b6..727551b217 100644 --- a/tests/affine-cloud-copilot/e2e/copilot.spec.ts +++ b/tests/affine-cloud-copilot/e2e/copilot.spec.ts @@ -599,20 +599,6 @@ test.describe('chat with block', () => { .waitForSelector('chat-panel-input .chat-panel-images') .then(el => el.waitForElementState('visible')); }); - - test('open ai chat', async ({ page }) => { - await page - .waitForSelector('.ai-item-open-ai-chat') - .then(i => i.click()); - const cards = await page.waitForSelector('chat-panel chat-cards'); - await cards.waitForElementState('visible'); - const cardTitles = await Promise.all( - await cards - .$$('.card-wrapper .card-title') - .then(els => els.map(async el => await el.innerText())) - ); - expect(cardTitles).toContain('Start with this Image'); - }); }); // TODO(@darkskygit): block by BS-1709, enable this after bug fix