From ad0597577a9e96b6066bbf6b6e12c19beb61d5e5 Mon Sep 17 00:00:00 2001 From: linonetwo Date: Wed, 11 Feb 2026 17:21:03 +0800 Subject: [PATCH] put 'Talk with AI' menu on top and attachment i18n Introduce a reusable createTalkWithAIMenuItems helper to build "Talk with AI" menu entries (default agent + other agents submenu) and integrate it into workspace menu generation. Add new i18n keys for Agent.Attachment and WikiEmbed across locales and update UI to use translation keys (remove hardcoded fallback strings). Improve chat input/attachment behavior: expose a test-id for the attachment listbox, use i18n for labels/placeholders, and tweak input component wiring. Fix Cucumber step handling by normalizing expected newline sequences and safely handling empty message content. Also adjust memo deps in SortableWorkspaceSelectorButton to include id. --- features/agent.feature | 11 ++- features/stepDefinitions/agent.ts | 8 +- localization/locales/en/agent.json | 11 +++ localization/locales/fr/agent.json | 11 +++ localization/locales/ja/agent.json | 11 +++ localization/locales/ru/agent.json | 11 +++ localization/locales/zh-Hans/agent.json | 11 +++ localization/locales/zh-Hant/agent.json | 11 +++ .../TabTypes/WikiEmbedTabContent.tsx | 4 +- .../components/InputContainer.tsx | 17 ++-- src/pages/ChatTabContent/index.tsx | 2 +- .../SortableWorkspaceSelectorButton.tsx | 2 +- .../menu/createTalkWithAIMenuItems.ts | 82 +++++++++++++++++++ src/services/menu/index.ts | 63 +------------- .../workspaces/getWorkspaceMenuTemplate.ts | 18 +++- 15 files changed, 192 insertions(+), 81 deletions(-) create mode 100644 src/services/menu/createTalkWithAIMenuItems.ts diff --git a/features/agent.feature b/features/agent.feature index e4bb64d4..4e6d1be9 100644 --- a/features/agent.feature +++ b/features/agent.feature @@ -148,8 +148,10 @@ Feature: Agent Workflow - Tool Usage and Multi-Round Conversation # Click attachment button to open autocomplete When I click on a "attach button" element with selector "[data-testid='agent-attach-button']" # Autocomplete should open showing image option + tiddler options - And I should see a "attachment autocomplete input" element with selector "[data-testid='attachment-autocomplete-input']" - And I should see a "attachment listbox" element with selector "[data-testid='attachment-listbox']" + And I should see "attachment autocomplete input and attachment listbox" elements with selectors: + | element description | selector | + | attachment autocomplete input | [data-testid='attachment-autocomplete-input'] | + | attachment listbox | [data-testid='attachment-listbox'] | # Click on our test tiddler option When I click on a "test tiddler option" element with selector "[data-testid='attachment-option-tiddler-TestAttachmentTiddler']" # Verify the chip is displayed @@ -159,9 +161,6 @@ Feature: Agent Workflow - Tool Usage and Multi-Round Conversation When I type "请分析这个条目" in "chat input" element with selector "[data-testid='agent-message-input']" And I press "Enter" key # Verify the mock server received the rendered content (wikitext converted to plain text) - Then the last AI request user message should contain "WikiTestContentMarker123" - And the last AI request user message should contain "Wiki Entry from" - And the last AI request user message should contain "TestAttachmentTiddler" + Then the last AI request user message should contain "WikiTestHeader\n\nThis is a test with WikiTestContentMarker123" # Verify wikitext was converted to plain text (!! becomes "Header", not raw !!) - And the last AI request user message should contain "WikiTestHeader" And the last AI request user message should not contain "!!" diff --git a/features/stepDefinitions/agent.ts b/features/stepDefinitions/agent.ts index 7d38a6b4..4fc5e511 100644 --- a/features/stepDefinitions/agent.ts +++ b/features/stepDefinitions/agent.ts @@ -284,8 +284,12 @@ Then('the last AI request user message should contain {string}', async function( } const lastUserMessage = userMessages[userMessages.length - 1]; - if (!lastUserMessage.content || !lastUserMessage.content.includes(expectedText)) { - throw new Error(`Expected user message to contain "${expectedText}", but got: "${lastUserMessage.content}"`); + const content = lastUserMessage.content ?? ''; + + const normalizedExpectedText = expectedText.replaceAll('\\n', '\n'); + const contentHasExpectedText = content.includes(expectedText) || content.includes(normalizedExpectedText); + if (!contentHasExpectedText) { + throw new Error(`Expected user message to contain "${expectedText}", but got: "${content}"`); } }); diff --git a/localization/locales/en/agent.json b/localization/locales/en/agent.json index f51a3f64..d140b772 100644 --- a/localization/locales/en/agent.json +++ b/localization/locales/en/agent.json @@ -16,6 +16,13 @@ "Title": "API Debug Logs" }, "Agent": { + "Attachment": { + "AddAttachment": "Add attachment", + "AddImage": "📷 Add Image", + "NoOptions": "No options available", + "SearchPlaceholder": "Search...", + "SelectAttachment": "Select attachment" + }, "EditTitle": "Edit agent name", "InvalidTabType": "Invalid tab type. A chat tab is required.", "LoadingChat": "Loading conversation...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "Configure your new agent by choosing a name and template", "Title": "Create New Agent" }, + "WikiEmbed": { + "Error": "Failed to embed wiki", + "Loading": "Loading wiki..." + }, "EditAgent": { "AgentDescription": "Agent Description", "AgentDescriptionHelper": "Describe the functionality and purpose of your intelligent agent.", diff --git a/localization/locales/fr/agent.json b/localization/locales/fr/agent.json index 736fe7c1..42a74a28 100644 --- a/localization/locales/fr/agent.json +++ b/localization/locales/fr/agent.json @@ -16,6 +16,13 @@ "Title": "Journal de débogage API" }, "Agent": { + "Attachment": { + "AddAttachment": "Ajouter une pièce jointe", + "AddImage": "📷 Ajouter une image", + "NoOptions": "Aucune option disponible", + "SearchPlaceholder": "Rechercher...", + "SelectAttachment": "Sélectionner une pièce jointe" + }, "EditTitle": "Modifier le nom de l'agent intelligent", "InvalidTabType": "Type d'onglet non valide. Un onglet de chat est requis.", "LoadingChat": "Chargement de la conversation en cours...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "Nommez votre agent et choisissez un modèle comme point de départ", "Title": "Créer un nouvel agent intelligent" }, + "WikiEmbed": { + "Error": "Échec de l’intégration du wiki", + "Loading": "Chargement du wiki..." + }, "EditAgent": { "AgentDescription": "Description de l'agent intelligent", "AgentDescriptionHelper": "Décrivez les fonctionnalités et l'utilité de votre agent intelligent", diff --git a/localization/locales/ja/agent.json b/localization/locales/ja/agent.json index cc8f4db9..95fbc861 100644 --- a/localization/locales/ja/agent.json +++ b/localization/locales/ja/agent.json @@ -16,6 +16,13 @@ "Title": "API デバッグログ" }, "Agent": { + "Attachment": { + "AddAttachment": "添付を追加", + "AddImage": "📷 画像を追加", + "NoOptions": "利用可能な項目がありません", + "SearchPlaceholder": "検索...", + "SelectAttachment": "添付を選択" + }, "EditTitle": "編集エージェント名", "InvalidTabType": "無効なタブタイプです。チャットタブが必要です。", "LoadingChat": "会話を読み込んでいます...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "あなたのエージェントに名前を付け、テンプレートを出発点として選択してください", "Title": "新しいインテリジェントエージェントを作成する" }, + "WikiEmbed": { + "Error": "Wiki の埋め込みに失敗しました", + "Loading": "Wiki を読み込んでいます..." + }, "EditAgent": { "AgentDescription": "エージェントの説明", "AgentDescriptionHelper": "あなたのエージェントの機能と用途を説明してください", diff --git a/localization/locales/ru/agent.json b/localization/locales/ru/agent.json index 42fede39..566a58f6 100644 --- a/localization/locales/ru/agent.json +++ b/localization/locales/ru/agent.json @@ -16,6 +16,13 @@ "Title": "Журнал отладки API" }, "Agent": { + "Attachment": { + "AddAttachment": "Добавить вложение", + "AddImage": "📷 Добавить изображение", + "NoOptions": "Нет доступных вариантов", + "SearchPlaceholder": "Поиск...", + "SelectAttachment": "Выбрать вложение" + }, "EditTitle": "редактировать имя интеллектуального агента", "InvalidTabType": "Неверный тип вкладки. Требуется вкладка чата.", "LoadingChat": "Загрузка диалога...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "Назовите своего агента и выберите шаблон в качестве отправной точки", "Title": "Создать нового интеллектуального агента" }, + "WikiEmbed": { + "Error": "Не удалось встроить Wiki", + "Loading": "Загрузка Wiki..." + }, "EditAgent": { "AgentDescription": "описание агента", "AgentDescriptionHelper": "Опишите функции и назначение вашего агента.", diff --git a/localization/locales/zh-Hans/agent.json b/localization/locales/zh-Hans/agent.json index 9af173f3..6ae9b05a 100644 --- a/localization/locales/zh-Hans/agent.json +++ b/localization/locales/zh-Hans/agent.json @@ -16,6 +16,13 @@ "Title": "外部接口调试日志" }, "Agent": { + "Attachment": { + "AddAttachment": "添加附件", + "AddImage": "📷 添加图片", + "NoOptions": "暂无可选项", + "SearchPlaceholder": "搜索...", + "SelectAttachment": "选择附件" + }, "EditTitle": "编辑智能体名字", "InvalidTabType": "无效的标签页类型。需要聊天标签页。", "LoadingChat": "正在加载对话...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "为您的智能体命名并选择一个模板作为起点", "Title": "创建新智能体" }, + "WikiEmbed": { + "Error": "嵌入 Wiki 失败", + "Loading": "正在加载 Wiki..." + }, "EditAgent": { "AgentDescription": "智能体描述", "AgentDescriptionHelper": "描述您的智能体的功能和用途", diff --git a/localization/locales/zh-Hant/agent.json b/localization/locales/zh-Hant/agent.json index 36952ea6..485ff0b8 100644 --- a/localization/locales/zh-Hant/agent.json +++ b/localization/locales/zh-Hant/agent.json @@ -16,6 +16,13 @@ "Title": "外部介面除錯日誌" }, "Agent": { + "Attachment": { + "AddAttachment": "新增附件", + "AddImage": "📷 新增圖片", + "NoOptions": "沒有可用選項", + "SearchPlaceholder": "搜尋...", + "SelectAttachment": "選擇附件" + }, "EditTitle": "編輯智慧體名字", "InvalidTabType": "無效的標籤頁類型。需要聊天標籤頁。", "LoadingChat": "正在載入對話...", @@ -95,6 +102,10 @@ "SetupAgentDescription": "為您的智慧體命名並選擇一個模板作為起點", "Title": "創建新智慧體" }, + "WikiEmbed": { + "Error": "嵌入 Wiki 失敗", + "Loading": "正在載入 Wiki..." + }, "EditAgent": { "AgentDescription": "智能體描述", "AgentDescriptionHelper": "描述您的智慧體的功能和用途", diff --git a/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx b/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx index 83f627d2..7f781370 100644 --- a/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx +++ b/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx @@ -165,7 +165,7 @@ export const WikiEmbedTabContent: React.FC = ({ tab, i return ( - {t('WikiEmbed.Error', 'Failed to embed wiki')} + {t('WikiEmbed.Error')} {error} @@ -178,7 +178,7 @@ export const WikiEmbedTabContent: React.FC = ({ tab, i - {t('WikiEmbed.Loading', 'Loading wiki...')} + {t('WikiEmbed.Loading')} )} diff --git a/src/pages/ChatTabContent/components/InputContainer.tsx b/src/pages/ChatTabContent/components/InputContainer.tsx index 5e1c0469..154f215e 100644 --- a/src/pages/ChatTabContent/components/InputContainer.tsx +++ b/src/pages/ChatTabContent/components/InputContainer.tsx @@ -48,6 +48,9 @@ interface InputContainerProps { onWikiTiddlerSelect?: (tiddler: WikiTiddlerAttachment) => void; onRemoveWikiTiddler?: (index: number) => void; } +const attachmentListboxSlotProps: React.HTMLAttributes & { 'data-testid': string } = { + 'data-testid': 'attachment-listbox', +}; /** * Input container component for message entry @@ -146,7 +149,7 @@ export const InputContainer: React.FC = ({ try { // Build options: first is "Add Image", then wiki tiddlers from all non-hibernated workspaces const options: Array<{ type: 'image' | 'tiddler'; title: string; workspaceName?: string; testId?: string }> = [ - { type: 'image', title: t('Agent.Attachment.AddImage', '📷 Add Image'), workspaceName: '', testId: 'AddImage' }, + { type: 'image', title: t('Agent.Attachment.AddImage'), workspaceName: '', testId: 'AddImage' }, ]; // Get all workspaces @@ -294,7 +297,7 @@ export const InputContainer: React.FC = ({ disabled={disabled || isStreaming} color={(selectedFile || selectedWikiTiddlers.length > 0) ? 'primary' : 'default'} data-testid='agent-attach-button' - title={t('Agent.Attachment.AddAttachment', 'Add attachment')} + title={t('Agent.Attachment.AddAttachment')} > @@ -346,15 +349,13 @@ export const InputContainer: React.FC = ({ getOptionLabel={(option) => option.title} onChange={handleSelectAttachment} slotProps={{ - listbox: { - 'data-testid': 'attachment-listbox', - }, + listbox: attachmentListboxSlotProps, }} renderInput={(parameters) => ( @@ -367,7 +368,7 @@ export const InputContainer: React.FC = ({ ); }} - noOptionsText={t('Agent.Attachment.NoOptions', 'No options available')} + noOptionsText={t('Agent.Attachment.NoOptions')} /> diff --git a/src/pages/ChatTabContent/index.tsx b/src/pages/ChatTabContent/index.tsx index 73e8a7d1..003b727b 100644 --- a/src/pages/ChatTabContent/index.tsx +++ b/src/pages/ChatTabContent/index.tsx @@ -50,7 +50,7 @@ export const ChatTabContent: React.FC = ({ tab, isSplitView return ( - {t('Agent.InvalidTabType', 'Invalid tab type. Expected chat tab.')} + {t('Agent.InvalidTabType')} ); diff --git a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx index edea9202..e3d63c9e 100644 --- a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx +++ b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx @@ -109,7 +109,7 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT editFlags: { canCopy: false }, }); }, - [t, workspace], + [t, workspace, id], ); return (
diff --git a/src/services/menu/createTalkWithAIMenuItems.ts b/src/services/menu/createTalkWithAIMenuItems.ts new file mode 100644 index 00000000..09a6381f --- /dev/null +++ b/src/services/menu/createTalkWithAIMenuItems.ts @@ -0,0 +1,82 @@ +import { IAskAIWithSelectionData, WindowChannel } from '@/constants/channels'; +import type { AgentDefinition, IAgentDefinitionService } from '@services/agentDefinition/interface'; +import type { IWindowService } from '@services/windows/interface'; +import { WindowNames } from '@services/windows/WindowProperties'; +import type { MenuItemConstructorOptions } from 'electron'; +import type { TFunction } from 'i18next'; + +interface ICreateTalkWithAIMenuItemsOptions { + /** Agent definition service to get available agents */ + agentDefinitionService: Pick; + /** Selected text to send to AI (empty string if none) */ + selectionText: string; + /** Translation function */ + t: TFunction; + /** Wiki URL for context */ + wikiUrl?: string; + /** Window service to get main window */ + windowService: Pick; + /** Workspace ID for context */ + workspaceId?: string; +} + +/** + * Create "Talk with AI" menu items with default agent and other agents submenu + * This is shared between context menu (page right-click) and workspace menu (workspace icon right-click) + */ +export async function createTalkWithAIMenuItems( + options: ICreateTalkWithAIMenuItemsOptions, +): Promise { + const { agentDefinitionService, selectionText, t, wikiUrl, windowService, workspaceId } = options; + + // Get all agent definitions + const defaultAgentDefinition = await agentDefinitionService.getAgentDef(); // No parameter = default agent + const allAgentDefinitions = await agentDefinitionService.getAgentDefs(); + const otherAgentDefinitions = allAgentDefinitions.filter((definition: AgentDefinition) => definition.id !== defaultAgentDefinition?.id); + + const menuItems: MenuItemConstructorOptions[] = []; + + // Add menu item for default agent + menuItems.push({ + id: 'talk-with-ai', + label: t('ContextMenu.TalkWithAI'), + click: async () => { + const data: IAskAIWithSelectionData = { + selectionText, + wikiUrl, + workspaceId, + agentDefId: undefined, // Use default agent + }; + // Send to main window + const mainWindow = windowService.get(WindowNames.main); + if (mainWindow !== undefined) { + mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); + } + }, + }); + + // Add submenu for other agents if there are any + if (otherAgentDefinitions.length > 0) { + menuItems.push({ + id: 'talk-with-ai-more', + label: t('ContextMenu.TalkWithAIMore'), + submenu: otherAgentDefinitions.map((agentDefinition: AgentDefinition) => ({ + label: agentDefinition.name, + click: async () => { + const data: IAskAIWithSelectionData = { + selectionText, + wikiUrl, + workspaceId, + agentDefId: agentDefinition.id, + }; + const mainWindow = windowService.get(WindowNames.main); + if (mainWindow !== undefined) { + mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); + } + }, + })), + }); + } + + return menuItems; +} diff --git a/src/services/menu/index.ts b/src/services/menu/index.ts index f651fa70..de9f1709 100644 --- a/src/services/menu/index.ts +++ b/src/services/menu/index.ts @@ -1,6 +1,5 @@ -import { IAskAIWithSelectionData, WindowChannel } from '@/constants/channels'; import { getWorkspaceIdFromUrl } from '@/constants/urls'; -import type { AgentDefinition, IAgentDefinitionService } from '@services/agentDefinition/interface'; +import type { IAgentDefinitionService } from '@services/agentDefinition/interface'; import type { IAuthenticationService } from '@services/auth/interface'; import { container } from '@services/container'; import type { IContextService } from '@services/context/interface'; @@ -149,6 +148,7 @@ export class MenuService implements IMenuService { // Add workspace-specific menu items if workspace exists and is a wiki if (workspace !== undefined && isWikiWorkspace(workspace)) { const services = { + agentDefinition: container.get(serviceIdentifier.AgentDefinition), auth: container.get(serviceIdentifier.Authentication), context: container.get(serviceIdentifier.Context), externalAPI: this.externalAPIService, @@ -318,6 +318,7 @@ export class MenuService implements IMenuService { const menu = contextMenuBuilder.buildMenuForElement(info); const workspaces = await workspaceService.getWorkspacesAsList(); const services = { + agentDefinition: container.get(serviceIdentifier.AgentDefinition), auth: authService, context: contextService, externalAPI: this.externalAPIService, @@ -336,64 +337,6 @@ export class MenuService implements IMenuService { // workspace menus (template items are added at the end via insert(0) in reverse order) menu.append(new MenuItem({ type: 'separator' })); - // Add "Talk with AI" menu items when there's selected text - if (info.selectionText && info.selectionText.trim().length > 0) { - const wikiUrl = webContents.getURL(); - const workspaceId = getWorkspaceIdFromUrl(wikiUrl); - - // Get all agent definitions - const agentDefinitionService = container.get(serviceIdentifier.AgentDefinition); - const defaultAgentDefinition = await agentDefinitionService.getAgentDef(); // No parameter = default agent - const allAgentDefinitions = await agentDefinitionService.getAgentDefs(); - const otherAgentDefinitions = allAgentDefinitions.filter((definition: AgentDefinition) => definition.id !== defaultAgentDefinition?.id); - - // Add menu item for default agent - menu.append( - new MenuItem({ - label: i18n.t('ContextMenu.TalkWithAI'), - click: async () => { - const data: IAskAIWithSelectionData = { - selectionText: info.selectionText!, - wikiUrl, - workspaceId: workspaceId ?? undefined, - agentDefId: undefined, // Use default agent - }; - // Only send to main window to avoid duplicate processing - const mainWindow = windowService.get(WindowNames.main); - if (mainWindow !== undefined) { - mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); - } - }, - }), - ); - - // Add submenu for other agents if there are any - if (otherAgentDefinitions.length > 0) { - menu.append( - new MenuItem({ - label: i18n.t('ContextMenu.TalkWithAIMore'), - submenu: otherAgentDefinitions.map((agentDefinition: AgentDefinition) => ({ - label: agentDefinition.name, - click: async () => { - const data: IAskAIWithSelectionData = { - selectionText: info.selectionText!, - wikiUrl, - workspaceId: workspaceId ?? undefined, - agentDefId: agentDefinition.id, - }; - const mainWindow = windowService.get(WindowNames.main); - if (mainWindow !== undefined) { - mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); - } - }, - })), - }), - ); - } - - menu.append(new MenuItem({ type: 'separator' })); - } - // Note: Simplified menu and "Current Workspace" are now provided by the frontend template // (from SortableWorkspaceSelectorButton or content view), so we don't add them here menu.append( diff --git a/src/services/workspaces/getWorkspaceMenuTemplate.ts b/src/services/workspaces/getWorkspaceMenuTemplate.ts index a12b5168..fa7ad3dd 100644 --- a/src/services/workspaces/getWorkspaceMenuTemplate.ts +++ b/src/services/workspaces/getWorkspaceMenuTemplate.ts @@ -1,10 +1,12 @@ import { WikiChannel } from '@/constants/channels'; import { getDefaultHTTPServerIP } from '@/constants/urls'; +import type { IAgentDefinitionService } from '@services/agentDefinition/interface'; import type { IAuthenticationService } from '@services/auth/interface'; import type { IContextService } from '@services/context/interface'; import type { IExternalAPIService } from '@services/externalAPI/interface'; import type { IGitService } from '@services/git/interface'; import { createBackupMenuItems, createSyncMenuItems } from '@services/git/menuItems'; +import { createTalkWithAIMenuItems } from '@services/menu/createTalkWithAIMenuItems'; import type { INativeService } from '@services/native/interface'; import type { IPreferenceService } from '@services/preferences/interface'; import type { ISyncService } from '@services/sync/interface'; @@ -22,6 +24,7 @@ import type { IWorkspace, IWorkspaceService } from './interface'; import { isWikiWorkspace } from './interface'; interface IWorkspaceMenuRequiredServices { + agentDefinition: Pick; auth: Pick; context: Pick; externalAPI: Pick; @@ -32,7 +35,7 @@ interface IWorkspaceMenuRequiredServices { view: Pick; wiki: Pick; wikiGitWorkspace: Pick; - window: Pick; + window: Pick; workspace: Pick; workspaceView: Pick< IWorkspaceViewService, @@ -62,6 +65,19 @@ export async function getSimplifiedWorkspaceMenuTemplate( const { id, storageService, isSubWiki } = workspace; const template: MenuItemConstructorOptions[] = []; + const lastUrl = await service.view.getViewCurrentUrl(id, WindowNames.main); + const talkWithAIMenuItems = await createTalkWithAIMenuItems({ + agentDefinitionService: service.agentDefinition, + selectionText: '', + t, + wikiUrl: lastUrl, + windowService: service.window, + workspaceId: id, + }); + + template.push(...talkWithAIMenuItems); + template.push({ type: 'separator' }); + // Add "Current Workspace" submenu with full menu const fullMenuTemplate = await getWorkspaceMenuTemplate(workspace, t, service); if (fullMenuTemplate.length > 0) {