From 24f1181069f6f4a7ffca4a245b7899380f022273 Mon Sep 17 00:00:00 2001 From: Wu Yue Date: Fri, 4 Jul 2025 14:51:35 +0800 Subject: [PATCH] feat(core): support ai recent session history (#13025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [AI-239](https://linear.app/affine-design/issue/AI-239) Close [AI-240](https://linear.app/affine-design/issue/AI-240) Close [AI-242](https://linear.app/affine-design/issue/AI-242) 截屏2025-07-04 13 49 25 ## Summary by CodeRabbit * **New Features** * Introduced a floating chat history menu, allowing users to view and switch between recent AI chat sessions grouped by recency. * Added a new component for displaying recent chat sessions with document icons and titles. * Enhanced chat toolbar with asynchronous confirmation dialogs before switching or creating sessions. * Added notification support for chat-related actions and history clearing. * Added ability to fetch and display recent AI chat sessions per workspace. * **Improvements** * Streamlined session management and event handling in the chat panel. * Improved embedding progress update and context change handling across chat components. * Refined UI for chat history, session switching, and notifications. * Updated chat components to use direct notification service injection for better user prompts and toasts. --- .../core/src/blocksuite/ai/actions/types.ts | 7 + .../src/blocksuite/ai/chat-panel/index.ts | 97 +++++--- .../ai-chat-composer/ai-chat-composer.ts | 4 +- .../ai-chat-content/ai-chat-content.ts | 15 +- .../ai-chat-toolbar/ai-chat-toolbar.ts | 112 ++++++++- .../ai-chat-toolbar/ai-session-history.ts | 232 ++++++++++++++++++ .../ai/components/ai-chat-toolbar/index.ts | 1 + .../ai-history-clear/ai-history-clear.ts | 14 +- .../ai/components/playground/chat.ts | 8 +- .../core/src/blocksuite/ai/effects.ts | 3 +- .../ai/peek-view/chat-block-peek-view.ts | 8 +- .../blocksuite/ai/provider/copilot-client.ts | 16 ++ .../blocksuite/ai/provider/setup-provider.tsx | 3 + .../desktop/pages/workspace/chat/index.tsx | 4 +- 14 files changed, 462 insertions(+), 62 deletions(-) create mode 100644 packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts index a2e5a110d0..87a2b6208e 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts @@ -5,6 +5,7 @@ import type { CopilotContextCategory, CopilotContextDoc, CopilotContextFile, + CopilotHistories, CopilotSessionType, getCopilotHistoriesQuery, QueryChatSessionsInput, @@ -387,6 +388,8 @@ declare global { reuseLatestChat?: boolean; } + type AIRecentSession = Omit; + interface AISessionService { createSession: (options: AICreateSessionOptions) => Promise; getSessions: ( @@ -394,6 +397,10 @@ declare global { docId?: string, options?: QueryChatSessionsInput ) => Promise; + getRecentSessions: ( + workspaceId: string, + limit?: number + ) => Promise; getSession: ( workspaceId: string, sessionId: string 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 e543ed430a..d7c0e4fdf5 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -6,6 +6,7 @@ import type { UpdateChatSessionInput, } from '@affine/graphql'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; +import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; @@ -27,6 +28,7 @@ import type { DocDisplayConfig, SearchMenuConfig, } from '../components/ai-chat-chips'; +import type { ChatContextValue } from '../components/ai-chat-content'; import type { AINetworkSearchConfig, AIPlaygroundConfig, @@ -125,6 +127,38 @@ export class ChatPanel extends SignalWatcher( return this.session !== undefined; } + private get chatTitle() { + const [done, total] = this.embeddingProgress; + const isEmbedding = total > 0 && done < total; + const notification = this.host.std.getOptional(NotificationProvider); + + return html` +
+ ${isEmbedding + ? html`Embedding ${done}/${total}` + : 'AFFiNE AI'} +
+ ${this.playgroundConfig.visible.value + ? html` +
+ ${CenterPeekIcon()} +
+ ` + : nothing} + + `; + } + private readonly initSession = async () => { if (!AIProvider.session) { return; @@ -132,7 +166,7 @@ export class ChatPanel extends SignalWatcher( const pinSessions = await AIProvider.session.getSessions( this.doc.workspace.id, undefined, - { pinned: true } + { pinned: true, limit: 1 } ); if (Array.isArray(pinSessions) && pinSessions[0]) { this.session = pinSessions[0]; @@ -140,7 +174,7 @@ export class ChatPanel extends SignalWatcher( const docSessions = await AIProvider.session.getSessions( this.doc.workspace.id, this.doc.id, - { action: false, fork: false } + { action: false, fork: false, limit: 1 } ); // the first item is the latest session this.session = docSessions?.[0] ?? null; @@ -186,6 +220,15 @@ export class ChatPanel extends SignalWatcher( }); }; + private readonly openSession = async (sessionId: string) => { + this.resetPanel(); + const session = await AIProvider.session?.getSession( + this.doc.workspace.id, + sessionId + ); + this.session = session ?? null; + }; + private readonly togglePin = async () => { const pinned = !this.session?.pinned; this.hasPinned = true; @@ -199,6 +242,18 @@ export class ChatPanel extends SignalWatcher( } }; + private readonly rebindSession = async () => { + if (!this.session) { + return; + } + if (this.session.docId !== this.doc.id) { + await this.updateSession({ + sessionId: this.session.id, + docId: this.doc.id, + }); + } + }; + private readonly initPanel = async () => { try { if (!this.isSidebarOpen.value) { @@ -218,13 +273,21 @@ export class ChatPanel extends SignalWatcher( this.hasPinned = false; }; - private readonly updateEmbeddingProgress = ( + private readonly onEmbeddingProgressChange = ( count: Record ) => { const total = count.finished + count.processing + count.failed; this.embeddingProgress = [count.finished, total]; }; + private readonly onContextChange = async ( + context: Partial + ) => { + if (context.status === 'success') { + await this.rebindSession(); + } + }; + private readonly openPlayground = () => { const playgroundContent = html` 540 ? '8px 24px 0 24px' : '8px 12px 0 12px', }); - const [done, total] = this.embeddingProgress; - const isEmbedding = total > 0 && done < total; - const title = html` -
- ${isEmbedding - ? html`Embedding ${done}/${total}` - : 'AFFiNE AI'} -
- ${this.playgroundConfig.visible.value - ? html` -
- ${CenterPeekIcon()} -
- ` - : nothing} - - `; const left = html`
${keyed( this.hasPinned ? this.session?.id : this.doc.id, html`` )} 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 e8ecd2013b..c4ec0a644c 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 @@ -75,7 +75,7 @@ export class AIChatComposer extends SignalWatcher( accessor updateContext!: (context: Partial) => void; @property({ attribute: false }) - accessor updateEmbeddingProgress!: ( + accessor onEmbeddingProgressChange!: ( count: Record ) => void; @@ -382,7 +382,7 @@ export class AIChatComposer extends SignalWatcher( return chip; }); this.updateChips(nextChips); - this.updateEmbeddingProgress(count); + this.onEmbeddingProgressChange(count); if (count.processing === 0) { this._abortPoll(); } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts index 94b051a983..9809ac9957 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts @@ -136,10 +136,13 @@ export class AIChatContent extends SignalWatcher( accessor affineWorkspaceDialogService!: WorkspaceDialogService; @property({ attribute: false }) - accessor updateEmbeddingProgress!: ( + accessor onEmbeddingProgressChange!: ( count: Record ) => void; + @property({ attribute: false }) + accessor onContextChange!: (context: Partial) => void; + @property({ attribute: false }) accessor width: Signal | undefined; @@ -177,14 +180,9 @@ export class AIChatContent extends SignalWatcher( } const sessionId = this.session?.id; - const pinned = this.session?.pinned; const [histories, actions] = await Promise.all([ sessionId - ? AIProvider.histories.chats( - this.workspaceId, - sessionId, - pinned ? undefined : this.docId - ) + ? AIProvider.histories.chats(this.workspaceId, sessionId) : Promise.resolve([]), this.docId ? AIProvider.histories.actions(this.workspaceId, this.docId) @@ -245,6 +243,7 @@ export class AIChatContent extends SignalWatcher( private readonly updateContext = (context: Partial) => { this.chatContextValue = { ...this.chatContextValue, ...context }; + this.onContextChange?.(context); }; private readonly scrollToEnd = () => { @@ -389,7 +388,7 @@ export class AIChatContent extends SignalWatcher( .createSession=${this.createSession} .chatContextValue=${this.chatContextValue} .updateContext=${this.updateContext} - .updateEmbeddingProgress=${this.updateEmbeddingProgress} + .onEmbeddingProgressChange=${this.onEmbeddingProgressChange} .networkSearchConfig=${this.networkSearchConfig} .reasoningConfig=${this.reasoningConfig} .docDisplayConfig=${this.docDisplayConfig} 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 edd2cb2e04..64edff5fd1 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 @@ -1,5 +1,7 @@ import type { CopilotSessionType } from '@affine/graphql'; +import { createLitPortal } from '@blocksuite/affine/components/portal'; import { WithDisposable } from '@blocksuite/affine/global/lit'; +import type { NotificationService } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { ShadowlessElement } from '@blocksuite/affine/std'; import { @@ -8,18 +10,38 @@ import { PinIcon, PlusIcon, } from '@blocksuite/icons/lit'; +import { flip, offset } from '@floating-ui/dom'; import { css, html } from 'lit'; -import { property } from 'lit/decorators.js'; +import { property, query } from 'lit/decorators.js'; + +import type { DocDisplayConfig } from '../ai-chat-chips'; export class AIChatToolbar extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor session!: CopilotSessionType | null | undefined; + @property({ attribute: false }) + accessor workspaceId!: string; + @property({ attribute: false }) accessor onNewSession!: () => void; @property({ attribute: false }) - accessor onTogglePin!: () => void; + accessor onTogglePin!: () => Promise; + + @property({ attribute: false }) + accessor onOpenSession!: (sessionId: string) => void; + + @property({ attribute: false }) + accessor docDisplayConfig!: DocDisplayConfig; + + @property({ attribute: false }) + accessor notification: NotificationService | null | undefined; + + @query('.history-button') + accessor historyButton!: HTMLDivElement; + + private abortController: AbortController | null = null; static override styles = css` .ai-chat-toolbar { @@ -49,24 +71,100 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { `; override render() { - const pined = this.session?.pinned; + const pinned = this.session?.pinned; return html`
-
+
${PlusIcon()} New Chat
- ${pined ? PinedIcon() : PinIcon()} + ${pinned ? PinedIcon() : PinIcon()} ${pined ? 'Unpin this Chat' : 'Pin this Chat'}${pinned ? 'Unpin this Chat' : 'Pin this Chat'}
-
+
${ArrowDownSmallIcon()} Chat History
`; } + + private readonly unpinConfirm = async () => { + if (this.session && this.session.pinned) { + try { + const confirm = this.notification + ? await this.notification.confirm({ + title: 'Switch Chat? Current chat is pinned', + message: + 'Switching will unpinned the current chat. This will change the active chat panel, allowing you to navigate between different conversation histories.', + confirmText: 'Switch Chat', + cancelText: 'Cancel', + }) + : true; + if (!confirm) { + return false; + } + await this.onTogglePin(); + } catch { + this.notification?.toast('Failed to unpin the chat'); + } + } + return true; + }; + + private readonly onPlusClick = async () => { + const confirm = await this.unpinConfirm(); + if (confirm) { + this.onNewSession(); + } + }; + + private readonly onSessionClick = async (sessionId: string) => { + const confirm = await this.unpinConfirm(); + if (confirm) { + this.onOpenSession(sessionId); + } + }; + + private readonly toggleHistoryMenu = () => { + if (this.abortController) { + this.abortController.abort(); + return; + } + + this.abortController = new AbortController(); + this.abortController.signal.addEventListener('abort', () => { + this.abortController = null; + }); + + createLitPortal({ + template: html` + + `, + portalStyles: { + zIndex: 'var(--affine-z-index-popover)', + }, + container: document.body, + computePosition: { + referenceElement: this.historyButton, + placement: 'bottom-end', + middleware: [offset({ crossAxis: 0, mainAxis: 5 }), flip()], + autoUpdate: { animationFrame: true }, + }, + abortController: this.abortController, + closeOnClickAway: true, + }); + }; } 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 new file mode 100644 index 0000000000..8b1fb890be --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts @@ -0,0 +1,232 @@ +import type { CopilotSessionType } from '@affine/graphql'; +import { WithDisposable } from '@blocksuite/affine/global/lit'; +import type { NotificationService } from '@blocksuite/affine/shared/services'; +import { scrollbarStyle } from '@blocksuite/affine/shared/styles'; +import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; +import { ShadowlessElement } from '@blocksuite/affine/std'; +import { css, html, nothing } from 'lit'; +import { property, state } from 'lit/decorators.js'; + +import { AIProvider } from '../../provider'; +import type { DocDisplayConfig } from '../ai-chat-chips'; + +interface GroupedSessions { + today: BlockSuitePresets.AIRecentSession[]; + last7Days: BlockSuitePresets.AIRecentSession[]; + last30Days: BlockSuitePresets.AIRecentSession[]; + older: BlockSuitePresets.AIRecentSession[]; +} + +export class AISessionHistory extends WithDisposable(ShadowlessElement) { + static override styles = css` + .ai-session-history { + width: 316px; + max-height: 344px; + padding: 12px 8px; + overflow-y: auto; + border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; + background: ${unsafeCSSVarV2('layer/background/primary')}; + border-radius: 4px; + background: ${unsafeCSSVarV2('layer/background/overlayPanel')}; + box-shadow: ${unsafeCSSVar('overlayPanelShadow')}; + + .ai-session-group { + display: flex; + flex-direction: column; + gap: 8px; + } + + .ai-session-group-title { + font-size: 12px; + font-weight: 400; + line-height: 20px; + height: 20px; + color: ${unsafeCSSVarV2('text/secondary')}; + } + + .ai-session-item { + display: flex; + height: 24px; + padding: 2px 4px; + justify-content: space-between; + align-items: center; + cursor: pointer; + } + + .ai-session-item:hover { + background: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; + border-color: ${unsafeCSSVarV2('layer/insideBorder/border')}; + } + + .ai-session-title { + font-size: 12px; + font-weight: 400; + line-height: 20px; + color: ${unsafeCSSVarV2('text/primary')}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .ai-session-doc { + display: flex; + width: 120px; + padding: 0px 4px; + align-items: center; + gap: 4px; + flex-shrink: 0; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + color: ${unsafeCSSVarV2('icon/primary')}; + } + + .doc-title { + font-size: 12px; + font-weight: 400; + line-height: 20px; + color: ${unsafeCSSVarV2('text/secondary')}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + ${scrollbarStyle('.ai-session-history')} + `; + + @property({ attribute: false }) + accessor session!: CopilotSessionType | null | undefined; + + @property({ attribute: false }) + accessor workspaceId!: string; + + @property({ attribute: false }) + accessor docDisplayConfig!: DocDisplayConfig; + + @property({ attribute: false }) + accessor onSessionClick!: (sessionId: string) => void; + + @property({ attribute: false }) + accessor notification: NotificationService | null | undefined; + + @state() + private accessor sessions: BlockSuitePresets.AIRecentSession[] = []; + + private groupSessionsByTime( + sessions: BlockSuitePresets.AIRecentSession[] + ): GroupedSessions { + const now = new Date(); + const todayStart = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + const last7DaysStart = new Date( + todayStart.getTime() - 6 * 24 * 60 * 60 * 1000 + ); + const last30DaysStart = new Date( + todayStart.getTime() - 29 * 24 * 60 * 60 * 1000 + ); + + const grouped: GroupedSessions = { + today: [], + last7Days: [], + last30Days: [], + older: [], + }; + + sessions.forEach(session => { + const updatedAt = new Date(session.updatedAt); + + if (updatedAt >= todayStart) { + grouped.today.push(session); + } else if (updatedAt >= last7DaysStart) { + grouped.last7Days.push(session); + } else if (updatedAt >= last30DaysStart) { + grouped.last30Days.push(session); + } else { + grouped.older.push(session); + } + }); + + // Sort each group by updatedAt in descending order (newest first) + (Object.keys(grouped) as Array).forEach(key => { + grouped[key].sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + }); + + return grouped; + } + + private async getRecentSessions() { + const limit = 50; + const sessions = await AIProvider.session?.getRecentSessions( + this.workspaceId, + limit + ); + if (sessions) { + this.sessions = sessions; + } + } + + override connectedCallback() { + super.connectedCallback(); + this.getRecentSessions().catch(console.error); + } + + private renderSessionGroup( + title: string, + sessions: BlockSuitePresets.AIRecentSession[] + ) { + if (sessions.length === 0) { + return nothing; + } + return html` +
+
${title}
+ ${sessions.map(session => { + return html` +
this.onSessionClick(session.sessionId)} + > +
${session.sessionId}
+ ${session.docId ? this.renderSessionDoc(session.docId) : nothing} +
+ `; + })} +
+ `; + } + + private renderSessionDoc(docId: string) { + const getIcon = this.docDisplayConfig.getIcon(docId); + const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon; + return html`
+ ${docIcon} + ${this.docDisplayConfig.getTitle(docId)} +
`; + } + + override render() { + if (this.sessions.length === 0) { + return nothing; + } + const groupedSessions = this.groupSessionsByTime(this.sessions); + + return html` +
+ ${this.renderSessionGroup('Today', groupedSessions.today)} + ${this.renderSessionGroup('Last 7 days', groupedSessions.last7Days)} + ${this.renderSessionGroup('Last 30 days', groupedSessions.last30Days)} + ${this.renderSessionGroup('Older', groupedSessions.older)} +
+ `; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/index.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/index.ts index 794632fb26..8576112c2f 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/index.ts @@ -1 +1,2 @@ export * from './ai-chat-toolbar'; +export * from './ai-session-history'; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts index fb05a6f7e3..c36aa294b8 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts @@ -1,8 +1,7 @@ import type { CopilotSessionType } from '@affine/graphql'; import { WithDisposable } from '@blocksuite/affine/global/lit'; -import { NotificationProvider } from '@blocksuite/affine/shared/services'; +import { type NotificationService } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; -import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; import type { Store } from '@blocksuite/affine/store'; import { css, html } from 'lit'; @@ -19,7 +18,7 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) { accessor session!: CopilotSessionType | null | undefined; @property({ attribute: false }) - accessor host: EditorHost | null | undefined; + accessor notification: NotificationService | null | undefined; @property({ attribute: false }) accessor doc!: Store; @@ -52,10 +51,9 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) { return; } const sessionId = this.session.id; - const notification = this.host?.std.getOptional(NotificationProvider); try { - const confirm = notification - ? await notification.confirm({ + const confirm = this.notification + ? await this.notification.confirm({ title: 'Clear History', message: 'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.', @@ -73,11 +71,11 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) { this.doc.id, [...(sessionId ? [sessionId] : []), ...(actionIds || [])] ); - notification?.toast('History cleared'); + this.notification?.toast('History cleared'); this.onHistoryCleared?.(); } } catch { - notification?.toast('Failed to clear history'); + this.notification?.toast('Failed to clear history'); } }; diff --git a/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts b/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts index c6b5730167..e0a68ee269 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts @@ -1,6 +1,7 @@ import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; +import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; @@ -229,7 +230,7 @@ export class PlaygroundChat extends SignalWatcher( this._scrollToEnd(); }; - private readonly _updateEmbeddingProgress = ( + private readonly onEmbeddingProgressChange = ( count: Record ) => { const total = count.finished + count.processing + count.failed; @@ -272,6 +273,7 @@ export class PlaygroundChat extends SignalWatcher( override render() { const [done, total] = this.embeddingProgress; const isEmbedding = total > 0 && done < total; + const notification = this.host.std.getOptional(NotificationProvider); return html`
@@ -287,9 +289,9 @@ export class PlaygroundChat extends SignalWatcher( Add chat
@@ -318,7 +320,7 @@ export class PlaygroundChat extends SignalWatcher( .createSession=${this._createSession} .chatContextValue=${this.chatContextValue} .updateContext=${this.updateContext} - .updateEmbeddingProgress=${this._updateEmbeddingProgress} + .onEmbeddingProgressChange=${this.onEmbeddingProgressChange} .networkSearchConfig=${this.networkSearchConfig} .reasoningConfig=${this.reasoningConfig} .playgroundConfig=${this.playgroundConfig} diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts index 466f0c502a..a18ea1f681 100644 --- a/packages/frontend/core/src/blocksuite/ai/effects.ts +++ b/packages/frontend/core/src/blocksuite/ai/effects.ts @@ -38,7 +38,7 @@ import { AIChatInput } from './components/ai-chat-input'; import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip'; import { ChatInputPreference } from './components/ai-chat-input/preference-popup'; import { AIChatMessages } from './components/ai-chat-messages/ai-chat-messages'; -import { AIChatToolbar } from './components/ai-chat-toolbar'; +import { AIChatToolbar, AISessionHistory } from './components/ai-chat-toolbar'; import { AIHistoryClear } from './components/ai-history-clear'; import { effects as componentAiItemEffects } from './components/ai-item'; import { AssistantAvatar } from './components/ai-message-content/assistant-avatar'; @@ -115,6 +115,7 @@ export function registerAIEffects() { customElements.define('ai-loading', AILoading); customElements.define('ai-chat-content', AIChatContent); customElements.define('ai-chat-toolbar', AIChatToolbar); + customElements.define('ai-session-history', AISessionHistory); customElements.define('ai-chat-messages', AIChatMessages); customElements.define('chat-panel', ChatPanel); customElements.define('ai-chat-input', AIChatInput); diff --git a/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts b/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts index f4e992f441..a741d0432f 100644 --- a/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts +++ b/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts @@ -10,6 +10,7 @@ import { ViewExtensionManagerIdentifier } from '@blocksuite/affine/ext-loader'; import { ConnectorMode } from '@blocksuite/affine/model'; import { DocModeProvider, + NotificationProvider, TelemetryProvider, } from '@blocksuite/affine/shared/services'; import type { EditorHost } from '@blocksuite/affine/std'; @@ -311,7 +312,7 @@ export class AIChatBlockPeekView extends LitElement { this.chatContext = { ...this.chatContext, ...context }; }; - private readonly _updateEmbeddingProgress = ( + private readonly onEmbeddingProgressChange = ( count: Record ) => { const total = count.finished + count.processing + count.failed; @@ -565,15 +566,16 @@ export class AIChatBlockPeekView extends LitElement { } = this; const { messages: currentChatMessages } = chatContext; + const notification = this.host.std.getOptional(NotificationProvider); return html`
@@ -595,7 +597,7 @@ export class AIChatBlockPeekView extends LitElement { .createSession=${this.createForkSession} .chatContextValue=${chatContext} .updateContext=${updateContext} - .updateEmbeddingProgress=${this._updateEmbeddingProgress} + .onEmbeddingProgressChange=${this.onEmbeddingProgressChange} .networkSearchConfig=${networkSearchConfig} .docDisplayConfig=${this.docDisplayConfig} .searchMenuConfig=${this.searchMenuConfig} diff --git a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts index 5304dde0b8..140c589480 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts @@ -11,6 +11,7 @@ import { forkCopilotSessionMutation, getCopilotHistoriesQuery, getCopilotHistoryIdsQuery, + getCopilotRecentSessionsQuery, getCopilotSessionQuery, getCopilotSessionsQuery, getWorkspaceEmbeddingStatusQuery, @@ -179,6 +180,21 @@ export class CopilotClient { } } + async getRecentSessions(workspaceId: string, limit?: number) { + try { + const res = await this.gql({ + query: getCopilotRecentSessionsQuery, + variables: { + workspaceId, + limit, + }, + }); + return res.currentUser?.copilot?.histories; + } catch (err) { + throw resolveError(err); + } + } + async getHistories( workspaceId: string, docId?: string, diff --git a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx index 6c4cd3caee..2c26f4dbbe 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx +++ b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx @@ -588,6 +588,9 @@ Could you make a new website based on these notes and send back just the html fi ) => { return client.getSessions(workspaceId, docId, options); }, + getRecentSessions: async (workspaceId: string, limit?: number) => { + return client.getRecentSessions(workspaceId, limit); + }, updateSession: async (options: UpdateChatSessionInput) => { return client.updateSession(options); }, diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx index acf4471039..fb89eb7a45 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx @@ -195,8 +195,8 @@ export const Component = () => { setCurrentSession(null); chatContent?.reset(); }; - tool.onTogglePin = () => { - togglePin().catch(console.error); + tool.onTogglePin = async () => { + await togglePin(); }; // mount chatToolContainerRef.current.append(tool);