Skip to content

Commit 973a390

Browse files
authored
Feature/run cmd (langgenius#23822)
1 parent a77dfb6 commit 973a390

10 files changed

Lines changed: 267 additions & 6 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export type CommandHandler = (args?: Record<string, any>) => void | Promise<void>
2+
3+
const handlers = new Map<string, CommandHandler>()
4+
5+
export const registerCommand = (name: string, handler: CommandHandler) => {
6+
handlers.set(name, handler)
7+
}
8+
9+
export const unregisterCommand = (name: string) => {
10+
handlers.delete(name)
11+
}
12+
13+
export const executeCommand = async (name: string, args?: Record<string, any>) => {
14+
const handler = handlers.get(name)
15+
if (!handler)
16+
return
17+
await handler(args)
18+
}
19+
20+
export const registerCommands = (map: Record<string, CommandHandler>) => {
21+
Object.entries(map).forEach(([name, handler]) => registerCommand(name, handler))
22+
}
23+
24+
export const unregisterCommands = (names: string[]) => {
25+
names.forEach(unregisterCommand)
26+
}

web/app/components/goto-anything/actions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { knowledgeAction } from './knowledge'
33
import { pluginAction } from './plugin'
44
import { workflowNodesAction } from './workflow-nodes'
55
import type { ActionItem, SearchResult } from './types'
6+
import { commandAction } from './run'
67

78
export const Actions = {
89
app: appAction,
910
knowledge: knowledgeAction,
1011
plugin: pluginAction,
12+
run: commandAction,
1113
node: workflowNodesAction,
1214
}
1315

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CommandSearchResult } from './types'
2+
import { languages } from '@/i18n-config/language'
3+
import { RiTranslate } from '@remixicon/react'
4+
import i18n from '@/i18n-config/i18next-config'
5+
6+
export const buildLanguageCommands = (query: string): CommandSearchResult[] => {
7+
const q = query.toLowerCase()
8+
const list = languages.filter(item => item.supported && (
9+
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
10+
))
11+
return list.map(item => ({
12+
id: `lang-${item.value}`,
13+
title: item.name,
14+
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
15+
type: 'command' as const,
16+
data: { command: 'i18n.set', args: { locale: item.value } },
17+
}))
18+
}
19+
20+
export const buildLanguageRootItem = (): CommandSearchResult => {
21+
return {
22+
id: 'category-language',
23+
title: i18n.t('app.gotoAnything.actions.languageCategoryTitle'),
24+
description: i18n.t('app.gotoAnything.actions.languageCategoryDesc'),
25+
type: 'command',
26+
icon: (
27+
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
28+
<RiTranslate className='h-4 w-4 text-text-tertiary' />
29+
</div>
30+
),
31+
data: { command: 'nav.search', args: { query: '@run language ' } },
32+
}
33+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { CommandSearchResult } from './types'
2+
import type { ReactNode } from 'react'
3+
import { RiComputerLine, RiMoonLine, RiPaletteLine, RiSunLine } from '@remixicon/react'
4+
import i18n from '@/i18n-config/i18next-config'
5+
6+
const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: string; icon: ReactNode }[] = [
7+
{
8+
id: 'system',
9+
titleKey: 'app.gotoAnything.actions.themeSystem',
10+
descKey: 'app.gotoAnything.actions.themeSystemDesc',
11+
icon: <RiComputerLine className='h-4 w-4 text-text-tertiary' />,
12+
},
13+
{
14+
id: 'light',
15+
titleKey: 'app.gotoAnything.actions.themeLight',
16+
descKey: 'app.gotoAnything.actions.themeLightDesc',
17+
icon: <RiSunLine className='h-4 w-4 text-text-tertiary' />,
18+
},
19+
{
20+
id: 'dark',
21+
titleKey: 'app.gotoAnything.actions.themeDark',
22+
descKey: 'app.gotoAnything.actions.themeDarkDesc',
23+
icon: <RiMoonLine className='h-4 w-4 text-text-tertiary' />,
24+
},
25+
]
26+
27+
export const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
28+
const q = query.toLowerCase()
29+
const list = THEME_ITEMS.filter(item =>
30+
!q
31+
|| i18n.t(item.titleKey, { lng: locale }).toLowerCase().includes(q)
32+
|| item.id.includes(q),
33+
)
34+
return list.map(item => ({
35+
id: item.id,
36+
title: i18n.t(item.titleKey, { lng: locale }),
37+
description: i18n.t(item.descKey, { lng: locale }),
38+
type: 'command' as const,
39+
icon: (
40+
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
41+
{item.icon}
42+
</div>
43+
),
44+
data: { command: 'theme.set', args: { value: item.id } },
45+
}))
46+
}
47+
48+
export const buildThemeRootItem = (): CommandSearchResult => {
49+
return {
50+
id: 'category-theme',
51+
title: i18n.t('app.gotoAnything.actions.themeCategoryTitle'),
52+
description: i18n.t('app.gotoAnything.actions.themeCategoryDesc'),
53+
type: 'command',
54+
icon: (
55+
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
56+
<RiPaletteLine className='h-4 w-4 text-text-tertiary' />
57+
</div>
58+
),
59+
data: { command: 'nav.search', args: { query: '@run theme ' } },
60+
}
61+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use client'
2+
import { useEffect } from 'react'
3+
import type { ActionItem, CommandSearchResult } from './types'
4+
import { buildLanguageCommands, buildLanguageRootItem } from './run-language'
5+
import { buildThemeCommands, buildThemeRootItem } from './run-theme'
6+
import i18n from '@/i18n-config/i18next-config'
7+
import { executeCommand, registerCommands, unregisterCommands } from './command-bus'
8+
import { useTheme } from 'next-themes'
9+
import { setLocaleOnClient } from '@/i18n-config'
10+
11+
const rootParser = (query: string): CommandSearchResult[] => {
12+
const q = query.toLowerCase()
13+
const items: CommandSearchResult[] = []
14+
if (!q || 'theme'.includes(q))
15+
items.push(buildThemeRootItem())
16+
if (!q || 'language'.includes(q) || 'lang'.includes(q))
17+
items.push(buildLanguageRootItem())
18+
return items
19+
}
20+
21+
type RunContext = {
22+
setTheme?: (value: 'light' | 'dark' | 'system') => void
23+
setLocale?: (locale: string) => Promise<void>
24+
search?: (query: string) => void
25+
}
26+
27+
export const commandAction: ActionItem = {
28+
key: '@run',
29+
shortcut: '@run',
30+
title: i18n.t('app.gotoAnything.actions.runTitle'),
31+
description: i18n.t('app.gotoAnything.actions.runDesc'),
32+
action: (result) => {
33+
if (result.type !== 'command') return
34+
const { command, args } = result.data
35+
if (command === 'theme.set') {
36+
executeCommand('theme.set', args)
37+
return
38+
}
39+
if (command === 'i18n.set') {
40+
executeCommand('i18n.set', args)
41+
return
42+
}
43+
if (command === 'nav.search')
44+
executeCommand('nav.search', args)
45+
},
46+
search: async (_, searchTerm = '') => {
47+
const q = searchTerm.trim()
48+
if (q.startsWith('theme'))
49+
return buildThemeCommands(q.replace(/^theme\s*/, ''), i18n.language)
50+
if (q.startsWith('language') || q.startsWith('lang'))
51+
return buildLanguageCommands(q.replace(/^(language|lang)\s*/, ''))
52+
53+
// root categories
54+
return rootParser(q)
55+
},
56+
}
57+
58+
// Register/unregister default handlers for @run commands with external dependencies.
59+
export const registerRunCommands = (deps: {
60+
setTheme?: (value: 'light' | 'dark' | 'system') => void
61+
setLocale?: (locale: string) => Promise<void>
62+
search?: (query: string) => void
63+
}) => {
64+
registerCommands({
65+
'theme.set': async (args) => {
66+
deps.setTheme?.(args?.value)
67+
},
68+
'i18n.set': async (args) => {
69+
const locale = args?.locale
70+
if (locale)
71+
await deps.setLocale?.(locale)
72+
},
73+
'nav.search': (args) => {
74+
const q = args?.query
75+
if (q)
76+
deps.search?.(q)
77+
},
78+
})
79+
}
80+
81+
export const unregisterRunCommands = () => {
82+
unregisterCommands(['theme.set', 'i18n.set', 'nav.search'])
83+
}
84+
85+
export const RunCommandProvider = ({ onNavSearch }: { onNavSearch?: (q: string) => void }) => {
86+
const theme = useTheme()
87+
useEffect(() => {
88+
registerRunCommands({
89+
setTheme: theme.setTheme,
90+
setLocale: setLocaleOnClient,
91+
search: onNavSearch,
92+
})
93+
return () => unregisterRunCommands()
94+
}, [theme.setTheme, onNavSearch])
95+
96+
return null
97+
}

web/app/components/goto-anything/actions/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Plugin } from '../../plugins/types'
55
import type { DataSet } from '@/models/datasets'
66
import type { CommonNodeType } from '../../workflow/types'
77

