akumatus
2025-03-19 04:22:10 +00:00
parent b470eafd55
commit 47206b8d47
12 changed files with 169 additions and 46 deletions

View File

@@ -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 { LinkedMenuGroup } from '@blocksuite/affine/blocks/root';
import type { Store } from '@blocksuite/affine/store'; import type { Store } from '@blocksuite/affine/store';
import type { Signal } from '@preact/signals-core'; import type { Signal } from '@preact/signals-core';
@@ -29,10 +32,15 @@ export interface DocDisplayConfig {
getDoc: (docId: string) => Store | null; getDoc: (docId: string) => Store | null;
} }
export interface DocSearchMenuConfig { export interface SearchMenuConfig {
getDocMenuGroup: ( getDocMenuGroup: (
query: string, query: string,
action: SearchDocMenuAction, action: SearchDocMenuAction,
abortSignal: AbortSignal abortSignal: AbortSignal
) => LinkedMenuGroup; ) => LinkedMenuGroup;
getTagMenuGroup: (
query: string,
action: SearchTagMenuAction,
abortSignal: AbortSignal
) => LinkedMenuGroup;
} }

View File

@@ -11,7 +11,7 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { AIProvider } from '../provider'; import { AIProvider } from '../provider';
import type { DocDisplayConfig, DocSearchMenuConfig } from './chat-config'; import type { DocDisplayConfig, SearchMenuConfig } from './chat-config';
import type { import type {
ChatChip, ChatChip,
ChatContextValue, ChatContextValue,
@@ -75,7 +75,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
accessor docDisplayConfig!: DocDisplayConfig; accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor docSearchMenuConfig!: DocSearchMenuConfig; accessor searchMenuConfig!: SearchMenuConfig;
@query('.add-button') @query('.add-button')
accessor addButton!: HTMLDivElement; accessor addButton!: HTMLDivElement;
@@ -158,7 +158,7 @@ export class ChatPanelChips extends WithDisposable(ShadowlessElement) {
template: html` template: html`
<chat-panel-add-popover <chat-panel-add-popover
.addChip=${this._addChip} .addChip=${this._addChip}
.docSearchMenuConfig=${this.docSearchMenuConfig} .searchMenuConfig=${this.searchMenuConfig}
.abortController=${this._abortController} .abortController=${this._abortController}
></chat-panel-add-popover> ></chat-panel-add-popover>
`, `,

View File

@@ -1,4 +1,5 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import type { 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';
@@ -13,7 +14,7 @@ import type { DocMeta } from '@blocksuite/store';
import { css, html, type TemplateResult } from 'lit'; import { css, html, type TemplateResult } from 'lit';
import { property, query, state } from 'lit/decorators.js'; 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'; import type { ChatChip } from '../chat-context';
enum AddPopoverMode { enum AddPopoverMode {
@@ -107,11 +108,21 @@ export class ChatPanelAddPopover extends SignalWatcher(
private accessor _query = ''; private accessor _query = '';
@state() @state()
private accessor _docGroup: MenuGroup = { private accessor _searchGroup: MenuGroup = {
name: 'No Result', name: 'No Result',
items: [], items: [],
}; };
private readonly _toggleMode = (mode: AddPopoverMode) => {
this._mode = mode;
this._activatedIndex = 0;
this._query = '';
this._updateSearchGroup();
requestAnimationFrame(() => {
this.searchInput.focus();
});
};
private readonly tcGroup: MenuGroup = { private readonly tcGroup: MenuGroup = {
name: 'Tag & Collection', name: 'Tag & Collection',
items: [ items: [
@@ -120,7 +131,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
name: 'Tags', name: 'Tags',
icon: TagsIcon(), icon: TagsIcon(),
action: () => { action: () => {
this._mode = AddPopoverMode.Tags; this._toggleMode(AddPopoverMode.Tags);
}, },
}, },
{ {
@@ -128,7 +139,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
name: 'Collections', name: 'Collections',
icon: CollectionsIcon(), icon: CollectionsIcon(),
action: () => { action: () => {
this._mode = AddPopoverMode.Collections; this._toggleMode(AddPopoverMode.Collections);
}, },
}, },
], ],
@@ -163,14 +174,14 @@ export class ChatPanelAddPopover extends SignalWatcher(
private get _menuGroup() { private get _menuGroup() {
switch (this._mode) { switch (this._mode) {
case AddPopoverMode.Tags: case AddPopoverMode.Tags:
return []; return [this._searchGroup];
case AddPopoverMode.Collections: case AddPopoverMode.Collections:
return []; return [this._searchGroup];
default: default:
if (this._query) { 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; private accessor _mode: AddPopoverMode = AddPopoverMode.Default;
@property({ attribute: false }) @property({ attribute: false })
accessor docSearchMenuConfig!: DocSearchMenuConfig; accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor addChip!: (chip: ChatChip) => void; accessor addChip!: (chip: ChatChip) => void;
@@ -200,7 +211,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._updateDocGroup(); this._updateSearchGroup();
this.addEventListener('keydown', this._handleKeyDown); this.addEventListener('keydown', this._handleKeyDown);
} }
@@ -291,15 +302,25 @@ export class ChatPanelAddPopover extends SignalWatcher(
private _onInput(event: Event) { private _onInput(event: Event) {
this._query = (event.target as HTMLInputElement).value; this._query = (event.target as HTMLInputElement).value;
this._activatedIndex = 0; this._activatedIndex = 0;
this._updateDocGroup(); this._updateSearchGroup();
} }
private _updateDocGroup() { private _updateSearchGroup() {
this._docGroup = this.docSearchMenuConfig.getDocMenuGroup( switch (this._mode) {
this._query, case AddPopoverMode.Tags:
this._addDocChip, this._searchGroup = this.searchMenuConfig.getTagMenuGroup(
this.abortController.signal 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) => { private readonly _addDocChip = (meta: DocMeta) => {
@@ -310,6 +331,10 @@ export class ChatPanelAddPopover extends SignalWatcher(
this.abortController.abort(); this.abortController.abort();
}; };
private readonly _addTagChip = (_tag: TagMeta) => {
this.abortController.abort();
};
private readonly _handleKeyDown = (event: KeyboardEvent) => { private readonly _handleKeyDown = (event: KeyboardEvent) => {
if (event.isComposing) return; if (event.isComposing) return;

View File

@@ -26,7 +26,7 @@ import type {
AINetworkSearchConfig, AINetworkSearchConfig,
AppSidebarConfig, AppSidebarConfig,
DocDisplayConfig, DocDisplayConfig,
DocSearchMenuConfig, SearchMenuConfig,
} from './chat-config'; } from './chat-config';
import type { import type {
ChatChip, ChatChip,
@@ -269,7 +269,7 @@ export class ChatPanel extends SignalWatcher(
accessor appSidebarConfig!: AppSidebarConfig; accessor appSidebarConfig!: AppSidebarConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor docSearchMenuConfig!: DocSearchMenuConfig; accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig; accessor docDisplayConfig!: DocDisplayConfig;
@@ -574,7 +574,7 @@ export class ChatPanel extends SignalWatcher(
.updateContext=${this.updateContext} .updateContext=${this.updateContext}
.pollContextDocsAndFiles=${this._pollContextDocsAndFiles} .pollContextDocsAndFiles=${this._pollContextDocsAndFiles}
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.docSearchMenuConfig=${this.docSearchMenuConfig} .searchMenuConfig=${this.searchMenuConfig}
></chat-panel-chips> ></chat-panel-chips>
<chat-panel-input <chat-panel-input
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}

View File

@@ -3,7 +3,7 @@ import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-
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 { AINetworkSearchService } from '@affine/core/modules/ai-button/services/network-search';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { DocSearchMenuService } from '@affine/core/modules/doc-search-menu/services'; import { SearchMenuService } from '@affine/core/modules/search-menu/services';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace'; import { WorkspaceService } from '@affine/core/modules/workspace';
import { RefNodeSlotsProvider } from '@blocksuite/affine/rich-text'; import { RefNodeSlotsProvider } from '@blocksuite/affine/rich-text';
@@ -57,7 +57,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
const searchService = framework.get(AINetworkSearchService); const searchService = framework.get(AINetworkSearchService);
const docDisplayMetaService = framework.get(DocDisplayMetaService); const docDisplayMetaService = framework.get(DocDisplayMetaService);
const workspaceService = framework.get(WorkspaceService); const workspaceService = framework.get(WorkspaceService);
const docSearchMenuService = framework.get(DocSearchMenuService); const searchMenuService = framework.get(SearchMenuService);
const workbench = framework.get(WorkbenchService).workbench; const workbench = framework.get(WorkbenchService).workbench;
chatPanelRef.current.appSidebarConfig = { chatPanelRef.current.appSidebarConfig = {
getWidth: () => { getWidth: () => {
@@ -87,13 +87,12 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
return doc; return doc;
}, },
}; };
chatPanelRef.current.docSearchMenuConfig = { chatPanelRef.current.searchMenuConfig = {
getDocMenuGroup: (query, action, abortSignal) => { getDocMenuGroup: (query, action, abortSignal) => {
return docSearchMenuService.getDocMenuGroup( return searchMenuService.getDocMenuGroup(query, action, abortSignal);
query, },
action, getTagMenuGroup: (query, action, abortSignal) => {
abortSignal return searchMenuService.getTagMenuGroup(query, action, abortSignal);
);
}, },
}; };
const previewSpecBuilder = enableFootnoteConfigExtension( const previewSpecBuilder = enableFootnoteConfigExtension(

View File

@@ -3,9 +3,9 @@ import { type Framework } from '@toeverything/infra';
import { WorkspaceDialogService } from '../dialogs'; import { WorkspaceDialogService } from '../dialogs';
import { DocsService } from '../doc'; import { DocsService } from '../doc';
import { DocDisplayMetaService } from '../doc-display-meta'; import { DocDisplayMetaService } from '../doc-display-meta';
import { DocSearchMenuService } from '../doc-search-menu/services';
import { EditorSettingService } from '../editor-setting'; import { EditorSettingService } from '../editor-setting';
import { JournalService } from '../journal'; import { JournalService } from '../journal';
import { SearchMenuService } from '../search-menu/services';
import { WorkspaceScope } from '../workspace'; import { WorkspaceScope } from '../workspace';
import { AtMenuConfigService } from './services'; import { AtMenuConfigService } from './services';
@@ -18,6 +18,6 @@ export function configAtMenuConfigModule(framework: Framework) {
WorkspaceDialogService, WorkspaceDialogService,
EditorSettingService, EditorSettingService,
DocsService, DocsService,
DocSearchMenuService, SearchMenuService,
]); ]);
} }

View File

@@ -24,9 +24,9 @@ import { html } from 'lit';
import type { WorkspaceDialogService } from '../../dialogs'; import type { WorkspaceDialogService } from '../../dialogs';
import type { DocsService } from '../../doc'; import type { DocsService } from '../../doc';
import type { DocDisplayMetaService } from '../../doc-display-meta'; import type { DocDisplayMetaService } from '../../doc-display-meta';
import type { DocSearchMenuService } from '../../doc-search-menu/services';
import type { EditorSettingService } from '../../editor-setting'; import type { EditorSettingService } from '../../editor-setting';
import { type JournalService, suggestJournalDate } from '../../journal'; import { type JournalService, suggestJournalDate } from '../../journal';
import type { SearchMenuService } from '../../search-menu/services';
function resolveSignal<T>(data: T | Signal<T>): T { function resolveSignal<T>(data: T | Signal<T>): T {
return data instanceof Signal ? data.value : data; return data instanceof Signal ? data.value : data;
@@ -45,7 +45,7 @@ export class AtMenuConfigService extends Service {
private readonly dialogService: WorkspaceDialogService, private readonly dialogService: WorkspaceDialogService,
private readonly editorSettingService: EditorSettingService, private readonly editorSettingService: EditorSettingService,
private readonly docsService: DocsService, private readonly docsService: DocsService,
private readonly docsSearchMenuService: DocSearchMenuService private readonly searchMenuService: SearchMenuService
) { ) {
super(); super();
} }
@@ -292,7 +292,7 @@ export class AtMenuConfigService extends Service {
track.doc.editor.atMenu.linkDoc(); track.doc.editor.atMenu.linkDoc();
this.insertDoc(inlineEditor, meta.id); this.insertDoc(inlineEditor, meta.id);
}; };
const result = this.docsSearchMenuService.getDocMenuGroup( const result = this.searchMenuService.getDocMenuGroup(
query, query,
action, action,
abortSignal abortSignal

View File

@@ -17,7 +17,6 @@ import { configureDocModule } from './doc';
import { configureDocDisplayMetaModule } from './doc-display-meta'; import { configureDocDisplayMetaModule } from './doc-display-meta';
import { configureDocInfoModule } from './doc-info'; import { configureDocInfoModule } from './doc-info';
import { configureDocLinksModule } from './doc-link'; import { configureDocLinksModule } from './doc-link';
import { configDocSearchMenuModule } from './doc-search-menu';
import { configureDocsSearchModule } from './docs-search'; import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor'; import { configureEditorModule } from './editor';
import { configureEditorSettingModule } from './editor-setting'; import { configureEditorSettingModule } from './editor-setting';
@@ -39,6 +38,7 @@ import { configurePDFModule } from './pdf';
import { configurePeekViewModule } from './peek-view'; import { configurePeekViewModule } from './peek-view';
import { configurePermissionsModule } from './permissions'; import { configurePermissionsModule } from './permissions';
import { configureQuickSearchModule } from './quicksearch'; import { configureQuickSearchModule } from './quicksearch';
import { configSearchMenuModule } from './search-menu';
import { configureShareDocsModule } from './share-doc'; import { configureShareDocsModule } from './share-doc';
import { configureShareSettingModule } from './share-setting'; import { configureShareSettingModule } from './share-setting';
import { import {
@@ -96,7 +96,7 @@ export function configureCommonModules(framework: Framework) {
configureDocInfoModule(framework); configureDocInfoModule(framework);
configureOpenInApp(framework); configureOpenInApp(framework);
configAtMenuConfigModule(framework); configAtMenuConfigModule(framework);
configDocSearchMenuModule(framework); configSearchMenuModule(framework);
configureDndModule(framework); configureDndModule(framework);
configureCommonGlobalStorageImpls(framework); configureCommonGlobalStorageImpls(framework);
configureAINetworkSearchModule(framework); configureAINetworkSearchModule(framework);

View File

@@ -3,16 +3,18 @@ import { type Framework } from '@toeverything/infra';
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';
import { TagService } from '../tag';
import { WorkspaceScope, WorkspaceService } from '../workspace'; import { WorkspaceScope, WorkspaceService } from '../workspace';
import { DocSearchMenuService } from './services'; import { SearchMenuService } from './services';
export function configDocSearchMenuModule(framework: Framework) { export function configSearchMenuModule(framework: Framework) {
framework framework
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(DocSearchMenuService, [ .service(SearchMenuService, [
WorkspaceService, WorkspaceService,
DocDisplayMetaService, DocDisplayMetaService,
RecentDocsService, RecentDocsService,
DocsSearchService, DocsSearchService,
TagService,
]); ]);
} }

View File

@@ -1,3 +1,4 @@
import type { 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 {
@@ -9,13 +10,16 @@ import type { DocMeta } from '@blocksuite/affine/store';
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 { 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 { 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';
import { highlighter } from '../../quicksearch/utils/highlighter';
import type { TagService } from '../../tag';
import type { WorkspaceService } from '../../workspace'; import type { WorkspaceService } from '../../workspace';
const MAX_DOCS = 3; const MAX_DOCS = 3;
@@ -26,12 +30,15 @@ type DocMetaWithHighlights = DocMeta & {
export type SearchDocMenuAction = (meta: DocMeta) => Promise<void> | void; export type SearchDocMenuAction = (meta: DocMeta) => Promise<void> | void;
export class DocSearchMenuService extends Service { export type SearchTagMenuAction = (tagId: TagMeta) => Promise<void> | void;
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
) { ) {
super(); 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,
`<span style="color: ${cssVarV2('text/emphasis')}">`,
'</span>',
titleMatches ?? []
);
return this.toTagMenuItem(
{
...item.item,
title: hTitle ?? item.item.title,
},
action
);
}),
};
}
private toTagMenuItem(
tag: TagMeta,
action: SearchTagMenuAction
): LinkedMenuItem {
const tagIcon = html`
<div style="display: flex; align-items: center; justify-content: center;">
<div
style="border-radius: 50%; height: 8px; width: 8px; margin: 4px; background-color: ${tag.color};"
></div>
</div>
`;
return {
key: tag.id,
name: html`${unsafeHTML(tag.title)}`,
icon: tagIcon,
action: async () => {
await action(tag);
},
};
}
} }

View File

@@ -6829,6 +6829,10 @@ export function useAFFiNEI18N(): {
* `Recent` * `Recent`
*/ */
["com.affine.editor.at-menu.recent-docs"](): string; ["com.affine.editor.at-menu.recent-docs"](): string;
/**
* `Tags`
*/
["com.affine.editor.at-menu.tags"](): string;
/** /**
* `Loading...` * `Loading...`
*/ */

View File

@@ -1697,6 +1697,7 @@
"com.affine.editor.journal-conflict.title": "Duplicate Entries in Today's Journal", "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.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.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",