feat: refactor copilot module (#14537)

This commit is contained in:
DarkSky
2026-03-02 13:57:55 +08:00
committed by GitHub
parent 60acd81d4b
commit c5d622531c
92 changed files with 5759 additions and 2170 deletions
@@ -411,6 +411,9 @@ declare global {
interface AISessionService {
createSession: (options: AICreateSessionOptions) => Promise<string>;
createSessionWithHistory: (
options: AICreateSessionOptions
) => Promise<CopilotChatHistoryFragment | undefined>;
getSession: (
workspaceId: string,
sessionId: string
@@ -185,6 +185,9 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
@state()
private accessor hasMore = true;
@state()
private accessor selectedSessionId: string | undefined;
private accessor currentOffset = 0;
private readonly pageSize = 10;
@@ -267,9 +270,16 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
override connectedCallback() {
super.connectedCallback();
this.selectedSessionId = this.session?.sessionId ?? undefined;
this.getRecentSessions().catch(console.error);
}
protected override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('session')) {
this.selectedSessionId = this.session?.sessionId ?? undefined;
}
}
override firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this.disposables.add(() => {
@@ -294,9 +304,10 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
class="ai-session-item"
@click=${(e: MouseEvent) => {
e.stopPropagation();
this.selectedSessionId = session.sessionId;
this.onSessionClick(session.sessionId);
}}
aria-selected=${this.session?.sessionId === session.sessionId}
aria-selected=${this.selectedSessionId === session.sessionId}
data-session-id=${session.sessionId}
>
<div class="ai-session-title">
@@ -332,6 +343,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
class="ai-session-doc"
@click=${(e: MouseEvent) => {
e.stopPropagation();
this.selectedSessionId = sessionId;
this.onDocClick(docId, sessionId);
}}
>
@@ -152,6 +152,7 @@ export class AIProvider {
}>(),
// downstream can emit this slot to notify ai presets that user info has been updated
userInfo: new Subject<AIUserInfo | null>(),
sessionReady: new BehaviorSubject<boolean>(false),
previewPanelOpenChange: new Subject<boolean>(),
/* eslint-enable rxjs/finnish */
};
@@ -344,6 +345,7 @@ export class AIProvider {
} else if (id === 'session') {
AIProvider.instance.session =
action as BlockSuitePresets.AISessionService;
AIProvider.instance.slots.sessionReady.next(true);
} else if (id === 'context') {
AIProvider.instance.context =
action as BlockSuitePresets.AIContextService;
@@ -11,6 +11,7 @@ import {
createCopilotContextMutation,
createCopilotMessageMutation,
createCopilotSessionMutation,
createCopilotSessionWithHistoryMutation,
forkCopilotSessionMutation,
getCopilotHistoriesQuery,
getCopilotHistoryIdsQuery,
@@ -41,7 +42,6 @@ import {
} from './error';
export enum Endpoint {
Stream = 'stream',
StreamObject = 'stream-object',
Workflow = 'workflow',
Images = 'images',
@@ -96,7 +96,6 @@ export class CopilotClient {
readonly gql: <Query extends GraphQLQuery>(
options: QueryOptions<Query>
) => Promise<QueryResponse<Query>>,
readonly fetcher: (input: string, init?: RequestInit) => Promise<Response>,
readonly eventSource: (
url: string,
eventSourceInitDict?: EventSourceInit
@@ -119,6 +118,20 @@ export class CopilotClient {
}
}
async createSessionWithHistory(
options: OptionsField<typeof createCopilotSessionWithHistoryMutation>
) {
try {
const res = await this.gql({
query: createCopilotSessionWithHistoryMutation,
variables: { options },
});
return res.createCopilotSessionWithHistory;
} catch (err) {
throw resolveError(err);
}
}
async updateSession(
options: OptionsField<typeof updateCopilotSessionMutation>
) {
@@ -150,7 +163,11 @@ export class CopilotClient {
}
async createMessage(
options: OptionsField<typeof createCopilotMessageMutation>
options: OptionsField<typeof createCopilotMessageMutation>,
requestOptions?: Pick<
RequestOptions<typeof createCopilotMessageMutation>,
'timeout' | 'signal'
>
) {
try {
const res = await this.gql({
@@ -158,6 +175,8 @@ export class CopilotClient {
variables: {
options,
},
timeout: requestOptions?.timeout,
signal: requestOptions?.signal,
});
return res.createCopilotMessage;
} catch (err) {
@@ -442,35 +461,6 @@ export class CopilotClient {
return { files, docs };
}
async chatText({
sessionId,
messageId,
reasoning,
modelId,
toolsConfig,
signal,
}: {
sessionId: string;
messageId?: string;
reasoning?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
signal?: AbortSignal;
}) {
let url = `/api/copilot/chat/${sessionId}`;
const queryString = this.paramsToQueryString({
messageId,
reasoning,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
}
const response = await this.fetcher(url.toString(), { signal });
return response.text();
}
// Text or image to text
chatTextStream(
{
@@ -486,7 +476,7 @@ export class CopilotClient {
modelId?: string;
toolsConfig?: AIToolsConfig;
},
endpoint = Endpoint.Stream
endpoint = Endpoint.StreamObject
) {
let url = `/api/copilot/chat/${sessionId}/${endpoint}`;
const queryString = this.paramsToQueryString({
@@ -3,7 +3,7 @@ import { partition } from 'lodash-es';
import { AIProvider } from './ai-provider';
import { type CopilotClient, Endpoint } from './copilot-client';
import { delay, toTextStream } from './event-source';
import { toTextStream } from './event-source';
const TIMEOUT = 50000;
@@ -67,6 +67,8 @@ interface CreateMessageOptions {
content?: string;
attachments?: (string | Blob | File)[];
params?: Record<string, any>;
timeout?: number;
signal?: AbortSignal;
}
async function createMessage({
@@ -75,6 +77,8 @@ async function createMessage({
content,
attachments,
params,
timeout,
signal,
}: CreateMessageOptions): Promise<string> {
const hasAttachments = attachments && attachments.length > 0;
const options: Parameters<CopilotClient['createMessage']>[0] = {
@@ -102,7 +106,7 @@ async function createMessage({
).filter(Boolean) as File[];
}
return await client.createMessage(options);
return await client.createMessage(options, { timeout, signal });
}
export function textToText({
@@ -115,7 +119,7 @@ export function textToText({
signal,
timeout = TIMEOUT,
retry = false,
endpoint = Endpoint.Stream,
endpoint = Endpoint.StreamObject,
postfix,
reasoning,
modelId,
@@ -133,6 +137,8 @@ export function textToText({
content,
attachments,
params,
timeout,
signal,
});
}
const eventSource = client.chatTextStream(
@@ -147,65 +153,105 @@ export function textToText({
);
AIProvider.LAST_ACTION_SESSIONID = sessionId;
if (signal) {
if (signal.aborted) {
eventSource.close();
return;
let onAbort: (() => void) | undefined;
try {
if (signal) {
if (signal.aborted) {
eventSource.close();
return;
}
onAbort = () => {
eventSource.close();
};
signal.addEventListener('abort', onAbort, { once: true });
}
signal.onabort = () => {
eventSource.close();
};
}
if (postfix) {
const messages: string[] = [];
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
messages.push(event.data);
if (postfix) {
const messages: string[] = [];
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
messages.push(event.data);
}
}
yield postfix(messages.join(''));
} else {
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
yield event.data;
}
}
}
yield postfix(messages.join(''));
} else {
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
yield event.data;
}
} finally {
eventSource.close();
if (signal && onAbort) {
signal.removeEventListener('abort', onAbort);
}
}
},
};
} else {
return Promise.race([
timeout
? delay(timeout).then(() => {
throw new Error('Timeout');
})
: null,
(async function () {
if (!retry) {
messageId = await createMessage({
client,
sessionId,
content,
attachments,
params,
});
}
AIProvider.LAST_ACTION_SESSIONID = sessionId;
return client.chatText({
return (async function () {
if (!retry) {
messageId = await createMessage({
client,
sessionId,
content,
attachments,
params,
timeout,
signal,
});
}
const eventSource = client.chatTextStream(
{
sessionId,
messageId,
reasoning,
modelId,
});
})(),
]);
toolsConfig,
},
endpoint
);
AIProvider.LAST_ACTION_SESSIONID = sessionId;
let onAbort: (() => void) | undefined;
try {
if (signal) {
if (signal.aborted) {
eventSource.close();
return '';
}
onAbort = () => {
eventSource.close();
};
signal.addEventListener('abort', onAbort, { once: true });
}
const messages: string[] = [];
for await (const event of toTextStream(eventSource, {
timeout,
signal,
})) {
if (event.type === 'message') {
messages.push(event.data);
}
}
const result = messages.join('');
return postfix ? postfix(result) : result;
} finally {
eventSource.close();
if (signal && onAbort) {
signal.removeEventListener('abort', onAbort);
}
}
})();
}
}
@@ -232,6 +278,8 @@ export function toImage({
content,
attachments,
params,
timeout,
signal,
});
}
const eventSource = client.imagesStream(
@@ -582,6 +582,21 @@ Could you make a new website based on these notes and send back just the html fi
AIProvider.provide('session', {
createSession,
createSessionWithHistory: async options => {
if (!options.sessionId && !options.retry) {
return client.createSessionWithHistory({
workspaceId: options.workspaceId,
docId: options.docId,
promptName: options.promptName,
pinned: options.pinned,
reuseLatestChat: options.reuseLatestChat,
});
}
const sessionId = await createSession(options);
if (!sessionId) return undefined;
return client.getSession(options.workspaceId, sessionId);
},
getSession: async (workspaceId: string, sessionId: string) => {
return client.getSession(workspaceId, sessionId);
},
@@ -823,7 +838,7 @@ Could you make a new website based on these notes and send back just the html fi
regular: string;
};
}[];
} = await client.fetcher(url.toString()).then(res => res.json());
} = await fetch(url.toString()).then((res: Response) => res.json());
if (!result.results) return [];
return result.results.map(r => {
const url = new URL(r.urls.regular);
@@ -14,7 +14,6 @@ import { OverCapacityNotification } from '@affine/core/components/over-capacity'
import {
AuthService,
EventSourceService,
FetchService,
GraphQLService,
} from '@affine/core/modules/cloud';
import {
@@ -140,16 +139,11 @@ export const WorkspaceSideEffects = () => {
const graphqlService = useService(GraphQLService);
const eventSourceService = useService(EventSourceService);
const fetchService = useService(FetchService);
const authService = useService(AuthService);
useEffect(() => {
const dispose = setupAIProvider(
new CopilotClient(
graphqlService.gql,
fetchService.fetch,
eventSourceService.eventSource
),
new CopilotClient(graphqlService.gql, eventSourceService.eventSource),
globalDialogService,
authService
);
@@ -158,7 +152,6 @@ export const WorkspaceSideEffects = () => {
};
}, [
eventSourceService,
fetchService,
workspaceDialogService,
graphqlService,
globalDialogService,
@@ -23,7 +23,6 @@ import {
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
import {
EventSourceService,
FetchService,
GraphQLService,
ServerService,
SubscriptionService,
@@ -58,16 +57,10 @@ type CopilotSession = Awaited<ReturnType<CopilotClient['getSession']>>;
function useCopilotClient() {
const graphqlService = useService(GraphQLService);
const eventSourceService = useService(EventSourceService);
const fetchService = useService(FetchService);
return useMemo(
() =>
new CopilotClient(
graphqlService.gql,
fetchService.fetch,
eventSourceService.eventSource
),
[graphqlService, eventSourceService, fetchService]
() => new CopilotClient(graphqlService.gql, eventSourceService.eventSource),
[graphqlService, eventSourceService]
);
}
@@ -106,6 +99,7 @@ export const Component = () => {
const [status, setStatus] = useState<ChatStatus>('idle');
const [isTogglingPin, setIsTogglingPin] = useState(false);
const [isOpeningSession, setIsOpeningSession] = useState(false);
const hasRestoredPinnedSessionRef = useRef(false);
const chatContainerRef = useRef<HTMLDivElement>(null);
const chatToolContainerRef = useRef<HTMLDivElement>(null);
const widthSignalRef = useRef<Signal<number>>(signal(0));
@@ -114,6 +108,10 @@ export const Component = () => {
const workspaceId = useService(WorkspaceService).workspace.id;
useEffect(() => {
hasRestoredPinnedSessionRef.current = false;
}, [workspaceId]);
const { docDisplayConfig, searchMenuConfig, reasoningConfig } =
useAIChatConfig();
@@ -122,14 +120,12 @@ export const Component = () => {
if (currentSession) {
return currentSession;
}
const sessionId = await client.createSession({
const session = await client.createSessionWithHistory({
workspaceId,
promptName: 'Chat With AFFiNE AI' satisfies PromptKey,
reuseLatestChat: false,
...options,
});
const session = await client.getSession(workspaceId, sessionId);
setCurrentSession(session);
return session;
},
@@ -169,23 +165,50 @@ export const Component = () => {
});
}, []);
const createFreshSession = useCallback(async () => {
if (isOpeningSession) {
return;
}
setIsOpeningSession(true);
try {
setCurrentSession(null);
reMountChatContent();
const session = await client.createSessionWithHistory({
workspaceId,
promptName: 'Chat With AFFiNE AI' satisfies PromptKey,
reuseLatestChat: false,
});
setCurrentSession(session);
} catch (error) {
console.error(error);
} finally {
setIsOpeningSession(false);
}
}, [client, isOpeningSession, reMountChatContent, workspaceId]);
const onOpenSession = useCallback(
(sessionId: string) => {
if (isOpeningSession) return;
async (sessionId: string) => {
if (isOpeningSession || currentSession?.sessionId === sessionId) return;
setIsOpeningSession(true);
client
.getSession(workspaceId, sessionId)
.then(session => {
setCurrentSession(session);
reMountChatContent();
chatTool?.closeHistoryMenu();
})
.catch(console.error)
.finally(() => {
setIsOpeningSession(false);
});
try {
const session = await client.getSession(workspaceId, sessionId);
setCurrentSession(session);
reMountChatContent();
chatTool?.closeHistoryMenu();
} catch (error) {
console.error(error);
} finally {
setIsOpeningSession(false);
}
},
[chatTool, client, isOpeningSession, reMountChatContent, workspaceId]
[
chatTool,
client,
currentSession?.sessionId,
isOpeningSession,
reMountChatContent,
workspaceId,
]
);
const onContextChange = useCallback((context: Partial<ChatContextValue>) => {
@@ -198,6 +221,16 @@ export const Component = () => {
},
[workbench]
);
const onOpenSessionDoc = useCallback(
(docId: string, sessionId: string) => {
const { workbench } = framework.get(WorkbenchService);
const viewService = framework.get(ViewService);
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
workbench.openSidebar();
viewService.view.activeSidebarTab('chat');
},
[framework]
);
const confirmModal = useConfirmModal();
const notificationService = useMemo(
@@ -286,7 +319,6 @@ export const Component = () => {
}
}, [
chatContent,
client,
createSession,
currentSession,
docDisplayConfig,
@@ -296,7 +328,6 @@ export const Component = () => {
reasoningConfig,
searchMenuConfig,
workspaceId,
confirmModal,
onContextChange,
notificationService,
specs,
@@ -316,19 +347,15 @@ export const Component = () => {
status,
docDisplayConfig,
notificationService,
onOpenSession,
onOpenSession: sessionId => {
onOpenSession(sessionId).catch(console.error);
},
onNewSession: () => {
if (!currentSession) return;
setCurrentSession(null);
reMountChatContent();
createFreshSession().catch(console.error);
},
onTogglePin: togglePin,
onOpenDoc: (docId: string, sessionId: string) => {
const { workbench } = framework.get(WorkbenchService);
const viewService = framework.get(ViewService);
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
workbench.openSidebar();
viewService.view.activeSidebarTab('chat');
onOpenSessionDoc(docId, sessionId);
},
onSessionDelete: (sessionToDelete: BlockSuitePresets.AIRecentSession) => {
deleteSession(sessionToDelete).catch(console.error);
@@ -349,12 +376,11 @@ export const Component = () => {
onOpenSession,
togglePin,
workspaceId,
confirmModal,
framework,
onOpenSessionDoc,
deleteSession,
status,
reMountChatContent,
notificationService,
createFreshSession,
]);
useEffect(() => {
@@ -375,30 +401,51 @@ export const Component = () => {
// restore pinned session
useEffect(() => {
if (hasRestoredPinnedSessionRef.current || currentSession) return;
hasRestoredPinnedSessionRef.current = true;
const controller = new AbortController();
const signal = controller.signal;
client
.getSessions(
workspaceId,
{},
undefined,
{ pinned: true, limit: 1 },
signal
)
.then(sessions => {
if (!Array.isArray(sessions)) return;
const session = sessions[0];
if (!session) return;
setCurrentSession(session);
reMountChatContent();
})
.catch(console.error);
const loadPinnedSession = async () => {
try {
const sessions = await client.getSessions(
workspaceId,
{},
undefined,
{ pinned: true, limit: 1 },
controller.signal
);
if (controller.signal.aborted || !Array.isArray(sessions)) {
return;
}
const pinnedSession = sessions[0];
if (!pinnedSession) {
return;
}
let shouldRemount = false;
setCurrentSession(prev => {
if (prev) return prev;
shouldRemount = true;
return pinnedSession;
});
if (shouldRemount) reMountChatContent();
} catch (error) {
if (controller.signal.aborted) {
return;
}
console.error(error);
}
};
loadPinnedSession().catch(error => {
if (controller.signal.aborted) return;
console.error(error);
});
// abort the request
return () => {
controller.abort();
};
}, [client, reMountChatContent, workspaceId]);
}, [client, currentSession, reMountChatContent, workspaceId]);
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
if (node) {
@@ -97,7 +97,9 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
const chatContainerRef = useRef<HTMLDivElement | null>(null);
const chatToolbarContainerRef = useRef<HTMLDivElement | null>(null);
const contentKeyRef = useRef<string | null>(null);
const prevSessionIdRef = useRef<string | null>(null);
const lastDocIdRef = useRef<string | null>(null);
const sessionLoadSeqRef = useRef(0);
const doc = editor?.doc;
const host = editor?.host;
@@ -127,12 +129,14 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
}, [appSidebarConfig]);
const resetPanel = useCallback(() => {
sessionLoadSeqRef.current += 1;
setSession(undefined);
setEmbeddingProgress([0, 0]);
setHasPinned(false);
}, []);
const initPanel = useCallback(async () => {
const requestSeq = ++sessionLoadSeqRef.current;
try {
const nextSession = await resolveInitialSession({
sessionService: AIProvider.session ?? undefined,
@@ -140,6 +144,7 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
workbench: workbench as WorkbenchLike,
});
if (requestSeq !== sessionLoadSeqRef.current) return;
if (nextSession === undefined) {
return;
}
@@ -156,22 +161,18 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
if (session || !AIProvider.session || !doc) {
return session ?? undefined;
}
const sessionId = await AIProvider.session.createSession({
const requestSeq = ++sessionLoadSeqRef.current;
const nextSession = await AIProvider.session.createSessionWithHistory({
docId: doc.id,
workspaceId: doc.workspace.id,
promptName: 'Chat With AFFiNE AI',
reuseLatestChat: false,
...options,
});
if (sessionId) {
const nextSession = await AIProvider.session.getSession(
doc.workspace.id,
sessionId
);
setSession(nextSession ?? null);
return nextSession ?? undefined;
}
return session ?? undefined;
if (requestSeq !== sessionLoadSeqRef.current) return undefined;
setSession(nextSession ?? null);
setHasPinned(!!nextSession?.pinned);
return nextSession ?? undefined;
},
[doc, session]
);
@@ -181,37 +182,64 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
if (!AIProvider.session || !doc) {
return undefined;
}
const requestSeq = ++sessionLoadSeqRef.current;
await AIProvider.session.updateSession(options);
const nextSession = await AIProvider.session.getSession(
doc.workspace.id,
options.sessionId
);
if (requestSeq !== sessionLoadSeqRef.current) return undefined;
setSession(nextSession ?? null);
setHasPinned(!!nextSession?.pinned);
return nextSession ?? undefined;
},
[doc]
);
const newSession = useCallback(() => {
const newSession = useCallback(async () => {
resetPanel();
requestAnimationFrame(() => {
setSession(null);
});
}, [resetPanel]);
const requestSeq = sessionLoadSeqRef.current;
setSession(null);
if (!AIProvider.session || !doc) {
return;
}
try {
const nextSession = await AIProvider.session.createSessionWithHistory({
docId: doc.id,
workspaceId: doc.workspace.id,
promptName: 'Chat With AFFiNE AI',
reuseLatestChat: false,
});
if (requestSeq === sessionLoadSeqRef.current) {
setSession(nextSession ?? null);
setHasPinned(!!nextSession?.pinned);
}
} catch (error) {
console.error(error);
}
}, [doc, resetPanel]);
const openSession = useCallback(
async (sessionId: string) => {
if (session?.sessionId === sessionId || !AIProvider.session || !doc) {
return;
}
resetPanel();
const nextSession = await AIProvider.session.getSession(
doc.workspace.id,
sessionId
);
setSession(nextSession ?? null);
const requestSeq = ++sessionLoadSeqRef.current;
try {
const nextSession = await AIProvider.session.getSession(
doc.workspace.id,
sessionId
);
if (requestSeq !== sessionLoadSeqRef.current) return;
setSession(nextSession ?? null);
setHasPinned(!!nextSession?.pinned);
} catch (error) {
console.error(error);
}
},
[doc, resetPanel, session?.sessionId]
[doc, session?.sessionId]
);
const openDoc = useCallback(
@@ -252,7 +280,9 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
},
isActiveSession: sessionToDelete =>
sessionToDelete.sessionId === session?.sessionId,
onActiveSessionDeleted: newSession,
onActiveSessionDeleted: () => {
newSession().catch(console.error);
},
}),
[newSession, notificationService, session?.sessionId, t]
);
@@ -342,35 +372,33 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
if (!doc || session !== undefined) {
return;
}
let cancelled = false;
let timerId: ReturnType<typeof setTimeout> | null = null;
const tryInit = () => {
if (cancelled || session !== undefined) {
return;
}
// Session service may be registered after the panel mounts.
if (AIProvider.session) {
initPanel().catch(console.error);
return;
}
timerId = setTimeout(tryInit, 200);
};
tryInit();
return () => {
cancelled = true;
if (timerId) {
clearTimeout(timerId);
}
};
if (AIProvider.session) {
initPanel().catch(console.error);
return;
}
const subscription = AIProvider.slots.sessionReady.subscribe(ready => {
if (!ready || session !== undefined) return;
initPanel().catch(console.error);
});
return () => subscription.unsubscribe();
}, [doc, initPanel, session]);
const contentKey = hasPinned
? (session?.sessionId ?? doc?.id ?? 'chat-panel')
: (doc?.id ?? 'chat-panel');
const hasSessionHistory = !!session?.messages?.length;
const sessionSwitched = !!(
session?.sessionId &&
prevSessionIdRef.current &&
prevSessionIdRef.current !== session.sessionId
);
const contentKey =
hasPinned || (session?.sessionId && (hasSessionHistory || sessionSwitched))
? (session?.sessionId ?? doc?.id ?? 'chat-panel')
: (doc?.id ?? 'chat-panel');
useEffect(() => {
if (session?.sessionId) {
prevSessionIdRef.current = session.sessionId;
}
}, [session?.sessionId]);
useEffect(() => {
if (!chatContent) {
@@ -469,7 +497,9 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
status,
docDisplayConfig,
notificationService,
onNewSession: newSession,
onNewSession: () => {
newSession().catch(console.error);
},
onTogglePin: togglePin,
onOpenSession: (sessionId: string) => {
openSession(sessionId).catch(console.error);