From 2f9a96f1c5624ef7bd2a72113afc1c685be08b86 Mon Sep 17 00:00:00 2001 From: Wu Yue Date: Fri, 4 Jul 2025 19:00:52 +0800 Subject: [PATCH] feat(core): support open doc in ai session history (#13035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [AI-240 截屏2025-07-04 18 04 39 ](https://linear.app/affine-design/issue/AI-240) ## Summary by CodeRabbit * **New Features** * Enhanced chat toolbar and session history with the ability to open specific documents directly from the chat interface. * Added tooltips and improved click handling for clearer user interactions in chat session and document lists. * **Bug Fixes** * Prevented redundant actions when attempting to open already active sessions or documents. * **Style** * Improved tooltip formatting and visual styling for error messages and tooltips. * Refined hover effects and layout in chat session history for better clarity. * **Refactor** * Updated tooltip configuration for more precise positioning and behavior. * **Chores** * Minor updates to property defaults for tooltips and chat panel components. Co-authored-by: fengmk2 --- .../affine/components/src/toolbar/tooltip.ts | 12 +++- .../src/blocksuite/ai/chat-panel/index.ts | 69 +++++++++++++++++-- .../ai-chat-toolbar/ai-chat-toolbar.ts | 25 ++++++- .../ai-chat-toolbar/ai-session-history.ts | 42 ++++++++--- .../core/src/blocksuite/ai/messages/error.ts | 6 +- .../ai-panel/components/state/error.ts | 13 ++-- .../ai-panel/components/state/input.ts | 8 +-- .../pages/workspace/detail-page/tabs/chat.tsx | 2 + 8 files changed, 145 insertions(+), 32 deletions(-) diff --git a/blocksuite/affine/components/src/toolbar/tooltip.ts b/blocksuite/affine/components/src/toolbar/tooltip.ts index 99aad4dd5..fffe48f22 100644 --- a/blocksuite/affine/components/src/toolbar/tooltip.ts +++ b/blocksuite/affine/components/src/toolbar/tooltip.ts @@ -190,7 +190,10 @@ export class Tooltip extends LitElement { middleware: [ this.autoFlip && flip({ padding: AUTO_FLIP_PADDING }), this.autoShift && shift({ padding: AUTO_SHIFT_PADDING }), - offset((this.arrow ? TRIANGLE_HEIGHT : 0) + this.offset), + offset({ + mainAxis: (this.arrow ? TRIANGLE_HEIGHT : 0) + this.offsetY, + crossAxis: this.offsetX, + }), arrow({ element: portalRoot.shadowRoot!.querySelector('.arrow')!, }), @@ -264,7 +267,7 @@ export class Tooltip extends LitElement { * Show a triangle arrow pointing to the reference element. */ @property({ attribute: false }) - accessor arrow = true; + accessor arrow = false; /** * changes the placement of the floating element in order to keep it in view, @@ -303,7 +306,10 @@ export class Tooltip extends LitElement { * See https://floating-ui.com/docs/offset */ @property({ attribute: false }) - accessor offset = 4; + accessor offsetY = 6; + + @property({ attribute: false }) + accessor offsetX = 0; @property({ attribute: 'tip-position' }) accessor placement: Placement = 'top'; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts index 7b1d4867e..5797aad91 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -1,5 +1,6 @@ import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import type { WorkbenchService } from '@affine/core/modules/workbench'; import type { ContextEmbedStatus, CopilotSessionType, @@ -98,6 +99,9 @@ export class ChatPanel extends SignalWatcher( @property({ attribute: false }) accessor affineWorkspaceDialogService!: WorkspaceDialogService; + @property({ attribute: false }) + accessor affineWorkbenchService!: WorkbenchService; + @state() accessor session: CopilotSessionType | null | undefined; @@ -137,34 +141,72 @@ export class ChatPanel extends SignalWatcher( `; } + private readonly getSessionIdFromUrl = () => { + if (this.affineWorkbenchService) { + const { workbench } = this.affineWorkbenchService; + const location = workbench.location$.value; + const searchParams = new URLSearchParams(location.search); + const sessionId = searchParams.get('sessionId'); + if (sessionId) { + workbench.activeView$.value.updateQueryString( + { sessionId: undefined }, + { replace: true } + ); + } + return sessionId; + } + return undefined; + }; + + private readonly setSession = ( + session: CopilotSessionType | null | undefined + ) => { + this.session = session ?? null; + }; + private readonly initSession = async () => { if (!AIProvider.session) { return; } + const sessionId = this.getSessionIdFromUrl(); const pinSessions = await AIProvider.session.getSessions( this.doc.workspace.id, undefined, { pinned: true, limit: 1 } ); + if (Array.isArray(pinSessions) && pinSessions[0]) { + // pinned session this.session = pinSessions[0]; + } else if (sessionId) { + // sessionId from url + const session = await AIProvider.session.getSession( + this.doc.workspace.id, + sessionId + ); + this.setSession(session); } else { + // latest doc session const docSessions = await AIProvider.session.getSessions( this.doc.workspace.id, this.doc.id, { action: false, fork: false, limit: 1 } ); + // sessions is descending ordered by updatedAt // the first item is the latest session - this.session = docSessions?.[0] ?? null; + const session = docSessions?.[0]; + this.setSession(session); } }; @@ -186,7 +228,7 @@ export class ChatPanel extends SignalWatcher( this.doc.workspace.id, sessionId ); - this.session = session ?? null; + this.setSession(session); } return this.session; }; @@ -197,7 +239,7 @@ export class ChatPanel extends SignalWatcher( this.doc.workspace.id, options.sessionId ); - this.session = session ?? null; + this.setSession(session); }; private readonly newSession = () => { @@ -208,12 +250,31 @@ export class ChatPanel extends SignalWatcher( }; private readonly openSession = async (sessionId: string) => { + if (this.session?.id === sessionId) { + return; + } this.resetPanel(); const session = await AIProvider.session?.getSession( this.doc.workspace.id, sessionId ); - this.session = session ?? null; + this.setSession(session); + }; + + private readonly openDoc = async (docId: string, sessionId: string) => { + if (this.doc.id === docId) { + if (this.session?.id === sessionId || this.session?.pinned) { + return; + } + await this.openSession(sessionId); + } else if (this.affineWorkbenchService) { + const { workbench } = this.affineWorkbenchService; + if (this.session?.pinned) { + workbench.open(`/${docId}`, { at: 'active' }); + } else { + workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' }); + } + } }; private readonly togglePin = async () => { diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts index 64edff5fd..fed17e468 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts @@ -23,6 +23,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor workspaceId!: string; + @property({ attribute: false }) + accessor docId: string | undefined; + @property({ attribute: false }) accessor onNewSession!: () => void; @@ -32,6 +35,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor onOpenSession!: (sessionId: string) => void; + @property({ attribute: false }) + accessor onOpenDoc!: (docId: string, sessionId: string) => void; + @property({ attribute: false }) accessor docDisplayConfig!: DocDisplayConfig; @@ -80,9 +86,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
${pinned ? PinedIcon() : PinIcon()} - ${pinned ? 'Unpin this Chat' : 'Pin this Chat'} + + ${pinned ? 'Unpin this Chat' : 'Pin this Chat'} +
{ + if (this.session?.id === sessionId) { + this.notification?.toast('You are already in this chat'); + return; + } const confirm = await this.unpinConfirm(); if (confirm) { this.onOpenSession(sessionId); } }; + private readonly onDocClick = async (docId: string, sessionId: string) => { + if (this.docId === docId && this.session?.id === sessionId) { + this.notification?.toast('You are already in this chat'); + return; + } + this.onOpenDoc(docId, sessionId); + }; + private readonly toggleHistoryMenu = () => { if (this.abortController) { this.abortController.abort(); @@ -150,6 +168,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { .workspaceId=${this.workspaceId} .docDisplayConfig=${this.docDisplayConfig} .onSessionClick=${this.onSessionClick} + .onDocClick=${this.onDocClick} .notification=${this.notification} > `, diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts index 8b1fb890b..6b22b41ed 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts @@ -45,17 +45,22 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { } .ai-session-item { + position: relative; display: flex; height: 24px; padding: 2px 4px; justify-content: space-between; align-items: center; + border-radius: 4px; cursor: pointer; } - .ai-session-item:hover { + .ai-session-item:hover:not(:has(.ai-session-doc:hover)) { + background: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; + } + + .ai-session-doc:hover { background: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; - border-color: ${unsafeCSSVarV2('layer/insideBorder/border')}; } .ai-session-title { @@ -75,6 +80,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { align-items: center; gap: 4px; flex-shrink: 0; + border-radius: 2px; cursor: pointer; svg { @@ -110,6 +116,9 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor onSessionClick!: (sessionId: string) => void; + @property({ attribute: false }) + accessor onDocClick!: (docId: string, sessionId: string) => void; + @property({ attribute: false }) accessor notification: NotificationService | null | undefined; @@ -194,10 +203,20 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { return html`
this.onSessionClick(session.sessionId)} + @click=${(e: MouseEvent) => { + e.stopPropagation(); + this.onSessionClick(session.sessionId); + }} > -
${session.sessionId}
- ${session.docId ? this.renderSessionDoc(session.docId) : nothing} +
+ ${session.sessionId} + + Click to open this chat + +
+ ${session.docId + ? this.renderSessionDoc(session.docId, session.sessionId) + : nothing}
`; })} @@ -205,12 +224,19 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { `; } - private renderSessionDoc(docId: string) { + private renderSessionDoc(docId: string, sessionId: string) { const getIcon = this.docDisplayConfig.getIcon(docId); const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon; - return html`
+ return html`
{ + e.stopPropagation(); + this.onDocClick(docId, sessionId); + }} + > ${docIcon} - ${this.docDisplayConfig.getTitle(docId)} + ${this.docDisplayConfig.getTitle(docId)} + Open this doc
`; } diff --git a/packages/frontend/core/src/blocksuite/ai/messages/error.ts b/packages/frontend/core/src/blocksuite/ai/messages/error.ts index 4b7190493..8e9c2fb91 100644 --- a/packages/frontend/core/src/blocksuite/ai/messages/error.ts +++ b/packages/frontend/core/src/blocksuite/ai/messages/error.ts @@ -155,9 +155,9 @@ export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) { > ${this.actionText} ${this.actionTooltip - ? html`${this.actionTooltip}` + ? html` + ${this.actionTooltip} + ` : nothing}
diff --git a/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/components/state/error.ts b/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/components/state/error.ts index 60a9dc0de..84d0c2710 100644 --- a/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/components/state/error.ts +++ b/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/components/state/error.ts @@ -182,13 +182,12 @@ export class AIPanelError extends WithDisposable(LitElement) { () => { const tip = this.config.error?.message; const error = tip - ? html`An error occurred${tip}` + ? html` + An error occurred + + ${tip} + + ` : 'An error occurred'; return html`