mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
feat(core): support collection search for ai chat (#10987)
Close [BS-2787](https://linear.app/affine-design/issue/BS-2787). Close [BS-2788](https://linear.app/affine-design/issue/BS-2788). 
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
SearchCollectionMenuAction,
|
||||||
SearchDocMenuAction,
|
SearchDocMenuAction,
|
||||||
SearchTagMenuAction,
|
SearchTagMenuAction,
|
||||||
} from '@affine/core/modules/search-menu/services';
|
} from '@affine/core/modules/search-menu/services';
|
||||||
@@ -43,4 +44,9 @@ export interface SearchMenuConfig {
|
|||||||
action: SearchTagMenuAction,
|
action: SearchTagMenuAction,
|
||||||
abortSignal: AbortSignal
|
abortSignal: AbortSignal
|
||||||
) => LinkedMenuGroup;
|
) => LinkedMenuGroup;
|
||||||
|
getCollectionMenuGroup: (
|
||||||
|
query: string,
|
||||||
|
action: SearchCollectionMenuAction,
|
||||||
|
abortSignal: AbortSignal
|
||||||
|
) => LinkedMenuGroup;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import type { TagMeta } from '@affine/core/components/page-list';
|
import type {
|
||||||
|
CollectionMeta,
|
||||||
|
TagMeta,
|
||||||
|
} from '@affine/core/components/page-list';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
import { ShadowlessElement } from '@blocksuite/affine/block-std';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||||
@@ -314,6 +317,13 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
this.abortController.signal
|
this.abortController.signal
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case AddPopoverMode.Collections:
|
||||||
|
this._searchGroup = this.searchMenuConfig.getCollectionMenuGroup(
|
||||||
|
this._query,
|
||||||
|
this._addCollectionChip,
|
||||||
|
this.abortController.signal
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this._searchGroup = this.searchMenuConfig.getDocMenuGroup(
|
this._searchGroup = this.searchMenuConfig.getDocMenuGroup(
|
||||||
this._query,
|
this._query,
|
||||||
@@ -335,6 +345,10 @@ export class ChatPanelAddPopover extends SignalWatcher(
|
|||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly _addCollectionChip = (_collection: CollectionMeta) => {
|
||||||
|
this.abortController.abort();
|
||||||
|
};
|
||||||
|
|
||||||
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.isComposing) return;
|
if (event.isComposing) return;
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
|||||||
getTagMenuGroup: (query, action, abortSignal) => {
|
getTagMenuGroup: (query, action, abortSignal) => {
|
||||||
return searchMenuService.getTagMenuGroup(query, action, abortSignal);
|
return searchMenuService.getTagMenuGroup(query, action, abortSignal);
|
||||||
},
|
},
|
||||||
|
getCollectionMenuGroup: (query, action, abortSignal) => {
|
||||||
|
return searchMenuService.getCollectionMenuGroup(
|
||||||
|
query,
|
||||||
|
action,
|
||||||
|
abortSignal
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const previewSpecBuilder = enableFootnoteConfigExtension(
|
const previewSpecBuilder = enableFootnoteConfigExtension(
|
||||||
SpecProvider._.getSpec('preview:page')
|
SpecProvider._.getSpec('preview:page')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { CollectionService } from '../collection';
|
||||||
import { DocDisplayMetaService } from '../doc-display-meta';
|
import { DocDisplayMetaService } from '../doc-display-meta';
|
||||||
import { DocsSearchService } from '../docs-search';
|
import { DocsSearchService } from '../docs-search';
|
||||||
import { RecentDocsService } from '../quicksearch';
|
import { RecentDocsService } from '../quicksearch';
|
||||||
@@ -16,5 +17,6 @@ export function configSearchMenuModule(framework: Framework) {
|
|||||||
RecentDocsService,
|
RecentDocsService,
|
||||||
DocsSearchService,
|
DocsSearchService,
|
||||||
TagService,
|
TagService,
|
||||||
|
CollectionService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { TagMeta } from '@affine/core/components/page-list';
|
import type {
|
||||||
|
CollectionMeta,
|
||||||
|
TagMeta,
|
||||||
|
} from '@affine/core/components/page-list';
|
||||||
import { fuzzyMatch } from '@affine/core/utils/fuzzy-match';
|
import { fuzzyMatch } from '@affine/core/utils/fuzzy-match';
|
||||||
import { I18n } from '@affine/i18n';
|
import { I18n } from '@affine/i18n';
|
||||||
import type {
|
import type {
|
||||||
@@ -7,14 +10,16 @@ import type {
|
|||||||
} from '@blocksuite/affine/blocks/root';
|
} from '@blocksuite/affine/blocks/root';
|
||||||
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
|
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
||||||
import { computed } from '@preact/signals-core';
|
import { computed } from '@preact/signals-core';
|
||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse, { type FuseResultMatch } from 'fuse.js';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||||
import { map, takeWhile } from 'rxjs';
|
import { map, takeWhile } from 'rxjs';
|
||||||
|
|
||||||
|
import type { CollectionService } from '../../collection';
|
||||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||||
import type { DocsSearchService } from '../../docs-search';
|
import type { DocsSearchService } from '../../docs-search';
|
||||||
import { type RecentDocsService } from '../../quicksearch';
|
import { type RecentDocsService } from '../../quicksearch';
|
||||||
@@ -32,13 +37,18 @@ export type SearchDocMenuAction = (meta: DocMeta) => Promise<void> | void;
|
|||||||
|
|
||||||
export type SearchTagMenuAction = (tagId: TagMeta) => Promise<void> | void;
|
export type SearchTagMenuAction = (tagId: TagMeta) => Promise<void> | void;
|
||||||
|
|
||||||
|
export type SearchCollectionMenuAction = (
|
||||||
|
collection: CollectionMeta
|
||||||
|
) => Promise<void> | void;
|
||||||
|
|
||||||
export class SearchMenuService extends Service {
|
export class SearchMenuService extends Service {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly docDisplayMetaService: DocDisplayMetaService,
|
private readonly docDisplayMetaService: DocDisplayMetaService,
|
||||||
private readonly recentDocsService: RecentDocsService,
|
private readonly recentDocsService: RecentDocsService,
|
||||||
private readonly docsSearch: DocsSearchService,
|
private readonly docsSearch: DocsSearchService,
|
||||||
private readonly tagService: TagService
|
private readonly tagService: TagService,
|
||||||
|
private readonly collectionService: CollectionService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -218,13 +228,38 @@ export class SearchMenuService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private highlightFuseTitle(
|
||||||
|
matches: readonly FuseResultMatch[] | undefined,
|
||||||
|
title: string,
|
||||||
|
key: string
|
||||||
|
): string {
|
||||||
|
if (!matches) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
const normalizedRange = ([start, end]: [number, number]) =>
|
||||||
|
[
|
||||||
|
start,
|
||||||
|
end + 1 /* in fuse, the `end` is different from the `substring` */,
|
||||||
|
] as [number, number];
|
||||||
|
const titleMatches = matches
|
||||||
|
?.filter(match => match.key === key)
|
||||||
|
.flatMap(match => match.indices.map(normalizedRange));
|
||||||
|
return (
|
||||||
|
highlighter(
|
||||||
|
title,
|
||||||
|
`<span style="color: ${cssVarV2('text/emphasis')}">`,
|
||||||
|
'</span>',
|
||||||
|
titleMatches ?? []
|
||||||
|
) ?? title
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getTagMenuGroup(
|
getTagMenuGroup(
|
||||||
query: string,
|
query: string,
|
||||||
action: SearchTagMenuAction,
|
action: SearchTagMenuAction,
|
||||||
_abortSignal: AbortSignal
|
_abortSignal: AbortSignal
|
||||||
): LinkedMenuGroup {
|
): LinkedMenuGroup {
|
||||||
const tags: TagMeta[] = this.tagService.tagList.tagMetas$.value;
|
const tags: TagMeta[] = this.tagService.tagList.tagMetas$.value;
|
||||||
|
|
||||||
if (query.trim().length === 0) {
|
if (query.trim().length === 0) {
|
||||||
return {
|
return {
|
||||||
name: I18n.t('com.affine.editor.at-menu.tags', {
|
name: I18n.t('com.affine.editor.at-menu.tags', {
|
||||||
@@ -241,7 +276,6 @@ export class SearchMenuService extends Service {
|
|||||||
ignoreLocation: true,
|
ignoreLocation: true,
|
||||||
threshold: 0.0,
|
threshold: 0.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = fuse.search(query);
|
const result = fuse.search(query);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -249,27 +283,12 @@ export class SearchMenuService extends Service {
|
|||||||
query,
|
query,
|
||||||
}),
|
}),
|
||||||
items: result.map(item => {
|
items: result.map(item => {
|
||||||
const normalizedRange = ([start, end]: [number, number]) =>
|
const title = this.highlightFuseTitle(
|
||||||
[
|
item.matches,
|
||||||
start,
|
|
||||||
end + 1 /* in fuse, the `end` is different from the `substring` */,
|
|
||||||
] as [number, number];
|
|
||||||
const titleMatches = item.matches
|
|
||||||
?.filter(match => match.key === 'title')
|
|
||||||
.flatMap(match => match.indices.map(normalizedRange));
|
|
||||||
const hTitle = highlighter(
|
|
||||||
item.item.title,
|
item.item.title,
|
||||||
`<span style="color: ${cssVarV2('text/emphasis')}">`,
|
'title'
|
||||||
'</span>',
|
|
||||||
titleMatches ?? []
|
|
||||||
);
|
|
||||||
return this.toTagMenuItem(
|
|
||||||
{
|
|
||||||
...item.item,
|
|
||||||
title: hTitle ?? item.item.title,
|
|
||||||
},
|
|
||||||
action
|
|
||||||
);
|
);
|
||||||
|
return this.toTagMenuItem({ ...item.item, title }, action);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -294,4 +313,65 @@ export class SearchMenuService extends Service {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCollectionMenuGroup(
|
||||||
|
query: string,
|
||||||
|
action: SearchCollectionMenuAction,
|
||||||
|
_abortSignal: AbortSignal
|
||||||
|
): LinkedMenuGroup {
|
||||||
|
const collections = this.collectionService.collections$.value;
|
||||||
|
if (query.trim().length === 0) {
|
||||||
|
return {
|
||||||
|
name: I18n.t('com.affine.editor.at-menu.collections', {
|
||||||
|
query,
|
||||||
|
}),
|
||||||
|
items: collections.map(collection =>
|
||||||
|
this.toCollectionMenuItem(
|
||||||
|
{
|
||||||
|
...collection,
|
||||||
|
title: collection.name,
|
||||||
|
},
|
||||||
|
action
|
||||||
|
)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuse = new Fuse(collections, {
|
||||||
|
keys: ['name'],
|
||||||
|
includeMatches: true,
|
||||||
|
includeScore: true,
|
||||||
|
ignoreLocation: true,
|
||||||
|
threshold: 0.0,
|
||||||
|
});
|
||||||
|
const result = fuse.search(query);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: I18n.t('com.affine.editor.at-menu.link-to-doc', {
|
||||||
|
query,
|
||||||
|
}),
|
||||||
|
items: result.map(item => {
|
||||||
|
const title = this.highlightFuseTitle(
|
||||||
|
item.matches,
|
||||||
|
item.item.name,
|
||||||
|
'name'
|
||||||
|
);
|
||||||
|
return this.toCollectionMenuItem({ ...item.item, title }, action);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toCollectionMenuItem(
|
||||||
|
collection: CollectionMeta,
|
||||||
|
action: SearchCollectionMenuAction
|
||||||
|
): LinkedMenuItem {
|
||||||
|
return {
|
||||||
|
key: collection.id,
|
||||||
|
name: html`${unsafeHTML(collection.title)}`,
|
||||||
|
icon: CollectionsIcon(),
|
||||||
|
action: async () => {
|
||||||
|
await action(collection);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6833,6 +6833,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Tags`
|
* `Tags`
|
||||||
*/
|
*/
|
||||||
["com.affine.editor.at-menu.tags"](): string;
|
["com.affine.editor.at-menu.tags"](): string;
|
||||||
|
/**
|
||||||
|
* `Collections`
|
||||||
|
*/
|
||||||
|
["com.affine.editor.at-menu.collections"](): string;
|
||||||
/**
|
/**
|
||||||
* `Loading...`
|
* `Loading...`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1698,6 +1698,7 @@
|
|||||||
"com.affine.editor.at-menu.link-to-doc": "Search for \"{{query}}\"",
|
"com.affine.editor.at-menu.link-to-doc": "Search for \"{{query}}\"",
|
||||||
"com.affine.editor.at-menu.recent-docs": "Recent",
|
"com.affine.editor.at-menu.recent-docs": "Recent",
|
||||||
"com.affine.editor.at-menu.tags": "Tags",
|
"com.affine.editor.at-menu.tags": "Tags",
|
||||||
|
"com.affine.editor.at-menu.collections": "Collections",
|
||||||
"com.affine.editor.at-menu.loading": "Loading...",
|
"com.affine.editor.at-menu.loading": "Loading...",
|
||||||
"com.affine.editor.at-menu.new-doc": "New",
|
"com.affine.editor.at-menu.new-doc": "New",
|
||||||
"com.affine.editor.at-menu.create-page": "New \"{{name}}\" page",
|
"com.affine.editor.at-menu.create-page": "New \"{{name}}\" page",
|
||||||
|
|||||||
Reference in New Issue
Block a user