diff --git a/web/app/components/goto-anything/actions/commands/account.tsx b/web/app/components/goto-anything/actions/commands/account.tsx new file mode 100644 index 000000000..2a3834b0d --- /dev/null +++ b/web/app/components/goto-anything/actions/commands/account.tsx @@ -0,0 +1,44 @@ +import type { SlashCommandHandler } from './types' +import React from 'react' +import { RiUser3Line } from '@remixicon/react' +import i18n from '@/i18n-config/i18next-config' +import { registerCommands, unregisterCommands } from './command-bus' + +// Account command dependency types - no external dependencies needed +type AccountDeps = Record + +/** + * Account command - Navigates to account page + */ +export const accountCommand: SlashCommandHandler = { + name: 'account', + description: 'Navigate to account page', + + async search(args: string, locale: string = 'en') { + return [{ + id: 'account', + title: i18n.t('common.account.account', { lng: locale }), + description: i18n.t('app.gotoAnything.actions.accountDesc', { lng: locale }), + type: 'command' as const, + icon: ( +
+ +
+ ), + data: { command: 'navigation.account', args: {} }, + }] + }, + + register(_deps: AccountDeps) { + registerCommands({ + 'navigation.account': async (_args) => { + // Navigate to account page + window.location.href = '/account' + }, + }) + }, + + unregister() { + unregisterCommands(['navigation.account']) + }, +} diff --git a/web/app/components/goto-anything/actions/commands/community.tsx b/web/app/components/goto-anything/actions/commands/community.tsx new file mode 100644 index 000000000..559608fb4 --- /dev/null +++ b/web/app/components/goto-anything/actions/commands/community.tsx @@ -0,0 +1,43 @@ +import type { SlashCommandHandler } from './types' +import React from 'react' +import { RiDiscordLine } from '@remixicon/react' +import i18n from '@/i18n-config/i18next-config' +import { registerCommands, unregisterCommands } from './command-bus' + +// Community command dependency types +type CommunityDeps = Record + +/** + * Community command - Opens Discord community + */ +export const communityCommand: SlashCommandHandler = { + name: 'community', + description: 'Open community Discord', + async search(args: string, locale: string = 'en') { + return [{ + id: 'community', + title: i18n.t('common.userProfile.community', { lng: locale }), + description: i18n.t('app.gotoAnything.actions.communityDesc', { lng: locale }) || 'Open Discord community', + type: 'command' as const, + icon: ( +
+ +
+ ), + data: { command: 'navigation.community', args: { url: 'https://discord.gg/5AEfbxcd9k' } }, + }] + }, + + register(_deps: CommunityDeps) { + registerCommands({ + 'navigation.community': async (args) => { + const url = args?.url || 'https://discord.gg/5AEfbxcd9k' + window.open(url, '_blank', 'noopener,noreferrer') + }, + }) + }, + + unregister() { + unregisterCommands(['navigation.community']) + }, +} diff --git a/web/app/components/goto-anything/actions/commands/doc.tsx b/web/app/components/goto-anything/actions/commands/doc.tsx new file mode 100644 index 000000000..ae38389af --- /dev/null +++ b/web/app/components/goto-anything/actions/commands/doc.tsx @@ -0,0 +1,44 @@ +import type { SlashCommandHandler } from './types' +import React from 'react' +import { RiBookOpenLine } from '@remixicon/react' +import i18n from '@/i18n-config/i18next-config' +import { registerCommands, unregisterCommands } from './command-bus' +import { defaultDocBaseUrl } from '@/context/i18n' + +// Documentation command dependency types - no external dependencies needed +type DocDeps = Record + +/** + * Documentation command - Opens help documentation + */ +export const docCommand: SlashCommandHandler = { + name: 'doc', + description: 'Open documentation', + async search(args: string, locale: string = 'en') { + return [{ + id: 'doc', + title: i18n.t('common.userProfile.helpCenter', { lng: locale }), + description: i18n.t('app.gotoAnything.actions.docDesc', { lng: locale }) || 'Open help documentation', + type: 'command' as const, + icon: ( +
+ +
+ ), + data: { command: 'navigation.doc', args: {} }, + }] + }, + + register(_deps: DocDeps) { + registerCommands({ + 'navigation.doc': async (_args) => { + const url = `${defaultDocBaseUrl}` + window.open(url, '_blank', 'noopener,noreferrer') + }, + }) + }, + + unregister() { + unregisterCommands(['navigation.doc']) + }, +} diff --git a/web/app/components/goto-anything/actions/commands/feedback.tsx b/web/app/components/goto-anything/actions/commands/feedback.tsx new file mode 100644 index 000000000..9306ffd89 --- /dev/null +++ b/web/app/components/goto-anything/actions/commands/feedback.tsx @@ -0,0 +1,43 @@ +import type { SlashCommandHandler } from './types' +import React from 'react' +import { RiFeedbackLine } from '@remixicon/react' +import i18n from '@/i18n-config/i18next-config' +import { registerCommands, unregisterCommands } from './command-bus' + +// Feedback command dependency types +type FeedbackDeps = Record + +/** + * Feedback command - Opens GitHub feedback discussions + */ +export const feedbackCommand: SlashCommandHandler = { + name: 'feedback', + description: 'Open feedback discussions', + async search(args: string, locale: string = 'en') { + return [{ + id: 'feedback', + title: i18n.t('common.userProfile.communityFeedback', { lng: locale }), + description: i18n.t('app.gotoAnything.actions.feedbackDesc', { lng: locale }) || 'Open community feedback discussions', + type: 'command' as const, + icon: ( +
+ +
+ ), + data: { command: 'navigation.feedback', args: { url: 'https://github.com/langgenius/dify/discussions/categories/feedbacks' } }, + }] + }, + + register(_deps: FeedbackDeps) { + registerCommands({ + 'navigation.feedback': async (args) => { + const url = args?.url || 'https://github.com/langgenius/dify/discussions/categories/feedbacks' + window.open(url, '_blank', 'noopener,noreferrer') + }, + }) + }, + + unregister() { + unregisterCommands(['navigation.feedback']) + }, +} diff --git a/web/app/components/goto-anything/actions/commands/slash.tsx b/web/app/components/goto-anything/actions/commands/slash.tsx index 46467aed1..6571d0e31 100644 --- a/web/app/components/goto-anything/actions/commands/slash.tsx +++ b/web/app/components/goto-anything/actions/commands/slash.tsx @@ -7,6 +7,10 @@ import { useTheme } from 'next-themes' import { setLocaleOnClient } from '@/i18n-config' import { themeCommand } from './theme' import { languageCommand } from './language' +import { feedbackCommand } from './feedback' +import { docCommand } from './doc' +import { communityCommand } from './community' +import { accountCommand } from './account' import i18n from '@/i18n-config/i18next-config' export const slashAction: ActionItem = { @@ -30,12 +34,20 @@ export const registerSlashCommands = (deps: Record) => { // Register command handlers to the registry system with their respective dependencies slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme }) slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale }) + slashCommandRegistry.register(feedbackCommand, {}) + slashCommandRegistry.register(docCommand, {}) + slashCommandRegistry.register(communityCommand, {}) + slashCommandRegistry.register(accountCommand, {}) } export const unregisterSlashCommands = () => { // Remove command handlers from registry system (automatically calls each command's unregister method) slashCommandRegistry.unregister('theme') slashCommandRegistry.unregister('language') + slashCommandRegistry.unregister('feedback') + slashCommandRegistry.unregister('doc') + slashCommandRegistry.unregister('community') + slashCommandRegistry.unregister('account') } export const SlashCommandProvider = () => { diff --git a/web/app/components/goto-anything/command-selector.tsx b/web/app/components/goto-anything/command-selector.tsx index 7bf47723e..37d5cd6e7 100644 --- a/web/app/components/goto-anything/command-selector.tsx +++ b/web/app/components/goto-anything/command-selector.tsx @@ -1,8 +1,9 @@ import type { FC } from 'react' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { Command } from 'cmdk' import { useTranslation } from 'react-i18next' import type { ActionItem } from './actions/types' +import { slashCommandRegistry } from './actions/commands/registry' type Props = { actions: Record @@ -10,27 +11,57 @@ type Props = { searchFilter?: string commandValue?: string onCommandValueChange?: (value: string) => void + originalQuery?: string } -const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange }) => { +const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange, originalQuery }) => { const { t } = useTranslation() - const filteredActions = Object.values(actions).filter((action) => { - if (!searchFilter) - return true - const filterLower = searchFilter.toLowerCase() - return action.shortcut.toLowerCase().includes(filterLower) - }) + // Check if we're in slash command mode + const isSlashMode = originalQuery?.trim().startsWith('/') || false + + // Get slash commands from registry + const slashCommands = useMemo(() => { + if (!isSlashMode) return [] + + const allCommands = slashCommandRegistry.getAllCommands() + const filter = searchFilter?.toLowerCase() || '' // searchFilter already has '/' removed + + return allCommands.filter((cmd) => { + if (!filter) return true + return cmd.name.toLowerCase().includes(filter) + }).map(cmd => ({ + key: `/${cmd.name}`, + shortcut: `/${cmd.name}`, + title: cmd.name, + description: cmd.description, + })) + }, [isSlashMode, searchFilter]) + + const filteredActions = useMemo(() => { + if (isSlashMode) return [] + + return Object.values(actions).filter((action) => { + // Exclude slash action when in @ mode + if (action.key === '/') return false + if (!searchFilter) + return true + const filterLower = searchFilter.toLowerCase() + return action.shortcut.toLowerCase().includes(filterLower) + }) + }, [actions, searchFilter, isSlashMode]) + + const allItems = isSlashMode ? slashCommands : filteredActions useEffect(() => { - if (filteredActions.length > 0 && onCommandValueChange) { - const currentValueExists = filteredActions.some(action => action.shortcut === commandValue) + if (allItems.length > 0 && onCommandValueChange) { + const currentValueExists = allItems.some(item => item.shortcut === commandValue) if (!currentValueExists) - onCommandValueChange(filteredActions[0].shortcut) + onCommandValueChange(allItems[0].shortcut) } - }, [searchFilter, filteredActions.length]) + }, [searchFilter, allItems.length]) - if (filteredActions.length === 0) { + if (allItems.length === 0) { return (
@@ -50,33 +81,46 @@ const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, co return (
- {t('app.gotoAnything.selectSearchType')} + {isSlashMode ? t('app.gotoAnything.groups.commands') : t('app.gotoAnything.selectSearchType')}
- {filteredActions.map(action => ( + {allItems.map(item => ( onCommandSelect(action.shortcut)} + onSelect={() => onCommandSelect(item.shortcut)} > - {action.shortcut} + {item.shortcut} - {(() => { - const keyMap: Record = { - '/': 'app.gotoAnything.actions.slashDesc', - '@app': 'app.gotoAnything.actions.searchApplicationsDesc', - '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', - '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', - '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', - } - return t(keyMap[action.key]) - })()} + {isSlashMode ? ( + (() => { + const slashKeyMap: Record = { + '/theme': 'app.gotoAnything.actions.themeCategoryDesc', + '/language': 'app.gotoAnything.actions.languageChangeDesc', + '/account': 'app.gotoAnything.actions.accountDesc', + '/feedback': 'app.gotoAnything.actions.feedbackDesc', + '/doc': 'app.gotoAnything.actions.docDesc', + '/community': 'app.gotoAnything.actions.communityDesc', + } + return t(slashKeyMap[item.key] || item.description) + })() + ) : ( + (() => { + const keyMap: Record = { + '@app': 'app.gotoAnything.actions.searchApplicationsDesc', + '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', + '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', + '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', + } + return t(keyMap[item.key]) + })() + )} ))} diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index cd4795735..a8d474b97 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -226,6 +226,7 @@ const GotoAnything: FC = ({
{t('app.gotoAnything.searchHint')}
{t('app.gotoAnything.commandHint')}
+
{t('app.gotoAnything.slashHint')}
) @@ -321,6 +322,7 @@ const GotoAnything: FC = ({ searchFilter={searchQuery.trim().substring(1)} commandValue={cmdVal} onCommandValueChange={setCmdVal} + originalQuery={searchQuery.trim()} /> ) : ( Object.entries(groupedResults).map(([type, results], groupIndex) => ( diff --git a/web/context/i18n.ts b/web/context/i18n.ts index c0031c5c7..6364cbf21 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -32,7 +32,7 @@ export const useGetPricingPageLanguage = () => { return getPricingPageLanguage(locale) } -const defaultDocBaseUrl = 'https://docs.dify.ai' +export const defaultDocBaseUrl = 'https://docs.dify.ai' export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => { let baseDocUrl = baseUrl || defaultDocBaseUrl baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index ce7474dda..b11d06449 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -269,6 +269,7 @@ const translation = { selectSearchType: 'Choose what to search for', searchHint: 'Start typing to search everything instantly', commandHint: 'Type @ to browse by category', + slashHint: 'Type / to see all available commands', actions: { searchApplications: 'Search Applications', searchApplicationsDesc: 'Search and navigate to your applications', @@ -292,7 +293,11 @@ const translation = { languageCategoryTitle: 'Language', languageCategoryDesc: 'Switch interface language', languageChangeDesc: 'Change UI language', - slashDesc: 'Execute commands like /theme, /lang', + slashDesc: 'Execute commands (type / to see all available commands)', + accountDesc: 'Navigate to account page', + communityDesc: 'Open Discord community', + docDesc: 'Open help documentation', + feedbackDesc: 'Open community feedback discussions', }, emptyState: { noAppsFound: 'No apps found', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index eb6a9ad40..9a5eee861 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -268,6 +268,7 @@ const translation = { selectSearchType: '选择搜索内容', searchHint: '开始输入即可立即搜索所有内容', commandHint: '输入 @ 按类别浏览', + slashHint: '输入 / 查看所有可用命令', actions: { searchApplications: '搜索应用程序', searchApplicationsDesc: '搜索并导航到您的应用程序', @@ -291,7 +292,11 @@ const translation = { languageCategoryTitle: '语言', languageCategoryDesc: '切换界面语言', languageChangeDesc: '更改界面语言', - slashDesc: '执行诸如 /theme、/lang 等命令', + slashDesc: '执行命令(输入 / 查看所有可用命令)', + accountDesc: '导航到账户页面', + communityDesc: '打开 Discord 社区', + docDesc: '打开帮助文档', + feedbackDesc: '打开社区反馈讨论', }, emptyState: { noAppsFound: '未找到应用',