refactor(core): lazy create copilot session and context (#10170)

This commit is contained in:
akumatus
2025-02-14 14:48:46 +00:00
parent 5a42edf076
commit 631c8b8145
11 changed files with 179 additions and 170 deletions

View File

@@ -83,7 +83,7 @@ export class ChatActionList extends LitElement {
accessor content: string = '';
@property({ attribute: false })
accessor chatSessionId: string | undefined = undefined;
accessor getSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor messageId: string | undefined = undefined;
@@ -100,7 +100,7 @@ export class ChatActionList extends LitElement {
return nothing;
}
const { host, content, chatSessionId, messageId, layoutDirection } = this;
const { host, content, messageId, layoutDirection } = this;
const classes = classMap({
'actions-container': true,
horizontal: layoutDirection === 'horizontal',
@@ -138,11 +138,12 @@ export class ChatActionList extends LitElement {
blocks: this._currentBlockSelections,
images: this._currentImageSelections,
};
const sessionId = await this.getSessionId();
const success = await action.handler(
host,
content,
currentSelections,
chatSessionId,
sessionId,
messageId
);
if (success) {

View File

@@ -115,7 +115,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
accessor content!: string;
@property({ attribute: false })
accessor chatSessionId: string | undefined = undefined;
accessor getSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor messageId: string | undefined = undefined;
@@ -162,7 +162,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
}
override render() {
const { host, content, isLast, messageId, chatSessionId, actions } = this;
const { host, content, isLast, messageId, actions } = this;
return html`<style>
.copy-more {
margin-top: ${this.withMargin ? '8px' : '0px'};
@@ -217,11 +217,12 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
};
return html`<div
@click=${async () => {
const sessionId = await this.getSessionId();
const success = await action.handler(
host,
content,
currentSelections,
chatSessionId,
sessionId,
messageId
);

View File

@@ -47,7 +47,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor chatContextId!: string | undefined;
accessor getContextId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@@ -196,34 +196,36 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
};
private readonly _addToContext = async (chip: ChatChip) => {
if (!AIProvider.context || !this.chatContextId) {
const contextId = await this.getContextId();
if (!contextId || !AIProvider.context) {
return;
}
if (isDocChip(chip)) {
await AIProvider.context.addContextDoc({
contextId: this.chatContextId,
contextId,
docId: chip.docId,
});
} else {
await AIProvider.context.addContextFile({
contextId: this.chatContextId,
contextId,
fileId: chip.fileId,
});
}
};
private readonly _removeFromContext = async (chip: ChatChip) => {
if (!AIProvider.context || !this.chatContextId) {
const contextId = await this.getContextId();
if (!contextId || !AIProvider.context) {
return;
}
if (isDocChip(chip)) {
await AIProvider.context.removeContextDoc({
contextId: this.chatContextId,
contextId,
docId: chip.docId,
});
} else {
await AIProvider.context.removeContextFile({
contextId: this.chatContextId,
contextId,
fileId: chip.fileId,
});
}

View File

@@ -260,7 +260,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor chatSessionId!: string | undefined;
accessor getSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@@ -300,11 +300,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
private async _updatePromptName() {
if (this._lastPromptName !== this._promptName) {
this._lastPromptName = this._promptName;
if (this.chatSessionId) {
await AIProvider.session?.updateSession(
this.chatSessionId,
this._promptName
);
const sessionId = await this.getSessionId();
if (sessionId) {
await AIProvider.session?.updateSession(sessionId, this._promptName);
}
}
}
@@ -559,45 +557,46 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
if (status === 'loading' || status === 'transmitting') return;
if (!text) return;
const { images } = this.chatContextValue;
const { doc } = this.host;
this.updateContext({
images: [],
status: 'loading',
error: null,
quote: '',
markdown: '',
});
await this._updatePromptName();
const attachments = await Promise.all(
images?.map(image => readBlobAsURL(image))
);
const userInput = (markdown ? `${markdown}\n` : '') + text;
this.updateContext({
items: [
...this.chatContextValue.items,
{
id: '',
role: 'user',
content: userInput,
createdAt: new Date().toISOString(),
attachments,
},
{
id: '',
role: 'assistant',
content: '',
createdAt: new Date().toISOString(),
},
],
});
try {
const { images } = this.chatContextValue;
const { doc } = this.host;
this.updateContext({
images: [],
status: 'loading',
error: null,
quote: '',
markdown: '',
});
const attachments = await Promise.all(
images?.map(image => readBlobAsURL(image))
);
const userInput = (markdown ? `${markdown}\n` : '') + text;
this.updateContext({
items: [
...this.chatContextValue.items,
{
id: '',
role: 'user',
content: userInput,
createdAt: new Date().toISOString(),
attachments,
},
{
id: '',
role: 'assistant',
content: '',
createdAt: new Date().toISOString(),
},
],
});
await this._updatePromptName();
const abortController = new AbortController();
const sessionId = await this.getSessionId();
const docs: DocContext[] = chips
.filter(isDocChip)
.filter(chip => !!chip.markdown?.value && chip.state === 'success')
@@ -606,7 +605,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
markdown: chip.markdown?.value || '',
}));
const stream = AIProvider.actions.chat?.({
sessionId: this.chatSessionId,
sessionId,
input: userInput,
docs: docs,
docId: doc.id,
@@ -638,7 +637,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
const historyIds = await AIProvider.histories?.ids(
doc.workspace.id,
doc.id,
{ sessionId: this.chatSessionId }
{ sessionId }
);
if (!historyIds || !historyIds[0]) return;
last.id = historyIds[0].messages.at(-1)?.id ?? '';

View File

@@ -134,7 +134,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor chatSessionId!: string | undefined;
accessor getSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@@ -415,7 +415,8 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
retry = async () => {
const { doc } = this.host;
try {
if (!this.chatSessionId) return;
const sessionId = await this.getSessionId();
if (!sessionId) return;
const abortController = new AbortController();
const items = [...this.chatContextValue.items];
@@ -427,7 +428,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
this.updateContext({ items, status: 'loading', error: null });
const stream = AIProvider.actions.chat?.({
sessionId: this.chatSessionId,
sessionId,
retry: true,
docId: doc.id,
workspaceId: doc.workspace.id,
@@ -482,7 +483,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
.actions=${actions}
.content=${content}
.isLast=${isLast}
.chatSessionId=${this.chatSessionId}
.getSessionId=${this.getSessionId}
.messageId=${messageId}
.withMargin=${true}
.retry=${() => this.retry()}
@@ -492,7 +493,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
.actions=${actions}
.host=${host}
.content=${content}
.chatSessionId=${this.chatSessionId}
.getSessionId=${this.getSessionId}
.messageId=${messageId ?? undefined}
.withMargin=${true}
></chat-action-list>`

View File

@@ -63,8 +63,8 @@ export class ChatPanelDocChip extends SignalWatcher(
super.updated(changedProperties);
if (
changedProperties.has('chip') &&
changedProperties.get('chip')?.state === 'candidate' &&
this.chip.state === 'processing'
this.chip.state === 'processing' &&
!this.chip.markdown
) {
this.processDocChip().catch(console.error);
}

View File

@@ -36,6 +36,17 @@ import type {
import type { ChatPanelMessages } from './chat-panel-messages';
import { isDocContext } from './components/utils';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
images: [],
abortController: null,
items: [],
chips: [],
status: 'idle',
error: null,
markdown: '',
};
export class ChatPanel extends WithDisposable(ShadowlessElement) {
static override styles = css`
chat-panel {
@@ -156,49 +167,80 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
};
private readonly _updateChips = async () => {
if (!this._chatSessionId || !this._chatContextId) return;
const candidateChip: DocChip = {
docId: this.doc.id,
state: 'candidate',
};
let chips: (DocChip | FileChip)[] = [];
if (this._chatContextId) {
const { docs = [], files = [] } =
(await AIProvider.context?.getContextDocsAndFiles(
this.doc.workspace.id,
this._chatSessionId,
this._chatContextId
)) || {};
const list = [...docs, ...files].sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
chips = list.map(item => {
let chip: DocChip | FileChip;
if (isDocContext(item)) {
chip = {
docId: item.id,
state: 'processing',
};
} else {
chip = {
fileId: item.id,
state: item.status === 'finished' ? 'success' : item.status,
fileName: item.name,
fileType: '',
};
}
return chip;
});
// context not initialized, show candidate chip
if (!this._chatSessionId || !this._chatContextId) {
this.chatContextValue = {
...this.chatContextValue,
chips: [candidateChip],
};
return;
}
// context initialized, show the chips
let chips: (DocChip | FileChip)[] = [];
const { docs = [], files = [] } =
(await AIProvider.context?.getContextDocsAndFiles(
this.doc.workspace.id,
this._chatSessionId,
this._chatContextId
)) || {};
const list = [...docs, ...files].sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
chips = list.map(item => {
let chip: DocChip | FileChip;
if (isDocContext(item)) {
chip = {
docId: item.id,
state: 'processing',
};
} else {
chip = {
fileId: item.id,
state: item.status === 'finished' ? 'success' : item.status,
fileName: item.name,
fileType: '',
};
}
return chip;
});
this.chatContextValue = {
...this.chatContextValue,
chips: chips.length === 0 ? [candidateChip] : chips,
};
};
private readonly _getSessionId = async () => {
if (this._chatSessionId) {
return this._chatSessionId;
}
this._chatSessionId = await AIProvider.session?.createSession(
this.doc.workspace.id,
this.doc.id
);
return this._chatSessionId;
};
private readonly _getContextId = async () => {
if (this._chatContextId) {
return this._chatContextId;
}
const sessionId = await this._getSessionId();
if (sessionId) {
this._chatContextId = await AIProvider.context?.createContext(
this.doc.workspace.id,
sessionId
);
}
return this._chatContextId;
};
@property({ attribute: false })
accessor host!: EditorHost;
@@ -221,20 +263,11 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
accessor isLoading = false;
@state()
accessor chatContextValue: ChatContextValue = {
quote: '',
images: [],
abortController: null,
items: [],
chips: [],
status: 'idle',
error: null,
markdown: '',
};
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
private _chatSessionId: string | undefined;
private _chatSessionId: string | null | undefined = null;
private _chatContextId: string | undefined;
private _chatContextId: string | null | undefined = null;
private readonly _scrollToEnd = () => {
this._chatMessages.value?.scrollToEnd();
@@ -272,26 +305,29 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
const userId = (await AIProvider.userInfo)?.id;
if (!userId) return;
try {
this._chatSessionId = await AIProvider.session?.createSession(
this.doc.workspace.id,
this.doc.id
);
if (this._chatSessionId) {
this._chatContextId = await AIProvider.context?.createContext(
this.doc.workspace.id,
this._chatSessionId
);
}
} catch (e) {
console.error('init panel error', e);
const sessionIds = await AIProvider.session?.getSessionIds(
this.doc.workspace.id,
this.doc.id
);
if (sessionIds?.length) {
this._chatSessionId = sessionIds[0];
await this._updateHistory();
}
if (this._chatSessionId) {
this._chatContextId = await AIProvider.context?.getContextId(
this.doc.workspace.id,
this._chatSessionId
);
}
await this._updateHistory();
await this._updateChips();
};
protected override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) {
this._chatSessionId = null;
this._chatContextId = null;
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
requestAnimationFrame(async () => {
await this._initPanel();
});
@@ -376,7 +412,7 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
<chat-panel-messages
${ref(this._chatMessages)}
.chatContextValue=${this.chatContextValue}
.chatSessionId=${this._chatSessionId}
.getSessionId=${this._getSessionId}
.updateContext=${this.updateContext}
.host=${this.host}
.isLoading=${this.isLoading}
@@ -385,14 +421,14 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
<chat-panel-chips
.host=${this.host}
.chatContextValue=${this.chatContextValue}
.chatContextId=${this._chatContextId}
.getContextId=${this._getContextId}
.updateContext=${this.updateContext}
.docDisplayConfig=${this.docDisplayConfig}
.docSearchMenuConfig=${this.docSearchMenuConfig}
></chat-panel-chips>
<chat-panel-input
.chatContextValue=${this.chatContextValue}
.chatSessionId=${this._chatSessionId}
.getSessionId=${this._getSessionId}
.networkSearchConfig=${this.networkSearchConfig}
.updateContext=${this.updateContext}
.host=${this.host}

View File

@@ -131,6 +131,10 @@ export class AIChatBlockPeekView extends LitElement {
});
};
private readonly _getSessionId = async () => {
return this.chatContext.currentSessionId ?? undefined;
};
/**
* Create a new AI chat block based on the current session and history messages
*/
@@ -408,7 +412,7 @@ export class AIChatBlockPeekView extends LitElement {
.actions=${actions}
.content=${message.content}
.isLast=${isLastReply}
.chatSessionId=${this.chatContext.currentSessionId ?? undefined}
.getSessionId=${this._getSessionId}
.messageId=${message.id ?? undefined}
.retry=${() => this.retry()}
></chat-copy-more>`
@@ -418,7 +422,7 @@ export class AIChatBlockPeekView extends LitElement {
.host=${host}
.actions=${actions}
.content=${message.content}
.chatSessionId=${this.chatContext.currentSessionId ?? undefined}
.getSessionId=${this._getSessionId}
.messageId=${message.id ?? undefined}
.layoutDirection=${'horizontal'}
></chat-action-list>`

View File

@@ -30,30 +30,6 @@ export type ToImageOptions = TextToTextOptions & {
seed?: string;
};
export async function createChatSession({
client,
workspaceId,
docId,
promptName = 'Chat With AFFiNE AI',
}: {
client: CopilotClient;
workspaceId: string;
docId: string;
promptName?: string;
}) {
const sessionId = await client.createSession({
workspaceId,
docId,
promptName,
});
// always update the prompt name
await client.updateSession({
sessionId,
promptName,
});
return sessionId;
}
async function resizeImage(blob: Blob | File): Promise<Blob | null> {
let src = '';
try {
@@ -90,7 +66,7 @@ async function createSessionMessage({
client,
docId,
workspaceId,
promptName,
promptName = 'Chat With AFFiNE AI',
content,
sessionId: providedSessionId,
attachments,
@@ -102,11 +78,10 @@ async function createSessionMessage({
}
const hasAttachments = attachments && attachments.length > 0;
const sessionId = await (providedSessionId ??
createChatSession({
client,
client.createSession({
workspaceId,
docId,
promptName: promptName as string,
promptName,
}));
const options: Parameters<CopilotClient['createMessage']>[0] = {

View File

@@ -11,7 +11,7 @@ import { z } from 'zod';
import type { CopilotClient } from './copilot-client';
import type { PromptKey } from './prompt';
import { createChatSession, textToText, toImage } from './request';
import { textToText, toImage } from './request';
import { setupTracker } from './tracker';
const filterStyleToPromptName = new Map(
@@ -37,13 +37,6 @@ export function setupAIProvider(
) {
//#region actions
AIProvider.provide('chat', options => {
const sessionId =
options.sessionId ??
createChatSession({
client,
workspaceId: options.workspaceId,
docId: options.docId,
});
const { input, docs, ...rest } = options;
const params = docs?.length
? {
@@ -58,7 +51,6 @@ export function setupAIProvider(
...rest,
client,
content: input,
sessionId,
params,
});
});
@@ -408,10 +400,9 @@ Could you make a new website based on these notes and send back just the html fi
createSession: async (
workspaceId: string,
docId: string,
promptName?: string
promptName = 'Chat With AFFiNE AI'
) => {
return createChatSession({
client,
return client.createSession({
workspaceId,
docId,
promptName,