8-
export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node'
8+
export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command'
99

1010
export type BaseSearchResult<T = any> = {
1111
id: string
@@ -37,10 +37,14 @@ export type WorkflowNodeSearchResult = {
3737
}
3838
} & BaseSearchResult<CommonNodeType>
3939

40-
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult
40+
export type CommandSearchResult = {
41+
type: 'command'
42+
} & BaseSearchResult<{ command: string; args?: Record<string, any> }>
43+
44+
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult
4145

4246
export type ActionItem = {
43-
key: '@app' | '@knowledge' | '@plugin' | '@node'
47+
key: '@app' | '@knowledge' | '@plugin' | '@node' | '@run'
4448
shortcut: string
4549
title: string | TypeWithI18N
4650
description: string

web/app/components/goto-anything/command-selector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co
7373
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
7474
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
7575
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
76+
'@run': 'app.gotoAnything.actions.runDesc',
7677
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
7778
}
7879
return t(keyMap[action.key])

web/app/components/goto-anything/index.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import InstallFromMarketplace from '../plugins/install-plugin/install-from-marke
1818
import type { Plugin } from '../plugins/types'
1919
import { Command } from 'cmdk'
2020
import CommandSelector from './command-selector'
21+
import { RunCommandProvider } from './actions/run'
2122

