diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts index 92e7c5810..9571060d2 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts @@ -407,7 +407,8 @@ declare global { ) => Promise; getRecentSessions: ( workspaceId: string, - limit?: number + limit?: number, + offset?: number ) => Promise; updateSession: (options: UpdateChatSessionInput) => Promise; } diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts index 9e8462b77..e01dc8031 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts @@ -106,7 +106,7 @@ export class AIChatPanelTitle extends SignalWatcher( accessor notificationService!: NotificationService; @property({ attribute: false }) - accessor session!: CopilotChatHistoryFragment; + accessor session!: CopilotChatHistoryFragment | null | undefined; @property({ attribute: false }) accessor status!: ChatStatus; 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 a7f56b392..85cdb184c 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 @@ -3,8 +3,8 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; 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 { css, html, nothing, type PropertyValues } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; import { AIProvider } from '../../provider'; import type { DocDisplayConfig } from '../ai-chat-chips'; @@ -133,11 +133,21 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor onDocClick!: (docId: string, sessionId: string) => void; - @state() - private accessor sessions: BlockSuitePresets.AIRecentSession[] = []; + @query('.ai-session-history') + accessor scrollContainer!: HTMLElement; @state() - private accessor loading = true; + private accessor sessions: BlockSuitePresets.AIRecentSession[] | undefined; + + @state() + private accessor loadingMore = false; + + @state() + private accessor hasMore = true; + + private accessor currentOffset = 0; + + private readonly pageSize = 10; private groupSessionsByTime( sessions: BlockSuitePresets.AIRecentSession[] @@ -188,23 +198,46 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { } private async getRecentSessions() { - this.loading = true; - const limit = 50; - const sessions = await AIProvider.session?.getRecentSessions( - this.workspaceId, - limit - ); - if (sessions) { - this.sessions = sessions; - } - this.loading = false; + this.loadingMore = true; + + const moreSessions = + (await AIProvider.session?.getRecentSessions( + this.workspaceId, + this.pageSize, + this.currentOffset + )) || []; + this.sessions = [...(this.sessions || []), ...moreSessions]; + + this.currentOffset += moreSessions.length; + this.hasMore = moreSessions.length === this.pageSize; + this.loadingMore = false; } + private readonly onScroll = () => { + if (!this.hasMore || this.loadingMore) { + return; + } + // load more when within 50px of bottom + const { scrollTop, scrollHeight, clientHeight } = this.scrollContainer; + const threshold = 50; + if (scrollTop + clientHeight >= scrollHeight - threshold) { + this.getRecentSessions().catch(console.error); + } + }; + override connectedCallback() { super.connectedCallback(); this.getRecentSessions().catch(console.error); } + override firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + this.disposables.add(() => { + this.scrollContainer.removeEventListener('scroll', this.onScroll); + }); + this.scrollContainer.addEventListener('scroll', this.onScroll); + } + private renderSessionGroup( title: string, sessions: BlockSuitePresets.AIRecentSession[] @@ -256,35 +289,43 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { `; } - override render() { - if (this.loading) { - return html` -
-
-
Loading history...
-
-
- `; + private renderLoading() { + return html` +
+
Loading history...
+
+ `; + } + + private renderEmpty() { + return html` +
+
Empty history
+
+ `; + } + + private renderHistory() { + if (!this.sessions) { + return this.renderLoading(); } if (this.sessions.length === 0) { - return html` -
-
-
Empty history
-
-
- `; + return this.renderEmpty(); } 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)} -
+ ${this.renderSessionGroup('Today', groupedSessions.today)} + ${this.renderSessionGroup('Last 7 days', groupedSessions.last7Days)} + ${this.renderSessionGroup('Last 30 days', groupedSessions.last30Days)} + ${this.renderSessionGroup('Older', groupedSessions.older)} + `; + } + + override render() { + return html` +
${this.renderHistory()}
`; } } 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 474b0d9c2..d46468788 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts @@ -186,13 +186,18 @@ export class CopilotClient { } } - async getRecentSessions(workspaceId: string, limit?: number) { + async getRecentSessions( + workspaceId: string, + limit?: number, + offset?: number + ) { try { const res = await this.gql({ query: getCopilotRecentSessionsQuery, variables: { workspaceId, limit, + offset, }, }); return res.currentUser?.copilot?.chats.edges.map(e => e.node); 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 b917a1b01..67ed87c4e 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx +++ b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx @@ -589,8 +589,12 @@ 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); + getRecentSessions: async ( + workspaceId: string, + limit?: number, + offset?: number + ) => { + return client.getRecentSessions(workspaceId, limit, offset); }, updateSession: async (options: UpdateChatSessionInput) => { return client.updateSession(options);