refactor(core): add useAIChatConfig hook (#11424)

Close [BS-2583](https://linear.app/affine-design/issue/BS-2583).
This commit is contained in:
akumatus
2025-04-03 14:53:49 +00:00
parent 363476a46c
commit 6033baeb86
6 changed files with 175 additions and 101 deletions

View File

@@ -100,6 +100,9 @@ export class ChatPanelChips extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig; accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor portalContainer: HTMLElement | null = null;
@property({ attribute: 'data-testid', reflect: true }) @property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-panel-chips'; accessor testId = 'chat-panel-chips';
@@ -267,7 +270,7 @@ export class ChatPanelChips extends SignalWatcher(
portalStyles: { portalStyles: {
zIndex: 'var(--affine-z-index-popover)', zIndex: 'var(--affine-z-index-popover)',
}, },
container: document.body, container: this.portalContainer ?? document.body,
computePosition: { computePosition: {
referenceElement: this.addButton, referenceElement: this.addButton,
placement: 'top-start', placement: 'top-start',
@@ -306,7 +309,7 @@ export class ChatPanelChips extends SignalWatcher(
portalStyles: { portalStyles: {
zIndex: 'var(--affine-z-index-popover)', zIndex: 'var(--affine-z-index-popover)',
}, },
container: document.body, container: this.portalContainer ?? document.body,
computePosition: { computePosition: {
referenceElement: this.moreCandidateButton, referenceElement: this.moreCandidateButton,
placement: 'top-start', placement: 'top-start',

View File

@@ -23,6 +23,11 @@ import {
queryHistoryMessages, queryHistoryMessages,
} from '../_common/chat-actions-handle'; } from '../_common/chat-actions-handle';
import { type AIChatBlockModel } from '../blocks'; import { type AIChatBlockModel } from '../blocks';
import type {
ChatChip,
DocDisplayConfig,
SearchMenuConfig,
} from '../components/ai-chat-chips';
import type { AINetworkSearchConfig } from '../components/ai-chat-input'; import type { AINetworkSearchConfig } from '../components/ai-chat-input';
import type { ChatMessage } from '../components/ai-chat-messages'; import type { ChatMessage } from '../components/ai-chat-messages';
import { ChatMessagesSchema } from '../components/ai-chat-messages'; import { ChatMessagesSchema } from '../components/ai-chat-messages';
@@ -155,6 +160,16 @@ export class AIChatBlockPeekView extends LitElement {
}; };
private readonly _getContextId = async () => { private readonly _getContextId = async () => {
if (this._chatContextId) {
return this._chatContextId;
}
const sessionId = await this._getSessionId();
if (sessionId) {
this._chatContextId = await AIProvider.context?.createContext(
this.host.doc.workspace.id,
sessionId
);
}
return this._chatContextId; return this._chatContextId;
}; };
@@ -273,6 +288,10 @@ export class AIChatBlockPeekView extends LitElement {
this.chatContext = { ...this.chatContext, ...context }; this.chatContext = { ...this.chatContext, ...context };
}; };
updateChips = (chips: ChatChip[]) => {
this.chips = chips;
};
/** /**
* Clean current chat messages and delete the newly created AI chat block * Clean current chat messages and delete the newly created AI chat block
*/ */
@@ -524,6 +543,7 @@ export class AIChatBlockPeekView extends LitElement {
</div> </div>
<chat-block-input <chat-block-input
.host=${host} .host=${host}
.chips=${this.chips}
.getSessionId=${this._getSessionId} .getSessionId=${this._getSessionId}
.getContextId=${this._getContextId} .getContextId=${this._getContextId}
.getBlockId=${this._getBlockId} .getBlockId=${this._getBlockId}
@@ -533,6 +553,7 @@ export class AIChatBlockPeekView extends LitElement {
.chatContextValue=${chatContext} .chatContextValue=${chatContext}
.updateContext=${updateContext} .updateContext=${updateContext}
.networkSearchConfig=${networkSearchConfig} .networkSearchConfig=${networkSearchConfig}
.docDisplayConfig=${this.docDisplayConfig}
></chat-block-input> ></chat-block-input>
<div class="peek-view-footer"> <div class="peek-view-footer">
${InformationIcon()} ${InformationIcon()}
@@ -556,6 +577,12 @@ export class AIChatBlockPeekView extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig; accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig;
@state() @state()
accessor _historyMessages: ChatMessage[] = []; accessor _historyMessages: ChatMessage[] = [];
@@ -567,6 +594,9 @@ export class AIChatBlockPeekView extends LitElement {
abortController: null, abortController: null,
messages: [], messages: [],
}; };
@state()
accessor chips: ChatChip[] = [];
} }
declare global { declare global {
@@ -579,6 +609,8 @@ export const AIChatBlockPeekViewTemplate = (
parentModel: AIChatBlockModel, parentModel: AIChatBlockModel,
host: EditorHost, host: EditorHost,
previewSpecBuilder: SpecBuilder, previewSpecBuilder: SpecBuilder,
docDisplayConfig: DocDisplayConfig,
searchMenuConfig: SearchMenuConfig,
networkSearchConfig: AINetworkSearchConfig networkSearchConfig: AINetworkSearchConfig
) => { ) => {
return html`<ai-chat-block-peek-view return html`<ai-chat-block-peek-view
@@ -586,5 +618,7 @@ export const AIChatBlockPeekViewTemplate = (
.host=${host} .host=${host}
.previewSpecBuilder=${previewSpecBuilder} .previewSpecBuilder=${previewSpecBuilder}
.networkSearchConfig=${networkSearchConfig} .networkSearchConfig=${networkSearchConfig}
.docDisplayConfig=${docDisplayConfig}
.searchMenuConfig=${searchMenuConfig}
></ai-chat-block-peek-view>`; ></ai-chat-block-peek-view>`;
}; };

View File

@@ -8,11 +8,9 @@ export const PeekViewStyles = css`
} }
.ai-chat-block-peek-view-container { .ai-chat-block-peek-view-container {
gap: 8px;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center;
box-sizing: border-box; box-sizing: border-box;
justify-content: start; justify-content: start;
flex-direction: column; flex-direction: column;
@@ -61,7 +59,7 @@ export const PeekViewStyles = css`
} }
.peek-view-footer { .peek-view-footer {
padding: 0 12px; margin-top: 8px;
width: 100%; width: 100%;
height: 20px; height: 20px;
display: flex; display: flex;

View File

@@ -0,0 +1,115 @@
// packages/frontend/core/src/blocksuite/ai/hooks/useChatPanelConfig.ts
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
import { CollectionService } from '@affine/core/modules/collection';
import { DocsService } from '@affine/core/modules/doc';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import {
type SearchCollectionMenuAction,
type SearchDocMenuAction,
SearchMenuService,
type SearchTagMenuAction,
} from '@affine/core/modules/search-menu/services';
import { TagService } from '@affine/core/modules/tag';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
import { useFramework } from '@toeverything/infra';
export function useAIChatConfig() {
const framework = useFramework();
const searchService = framework.get(AINetworkSearchService);
const docDisplayMetaService = framework.get(DocDisplayMetaService);
const workspaceService = framework.get(WorkspaceService);
const searchMenuService = framework.get(SearchMenuService);
const docsSearchService = framework.get(DocsSearchService);
const tagService = framework.get(TagService);
const collectionService = framework.get(CollectionService);
const docsService = framework.get(DocsService);
const networkSearchConfig = {
visible: searchService.visible,
enabled: searchService.enabled,
setEnabled: searchService.setEnabled,
};
const docDisplayConfig = {
getIcon: (docId: string) => {
return docDisplayMetaService.icon$(docId, { type: 'lit' }).value;
},
getTitle: (docId: string) => {
return docDisplayMetaService.title$(docId).value;
},
getTitleSignal: (docId: string) => {
const title$ = docDisplayMetaService.title$(docId);
return createSignalFromObservable(title$, '');
},
getDocMeta: (docId: string) => {
const docRecord = docsService.list.doc$(docId).value;
return docRecord?.meta$.value ?? null;
},
getDocPrimaryMode: (docId: string) => {
const docRecord = docsService.list.doc$(docId).value;
return docRecord?.primaryMode$.value ?? 'page';
},
getDoc: (docId: string) => {
const doc = workspaceService.workspace.docCollection.getDoc(docId);
return doc?.getStore() ?? null;
},
getReferenceDocs: (docIds: string[]) => {
const docs$ = docsSearchService.watchRefsFrom(docIds);
return createSignalFromObservable(docs$, []);
},
getTags: () => {
const tagMetas$ = tagService.tagList.tagMetas$;
return createSignalFromObservable(tagMetas$, []);
},
getTagTitle: (tagId: string) => {
const tag$ = tagService.tagList.tagByTagId$(tagId);
return tag$.value?.value$.value ?? '';
},
getTagPageIds: (tagId: string) => {
const tag$ = tagService.tagList.tagByTagId$(tagId);
if (!tag$) return [];
return tag$.value?.pageIds$.value ?? [];
},
getCollections: () => {
const collections$ = collectionService.collections$;
return createSignalFromObservable(collections$, []);
},
};
const searchMenuConfig = {
getDocMenuGroup: (
query: string,
action: SearchDocMenuAction,
abortSignal: AbortSignal
) => {
return searchMenuService.getDocMenuGroup(query, action, abortSignal);
},
getTagMenuGroup: (
query: string,
action: SearchTagMenuAction,
abortSignal: AbortSignal
) => {
return searchMenuService.getTagMenuGroup(query, action, abortSignal);
},
getCollectionMenuGroup: (
query: string,
action: SearchCollectionMenuAction,
abortSignal: AbortSignal
) => {
return searchMenuService.getCollectionMenuGroup(
query,
action,
abortSignal
);
},
};
return {
networkSearchConfig,
docDisplayConfig,
searchMenuConfig,
};
}

View File

@@ -1,15 +1,8 @@
import { ChatPanel } from '@affine/core/blocksuite/ai'; import { ChatPanel } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor'; import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
import { enableFootnoteConfigExtension } from '@affine/core/blocksuite/extensions'; import { enableFootnoteConfigExtension } from '@affine/core/blocksuite/extensions';
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search'; import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
import { CollectionService } from '@affine/core/modules/collection';
import { DocsService } from '@affine/core/modules/doc';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import { SearchMenuService } from '@affine/core/modules/search-menu/services';
import { TagService } from '@affine/core/modules/tag';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import { DocModeProvider } from '@blocksuite/affine/shared/services'; import { DocModeProvider } from '@blocksuite/affine/shared/services';
import { import {
@@ -51,6 +44,9 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
} }
}, [onLoad, ref]); }, [onLoad, ref]);
const { docDisplayConfig, searchMenuConfig, networkSearchConfig } =
useAIChatConfig();
useEffect(() => { useEffect(() => {
if (!editor || !editor.host) return; if (!editor || !editor.host) return;
@@ -59,16 +55,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
chatPanelRef.current.host = editor.host; chatPanelRef.current.host = editor.host;
chatPanelRef.current.doc = editor.doc; chatPanelRef.current.doc = editor.doc;
const searchService = framework.get(AINetworkSearchService);
const docDisplayMetaService = framework.get(DocDisplayMetaService);
const workspaceService = framework.get(WorkspaceService);
const searchMenuService = framework.get(SearchMenuService);
const workbench = framework.get(WorkbenchService).workbench; const workbench = framework.get(WorkbenchService).workbench;
const docsSearchService = framework.get(DocsSearchService);
const tagService = framework.get(TagService);
const collectionService = framework.get(CollectionService);
const docsService = framework.get(DocsService);
chatPanelRef.current.appSidebarConfig = { chatPanelRef.current.appSidebarConfig = {
getWidth: () => { getWidth: () => {
const width$ = workbench.sidebarWidth$; const width$ = workbench.sidebarWidth$;
@@ -80,74 +67,9 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
}, },
}; };
chatPanelRef.current.networkSearchConfig = { chatPanelRef.current.docDisplayConfig = docDisplayConfig;
visible: searchService.visible, chatPanelRef.current.searchMenuConfig = searchMenuConfig;
enabled: searchService.enabled, chatPanelRef.current.networkSearchConfig = networkSearchConfig;
setEnabled: searchService.setEnabled,
};
chatPanelRef.current.docDisplayConfig = {
getIcon: (docId: string) => {
return docDisplayMetaService.icon$(docId, { type: 'lit' }).value;
},
getTitle: (docId: string) => {
return docDisplayMetaService.title$(docId).value;
},
getTitleSignal: (docId: string) => {
const title$ = docDisplayMetaService.title$(docId);
return createSignalFromObservable(title$, '');
},
getDocMeta: (docId: string) => {
const docRecord = docsService.list.doc$(docId).value;
return docRecord?.meta$.value ?? null;
},
getDocPrimaryMode: (docId: string) => {
const docRecord = docsService.list.doc$(docId).value;
return docRecord?.primaryMode$.value ?? 'page';
},
getDoc: (docId: string) => {
const doc = workspaceService.workspace.docCollection.getDoc(docId);
return doc?.getStore() ?? null;
},
getReferenceDocs: (docIds: string[]) => {
const docs$ = docsSearchService.watchRefsFrom(docIds);
return createSignalFromObservable(docs$, []);
},
getTags: () => {
const tagMetas$ = tagService.tagList.tagMetas$;
return createSignalFromObservable(tagMetas$, []);
},
getTagTitle: (tagId: string) => {
const tag$ = tagService.tagList.tagByTagId$(tagId);
return tag$.value?.value$.value ?? '';
},
getTagPageIds: (tagId: string) => {
const tag$ = tagService.tagList.tagByTagId$(tagId);
if (!tag$) return [];
return tag$.value?.pageIds$.value ?? [];
},
getCollections: () => {
const collections$ = collectionService.collections$;
return createSignalFromObservable(collections$, []);
},
};
chatPanelRef.current.searchMenuConfig = {
getDocMenuGroup: (query, action, abortSignal) => {
return searchMenuService.getDocMenuGroup(query, action, abortSignal);
},
getTagMenuGroup: (query, action, abortSignal) => {
return searchMenuService.getTagMenuGroup(query, action, abortSignal);
},
getCollectionMenuGroup: (query, action, abortSignal) => {
return searchMenuService.getCollectionMenuGroup(
query,
action,
abortSignal
);
},
};
chatPanelRef.current.previewSpecBuilder = enableFootnoteConfigExtension( chatPanelRef.current.previewSpecBuilder = enableFootnoteConfigExtension(
SpecProvider._.getSpec('preview:page') SpecProvider._.getSpec('preview:page')
); );
@@ -173,7 +95,13 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
]; ];
return () => disposable.forEach(d => d?.unsubscribe()); return () => disposable.forEach(d => d?.unsubscribe());
}, [editor, framework]); }, [
docDisplayConfig,
editor,
framework,
networkSearchConfig,
searchMenuConfig,
]);
return <div className={styles.root} ref={containerRef} />; return <div className={styles.root} ref={containerRef} />;
}); });

