feat(core): support lazy load for ai session history (#13221)

Close [AI-331](https://linear.app/affine-design/issue/AI-331)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added infinite scroll and incremental loading for AI session history,
allowing users to load more sessions as they scroll.

* **Refactor**
* Improved session history component with better state management and
modular rendering for loading, empty, and history states.

* **Bug Fixes**
* Enhanced handling of absent or uninitialized chat sessions, reducing
potential errors when session data is missing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-15 19:43:36 +08:00
committed by GitHub
parent c797cac87d
commit a4b535a42a
5 changed files with 93 additions and 42 deletions

View File

@@ -407,7 +407,8 @@ declare global {
) => Promise<CopilotChatHistoryFragment[] | undefined>;
getRecentSessions: (
workspaceId: string,
limit?: number
limit?: number,
offset?: number
) => Promise<AIRecentSession[] | undefined>;
updateSession: (options: UpdateChatSessionInput) => Promise<string>;
}

View File

@@ -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;

View File

@@ -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) {
</div>`;
}
override render() {
if (this.loading) {
return html`
<div class="ai-session-history">
<div class="loading-container">
<div class="loading-title">Loading history...</div>
</div>
</div>
`;
private renderLoading() {
return html`
<div class="loading-container">
<div class="loading-title">Loading history...</div>
</div>
`;
}
private renderEmpty() {
return html`
<div class="empty-container">
<div class="empty-title">Empty history</div>
</div>
`;
}
private renderHistory() {
if (!this.sessions) {
return this.renderLoading();
}
if (this.sessions.length === 0) {
return html`
<div class="ai-session-history">
<div class="empty-container">
<div class="empty-title">Empty history</div>
</div>
</div>
`;
return this.renderEmpty();
}
const groupedSessions = this.groupSessionsByTime(this.sessions);
return html`
<div class="ai-session-history">
${this.renderSessionGroup('Today', groupedSessions.today)}
${this.renderSessionGroup('Last 7 days', groupedSessions.last7Days)}
${this.renderSessionGroup('Last 30 days', groupedSessions.last30Days)}
${this.renderSessionGroup('Older', groupedSessions.older)}
</div>
${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`
<div class="ai-session-history">${this.renderHistory()}</div>
`;
}
}

View File

@@ -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);

View File

@@ -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);