You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.ts 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * Goto Anything - Action System
  3. *
  4. * This file defines the action registry for the goto-anything search system.
  5. * Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands.
  6. *
  7. * ## How to Add a New Slash Command
  8. *
  9. * 1. **Create Command Handler File** (in `./commands/` directory):
  10. * ```typescript
  11. * // commands/my-command.ts
  12. * import type { SlashCommandHandler } from './types'
  13. * import type { CommandSearchResult } from '../types'
  14. * import { registerCommands, unregisterCommands } from './command-bus'
  15. *
  16. * interface MyCommandDeps {
  17. * myService?: (data: any) => Promise<void>
  18. * }
  19. *
  20. * export const myCommand: SlashCommandHandler<MyCommandDeps> = {
  21. * name: 'mycommand',
  22. * aliases: ['mc'], // Optional aliases
  23. * description: 'My custom command description',
  24. *
  25. * async search(args: string, locale: string = 'en') {
  26. * // Return search results based on args
  27. * return [{
  28. * id: 'my-result',
  29. * title: 'My Command Result',
  30. * description: 'Description of the result',
  31. * type: 'command' as const,
  32. * data: { command: 'my.action', args: { value: args } }
  33. * }]
  34. * },
  35. *
  36. * register(deps: MyCommandDeps) {
  37. * registerCommands({
  38. * 'my.action': async (args) => {
  39. * await deps.myService?.(args?.value)
  40. * }
  41. * })
  42. * },
  43. *
  44. * unregister() {
  45. * unregisterCommands(['my.action'])
  46. * }
  47. * }
  48. * ```
  49. *
  50. * **Example for Self-Contained Command (no external dependencies):**
  51. * ```typescript
  52. * // commands/calculator-command.ts
  53. * export const calculatorCommand: SlashCommandHandler = {
  54. * name: 'calc',
  55. * aliases: ['calculator'],
  56. * description: 'Simple calculator',
  57. *
  58. * async search(args: string) {
  59. * if (!args.trim()) return []
  60. * try {
  61. * // Safe math evaluation (implement proper parser in real use)
  62. * const result = Function('"use strict"; return (' + args + ')')()
  63. * return [{
  64. * id: 'calc-result',
  65. * title: `${args} = ${result}`,
  66. * description: 'Calculator result',
  67. * type: 'command' as const,
  68. * data: { command: 'calc.copy', args: { result: result.toString() } }
  69. * }]
  70. * } catch {
  71. * return [{
  72. * id: 'calc-error',
  73. * title: 'Invalid expression',
  74. * description: 'Please enter a valid math expression',
  75. * type: 'command' as const,
  76. * data: { command: 'calc.noop', args: {} }
  77. * }]
  78. * }
  79. * },
  80. *
  81. * register() {
  82. * registerCommands({
  83. * 'calc.copy': (args) => navigator.clipboard.writeText(args.result),
  84. * 'calc.noop': () => {} // No operation
  85. * })
  86. * },
  87. *
  88. * unregister() {
  89. * unregisterCommands(['calc.copy', 'calc.noop'])
  90. * }
  91. * }
  92. * ```
  93. *
  94. * 2. **Register Command** (in `./commands/slash.tsx`):
  95. * ```typescript
  96. * import { myCommand } from './my-command'
  97. * import { calculatorCommand } from './calculator-command' // For self-contained commands
  98. *
  99. * export const registerSlashCommands = (deps: Record<string, any>) => {
  100. * slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
  101. * slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
  102. * slashCommandRegistry.register(myCommand, { myService: deps.myService }) // With dependencies
  103. * slashCommandRegistry.register(calculatorCommand) // Self-contained, no dependencies
  104. * }
  105. *
  106. * export const unregisterSlashCommands = () => {
  107. * slashCommandRegistry.unregister('theme')
  108. * slashCommandRegistry.unregister('language')
  109. * slashCommandRegistry.unregister('mycommand')
  110. * slashCommandRegistry.unregister('calc') // Add this line
  111. * }
  112. * ```
  113. *
  114. *
  115. * 3. **Update SlashCommandProvider** (in `./commands/slash.tsx`):
  116. * ```typescript
  117. * export const SlashCommandProvider = () => {
  118. * const theme = useTheme()
  119. * const myService = useMyService() // Add external dependency if needed
  120. *
  121. * useEffect(() => {
  122. * registerSlashCommands({
  123. * setTheme: theme.setTheme, // Required for theme command
  124. * setLocale: setLocaleOnClient, // Required for language command
  125. * myService: myService, // Required for your custom command
  126. * // Note: calculatorCommand doesn't need dependencies, so not listed here
  127. * })
  128. * return () => unregisterSlashCommands()
  129. * }, [theme.setTheme, myService]) // Update dependency array for all dynamic deps
  130. *
  131. * return null
  132. * }
  133. * ```
  134. *
  135. * **Note:** Self-contained commands (like calculator) don't require dependencies but are
  136. * still registered through the same system for consistent lifecycle management.
  137. *
  138. * 4. **Usage**: Users can now type `/mycommand` or `/mc` to use your command
  139. *
  140. * ## Command System Architecture
  141. * - Commands are registered via `SlashCommandRegistry`
  142. * - Each command is self-contained with its own dependencies
  143. * - Commands support aliases for easier access
  144. * - Command execution is handled by the command bus system
  145. * - All commands should be registered through `SlashCommandProvider` for consistent lifecycle management
  146. *
  147. * ## Command Types
  148. * **Commands with External Dependencies:**
  149. * - Require external services, APIs, or React hooks
  150. * - Must provide dependencies in `SlashCommandProvider`
  151. * - Example: theme commands (needs useTheme), API commands (needs service)
  152. *
  153. * **Self-Contained Commands:**
  154. * - Pure logic operations, no external dependencies
  155. * - Still recommended to register through `SlashCommandProvider` for consistency
  156. * - Example: calculator, text manipulation commands
  157. *
  158. * ## Available Actions
  159. * - `@app` - Search applications
  160. * - `@knowledge` / `@kb` - Search knowledge bases
  161. * - `@plugin` - Search plugins
  162. * - `@node` - Search workflow nodes (workflow pages only)
  163. * - `/` - Execute slash commands (theme, language, etc.)
  164. */
  165. import { appAction } from './app'
  166. import { knowledgeAction } from './knowledge'
  167. import { pluginAction } from './plugin'
  168. import { workflowNodesAction } from './workflow-nodes'
  169. import { ragPipelineNodesAction } from './rag-pipeline-nodes'
  170. import type { ActionItem, SearchResult } from './types'
  171. import { slashAction } from './commands'
  172. import { slashCommandRegistry } from './commands/registry'
  173. // Create dynamic Actions based on context
  174. export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => {
  175. const baseActions = {
  176. slash: slashAction,
  177. app: appAction,
  178. knowledge: knowledgeAction,
  179. plugin: pluginAction,
  180. }
  181. // Add appropriate node search based on context
  182. if (isRagPipelinePage) {
  183. return {
  184. ...baseActions,
  185. node: ragPipelineNodesAction,
  186. }
  187. }
  188. else if (isWorkflowPage) {
  189. return {
  190. ...baseActions,
  191. node: workflowNodesAction,
  192. }
  193. }
  194. // Default actions without node search
  195. return baseActions
  196. }
  197. // Legacy export for backward compatibility
  198. export const Actions = {
  199. slash: slashAction,
  200. app: appAction,
  201. knowledge: knowledgeAction,
  202. plugin: pluginAction,
  203. node: workflowNodesAction,
  204. }
  205. export const searchAnything = async (
  206. locale: string,
  207. query: string,
  208. actionItem?: ActionItem,
  209. dynamicActions?: Record<string, ActionItem>,
  210. ): Promise<SearchResult[]> => {
  211. if (actionItem) {
  212. const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
  213. try {
  214. return await actionItem.search(query, searchTerm, locale)
  215. }
  216. catch (error) {
  217. console.warn(`Search failed for ${actionItem.key}:`, error)
  218. return []
  219. }
  220. }
  221. if (query.startsWith('@') || query.startsWith('/'))
  222. return []
  223. const globalSearchActions = Object.values(dynamicActions || Actions)
  224. // Use Promise.allSettled to handle partial failures gracefully
  225. const searchPromises = globalSearchActions.map(async (action) => {
  226. try {
  227. const results = await action.search(query, query, locale)
  228. return { success: true, data: results, actionType: action.key }
  229. }
  230. catch (error) {
  231. console.warn(`Search failed for ${action.key}:`, error)
  232. return { success: false, data: [], actionType: action.key, error }
  233. }
  234. })
  235. const settledResults = await Promise.allSettled(searchPromises)
  236. const allResults: SearchResult[] = []
  237. const failedActions: string[] = []
  238. settledResults.forEach((result, index) => {
  239. if (result.status === 'fulfilled' && result.value.success) {
  240. allResults.push(...result.value.data)
  241. }
  242. else {
  243. const actionKey = globalSearchActions[index]?.key || 'unknown'
  244. failedActions.push(actionKey)
  245. }
  246. })
  247. if (failedActions.length > 0)
  248. console.warn(`Some search actions failed: ${failedActions.join(', ')}`)
  249. return allResults
  250. }
  251. export const matchAction = (query: string, actions: Record<string, ActionItem>) => {
  252. return Object.values(actions).find((action) => {
  253. // Special handling for slash commands
  254. if (action.key === '/') {
  255. // Get all registered commands from the registry
  256. const allCommands = slashCommandRegistry.getAllCommands()
  257. // Check if query matches any registered command
  258. return allCommands.some((cmd) => {
  259. const cmdPattern = `/${cmd.name}`
  260. // For direct mode commands, don't match (keep in command selector)
  261. if (cmd.mode === 'direct')
  262. return false
  263. // For submenu mode commands, match when complete command is entered
  264. return query === cmdPattern || query.startsWith(`${cmdPattern} `)
  265. })
  266. }
  267. const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
  268. return reg.test(query)
  269. })
  270. }
  271. export * from './types'
  272. export * from './commands'
  273. export { appAction, knowledgeAction, pluginAction, workflowNodesAction }