| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- /**
- * Goto Anything - Action System
- *
- * This file defines the action registry for the goto-anything search system.
- * Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands.
- *
- * ## How to Add a New Slash Command
- *
- * 1. **Create Command Handler File** (in `./commands/` directory):
- * ```typescript
- * // commands/my-command.ts
- * import type { SlashCommandHandler } from './types'
- * import type { CommandSearchResult } from '../types'
- * import { registerCommands, unregisterCommands } from './command-bus'
- *
- * interface MyCommandDeps {
- * myService?: (data: any) => Promise<void>
- * }
- *
- * export const myCommand: SlashCommandHandler<MyCommandDeps> = {
- * name: 'mycommand',
- * aliases: ['mc'], // Optional aliases
- * description: 'My custom command description',
- *
- * async search(args: string, locale: string = 'en') {
- * // Return search results based on args
- * return [{
- * id: 'my-result',
- * title: 'My Command Result',
- * description: 'Description of the result',
- * type: 'command' as const,
- * data: { command: 'my.action', args: { value: args } }
- * }]
- * },
- *
- * register(deps: MyCommandDeps) {
- * registerCommands({
- * 'my.action': async (args) => {
- * await deps.myService?.(args?.value)
- * }
- * })
- * },
- *
- * unregister() {
- * unregisterCommands(['my.action'])
- * }
- * }
- * ```
- *
- * **Example for Self-Contained Command (no external dependencies):**
- * ```typescript
- * // commands/calculator-command.ts
- * export const calculatorCommand: SlashCommandHandler = {
- * name: 'calc',
- * aliases: ['calculator'],
- * description: 'Simple calculator',
- *
- * async search(args: string) {
- * if (!args.trim()) return []
- * try {
- * // Safe math evaluation (implement proper parser in real use)
- * const result = Function('"use strict"; return (' + args + ')')()
- * return [{
- * id: 'calc-result',
- * title: `${args} = ${result}`,
- * description: 'Calculator result',
- * type: 'command' as const,
- * data: { command: 'calc.copy', args: { result: result.toString() } }
- * }]
- * } catch {
- * return [{
- * id: 'calc-error',
- * title: 'Invalid expression',
- * description: 'Please enter a valid math expression',
- * type: 'command' as const,
- * data: { command: 'calc.noop', args: {} }
- * }]
- * }
- * },
- *
- * register() {
- * registerCommands({
- * 'calc.copy': (args) => navigator.clipboard.writeText(args.result),
- * 'calc.noop': () => {} // No operation
- * })
- * },
- *
- * unregister() {
- * unregisterCommands(['calc.copy', 'calc.noop'])
- * }
- * }
- * ```
- *
- * 2. **Register Command** (in `./commands/slash.tsx`):
- * ```typescript
- * import { myCommand } from './my-command'
- * import { calculatorCommand } from './calculator-command' // For self-contained commands
- *
- * export const registerSlashCommands = (deps: Record<string, any>) => {
- * slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
- * slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
- * slashCommandRegistry.register(myCommand, { myService: deps.myService }) // With dependencies
- * slashCommandRegistry.register(calculatorCommand) // Self-contained, no dependencies
- * }
- *
- * export const unregisterSlashCommands = () => {
- * slashCommandRegistry.unregister('theme')
- * slashCommandRegistry.unregister('language')
- * slashCommandRegistry.unregister('mycommand')
- * slashCommandRegistry.unregister('calc') // Add this line
- * }
- * ```
- *
- *
- * 3. **Update SlashCommandProvider** (in `./commands/slash.tsx`):
- * ```typescript
- * export const SlashCommandProvider = () => {
- * const theme = useTheme()
- * const myService = useMyService() // Add external dependency if needed
- *
- * useEffect(() => {
- * registerSlashCommands({
- * setTheme: theme.setTheme, // Required for theme command
- * setLocale: setLocaleOnClient, // Required for language command
- * myService: myService, // Required for your custom command
- * // Note: calculatorCommand doesn't need dependencies, so not listed here
- * })
- * return () => unregisterSlashCommands()
- * }, [theme.setTheme, myService]) // Update dependency array for all dynamic deps
- *
- * return null
- * }
- * ```
- *
- * **Note:** Self-contained commands (like calculator) don't require dependencies but are
- * still registered through the same system for consistent lifecycle management.
- *
- * 4. **Usage**: Users can now type `/mycommand` or `/mc` to use your command
- *
- * ## Command System Architecture
- * - Commands are registered via `SlashCommandRegistry`
- * - Each command is self-contained with its own dependencies
- * - Commands support aliases for easier access
- * - Command execution is handled by the command bus system
- * - All commands should be registered through `SlashCommandProvider` for consistent lifecycle management
- *
- * ## Command Types
- * **Commands with External Dependencies:**
- * - Require external services, APIs, or React hooks
- * - Must provide dependencies in `SlashCommandProvider`
- * - Example: theme commands (needs useTheme), API commands (needs service)
- *
- * **Self-Contained Commands:**
- * - Pure logic operations, no external dependencies
- * - Still recommended to register through `SlashCommandProvider` for consistency
- * - Example: calculator, text manipulation commands
- *
- * ## Available Actions
- * - `@app` - Search applications
- * - `@knowledge` / `@kb` - Search knowledge bases
- * - `@plugin` - Search plugins
- * - `@node` - Search workflow nodes (workflow pages only)
- * - `/` - Execute slash commands (theme, language, etc.)
- */
-
- import { appAction } from './app'
- import { knowledgeAction } from './knowledge'
- import { pluginAction } from './plugin'
- import { workflowNodesAction } from './workflow-nodes'
- import { ragPipelineNodesAction } from './rag-pipeline-nodes'
- import type { ActionItem, SearchResult } from './types'
- import { slashAction } from './commands'
- import { slashCommandRegistry } from './commands/registry'
-
- // Create dynamic Actions based on context
- export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => {
- const baseActions = {
- slash: slashAction,
- app: appAction,
- knowledge: knowledgeAction,
- plugin: pluginAction,
- }
-
- // Add appropriate node search based on context
- if (isRagPipelinePage) {
- return {
- ...baseActions,
- node: ragPipelineNodesAction,
- }
- }
- else if (isWorkflowPage) {
- return {
- ...baseActions,
- node: workflowNodesAction,
- }
- }
-
- // Default actions without node search
- return baseActions
- }
-
- // Legacy export for backward compatibility
- export const Actions = {
- slash: slashAction,
- app: appAction,
- knowledge: knowledgeAction,
- plugin: pluginAction,
- node: workflowNodesAction,
- }
-
- export const searchAnything = async (
- locale: string,
- query: string,
- actionItem?: ActionItem,
- dynamicActions?: Record<string, ActionItem>,
- ): Promise<SearchResult[]> => {
- if (actionItem) {
- const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
- try {
- return await actionItem.search(query, searchTerm, locale)
- }
- catch (error) {
- console.warn(`Search failed for ${actionItem.key}:`, error)
- return []
- }
- }
-
- if (query.startsWith('@') || query.startsWith('/'))
- return []
-
- const globalSearchActions = Object.values(dynamicActions || Actions)
-
- // Use Promise.allSettled to handle partial failures gracefully
- const searchPromises = globalSearchActions.map(async (action) => {
- try {
- const results = await action.search(query, query, locale)
- return { success: true, data: results, actionType: action.key }
- }
- catch (error) {
- console.warn(`Search failed for ${action.key}:`, error)
- return { success: false, data: [], actionType: action.key, error }
- }
- })
-
- const settledResults = await Promise.allSettled(searchPromises)
-
- const allResults: SearchResult[] = []
- const failedActions: string[] = []
-
- settledResults.forEach((result, index) => {
- if (result.status === 'fulfilled' && result.value.success) {
- allResults.push(...result.value.data)
- }
- else {
- const actionKey = globalSearchActions[index]?.key || 'unknown'
- failedActions.push(actionKey)
- }
- })
-
- if (failedActions.length > 0)
- console.warn(`Some search actions failed: ${failedActions.join(', ')}`)
-
- return allResults
- }
-
- export const matchAction = (query: string, actions: Record<string, ActionItem>) => {
- return Object.values(actions).find((action) => {
- // Special handling for slash commands
- if (action.key === '/') {
- // Get all registered commands from the registry
- const allCommands = slashCommandRegistry.getAllCommands()
-
- // Check if query matches any registered command
- return allCommands.some((cmd) => {
- const cmdPattern = `/${cmd.name}`
-
- // For direct mode commands, don't match (keep in command selector)
- if (cmd.mode === 'direct')
- return false
-
- // For submenu mode commands, match when complete command is entered
- return query === cmdPattern || query.startsWith(`${cmdPattern} `)
- })
- }
-
- const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
- return reg.test(query)
- })
- }
-
- export * from './types'
- export * from './commands'
- export { appAction, knowledgeAction, pluginAction, workflowNodesAction }
|