From a92515b5aa5089c62c34e9382838fd4668dcff87 Mon Sep 17 00:00:00 2001 From: fundon Date: Tue, 25 Jun 2024 15:56:44 +0000 Subject: [PATCH] fix(core): ai chat opening and append card (#7322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [BS-642](https://linear.app/affine-design/issue/BS-642/continue-with-ai-:-侧边栏关闭的情况下,先唤起-侧边栏,同时插入选中的内容) https://github.com/toeverything/AFFiNE/assets/27926/3022d808-d560-4bc8-8a04-735b76056121 --- .../blocksuite/presets/ai/_common/config.ts | 9 +-- .../presets/ai/actions/edgeless-response.ts | 6 +- .../src/blocksuite/presets/ai/ai-panel.ts | 6 +- .../presets/ai/chat-panel/chat-cards.ts | 70 +++++++++++-------- .../ai/entries/edgeless/actions-config.ts | 10 +-- .../src/blocksuite/presets/ai/provider.ts | 32 +++++++-- .../multi-tabs/sidebar-tab.ts | 1 + .../multi-tabs/tabs/chat.tsx | 12 +++- .../workspace/detail-page/detail-page.tsx | 33 +++++++-- 9 files changed, 114 insertions(+), 65 deletions(-) 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 c98d3b6887..2a5a9bb192 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/_common/config.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/_common/config.ts @@ -401,8 +401,7 @@ const OthersAIGroup: AIItemGroupConfig = { icon: CommentIcon, handler: host => { const panel = getAIPanel(host); - AIProvider.slots.requestOpenWithChat.emit(); - AIProvider.slots.requestContinueWithAIInChat.emit({ host }); + AIProvider.slots.requestOpenWithChat.emit({ host, autoSelect: true }); panel.hide(); }, }, @@ -411,11 +410,7 @@ const OthersAIGroup: AIItemGroupConfig = { icon: ChatWithAIIcon, handler: host => { const panel = getAIPanel(host); - AIProvider.slots.requestOpenWithChat.emit(); - AIProvider.slots.requestContinueInChat.emit({ - host: host, - show: true, - }); + AIProvider.slots.requestOpenWithChat.emit({ host }); 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 61d426a751..1e7011b88b 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 @@ -463,11 +463,7 @@ export function actionToResponse( handler: () => { reportResponse('result:continue-in-chat'); const panel = getAIPanel(host); - AIProvider.slots.requestOpenWithChat.emit(); - AIProvider.slots.requestContinueInChat.emit({ - host: host, - show: 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 d41e084a16..ef013e98b7 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/ai-panel.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/ai-panel.ts @@ -233,11 +233,7 @@ export function buildTextResponseConfig< icon: ChatWithAIIcon, handler: () => { reportResponse('result:continue-in-chat'); - AIProvider.slots.requestOpenWithChat.emit(); - AIProvider.slots.requestContinueInChat.emit({ - host: panel.host, - show: 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 index c8d671b96e..f3440224ea 100644 --- 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 @@ -2,7 +2,6 @@ import type { EditorHost } from '@blocksuite/block-std'; import { WithDisposable } from '@blocksuite/block-std'; import { type ImageBlockModel, - isInsidePageEditor, type NoteBlockModel, NoteDisplayMode, } from '@blocksuite/blocks'; @@ -24,7 +23,7 @@ import { DocIcon, SmallImageIcon, } from '../_common/icons'; -import { AIProvider } from '../provider'; +import { type AIChatParams, AIProvider } from '../provider'; import { getSelectedImagesAsBlobs, getSelectedTextContent, @@ -112,6 +111,9 @@ export class ChatCards extends WithDisposable(LitElement) { @property({ attribute: false }) accessor updateContext!: (context: Partial) => void; + @property({ attribute: false }) + accessor temporaryParams: AIChatParams | null = null; + @state() accessor cards: Card[] = []; @@ -421,7 +423,36 @@ export class ChatCards extends WithDisposable(LitElement) { }; } - protected override async updated(changedProperties: PropertyValues) { + private readonly _appendCardWithParams = async ({ + // host: _, + mode, + autoSelect, + }: AIChatParams) => { + if (mode === 'edgeless') { + await this._extractOnEdgeless(); + } 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 async willUpdate(changedProperties: PropertyValues) { + 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; @@ -443,40 +474,19 @@ export class ChatCards extends WithDisposable(LitElement) { if (hasImages) { card.images = images; } - this._updateCards(card); + + this.cards.push(card); + this.requestUpdate(); } } } - override async connectedCallback() { + override connectedCallback() { super.connectedCallback(); this._disposables.add( - AIProvider.slots.requestContinueWithAIInChat.on(async ({ mode }) => { - if (mode === 'edgeless') { - await this._extractOnEdgeless(); - } else { - await this._extract(); - } - - if (this.cards.length > 0) { - const card = this.cards[0]; - if (card.type === CardType.Doc) return; - - await this._selectCard(card); - } - }) - ); - - this._disposables.add( - AIProvider.slots.requestContinueInChat.on(async ({ host, show }) => { - if (show) { - if (isInsidePageEditor(host)) { - await this._extract(); - } else { - await this._extractOnEdgeless(); - } - } + AIProvider.slots.requestOpenWithChat.on(async params => { + await this._appendCardWithParams(params); }) ); 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 df26d451ab..49afb26cda 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 @@ -105,10 +105,10 @@ const othersGroup: AIItemGroupConfig = { showWhen: () => true, handler: host => { const panel = getAIPanel(host); - AIProvider.slots.requestOpenWithChat.emit(); - AIProvider.slots.requestContinueWithAIInChat.emit({ + AIProvider.slots.requestOpenWithChat.emit({ host, mode: 'edgeless', + autoSelect: true, }); panel.hide(); }, @@ -119,11 +119,7 @@ const othersGroup: AIItemGroupConfig = { showWhen: () => true, handler: host => { const panel = getAIPanel(host); - AIProvider.slots.requestOpenWithChat.emit(); - AIProvider.slots.requestContinueInChat.emit({ - host: host, - show: true, - }); + AIProvider.slots.requestOpenWithChat.emit({ host, mode: 'edgeless' }); panel.hide(); }, }, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/provider.ts b/packages/frontend/core/src/blocksuite/presets/ai/provider.ts index a120332d91..6fe8f6e085 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/provider.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/provider.ts @@ -2,6 +2,8 @@ import type { EditorHost } from '@blocksuite/block-std'; import { PaymentRequiredError, UnauthorizedError } from '@blocksuite/blocks'; import { Slot } from '@blocksuite/store'; +import type { ChatCards } from './chat-panel/chat-cards'; + export interface AIUserInfo { id: string; email: string; @@ -9,6 +11,17 @@ export interface AIUserInfo { avatarUrl: string | null; } +export interface AIChatParams { + host: EditorHost; + mode?: 'page' | 'edgeless'; + // Auto select and append selection to input via `Continue with AI` action. + autoSelect?: boolean; +} + +type RequestChatCardsElement = ( + chatPanel: HTMLElement +) => Promise; + export type ActionEventType = | 'started' | 'finished' @@ -63,6 +76,10 @@ export class AIProvider { return AIProvider.instance.toggleGeneralAIOnboarding; } + static get requestChatCardsElement() { + return AIProvider.instance.requestChatCardsElement; + } + private static readonly instance = new AIProvider(); static LAST_ACTION_SESSIONID = ''; @@ -77,15 +94,18 @@ export class AIProvider { private toggleGeneralAIOnboarding: ((value: boolean) => void) | null = null; + private readonly requestChatCardsElement: RequestChatCardsElement = ( + chatPanel: HTMLElement + ) => { + return new Promise(resolve => { + resolve(chatPanel.querySelector('chat-cards')); + }); + }; + private readonly slots = { // use case: when user selects "continue in chat" in an ask ai result panel // do we need to pass the context to the chat panel? - requestOpenWithChat: new Slot(), - requestContinueInChat: new Slot<{ host: EditorHost; show: boolean }>(), - requestContinueWithAIInChat: new Slot<{ - host: EditorHost; - mode?: 'page' | 'edgeless'; - }>(), + requestOpenWithChat: new Slot(), requestLogin: new Slot<{ host: EditorHost }>(), requestUpgradePlan: new Slot<{ host: EditorHost }>(), // when an action is requested to run in edgeless mode (show a toast in affine) diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts b/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts index 8258a9d580..284ba74598 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/sidebar-tab.ts @@ -4,6 +4,7 @@ export type SidebarTabName = 'outline' | 'frame' | 'chat' | 'journal'; export interface SidebarTabProps { editor: AffineEditorContainer | null; + onLoad: ((component: HTMLElement) => void) | null; } export interface SidebarTab { diff --git a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx b/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx index d35a11f05b..9e8f38fc6c 100644 --- a/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx +++ b/packages/frontend/core/src/modules/multi-tab-sidebar/multi-tabs/tabs/chat.tsx @@ -7,7 +7,7 @@ import type { SidebarTab, SidebarTabProps } from '../sidebar-tab'; import * as styles from './chat.css'; // A wrapper for CopilotPanel -const EditorChatPanel = ({ editor }: SidebarTabProps) => { +const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => { const chatPanelRef = useRef(null); const onRefChange = useCallback((container: HTMLDivElement | null) => { @@ -17,6 +17,16 @@ const EditorChatPanel = ({ editor }: SidebarTabProps) => { } }, []); + useEffect(() => { + if (onLoad && chatPanelRef.current) { + (chatPanelRef.current as ChatPanel).updateComplete + .then(() => { + onLoad(chatPanelRef.current as HTMLElement); + }) + .catch(console.error); + } + }, [onLoad]); + useEffect(() => { if (!editor) return; const pageService = editor.host.spec.getService('affine:page'); diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 931bf2e550..6bd5c010ce 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -76,6 +76,9 @@ const DetailPageImpl = memo(function DetailPageImpl() { const docCollection = workspace.docCollection; const mode = useLiveData(doc.mode$); const { appSettings } = useAppSettingHelper(); + const [tabOnLoad, setTabOnLoad] = useState< + ((component: HTMLElement) => void) | null + >(null); const isActiveView = useIsActiveView(); // TODO(@eyhn): remove jotai here @@ -94,13 +97,34 @@ const DetailPageImpl = memo(function DetailPageImpl() { }, [editor, isActiveView, setActiveBlockSuiteEditor]); useEffect(() => { - AIProvider.slots.requestOpenWithChat.on(() => { - rightSidebar.open(); - if (activeTabName !== 'chat') { + AIProvider.slots.requestOpenWithChat.on(params => { + const opened = rightSidebar.isOpen$.value; + const actived = activeTabName === 'chat'; + + if (!opened) { + rightSidebar.open(); + } + + if (!actived) { setActiveTabName('chat'); } + + // Save chat parameters: + // * The right sidebar is not open + // * Chat panel is not activated + if (!opened || !actived) { + const callback = async (chatPanel: HTMLElement) => { + const chatCards = await AIProvider.requestChatCardsElement(chatPanel); + if (!chatCards) return; + if (chatCards.temporaryParams) return; + chatCards.temporaryParams = params; + }; + setTabOnLoad(() => callback); + } else { + setTabOnLoad(null); + } }); - }, [activeTabName, rightSidebar, setActiveTabName]); + }, [activeTabName, rightSidebar, setActiveTabName, setTabOnLoad]); useEffect(() => { if (isActiveView) { @@ -274,6 +298,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { sidebarTabs.find(ext => ext.name === activeTabName) ?? sidebarTabs[0] } + onLoad={tabOnLoad} > {/* Show switcher in body for windows desktop */} {isWindowsDesktop && (