View File

@@ -2,10 +2,9 @@ import { toReactNode } from '@affine/component';
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/ai'; import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/ai';
import type { AIChatBlockModel } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/model/ai-chat-model'; import type { AIChatBlockModel } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/model/ai-chat-model';
import { enableFootnoteConfigExtension } from '@affine/core/blocksuite/extensions'; import { enableFootnoteConfigExtension } from '@affine/core/blocksuite/extensions';
import { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search'; import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
import { SpecProvider } from '@blocksuite/affine/shared/utils'; import { SpecProvider } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { useFramework } from '@toeverything/infra';
import { useMemo } from 'react'; import { useMemo } from 'react';
export type AIChatBlockPeekViewProps = { export type AIChatBlockPeekViewProps = {
@@ -17,23 +16,20 @@ export const AIChatBlockPeekView = ({
model, model,
host, host,
}: AIChatBlockPeekViewProps) => { }: AIChatBlockPeekViewProps) => {
const framework = useFramework(); const { docDisplayConfig, searchMenuConfig, networkSearchConfig } =
const searchService = framework.get(AINetworkSearchService); useAIChatConfig();
return useMemo(() => { return useMemo(() => {
const previewSpecBuilder = enableFootnoteConfigExtension( const previewSpecBuilder = enableFootnoteConfigExtension(
SpecProvider._.getSpec('preview:page') SpecProvider._.getSpec('preview:page')
); );
const networkSearchConfig = {
visible: searchService.visible,
enabled: searchService.enabled,
setEnabled: searchService.setEnabled,
};
const template = AIChatBlockPeekViewTemplate( const template = AIChatBlockPeekViewTemplate(
model, model,
host, host,
previewSpecBuilder, previewSpecBuilder,
docDisplayConfig,
searchMenuConfig,
networkSearchConfig networkSearchConfig
); );
return toReactNode(template); return toReactNode(template);
}, [model, host, searchService]); }, [model, host, docDisplayConfig, searchMenuConfig, networkSearchConfig]);
}; };