diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-config.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-config.ts
index 1311d97e68..509c5fe037 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-config.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-config.ts
@@ -1,4 +1,7 @@
-import type { SearchDocMenuAction } from '@affine/core/modules/doc-search-menu/services';
+import type {
+ SearchDocMenuAction,
+ SearchTagMenuAction,
+} from '@affine/core/modules/search-menu/services';
import type { LinkedMenuGroup } from '@blocksuite/affine/blocks/root';
import type { Store } from '@blocksuite/affine/store';
import type { Signal } from '@preact/signals-core';
@@ -29,10 +32,15 @@ export interface DocDisplayConfig {
getDoc: (docId: string) => Store | null;
}
-export interface DocSearchMenuConfig {
+export interface SearchMenuConfig {
getDocMenuGroup: (
query: string,
action: SearchDocMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
+ getTagMenuGroup: (
+ query: string,
+ action: SearchTagMenuAction,
+ abortSignal: AbortSignal
+ ) => LinkedMenuGroup;
}
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-chips.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-chips.ts
index 371bf7b097..0c49d33e80 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-chips.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-chips.ts
@@ -11,7 +11,7 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { AIProvider } from '../provider';
-import type { DocDisplayConfig, DocSearchMenuConfig } from './chat-config';
+import type { DocDisplayConfig, SearchMenuConfig } from './chat-config';
import type {
ChatChip,
ChatContextValue,
@@ -75,7 +75,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
- accessor docSearchMenuConfig!: DocSearchMenuConfig;
+ accessor searchMenuConfig!: SearchMenuConfig;
@query('.add-button')
accessor addButton!: HTMLDivElement;
@@ -158,7 +158,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
template: html`
`,
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/components/add-popover.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/components/add-popover.ts
index e72959702a..42b4f57438 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/components/add-popover.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/components/add-popover.ts
@@ -1,4 +1,5 @@
import { toast } from '@affine/component';
+import type { TagMeta } from '@affine/core/components/page-list';
import { ShadowlessElement } from '@blocksuite/affine/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
@@ -13,7 +14,7 @@ import type { DocMeta } from '@blocksuite/store';
import { css, html, type TemplateResult } from 'lit';
import { property, query, state } from 'lit/decorators.js';
-import type { DocSearchMenuConfig } from '../chat-config';
+import type { SearchMenuConfig } from '../chat-config';
import type { ChatChip } from '../chat-context';
enum AddPopoverMode {
@@ -107,11 +108,21 @@ export class ChatPanelAddPopover extends SignalWatcher(
private accessor _query = '';
@state()
- private accessor _docGroup: MenuGroup = {
+ private accessor _searchGroup: MenuGroup = {
name: 'No Result',
items: [],
};
+ private readonly _toggleMode = (mode: AddPopoverMode) => {
+ this._mode = mode;
+ this._activatedIndex = 0;
+ this._query = '';
+ this._updateSearchGroup();
+ requestAnimationFrame(() => {
+ this.searchInput.focus();
+ });
+ };
+
private readonly tcGroup: MenuGroup = {
name: 'Tag & Collection',
items: [
@@ -120,7 +131,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
name: 'Tags',
icon: TagsIcon(),
action: () => {
- this._mode = AddPopoverMode.Tags;
+ this._toggleMode(AddPopoverMode.Tags);
},
},
{
@@ -128,7 +139,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
name: 'Collections',
icon: CollectionsIcon(),
action: () => {
- this._mode = AddPopoverMode.Collections;
+ this._toggleMode(AddPopoverMode.Collections);
},
},
],
@@ -163,14 +174,14 @@ export class ChatPanelAddPopover extends SignalWatcher(
private get _menuGroup() {
switch (this._mode) {
case AddPopoverMode.Tags:
- return [];
+ return [this._searchGroup];
case AddPopoverMode.Collections:
- return [];
+ return [this._searchGroup];
default:
if (this._query) {
- return [this._docGroup, this.uploadGroup];
+ return [this._searchGroup, this.uploadGroup];
}
- return [this._docGroup, this.tcGroup, this.uploadGroup];
+ return [this._searchGroup, this.tcGroup, this.uploadGroup];
}
}
@@ -187,7 +198,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
private accessor _mode: AddPopoverMode = AddPopoverMode.Default;
@property({ attribute: false })
- accessor docSearchMenuConfig!: DocSearchMenuConfig;
+ accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor addChip!: (chip: ChatChip) => void;
@@ -200,7 +211,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
- this._updateDocGroup();
+ this._updateSearchGroup();
this.addEventListener('keydown', this._handleKeyDown);
}
@@ -291,15 +302,25 @@ export class ChatPanelAddPopover extends SignalWatcher(
private _onInput(event: Event) {
this._query = (event.target as HTMLInputElement).value;
this._activatedIndex = 0;
- this._updateDocGroup();
+ this._updateSearchGroup();
}
- private _updateDocGroup() {
- this._docGroup = this.docSearchMenuConfig.getDocMenuGroup(
- this._query,
- this._addDocChip,
- this.abortController.signal
- );
+ private _updateSearchGroup() {
+ switch (this._mode) {
+ case AddPopoverMode.Tags:
+ this._searchGroup = this.searchMenuConfig.getTagMenuGroup(
+ this._query,
+ this._addTagChip,
+ this.abortController.signal
+ );
+ break;
+ default:
+ this._searchGroup = this.searchMenuConfig.getDocMenuGroup(
+ this._query,
+ this._addDocChip,
+ this.abortController.signal
+ );
+ }
}
private readonly _addDocChip = (meta: DocMeta) => {
@@ -310,6 +331,10 @@ export class ChatPanelAddPopover extends SignalWatcher(
this.abortController.abort();
};
+ private readonly _addTagChip = (_tag: TagMeta) => {
+ this.abortController.abort();
+ };
+
private readonly _handleKeyDown = (event: KeyboardEvent) => {
if (event.isComposing) return;
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
index 45b54bc9af..b424346807 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
@@ -26,7 +26,7 @@ import type {
AINetworkSearchConfig,
AppSidebarConfig,
DocDisplayConfig,
- DocSearchMenuConfig,
+ SearchMenuConfig,
} from './chat-config';
import type {
ChatChip,
@@ -269,7 +269,7 @@ export class ChatPanel extends SignalWatcher(
accessor appSidebarConfig!: AppSidebarConfig;
@property({ attribute: false })
- accessor docSearchMenuConfig!: DocSearchMenuConfig;
+ accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@@ -574,7 +574,7 @@ export class ChatPanel extends SignalWatcher(
.updateContext=${this.updateContext}
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
.docDisplayConfig=${this.docDisplayConfig}
- .docSearchMenuConfig=${this.docSearchMenuConfig}
+ .searchMenuConfig=${this.searchMenuConfig}
>
{
@@ -87,13 +87,12 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
return doc;
},
};
- chatPanelRef.current.docSearchMenuConfig = {
+ chatPanelRef.current.searchMenuConfig = {
getDocMenuGroup: (query, action, abortSignal) => {
- return docSearchMenuService.getDocMenuGroup(
- query,
- action,
- abortSignal
- );
+ return searchMenuService.getDocMenuGroup(query, action, abortSignal);
+ },
+ getTagMenuGroup: (query, action, abortSignal) => {
+ return searchMenuService.getTagMenuGroup(query, action, abortSignal);
},
};
const previewSpecBuilder = enableFootnoteConfigExtension(
diff --git a/packages/frontend/core/src/modules/at-menu-config/index.ts b/packages/frontend/core/src/modules/at-menu-config/index.ts
index 17fe8b6749..c54a715268 100644
--- a/packages/frontend/core/src/modules/at-menu-config/index.ts
+++ b/packages/frontend/core/src/modules/at-menu-config/index.ts
@@ -3,9 +3,9 @@ import { type Framework } from '@toeverything/infra';
import { WorkspaceDialogService } from '../dialogs';
import { DocsService } from '../doc';
import { DocDisplayMetaService } from '../doc-display-meta';
-import { DocSearchMenuService } from '../doc-search-menu/services';
import { EditorSettingService } from '../editor-setting';
import { JournalService } from '../journal';
+import { SearchMenuService } from '../search-menu/services';
import { WorkspaceScope } from '../workspace';
import { AtMenuConfigService } from './services';
@@ -18,6 +18,6 @@ export function configAtMenuConfigModule(framework: Framework) {
WorkspaceDialogService,
EditorSettingService,
DocsService,
- DocSearchMenuService,
+ SearchMenuService,
]);
}
diff --git a/packages/frontend/core/src/modules/at-menu-config/services/index.ts b/packages/frontend/core/src/modules/at-menu-config/services/index.ts
index a72ce19f2d..c4928d0958 100644
--- a/packages/frontend/core/src/modules/at-menu-config/services/index.ts
+++ b/packages/frontend/core/src/modules/at-menu-config/services/index.ts
@@ -24,9 +24,9 @@ import { html } from 'lit';
import type { WorkspaceDialogService } from '../../dialogs';
import type { DocsService } from '../../doc';
import type { DocDisplayMetaService } from '../../doc-display-meta';
-import type { DocSearchMenuService } from '../../doc-search-menu/services';
import type { EditorSettingService } from '../../editor-setting';
import { type JournalService, suggestJournalDate } from '../../journal';
+import type { SearchMenuService } from '../../search-menu/services';
function resolveSignal(data: T | Signal): T {
return data instanceof Signal ? data.value : data;
@@ -45,7 +45,7 @@ export class AtMenuConfigService extends Service {
private readonly dialogService: WorkspaceDialogService,
private readonly editorSettingService: EditorSettingService,
private readonly docsService: DocsService,
- private readonly docsSearchMenuService: DocSearchMenuService
+ private readonly searchMenuService: SearchMenuService
) {
super();
}
@@ -292,7 +292,7 @@ export class AtMenuConfigService extends Service {
track.doc.editor.atMenu.linkDoc();
this.insertDoc(inlineEditor, meta.id);
};
- const result = this.docsSearchMenuService.getDocMenuGroup(
+ const result = this.searchMenuService.getDocMenuGroup(
query,
action,
abortSignal
diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts
index fc2a790b17..f7a0e95479 100644
--- a/packages/frontend/core/src/modules/index.ts
+++ b/packages/frontend/core/src/modules/index.ts
@@ -17,7 +17,6 @@ import { configureDocModule } from './doc';
import { configureDocDisplayMetaModule } from './doc-display-meta';
import { configureDocInfoModule } from './doc-info';
import { configureDocLinksModule } from './doc-link';
-import { configDocSearchMenuModule } from './doc-search-menu';
import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor';
import { configureEditorSettingModule } from './editor-setting';
@@ -39,6 +38,7 @@ import { configurePDFModule } from './pdf';
import { configurePeekViewModule } from './peek-view';
import { configurePermissionsModule } from './permissions';
import { configureQuickSearchModule } from './quicksearch';
+import { configSearchMenuModule } from './search-menu';
import { configureShareDocsModule } from './share-doc';
import { configureShareSettingModule } from './share-setting';
import {
@@ -96,7 +96,7 @@ export function configureCommonModules(framework: Framework) {
configureDocInfoModule(framework);
configureOpenInApp(framework);
configAtMenuConfigModule(framework);
- configDocSearchMenuModule(framework);
+ configSearchMenuModule(framework);
configureDndModule(framework);
configureCommonGlobalStorageImpls(framework);
configureAINetworkSearchModule(framework);
diff --git a/packages/frontend/core/src/modules/doc-search-menu/index.ts b/packages/frontend/core/src/modules/search-menu/index.ts
similarity index 68%
rename from packages/frontend/core/src/modules/doc-search-menu/index.ts
rename to packages/frontend/core/src/modules/search-menu/index.ts
index 8c883fd40b..0f2b86544e 100644
--- a/packages/frontend/core/src/modules/doc-search-menu/index.ts
+++ b/packages/frontend/core/src/modules/search-menu/index.ts
@@ -3,16 +3,18 @@ import { type Framework } from '@toeverything/infra';
import { DocDisplayMetaService } from '../doc-display-meta';
import { DocsSearchService } from '../docs-search';
import { RecentDocsService } from '../quicksearch';
+import { TagService } from '../tag';
import { WorkspaceScope, WorkspaceService } from '../workspace';
-import { DocSearchMenuService } from './services';
+import { SearchMenuService } from './services';
-export function configDocSearchMenuModule(framework: Framework) {
+export function configSearchMenuModule(framework: Framework) {
framework
.scope(WorkspaceScope)
- .service(DocSearchMenuService, [
+ .service(SearchMenuService, [
WorkspaceService,
DocDisplayMetaService,
RecentDocsService,
DocsSearchService,
+ TagService,
]);
}
diff --git a/packages/frontend/core/src/modules/doc-search-menu/services/index.ts b/packages/frontend/core/src/modules/search-menu/services/index.ts
similarity index 69%
rename from packages/frontend/core/src/modules/doc-search-menu/services/index.ts
rename to packages/frontend/core/src/modules/search-menu/services/index.ts
index 31b086c6d1..07f9cf245e 100644
--- a/packages/frontend/core/src/modules/doc-search-menu/services/index.ts
+++ b/packages/frontend/core/src/modules/search-menu/services/index.ts
@@ -1,3 +1,4 @@
+import type { TagMeta } from '@affine/core/components/page-list';
import { fuzzyMatch } from '@affine/core/utils/fuzzy-match';
import { I18n } from '@affine/i18n';
import type {
@@ -9,13 +10,16 @@ import type { DocMeta } from '@blocksuite/affine/store';
import { computed } from '@preact/signals-core';
import { Service } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
+import Fuse from 'fuse.js';
import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { map, takeWhile } from 'rxjs';
import type { DocDisplayMetaService } from '../../doc-display-meta';
import type { DocsSearchService } from '../../docs-search';
-import type { RecentDocsService } from '../../quicksearch';
+import { type RecentDocsService } from '../../quicksearch';
+import { highlighter } from '../../quicksearch/utils/highlighter';
+import type { TagService } from '../../tag';
import type { WorkspaceService } from '../../workspace';
const MAX_DOCS = 3;
@@ -26,12 +30,15 @@ type DocMetaWithHighlights = DocMeta & {
export type SearchDocMenuAction = (meta: DocMeta) => Promise | void;
-export class DocSearchMenuService extends Service {
+export type SearchTagMenuAction = (tagId: TagMeta) => Promise | void;
+
+export class SearchMenuService extends Service {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly docDisplayMetaService: DocDisplayMetaService,
private readonly recentDocsService: RecentDocsService,
- private readonly docsSearch: DocsSearchService
+ private readonly docsSearch: DocsSearchService,
+ private readonly tagService: TagService
) {
super();
}
@@ -210,4 +217,81 @@ export class DocSearchMenuService extends Service {
},
};
}
+
+ getTagMenuGroup(
+ query: string,
+ action: SearchTagMenuAction,
+ _abortSignal: AbortSignal
+ ): LinkedMenuGroup {
+ const tags: TagMeta[] = this.tagService.tagList.tagMetas$.value;
+
+ if (query.trim().length === 0) {
+ return {
+ name: I18n.t('com.affine.editor.at-menu.tags', {
+ query,
+ }),
+ items: tags.map(tag => this.toTagMenuItem(tag, action)),
+ };
+ }
+
+ const fuse = new Fuse(tags, {
+ keys: ['title'],
+ 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 normalizedRange = ([start, end]: [number, number]) =>
+ [
+ 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,
+ ``,
+ '',
+ titleMatches ?? []
+ );
+ return this.toTagMenuItem(
+ {
+ ...item.item,
+ title: hTitle ?? item.item.title,
+ },
+ action
+ );
+ }),
+ };
+ }
+
+ private toTagMenuItem(
+ tag: TagMeta,
+ action: SearchTagMenuAction
+ ): LinkedMenuItem {
+ const tagIcon = html`
+
+ `;
+ return {
+ key: tag.id,
+ name: html`${unsafeHTML(tag.title)}`,
+ icon: tagIcon,
+ action: async () => {
+ await action(tag);
+ },
+ };
+ }
}
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index b931d18888..3cc3c40dc9 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -6829,6 +6829,10 @@ export function useAFFiNEI18N(): {
* `Recent`
*/
["com.affine.editor.at-menu.recent-docs"](): string;
+ /**
+ * `Tags`
+ */
+ ["com.affine.editor.at-menu.tags"](): string;
/**
* `Loading...`
*/
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 8ad9c9c0e7..49d37afb39 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1697,6 +1697,7 @@
"com.affine.editor.journal-conflict.title": "Duplicate Entries in Today's Journal",
"com.affine.editor.at-menu.link-to-doc": "Search for \"{{query}}\"",
"com.affine.editor.at-menu.recent-docs": "Recent",
+ "com.affine.editor.at-menu.tags": "Tags",
"com.affine.editor.at-menu.loading": "Loading...",
"com.affine.editor.at-menu.new-doc": "New",
"com.affine.editor.at-menu.create-page": "New \"{{name}}\" page",