2223
type Props = {
2324
onHide?: () => void
@@ -33,7 +34,11 @@ const GotoAnything: FC<Props> = ({
3334
const [searchQuery, setSearchQuery] = useState<string>('')
3435
const [cmdVal, setCmdVal] = useState<string>('')
3536
const inputRef = useRef<HTMLInputElement>(null)
36-
37+
const handleNavSearch = useCallback((q: string) => {
38+
setShow(true)
39+
setSearchQuery(q)
40+
requestAnimationFrame(() => inputRef.current?.focus())
41+
}, [])
3742
// Filter actions based on context
3843
const Actions = useMemo(() => {
3944
// Create a filtered copy of actions based on current page context
@@ -43,8 +48,8 @@ const GotoAnything: FC<Props> = ({
4348
}
4449
else {
4550
// Exclude node action on non-workflow pages
46-
const { app, knowledge, plugin } = AllActions
47-
return { app, knowledge, plugin }
51+
const { app, knowledge, plugin, run } = AllActions
52+
return { app, knowledge, plugin, run }
4853
}
4954
}, [isWorkflowPage])
5055

@@ -128,6 +133,11 @@ const GotoAnything: FC<Props> = ({
128133
setSearchQuery('')
129134

130135
switch (result.type) {
136+
case 'command': {
137+
const action = Object.values(Actions).find(a => a.key === '@run')
138+
action?.action?.(result)
139+
break
140+
}
131141
case 'plugin':
132142
setActivePlugin(result.data)
133143
break
@@ -381,6 +391,7 @@ const GotoAnything: FC<Props> = ({
381391
</div>
382392

383393
</Modal>
394+
<RunCommandProvider onNavSearch={handleNavSearch} />
384395
{
385396
activePlugin && (
386397
<InstallFromMarketplace

web/i18n/en-US/app.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,19 @@ const translation = {
279279
searchWorkflowNodes: 'Search Workflow Nodes',
280280
searchWorkflowNodesDesc: 'Find and jump to nodes in the current workflow by name or type',
281281
searchWorkflowNodesHelp: 'This feature only works when viewing a workflow. Navigate to a workflow first.',
282+
runTitle: 'Commands',
283+
runDesc: 'Run quick commands (theme, language, ...)',
284+
themeCategoryTitle: 'Theme',
285+
themeCategoryDesc: 'Switch application theme',
286+
themeSystem: 'System Theme',
287+
themeSystemDesc: 'Follow your OS appearance',
288+
themeLight: 'Light Theme',
289+
themeLightDesc: 'Use light appearance',
290+
themeDark: 'Dark Theme',
291+
themeDarkDesc: 'Use dark appearance',
292+
languageCategoryTitle: 'Language',
293+
languageCategoryDesc: 'Switch interface language',
294+
languageChangeDesc: 'Change UI language',
282295
},
283296
emptyState: {
284297
noAppsFound: 'No apps found',

web/i18n/zh-Hans/app.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,19 @@ const translation = {
278278
searchWorkflowNodes: '搜索工作流节点',
279279
searchWorkflowNodesDesc: '按名称或类型查找并跳转到当前工作流中的节点',
280280
searchWorkflowNodesHelp: '此功能仅在查看工作流时有效。首先导航到工作流。',
281+
runTitle: '命令',
282+
runDesc: '快速执行命令(主题、语言等)',
283+
themeCategoryTitle: '主题',
284+
themeCategoryDesc: '切换应用主题',
285+
themeSystem: '系统主题',
286+
themeSystemDesc: '跟随系统外观',
287+
themeLight: '浅色主题',
288+
themeLightDesc: '使用浅色外观',
289+
themeDark: '深色主题',
290+
themeDarkDesc: '使用深色外观',
291+
languageCategoryTitle: '语言',
292+
languageCategoryDesc: '切换界面语言',
293+
languageChangeDesc: '更改界面语言',
281294
},
282295
emptyState: {
283296
noAppsFound: '未找到应用',

0 commit comments

Comments
 (0)