| 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']) | |||||
| }, | |||||
| } |
| 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']) | |||||
| }, | |||||
| } |
| 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']) | |||||
| }, | |||||
| } |
| 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']) | |||||
| }, | |||||
| } |
| import { setLocaleOnClient } from '@/i18n-config' | import { setLocaleOnClient } from '@/i18n-config' | ||||
| import { themeCommand } from './theme' | import { themeCommand } from './theme' | ||||
| import { languageCommand } from './language' | 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' | import i18n from '@/i18n-config/i18next-config' | ||||
| export const slashAction: ActionItem = { | export const slashAction: ActionItem = { | ||||
| // Register command handlers to the registry system with their respective dependencies | // Register command handlers to the registry system with their respective dependencies | ||||
| slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme }) | slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme }) | ||||
| slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale }) | slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale }) | ||||
| slashCommandRegistry.register(feedbackCommand, {}) | |||||
| slashCommandRegistry.register(docCommand, {}) | |||||
| slashCommandRegistry.register(communityCommand, {}) | |||||
| slashCommandRegistry.register(accountCommand, {}) | |||||
| } | } | ||||
| export const unregisterSlashCommands = () => { | export const unregisterSlashCommands = () => { | ||||
| // Remove command handlers from registry system (automatically calls each command's unregister method) | // Remove command handlers from registry system (automatically calls each command's unregister method) | ||||
| slashCommandRegistry.unregister('theme') | slashCommandRegistry.unregister('theme') | ||||
| slashCommandRegistry.unregister('language') | slashCommandRegistry.unregister('language') | ||||
| slashCommandRegistry.unregister('feedback') | |||||
| slashCommandRegistry.unregister('doc') | |||||
| slashCommandRegistry.unregister('community') | |||||
| slashCommandRegistry.unregister('account') | |||||
| } | } | ||||
| export const SlashCommandProvider = () => { | export const SlashCommandProvider = () => { |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useEffect } from 'react' | |||||
| import { useEffect, useMemo } from 'react' | |||||
| import { Command } from 'cmdk' | import { Command } from 'cmdk' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import type { ActionItem } from './actions/types' | import type { ActionItem } from './actions/types' | ||||
| import { slashCommandRegistry } from './actions/commands/registry' | |||||
| type Props = { | type Props = { | ||||
| actions: Record<string, ActionItem> | actions: Record<string, ActionItem> | ||||
| searchFilter?: string | searchFilter?: string | ||||
| commandValue?: string | commandValue?: string | ||||
| onCommandValueChange?: (value: string) => void | 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 { 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(() => { | 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) | 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 ( | return ( | ||||
| <div className="p-4"> | <div className="p-4"> | ||||
| <div className="flex items-center justify-center py-8 text-center text-text-tertiary"> | <div className="flex items-center justify-center py-8 text-center text-text-tertiary"> | ||||
| return ( | return ( | ||||
| <div className="p-4"> | <div className="p-4"> | ||||
| <div className="mb-3 text-left text-sm font-medium text-text-secondary"> | <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> | </div> | ||||
| <Command.Group className="space-y-1"> | <Command.Group className="space-y-1"> | ||||
| {filteredActions.map(action => ( | |||||
| {allItems.map(item => ( | |||||
| <Command.Item | <Command.Item | ||||
| key={action.key} | |||||
| value={action.shortcut} | |||||
| key={item.key} | |||||
| value={item.shortcut} | |||||
| className="flex cursor-pointer items-center rounded-md | className="flex cursor-pointer items-center rounded-md | ||||
| p-2.5 | p-2.5 | ||||
| transition-all | transition-all | ||||
| duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt" | 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"> | <span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary"> | ||||
| {action.shortcut} | |||||
| {item.shortcut} | |||||
| </span> | </span> | ||||
| <span className="ml-3 text-sm text-text-secondary"> | <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> | </span> | ||||
| </Command.Item> | </Command.Item> | ||||
| ))} | ))} |
| <div className='mt-3 space-y-1 text-xs text-text-quaternary'> | <div className='mt-3 space-y-1 text-xs text-text-quaternary'> | ||||
| <div>{t('app.gotoAnything.searchHint')}</div> | <div>{t('app.gotoAnything.searchHint')}</div> | ||||
| <div>{t('app.gotoAnything.commandHint')}</div> | <div>{t('app.gotoAnything.commandHint')}</div> | ||||
| <div>{t('app.gotoAnything.slashHint')}</div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div>) | </div>) | ||||
| searchFilter={searchQuery.trim().substring(1)} | searchFilter={searchQuery.trim().substring(1)} | ||||
| commandValue={cmdVal} | commandValue={cmdVal} | ||||
| onCommandValueChange={setCmdVal} | onCommandValueChange={setCmdVal} | ||||
| originalQuery={searchQuery.trim()} | |||||
| /> | /> | ||||
| ) : ( | ) : ( | ||||
| Object.entries(groupedResults).map(([type, results], groupIndex) => ( | Object.entries(groupedResults).map(([type, results], groupIndex) => ( |
| return getPricingPageLanguage(locale) | 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) => { | export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => { | ||||
| let baseDocUrl = baseUrl || defaultDocBaseUrl | let baseDocUrl = baseUrl || defaultDocBaseUrl | ||||
| baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl | baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl |
| selectSearchType: 'Choose what to search for', | selectSearchType: 'Choose what to search for', | ||||
| searchHint: 'Start typing to search everything instantly', | searchHint: 'Start typing to search everything instantly', | ||||
| commandHint: 'Type @ to browse by category', | commandHint: 'Type @ to browse by category', | ||||
| slashHint: 'Type / to see all available commands', | |||||
| actions: { | actions: { | ||||
| searchApplications: 'Search Applications', | searchApplications: 'Search Applications', | ||||
| searchApplicationsDesc: 'Search and navigate to your applications', | searchApplicationsDesc: 'Search and navigate to your applications', | ||||
| languageCategoryTitle: 'Language', | languageCategoryTitle: 'Language', | ||||
| languageCategoryDesc: 'Switch interface language', | languageCategoryDesc: 'Switch interface language', | ||||
| languageChangeDesc: 'Change UI 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: { | emptyState: { | ||||
| noAppsFound: 'No apps found', | noAppsFound: 'No apps found', |
| selectSearchType: '选择搜索内容', | selectSearchType: '选择搜索内容', | ||||
| searchHint: '开始输入即可立即搜索所有内容', | searchHint: '开始输入即可立即搜索所有内容', | ||||
| commandHint: '输入 @ 按类别浏览', | commandHint: '输入 @ 按类别浏览', | ||||
| slashHint: '输入 / 查看所有可用命令', | |||||
| actions: { | actions: { | ||||
| searchApplications: '搜索应用程序', | searchApplications: '搜索应用程序', | ||||
| searchApplicationsDesc: '搜索并导航到您的应用程序', | searchApplicationsDesc: '搜索并导航到您的应用程序', | ||||
| languageCategoryTitle: '语言', | languageCategoryTitle: '语言', | ||||
| languageCategoryDesc: '切换界面语言', | languageCategoryDesc: '切换界面语言', | ||||
| languageChangeDesc: '更改界面语言', | languageChangeDesc: '更改界面语言', | ||||
| slashDesc: '执行诸如 /theme、/lang 等命令', | |||||
| slashDesc: '执行命令(输入 / 查看所有可用命令)', | |||||
| accountDesc: '导航到账户页面', | |||||
| communityDesc: '打开 Discord 社区', | |||||
| docDesc: '打开帮助文档', | |||||
| feedbackDesc: '打开社区反馈讨论', | |||||
| }, | }, | ||||
| emptyState: { | emptyState: { | ||||
| noAppsFound: '未找到应用', | noAppsFound: '未找到应用', |