feat(core): support ai recent session history (#13025)

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)

<img width="365" alt="截屏2025-07-04 13 49 25"
src="https://github.com/user-attachments/assets/d7c830f0-cc16-4a26-baf1-480c7d42838f"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-04 14:51:35 +08:00
committed by GitHub
parent eb73c90b2e
commit 24f1181069
14 changed files with 462 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ import type {
CopilotContextCategory,
CopilotContextDoc,
CopilotContextFile,
CopilotHistories,
CopilotSessionType,
getCopilotHistoriesQuery,
QueryChatSessionsInput,
@@ -387,6 +388,8 @@ declare global {
reuseLatestChat?: boolean;
}
type AIRecentSession = Omit<CopilotHistories, 'messages'>;
interface AISessionService {
createSession: (options: AICreateSessionOptions) => Promise<string>;
getSessions: (
@@ -394,6 +397,10 @@ declare global {
docId?: string,
options?: QueryChatSessionsInput
) => Promise<CopilotSessionType[] | undefined>;
getRecentSessions: (
workspaceId: string,
limit?: number
) => Promise<AIRecentSession[] | undefined>;
getSession: (
workspaceId: string,
sessionId: string

View File

@@ -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`
<div class="chat-panel-title-text">
${isEmbedding
? html`<span data-testid="chat-panel-embedding-progress"
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this.openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
<ai-chat-toolbar
.session=${this.session}
.workspaceId=${this.doc.workspace.id}
.onNewSession=${this.newSession}
.onTogglePin=${this.togglePin}
.onOpenSession=${this.openSession}
.docDisplayConfig=${this.docDisplayConfig}
.notification=${notification}
></ai-chat-toolbar>
`;
}
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<ContextEmbedStatus, number>
) => {
const total = count.finished + count.processing + count.failed;
this.embeddingProgress = [count.finished, total];
};
private readonly onContextChange = async (
context: Partial<ChatContextValue>
) => {
if (context.status === 'success') {
await this.rebindSession();
}
};
private readonly openPlayground = () => {
const playgroundContent = html`
<playground-content
@@ -291,35 +354,12 @@ export class ChatPanel extends SignalWatcher(
const style = styleMap({
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px',
});
const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total;
const title = html`
<div class="chat-panel-title-text">
${isEmbedding
? html`<span data-testid="chat-panel-embedding-progress"
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this.openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
<ai-chat-toolbar
.session=${this.session}
.onNewSession=${this.newSession}
.onTogglePin=${this.togglePin}
></ai-chat-toolbar>
`;
const left = html`<div class="chat-panel-container" style=${style}>
${keyed(
this.hasPinned ? this.session?.id : this.doc.id,
html`<ai-chat-content
.chatTitle=${title}
.chatTitle=${this.chatTitle}
.host=${this.host}
.session=${this.session}
.createSession=${this.createSession}
@@ -332,7 +372,8 @@ export class ChatPanel extends SignalWatcher(
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange}
.width=${this.sidebarWidth}
></ai-chat-content>`
)}

View File

@@ -75,7 +75,7 @@ export class AIChatComposer extends SignalWatcher(
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
@property({ attribute: false })
accessor updateEmbeddingProgress!: (
accessor onEmbeddingProgressChange!: (
count: Record<ContextEmbedStatus, number>
) => 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();
}

View File

@@ -136,10 +136,13 @@ export class AIChatContent extends SignalWatcher(
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor updateEmbeddingProgress!: (
accessor onEmbeddingProgressChange!: (
count: Record<ContextEmbedStatus, number>
) => void;
@property({ attribute: false })
accessor onContextChange!: (context: Partial<ChatContextValue>) => void;
@property({ attribute: false })
accessor width: Signal<number | undefined> | 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<ChatContextValue>) => {
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}

View File

@@ -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<void>;
@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`
<div class="ai-chat-toolbar">
<div class="chat-toolbar-icon" @click=${this.onNewSession}>
<div class="chat-toolbar-icon" @click=${this.onPlusClick}>
${PlusIcon()}
<affine-tooltip>New Chat</affine-tooltip>
</div>
<div class="chat-toolbar-icon" @click=${this.onTogglePin}>
${pined ? PinedIcon() : PinIcon()}
${pinned ? PinedIcon() : PinIcon()}
<affine-tooltip
>${pined ? 'Unpin this Chat' : 'Pin this Chat'}</affine-tooltip
>${pinned ? 'Unpin this Chat' : 'Pin this Chat'}</affine-tooltip
>
</div>
<div class="chat-toolbar-icon">
<div
class="chat-toolbar-icon history-button"
@click=${this.toggleHistoryMenu}
>
${ArrowDownSmallIcon()}
<affine-tooltip>Chat History</affine-tooltip>
</div>
</div>
`;
}
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`
<ai-session-history
.session=${this.session}
.workspaceId=${this.workspaceId}
.docDisplayConfig=${this.docDisplayConfig}
.onSessionClick=${this.onSessionClick}
.notification=${this.notification}
></ai-session-history>
`,
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,
});
};
}

View File

@@ -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<keyof GroupedSessions>).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`
<div class="ai-session-group">
<div class="ai-session-group-title">${title}</div>
${sessions.map(session => {
return html`
<div
class="ai-session-item"
@click=${() => this.onSessionClick(session.sessionId)}
>
<div class="ai-session-title">${session.sessionId}</div>
${session.docId ? this.renderSessionDoc(session.docId) : nothing}
</div>
`;
})}
</div>
`;
}
private renderSessionDoc(docId: string) {
const getIcon = this.docDisplayConfig.getIcon(docId);
const docIcon = typeof getIcon === 'function' ? getIcon() : getIcon;
return html`<div class="ai-session-doc">
${docIcon}
<span class="doc-title">${this.docDisplayConfig.getTitle(docId)}</span>
</div>`;
}
override render() {
if (this.sessions.length === 0) {
return nothing;
}
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>
`;
}
}

View File

@@ -1 +1,2 @@
export * from './ai-chat-toolbar';
export * from './ai-session-history';

View File

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

View File

@@ -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<ContextEmbedStatus, number>
) => {
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`<div class="chat-panel-container">
<div class="chat-panel-title">
@@ -287,9 +289,9 @@ export class PlaygroundChat extends SignalWatcher(
<affine-tooltip>Add chat</affine-tooltip>
</div>
<ai-history-clear
.host=${this.host}
.doc=${this.doc}
.session=${this.session}
.notification=${notification}
.onHistoryCleared=${this._updateHistory}
.chatContextValue=${this.chatContextValue}
></ai-history-clear>
@@ -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}

View File

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

View File

@@ -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<ContextEmbedStatus, number>
) => {
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`<div class="ai-chat-block-peek-view-container">
<div class="history-clear-container">
<ai-history-clear
.host=${this.host}
.doc=${this.host.store}
.session=${this.forkSession}
.onHistoryCleared=${this._onHistoryCleared}
.chatContextValue=${chatContext}
.notification=${notification}
></ai-history-clear>
</div>
<div class="ai-chat-messages-container">
@@ -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}

View File

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

View File

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

View File

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