| @@ -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<string, never> | |||
| /** | |||
| * Account command - Navigates to account page | |||
| */ | |||
| export const accountCommand: SlashCommandHandler<AccountDeps> = { | |||
| 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: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiUser3Line className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| 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']) | |||
| }, | |||
| } | |||
| @@ -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<string, never> | |||
| /** | |||
| * Community command - Opens Discord community | |||
| */ | |||
| export const communityCommand: SlashCommandHandler<CommunityDeps> = { | |||
| 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: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiDiscordLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| 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']) | |||
| }, | |||
| } | |||
| @@ -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<string, never> | |||
| /** | |||
| * Documentation command - Opens help documentation | |||
| */ | |||
| export const docCommand: SlashCommandHandler<DocDeps> = { | |||
| 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: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiBookOpenLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| 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']) | |||
| }, | |||
| } | |||
| @@ -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<string, never> | |||
| /** | |||
| * Feedback command - Opens GitHub feedback discussions | |||
| */ | |||
| export const feedbackCommand: SlashCommandHandler<FeedbackDeps> = { | |||
| 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: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiFeedbackLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| 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']) | |||
| }, | |||
| } | |||
| @@ -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<string, any>) => { | |||
| // 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 = () => { | |||
| @@ -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<string, ActionItem> | |||
| @@ -10,27 +11,57 @@ type Props = { | |||
| searchFilter?: string | |||
| commandValue?: string | |||
| onCommandValueChange?: (value: string) => void | |||
| originalQuery?: string | |||
| } | |||
| const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange }) => { | |||
| const CommandSelector: FC<Props> = ({ 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 ( | |||
| <div className="p-4"> | |||
| <div className="flex items-center justify-center py-8 text-center text-text-tertiary"> | |||
| @@ -50,33 +81,46 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co | |||
| return ( | |||
| <div className="p-4"> | |||
| <div className="mb-3 text-left text-sm font-medium text-text-secondary"> | |||
| {t('app.gotoAnything.selectSearchType')} | |||
| {isSlashMode ? t('app.gotoAnything.groups.commands') : t('app.gotoAnything.selectSearchType')} | |||
| </div> | |||
| <Command.Group className="space-y-1"> | |||
| {filteredActions.map(action => ( | |||
| {allItems.map(item => ( | |||
| <Command.Item | |||
| key={action.key} | |||
| value={action.shortcut} | |||
| key={item.key} | |||
| value={item.shortcut} | |||
| className="flex cursor-pointer items-center rounded-md | |||
| p-2.5 | |||
| transition-all | |||
| duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt" | |||
| onSelect={() => onCommandSelect(action.shortcut)} | |||
| onSelect={() => onCommandSelect(item.shortcut)} | |||
| > | |||
| <span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary"> | |||
| {action.shortcut} | |||
| {item.shortcut} | |||
| </span> | |||
| <span className="ml-3 text-sm text-text-secondary"> | |||
| {(() => { | |||
| const keyMap: Record<string, string> = { | |||
| '/': '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<string, string> = { | |||
| '/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<string, string> = { | |||
| '@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]) | |||
| })() | |||
| )} | |||
| </span> | |||
| </Command.Item> | |||
| ))} | |||
| @@ -226,6 +226,7 @@ const GotoAnything: FC<Props> = ({ | |||
| <div className='mt-3 space-y-1 text-xs text-text-quaternary'> | |||
| <div>{t('app.gotoAnything.searchHint')}</div> | |||
| <div>{t('app.gotoAnything.commandHint')}</div> | |||
| <div>{t('app.gotoAnything.slashHint')}</div> | |||
| </div> | |||
| </div> | |||
| </div>) | |||
| @@ -321,6 +322,7 @@ const GotoAnything: FC<Props> = ({ | |||
| searchFilter={searchQuery.trim().substring(1)} | |||
| commandValue={cmdVal} | |||
| onCommandValueChange={setCmdVal} | |||
| originalQuery={searchQuery.trim()} | |||
| /> | |||
| ) : ( | |||
| Object.entries(groupedResults).map(([type, results], groupIndex) => ( | |||
| @@ -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 | |||
| @@ -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', | |||
| @@ -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: '未找到应用', | |||