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",