dify
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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',
|
||||
mode: 'direct',
|
||||
|
||||
// Direct execution function
|
||||
execute: () => {
|
||||
window.location.href = '/account'
|
||||
},
|
||||
|
||||
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,26 @@
|
||||
export type CommandHandler = (args?: Record<string, any>) => void | Promise<void>
|
||||
|
||||
const handlers = new Map<string, CommandHandler>()
|
||||
|
||||
const registerCommand = (name: string, handler: CommandHandler) => {
|
||||
handlers.set(name, handler)
|
||||
}
|
||||
|
||||
const unregisterCommand = (name: string) => {
|
||||
handlers.delete(name)
|
||||
}
|
||||
|
||||
export const executeCommand = async (name: string, args?: Record<string, any>) => {
|
||||
const handler = handlers.get(name)
|
||||
if (!handler)
|
||||
return
|
||||
await handler(args)
|
||||
}
|
||||
|
||||
export const registerCommands = (map: Record<string, CommandHandler>) => {
|
||||
Object.entries(map).forEach(([name, handler]) => registerCommand(name, handler))
|
||||
}
|
||||
|
||||
export const unregisterCommands = (names: string[]) => {
|
||||
names.forEach(unregisterCommand)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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',
|
||||
mode: 'direct',
|
||||
|
||||
// Direct execution function
|
||||
execute: () => {
|
||||
const url = 'https://discord.gg/5AEfbxcd9k'
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
|
||||
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,58 @@
|
||||
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'
|
||||
import { getDocLanguage } from '@/i18n-config/language'
|
||||
|
||||
// Documentation command dependency types - no external dependencies needed
|
||||
type DocDeps = Record<string, never>
|
||||
|
||||
/**
|
||||
* Documentation command - Opens help documentation
|
||||
*/
|
||||
export const docsCommand: SlashCommandHandler<DocDeps> = {
|
||||
name: 'docs',
|
||||
description: 'Open documentation',
|
||||
mode: 'direct',
|
||||
|
||||
// Direct execution function
|
||||
execute: () => {
|
||||
const currentLocale = i18n.language
|
||||
const docLanguage = getDocLanguage(currentLocale)
|
||||
const url = `${defaultDocBaseUrl}/${docLanguage}`
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
|
||||
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) => {
|
||||
// Get the current language from i18n
|
||||
const currentLocale = i18n.language
|
||||
const docLanguage = getDocLanguage(currentLocale)
|
||||
const url = `${defaultDocBaseUrl}/${docLanguage}`
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['navigation.doc'])
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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'
|
||||
|
||||
// Forum command dependency types
|
||||
type ForumDeps = Record<string, never>
|
||||
|
||||
/**
|
||||
* Forum command - Opens Dify community forum
|
||||
*/
|
||||
export const forumCommand: SlashCommandHandler<ForumDeps> = {
|
||||
name: 'forum',
|
||||
description: 'Open Dify community forum',
|
||||
mode: 'direct',
|
||||
|
||||
// Direct execution function
|
||||
execute: () => {
|
||||
const url = 'https://forum.dify.ai'
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
|
||||
async search(args: string, locale: string = 'en') {
|
||||
return [{
|
||||
id: 'forum',
|
||||
title: i18n.t('common.userProfile.forum', { 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.forum', args: { url: 'https://forum.dify.ai' } },
|
||||
}]
|
||||
},
|
||||
|
||||
register(_deps: ForumDeps) {
|
||||
registerCommands({
|
||||
'navigation.forum': async (args) => {
|
||||
const url = args?.url || 'https://forum.dify.ai'
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['navigation.forum'])
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Command system exports
|
||||
export { slashAction } from './slash'
|
||||
export { registerSlashCommands, unregisterSlashCommands, SlashCommandProvider } from './slash'
|
||||
|
||||
// Command registry system (for extending with custom commands)
|
||||
export { slashCommandRegistry, SlashCommandRegistry } from './registry'
|
||||
export type { SlashCommandHandler } from './types'
|
||||
|
||||
// Command bus (for extending with custom commands)
|
||||
export {
|
||||
executeCommand,
|
||||
registerCommands,
|
||||
unregisterCommands,
|
||||
type CommandHandler,
|
||||
} from './command-bus'
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { SlashCommandHandler } from './types'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { registerCommands, unregisterCommands } from './command-bus'
|
||||
|
||||
// Language dependency types
|
||||
type LanguageDeps = {
|
||||
setLocale?: (locale: string) => Promise<void>
|
||||
}
|
||||
|
||||
const buildLanguageCommands = (query: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = languages.filter(item => item.supported && (
|
||||
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
|
||||
))
|
||||
return list.map(item => ({
|
||||
id: `lang-${item.value}`,
|
||||
title: item.name,
|
||||
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
|
||||
type: 'command' as const,
|
||||
data: { command: 'i18n.set', args: { locale: item.value } },
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Language command handler
|
||||
* Integrates UI building, search, and registration logic
|
||||
*/
|
||||
export const languageCommand: SlashCommandHandler<LanguageDeps> = {
|
||||
name: 'language',
|
||||
aliases: ['lang'],
|
||||
description: 'Switch between different languages',
|
||||
mode: 'submenu', // Explicitly set submenu mode
|
||||
|
||||
async search(args: string, _locale: string = 'en') {
|
||||
// Return language options directly, regardless of parameters
|
||||
return buildLanguageCommands(args)
|
||||
},
|
||||
|
||||
register(deps: LanguageDeps) {
|
||||
registerCommands({
|
||||
'i18n.set': async (args) => {
|
||||
const locale = args?.locale
|
||||
if (locale)
|
||||
await deps.setLocale?.(locale)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['i18n.set'])
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import type { SlashCommandHandler } from './types'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
|
||||
/**
|
||||
* Slash Command Registry System
|
||||
* Responsible for managing registration, lookup, and search of all slash commands
|
||||
*/
|
||||
export class SlashCommandRegistry {
|
||||
private commands = new Map<string, SlashCommandHandler>()
|
||||
private commandDeps = new Map<string, any>()
|
||||
|
||||
/**
|
||||
* Register command handler
|
||||
*/
|
||||
register<TDeps = any>(handler: SlashCommandHandler<TDeps>, deps?: TDeps) {
|
||||
// Register main command name
|
||||
this.commands.set(handler.name, handler)
|
||||
|
||||
// Register aliases
|
||||
if (handler.aliases) {
|
||||
handler.aliases.forEach((alias) => {
|
||||
this.commands.set(alias, handler)
|
||||
})
|
||||
}
|
||||
|
||||
// Store dependencies and call registration method
|
||||
if (deps) {
|
||||
this.commandDeps.set(handler.name, deps)
|
||||
handler.register?.(deps)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister command
|
||||
*/
|
||||
unregister(name: string) {
|
||||
const handler = this.commands.get(name)
|
||||
if (handler) {
|
||||
// Call the command's unregister method
|
||||
handler.unregister?.()
|
||||
|
||||
// Remove dependencies
|
||||
this.commandDeps.delete(handler.name)
|
||||
|
||||
// Remove main command name
|
||||
this.commands.delete(handler.name)
|
||||
|
||||
// Remove all aliases
|
||||
if (handler.aliases) {
|
||||
handler.aliases.forEach((alias) => {
|
||||
this.commands.delete(alias)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find command handler
|
||||
*/
|
||||
findCommand(commandName: string): SlashCommandHandler | undefined {
|
||||
return this.commands.get(commandName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart partial command matching
|
||||
* Prioritize alias matching, then match command name prefix
|
||||
*/
|
||||
private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined {
|
||||
const lowerPartial = partialName.toLowerCase()
|
||||
|
||||
// First check if any alias starts with this
|
||||
const aliasMatch = this.findHandlerByAliasPrefix(lowerPartial)
|
||||
if (aliasMatch)
|
||||
return aliasMatch
|
||||
|
||||
// Then check if command name starts with this
|
||||
return this.findHandlerByNamePrefix(lowerPartial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find handler by alias prefix
|
||||
*/
|
||||
private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined {
|
||||
for (const handler of this.getAllCommands()) {
|
||||
if (handler.aliases?.some(alias => alias.toLowerCase().startsWith(prefix)))
|
||||
return handler
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Find handler by name prefix
|
||||
*/
|
||||
private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined {
|
||||
return this.getAllCommands().find(handler =>
|
||||
handler.name.toLowerCase().startsWith(prefix),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered commands (deduplicated)
|
||||
*/
|
||||
getAllCommands(): SlashCommandHandler[] {
|
||||
const uniqueCommands = new Map<string, SlashCommandHandler>()
|
||||
this.commands.forEach((handler) => {
|
||||
uniqueCommands.set(handler.name, handler)
|
||||
})
|
||||
return Array.from(uniqueCommands.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Search commands
|
||||
* @param query Full query (e.g., "/theme dark" or "/lang en")
|
||||
* @param locale Current language
|
||||
*/
|
||||
async search(query: string, locale: string = 'en'): Promise<CommandSearchResult[]> {
|
||||
const trimmed = query.trim()
|
||||
|
||||
// Handle root level search "/"
|
||||
if (trimmed === '/' || !trimmed.replace('/', '').trim())
|
||||
return await this.getRootCommands()
|
||||
|
||||
// Parse command and arguments
|
||||
const afterSlash = trimmed.substring(1).trim()
|
||||
const spaceIndex = afterSlash.indexOf(' ')
|
||||
const commandName = spaceIndex === -1 ? afterSlash : afterSlash.substring(0, spaceIndex)
|
||||
const args = spaceIndex === -1 ? '' : afterSlash.substring(spaceIndex + 1).trim()
|
||||
|
||||
// First try exact match
|
||||
let handler = this.findCommand(commandName)
|
||||
if (handler) {
|
||||
try {
|
||||
return await handler.search(args, locale)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Command search failed for ${commandName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match, try smart partial matching
|
||||
handler = this.findBestPartialMatch(commandName)
|
||||
if (handler) {
|
||||
try {
|
||||
return await handler.search(args, locale)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Command search failed for ${handler.name}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Finally perform fuzzy search
|
||||
return this.fuzzySearchCommands(afterSlash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get root level command list
|
||||
*/
|
||||
private async getRootCommands(): Promise<CommandSearchResult[]> {
|
||||
const results: CommandSearchResult[] = []
|
||||
|
||||
// Generate a root level item for each command
|
||||
for (const handler of this.getAllCommands()) {
|
||||
results.push({
|
||||
id: `root-${handler.name}`,
|
||||
title: `/${handler.name}`,
|
||||
description: handler.description,
|
||||
type: 'command' as const,
|
||||
data: {
|
||||
command: `root.${handler.name}`,
|
||||
args: { name: handler.name },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search commands
|
||||
*/
|
||||
private fuzzySearchCommands(query: string): CommandSearchResult[] {
|
||||
const lowercaseQuery = query.toLowerCase()
|
||||
const matches: CommandSearchResult[] = []
|
||||
|
||||
this.getAllCommands().forEach((handler) => {
|
||||
// Check if command name matches
|
||||
if (handler.name.toLowerCase().includes(lowercaseQuery)) {
|
||||
matches.push({
|
||||
id: `fuzzy-${handler.name}`,
|
||||
title: `/${handler.name}`,
|
||||
description: handler.description,
|
||||
type: 'command' as const,
|
||||
data: {
|
||||
command: `root.${handler.name}`,
|
||||
args: { name: handler.name },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Check if aliases match
|
||||
if (handler.aliases) {
|
||||
handler.aliases.forEach((alias) => {
|
||||
if (alias.toLowerCase().includes(lowercaseQuery)) {
|
||||
matches.push({
|
||||
id: `fuzzy-${alias}`,
|
||||
title: `/${alias}`,
|
||||
description: `${handler.description} (alias for /${handler.name})`,
|
||||
type: 'command' as const,
|
||||
data: {
|
||||
command: `root.${handler.name}`,
|
||||
args: { name: handler.name },
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command dependencies
|
||||
*/
|
||||
getCommandDependencies(commandName: string): any {
|
||||
return this.commandDeps.get(commandName)
|
||||
}
|
||||
}
|
||||
|
||||
// Global registry instance
|
||||
export const slashCommandRegistry = new SlashCommandRegistry()
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import type { ActionItem } from '../types'
|
||||
import { slashCommandRegistry } from './registry'
|
||||
import { executeCommand } from './command-bus'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { themeCommand } from './theme'
|
||||
import { languageCommand } from './language'
|
||||
import { forumCommand } from './forum'
|
||||
import { docsCommand } from './docs'
|
||||
import { communityCommand } from './community'
|
||||
import { accountCommand } from './account'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
|
||||
export const slashAction: ActionItem = {
|
||||
key: '/',
|
||||
shortcut: '/',
|
||||
title: i18n.t('app.gotoAnything.actions.slashTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.slashDesc'),
|
||||
action: (result) => {
|
||||
if (result.type !== 'command') return
|
||||
const { command, args } = result.data
|
||||
executeCommand(command, args)
|
||||
},
|
||||
search: async (query, _searchTerm = '') => {
|
||||
// Delegate all search logic to the command registry system
|
||||
return slashCommandRegistry.search(query, i18n.language)
|
||||
},
|
||||
}
|
||||
|
||||
// Register/unregister default handlers for slash commands with external dependencies.
|
||||
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(forumCommand, {})
|
||||
slashCommandRegistry.register(docsCommand, {})
|
||||
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('forum')
|
||||
slashCommandRegistry.unregister('docs')
|
||||
slashCommandRegistry.unregister('community')
|
||||
slashCommandRegistry.unregister('account')
|
||||
}
|
||||
|
||||
export const SlashCommandProvider = () => {
|
||||
const theme = useTheme()
|
||||
useEffect(() => {
|
||||
registerSlashCommands({
|
||||
setTheme: theme.setTheme,
|
||||
setLocale: setLocaleOnClient,
|
||||
})
|
||||
return () => unregisterSlashCommands()
|
||||
}, [theme.setTheme])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { SlashCommandHandler } from './types'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
import type { ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import { RiComputerLine, RiMoonLine, RiSunLine } from '@remixicon/react'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { registerCommands, unregisterCommands } from './command-bus'
|
||||
|
||||
// Theme dependency types
|
||||
type ThemeDeps = {
|
||||
setTheme?: (value: 'light' | 'dark' | 'system') => void
|
||||
}
|
||||
|
||||
const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: string; icon: ReactNode }[] = [
|
||||
{
|
||||
id: 'system',
|
||||
titleKey: 'app.gotoAnything.actions.themeSystem',
|
||||
descKey: 'app.gotoAnything.actions.themeSystemDesc',
|
||||
icon: <RiComputerLine className='h-4 w-4 text-text-tertiary' />,
|
||||
},
|
||||
{
|
||||
id: 'light',
|
||||
titleKey: 'app.gotoAnything.actions.themeLight',
|
||||
descKey: 'app.gotoAnything.actions.themeLightDesc',
|
||||
icon: <RiSunLine className='h-4 w-4 text-text-tertiary' />,
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
titleKey: 'app.gotoAnything.actions.themeDark',
|
||||
descKey: 'app.gotoAnything.actions.themeDarkDesc',
|
||||
icon: <RiMoonLine className='h-4 w-4 text-text-tertiary' />,
|
||||
},
|
||||
]
|
||||
|
||||
const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = THEME_ITEMS.filter(item =>
|
||||
!q
|
||||
|| i18n.t(item.titleKey, { lng: locale }).toLowerCase().includes(q)
|
||||
|| item.id.includes(q),
|
||||
)
|
||||
return list.map(item => ({
|
||||
id: item.id,
|
||||
title: i18n.t(item.titleKey, { lng: locale }),
|
||||
description: i18n.t(item.descKey, { 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'>
|
||||
{item.icon}
|
||||
</div>
|
||||
),
|
||||
data: { command: 'theme.set', args: { value: item.id } },
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme command handler
|
||||
* Integrates UI building, search, and registration logic
|
||||
*/
|
||||
export const themeCommand: SlashCommandHandler<ThemeDeps> = {
|
||||
name: 'theme',
|
||||
description: 'Switch between light and dark themes',
|
||||
mode: 'submenu', // Explicitly set submenu mode
|
||||
|
||||
async search(args: string, locale: string = 'en') {
|
||||
// Return theme options directly, regardless of parameters
|
||||
return buildThemeCommands(args, locale)
|
||||
},
|
||||
|
||||
register(deps: ThemeDeps) {
|
||||
registerCommands({
|
||||
'theme.set': async (args) => {
|
||||
deps.setTheme?.(args?.value)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['theme.set'])
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { CommandSearchResult } from '../types'
|
||||
|
||||
/**
|
||||
* Slash command handler interface
|
||||
* Each slash command should implement this interface
|
||||
*/
|
||||
export type SlashCommandHandler<TDeps = any> = {
|
||||
/** Command name (e.g., 'theme', 'language') */
|
||||
name: string
|
||||
|
||||
/** Command alias list (e.g., ['lang'] for language) */
|
||||
aliases?: string[]
|
||||
|
||||
/** Command description */
|
||||
description: string
|
||||
|
||||
/**
|
||||
* Command mode:
|
||||
* - 'direct': Execute immediately when selected (e.g., /docs, /community)
|
||||
* - 'submenu': Show submenu options (e.g., /theme, /language)
|
||||
*/
|
||||
mode?: 'direct' | 'submenu'
|
||||
|
||||
/**
|
||||
* Direct execution function for 'direct' mode commands
|
||||
* Called when the command is selected and should execute immediately
|
||||
*/
|
||||
execute?: () => void | Promise<void>
|
||||
|
||||
/**
|
||||
* Search command results (for 'submenu' mode or showing options)
|
||||
* @param args Command arguments (part after removing command name)
|
||||
* @param locale Current language
|
||||
*/
|
||||
search: (args: string, locale?: string) => Promise<CommandSearchResult[]>
|
||||
|
||||
/**
|
||||
* Called when registering command, passing external dependencies
|
||||
*/
|
||||
register?: (deps: TDeps) => void
|
||||
|
||||
/**
|
||||
* Called when unregistering command
|
||||
*/
|
||||
unregister?: () => void
|
||||
}
|
||||
Reference in New Issue
Block a user