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