Merge branch 'main' into feat/rag-2

# Conflicts:
#	api/core/workflow/entities/variable_pool.py
This commit is contained in:
jyong
2025-08-12 11:13:04 +08:00
75 changed files with 2797 additions and 481 deletions

View File

@ -15,6 +15,7 @@ type IModal = {
children?: React.ReactNode
closable?: boolean
overflowVisible?: boolean
highPriority?: boolean // For modals that need to appear above dropdowns
}
export default function Modal({
@ -27,10 +28,11 @@ export default function Modal({
children,
closable = false,
overflowVisible = false,
highPriority = false,
}: IModal) {
return (
<Transition appear show={isShow} as={Fragment}>
<Dialog as="div" className={classNames('relative z-[60]', wrapperClassName)} onClose={onClose}>
<Dialog as="div" className={classNames('relative', highPriority ? 'z-[1100]' : 'z-[60]', wrapperClassName)} onClose={onClose}>
<TransitionChild>
<div className={classNames(
'fixed inset-0 bg-background-overlay',

View File

@ -79,7 +79,7 @@ const TagFilter: FC<TagFilterProps> = ({
className='block'
>
<div className={cn(
'flex h-8 cursor-pointer items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2',
'flex h-8 cursor-pointer select-none items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2',
!open && !!value.length && 'shadow-xs',
open && !!value.length && 'shadow-xs',
)}>
@ -123,7 +123,7 @@ const TagFilter: FC<TagFilterProps> = ({
{filteredTagList.map(tag => (
<div
key={tag.id}
className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover'
className='flex cursor-pointer select-none items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover'
onClick={() => selectTag(tag)}
>
<div title={tag.name} className='grow truncate text-sm leading-5 text-text-tertiary'>{tag.name}</div>
@ -139,7 +139,7 @@ const TagFilter: FC<TagFilterProps> = ({
</div>
<div className='border-t-[0.5px] border-divider-regular' />
<div className='p-1'>
<div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => {
<div className='flex cursor-pointer select-none items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => {
setShowTagManagementModal(true)
setOpen(false)
}}>

View File

@ -38,16 +38,21 @@ export const appAction: ActionItem = {
title: 'Search Applications',
description: 'Search and navigate to your applications',
// action,
search: async (_, searchTerm = '', locale) => {
const response = (await fetchAppList({
url: 'apps',
params: {
page: 1,
name: searchTerm,
},
}))
const apps = response.data || []
return parser(apps)
search: async (_, searchTerm = '', _locale) => {
try {
const response = await fetchAppList({
url: 'apps',
params: {
page: 1,
name: searchTerm,
},
})
const apps = response?.data || []
return parser(apps)
}
catch (error) {
console.warn('App search failed:', error)
return []
}
},
}

View File

@ -18,7 +18,13 @@ export const searchAnything = async (
): Promise<SearchResult[]> => {
if (actionItem) {
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
return await actionItem.search(query, searchTerm, locale)
try {
return await actionItem.search(query, searchTerm, locale)
}
catch (error) {
console.warn(`Search failed for ${actionItem.key}:`, error)
return []
}
}
if (query.startsWith('@'))

View File

@ -35,16 +35,22 @@ export const knowledgeAction: ActionItem = {
title: 'Search Knowledge Bases',
description: 'Search and navigate to your knowledge bases',
// action,
search: async (_, searchTerm = '', locale) => {
const response = await fetchDatasets({
url: '/datasets',
params: {
page: 1,
limit: 10,
keyword: searchTerm,
},
})
return parser(response.data)
search: async (_, searchTerm = '', _locale) => {
try {
const response = await fetchDatasets({
url: '/datasets',
params: {
page: 1,
limit: 10,
keyword: searchTerm,
},
})
const datasets = response?.data || []
return parser(datasets)
}
catch (error) {
console.warn('Knowledge search failed:', error)
return []
}
},
}

View File

@ -24,18 +24,30 @@ export const pluginAction: ActionItem = {
title: 'Search Plugins',
description: 'Search and navigate to your plugins',
search: async (_, searchTerm = '', locale) => {
const response = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/search/advanced', {
body: {
page: 1,
page_size: 10,
query: searchTerm,
type: 'plugin',
},
})
const list = (response.data.plugins || []).map(plugin => ({
...plugin,
icon: getPluginIconInMarketplace(plugin),
}))
return parser(list, locale!)
try {
const response = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/search/advanced', {
body: {
page: 1,
page_size: 10,
query: searchTerm,
type: 'plugin',
},
})
if (!response?.data?.plugins) {
console.warn('Plugin search: Unexpected response structure', response)
return []
}
const list = response.data.plugins.map(plugin => ({
...plugin,
icon: getPluginIconInMarketplace(plugin),
}))
return parser(list, locale!)
}
catch (error) {
console.warn('Plugin search failed:', error)
return []
}
},
}

View File

@ -1,6 +1,4 @@
import type { ActionItem } from './types'
import { BoltIcon } from '@heroicons/react/24/outline'
import i18n from 'i18next'
// Create the workflow nodes action
export const workflowNodesAction: ActionItem = {
@ -12,32 +10,14 @@ export const workflowNodesAction: ActionItem = {
search: async (_, searchTerm = '', locale) => {
try {
// Use the searchFn if available (set by useWorkflowSearch hook)
if (workflowNodesAction.searchFn) {
// searchFn already returns SearchResult[] type, no need to use parser
if (workflowNodesAction.searchFn)
return workflowNodesAction.searchFn(searchTerm)
}
// If not in workflow context or search function not registered
if (!searchTerm.trim()) {
return [{
id: 'help',
title: i18n.t('app.gotoAnything.actions.searchWorkflowNodes', { lng: locale }),
description: i18n.t('app.gotoAnything.actions.searchWorkflowNodesHelp', { lng: locale }),
type: 'workflow-node',
path: '#',
data: {} as any,
icon: (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-50 text-blue-600">
<BoltIcon className="h-5 w-5" />
</div>
),
}]
}
// If not in workflow context, return empty array
return []
}
catch (error) {
console.error('Error searching workflow nodes:', error)
catch (error) {
console.warn('Workflow nodes search failed:', error)
return []
}
},

View File

@ -0,0 +1,88 @@
import type { FC } from 'react'
import { useEffect } from 'react'
import { Command } from 'cmdk'
import { useTranslation } from 'react-i18next'
import type { ActionItem } from './actions/types'
type Props = {
actions: Record<string, ActionItem>
onCommandSelect: (commandKey: string) => void
searchFilter?: string
commandValue?: string
onCommandValueChange?: (value: string) => void
}
const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange }) => {
const { t } = useTranslation()
const filteredActions = Object.values(actions).filter((action) => {
if (!searchFilter)
return true
const filterLower = searchFilter.toLowerCase()
return action.shortcut.toLowerCase().includes(filterLower)
|| action.key.toLowerCase().includes(filterLower)
})
useEffect(() => {
if (filteredActions.length > 0 && onCommandValueChange) {
const currentValueExists = filteredActions.some(action => action.shortcut === commandValue)
if (!currentValueExists)
onCommandValueChange(filteredActions[0].shortcut)
}
}, [searchFilter, filteredActions.length])
if (filteredActions.length === 0) {
return (
<div className="p-4">
<div className="flex items-center justify-center py-8 text-center text-text-tertiary">
<div>
<div className="text-sm font-medium text-text-tertiary">
{t('app.gotoAnything.noMatchingCommands')}
</div>
<div className="mt-1 text-xs text-text-quaternary">
{t('app.gotoAnything.tryDifferentSearch')}
</div>
</div>
</div>
</div>
)
}
return (
<div className="p-4">
<div className="mb-3 text-left text-sm font-medium text-text-secondary">
{t('app.gotoAnything.selectSearchType')}
</div>
<Command.Group className="space-y-1">
{filteredActions.map(action => (
<Command.Item
key={action.key}
value={action.shortcut}
className="flex cursor-pointer items-center rounded-md
p-2.5
transition-all
duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover"
onSelect={() => onCommandSelect(action.shortcut)}
>
<span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary">
{action.shortcut}
</span>
<span className="ml-3 text-sm text-text-secondary">
{(() => {
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[action.key])
})()}
</span>
</Command.Item>
))}
</Command.Group>
</div>
)
}
export default CommandSelector

View File

@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
import type { Plugin } from '../plugins/types'
import { Command } from 'cmdk'
import CommandSelector from './command-selector'
type Props = {
onHide?: () => void
@ -81,11 +82,15 @@ const GotoAnything: FC<Props> = ({
wait: 300,
})
const isCommandsMode = searchQuery.trim() === '@' || (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), Actions))
const searchMode = useMemo(() => {
if (isCommandsMode) return 'commands'
const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions)
return action ? action.key : 'general'
}, [searchQueryDebouncedValue, Actions])
}, [searchQueryDebouncedValue, Actions, isCommandsMode])
const { data: searchResults = [], isLoading, isError, error } = useQuery(
{
@ -103,12 +108,20 @@ const GotoAnything: FC<Props> = ({
const action = matchAction(query, Actions)
return await searchAnything(defaultLocale, query, action)
},
enabled: !!searchQueryDebouncedValue,
enabled: !!searchQueryDebouncedValue && !isCommandsMode,
staleTime: 30000,
gcTime: 300000,
},
)
const handleCommandSelect = useCallback((commandKey: string) => {
setSearchQuery(`${commandKey} `)
setCmdVal('')
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}, [])
// Handle navigation to selected result
const handleNavigate = useCallback((result: SearchResult) => {
setShow(false)
@ -141,7 +154,7 @@ const GotoAnything: FC<Props> = ({
[searchResults])
const emptyResult = useMemo(() => {
if (searchResults.length || !searchQueryDebouncedValue.trim() || isLoading)
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
return null
const isCommandSearch = searchMode !== 'general'
@ -186,34 +199,22 @@ const GotoAnything: FC<Props> = ({
</div>
</div>
)
}, [searchResults, searchQueryDebouncedValue, Actions, searchMode, isLoading, isError])
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
const defaultUI = useMemo(() => {
if (searchQueryDebouncedValue.trim())
if (searchQuery.trim())
return null
return (<div className="flex items-center justify-center py-8 text-center text-text-tertiary">
return (<div className="flex items-center justify-center py-12 text-center text-text-tertiary">
<div>
<div className='text-sm font-medium'>{t('app.gotoAnything.searchTitle')}</div>
<div className='mt-3 space-y-2 text-xs text-text-quaternary'>
{Object.values(Actions).map(action => (
<div key={action.key} className='flex items-center gap-2'>
<span className='inline-flex items-center rounded bg-gray-200 px-2 py-1 font-mono text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-200'>{action.shortcut}</span>
<span>{(() => {
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[action.key])
})()}</span>
</div>
))}
<div className='mt-3 space-y-1 text-xs text-text-quaternary'>
<div>{t('app.gotoAnything.searchHint')}</div>
<div>{t('app.gotoAnything.commandHint')}</div>
</div>
</div>
</div>)
}, [searchQueryDebouncedValue, Actions])
}, [searchQuery, Actions])
useEffect(() => {
if (show) {
@ -237,6 +238,7 @@ const GotoAnything: FC<Props> = ({
}}
closable={false}
className='!w-[480px] !p-0'
highPriority={true}
>
<div className='flex flex-col rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'>
<Command
@ -252,8 +254,9 @@ const GotoAnything: FC<Props> = ({
value={searchQuery}
placeholder={t('app.gotoAnything.searchPlaceholder')}
onChange={(e) => {
setCmdVal('')
setSearchQuery(e.target.value)
if (!e.target.value.startsWith('@'))
setCmdVal('')
}}
className='flex-1 !border-0 !bg-transparent !shadow-none'
wrapperClassName='flex-1 !border-0 !bg-transparent'
@ -296,7 +299,16 @@ const GotoAnything: FC<Props> = ({
)}
{!isLoading && !isError && (
<>
{Object.entries(groupedResults).map(([type, results], groupIndex) => (
{isCommandsMode ? (
<CommandSelector
actions={Actions}
onCommandSelect={handleCommandSelect}
searchFilter={searchQuery.trim().substring(1)}
commandValue={cmdVal}
onCommandValueChange={setCmdVal}
/>
) : (
Object.entries(groupedResults).map(([type, results], groupIndex) => (
<Command.Group key={groupIndex} heading={(() => {
const typeMap: Record<string, string> = {
'app': 'app.gotoAnything.groups.apps',
@ -330,9 +342,10 @@ const GotoAnything: FC<Props> = ({
</Command.Item>
))}
</Command.Group>
))}
{emptyResult}
{defaultUI}
))
)}
{!isCommandsMode && emptyResult}
{!isCommandsMode && defaultUI}
</>
)}
</Command.List>

View File

@ -1,6 +1,7 @@
import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import type { ModelParameterRule } from '../declarations'
import { useLanguage } from '../hooks'
import { isNullOrUndefined } from '../utils'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'
@ -26,6 +27,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
onSwitch,
isInWorkflow,
}) => {
const language = useLanguage()
const [localValue, setLocalValue] = useState(value)
const numberInputRef = useRef<HTMLInputElement>(null)

View File

@ -64,7 +64,7 @@ const TagsFilter = ({
onClick={() => setOpen(v => !v)}
>
<div className={cn(
'ml-0.5 mr-1.5 flex items-center text-text-tertiary ',
'ml-0.5 mr-1.5 flex select-none items-center text-text-tertiary',
size === 'large' && 'h-8 py-1',
size === 'small' && 'h-7 py-0.5 ',
className,
@ -128,7 +128,7 @@ const TagsFilter = ({
filteredOptions.map(option => (
<div
key={option.name}
className='flex h-7 cursor-pointer items-center rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
className='flex h-7 cursor-pointer select-none items-center rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => handleCheck(option.name)}
>
<Checkbox

View File

@ -48,7 +48,7 @@ const TagsFilter = ({
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn(
'flex h-8 cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-2 py-1 text-text-tertiary hover:bg-state-base-hover-alt',
'flex h-8 cursor-pointer select-none items-center rounded-lg bg-components-input-bg-normal px-2 py-1 text-text-tertiary hover:bg-state-base-hover-alt',
selectedTagsLength && 'text-text-secondary',
open && 'bg-state-base-hover',
)}>
@ -99,7 +99,7 @@ const TagsFilter = ({
filteredOptions.map(option => (
<div
key={option.name}
className='flex h-7 cursor-pointer items-center rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
className='flex h-7 cursor-pointer select-none items-center rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => handleCheck(option.name)}
>
<Checkbox

View File

@ -67,7 +67,7 @@ const LabelFilter: FC<LabelFilterProps> = ({
className='block'
>
<div className={cn(
'flex h-8 cursor-pointer items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 hover:bg-components-input-bg-hover',
'flex h-8 cursor-pointer select-none items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 hover:bg-components-input-bg-hover',
!open && !!value.length && 'shadow-xs',
open && !!value.length && 'shadow-xs',
)}>
@ -111,7 +111,7 @@ const LabelFilter: FC<LabelFilterProps> = ({
{filteredLabelList.map(label => (
<div
key={label.name}
className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover'
className='flex cursor-pointer select-none items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover'
onClick={() => selectLabel(label)}
>
<div title={label.label} className='grow truncate text-sm leading-5 text-text-secondary'>{label.label}</div>

View File

@ -46,9 +46,9 @@ export const useWorkflowSearch = () => {
// Create search function for workflow nodes
const searchWorkflowNodes = useCallback((query: string) => {
if (!searchableNodes.length || !query.trim()) return []
if (!searchableNodes.length) return []
const searchTerm = query.toLowerCase()
const searchTerm = query.toLowerCase().trim()
const results = searchableNodes
.map((node) => {
@ -58,11 +58,18 @@ export const useWorkflowSearch = () => {
let score = 0
if (titleMatch.startsWith(searchTerm)) score += 100
else if (titleMatch.includes(searchTerm)) score += 50
else if (typeMatch === searchTerm) score += 80
else if (typeMatch.includes(searchTerm)) score += 30
else if (descMatch.includes(searchTerm)) score += 20
// If no search term, show all nodes with base score
if (!searchTerm) {
score = 1
}
else {
// Score based on search relevance
if (titleMatch.startsWith(searchTerm)) score += 100
else if (titleMatch.includes(searchTerm)) score += 50
else if (typeMatch === searchTerm) score += 80
else if (typeMatch.includes(searchTerm)) score += 30
else if (descMatch.includes(searchTerm)) score += 20
}
return score > 0
? {
@ -89,6 +96,11 @@ export const useWorkflowSearch = () => {
})
.filter((node): node is NonNullable<typeof node> => node !== null)
.sort((a, b) => {
// If no search term, sort alphabetically
if (!searchTerm)
return a.title.localeCompare(b.title)
// Sort by relevance when searching
const aTitle = a.title.toLowerCase()
const bTitle = b.title.toLowerCase()

View File

@ -31,7 +31,8 @@ const Placeholder = () => {
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
<div
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
onClick={((e) => {
onMouseDown={((e) => {
e.preventDefault()
e.stopPropagation()
handleInsert('/')
})}