mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
feat: refactor copilot module (#14537)
This commit is contained in:
@@ -411,6 +411,9 @@ declare global {
|
||||
|
||||
interface AISessionService {
|
||||
createSession: (options: AICreateSessionOptions) => Promise<string>;
|
||||
createSessionWithHistory: (
|
||||
options: AICreateSessionOptions
|
||||
) => Promise<CopilotChatHistoryFragment | undefined>;
|
||||
getSession: (
|
||||
workspaceId: string,
|
||||
sessionId: string
|
||||
|
||||
+13
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user