Merge branch 'feat/plugins' into dev/plugin-deploy

pull/12372/head
zxhlyh 1 year ago
commit a66d92054a

@ -3,7 +3,7 @@ import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
type InstallButtonProps = { type InstallButtonProps = {
loading: boolean loading: boolean
onInstall: () => void onInstall: (e: React.MouseEvent) => void
t: any t: any
} }

@ -32,7 +32,7 @@ const MenuDialog = ({
return ( return (
<Transition appear show={show} as={Fragment}> <Transition appear show={show} as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={() => {}}> <Dialog as="div" className="relative z-[60]" onClose={() => {}}>
<div className="fixed inset-0"> <div className="fixed inset-0">
<div className="flex flex-col items-center justify-center min-h-full"> <div className="flex flex-col items-center justify-center min-h-full">
<Transition.Child <Transition.Child

@ -22,18 +22,18 @@ const ModelIcon: FC<ModelIconProps> = ({
}) => { }) => {
const language = useLanguage() const language = useLanguage()
if (provider?.provider.includes('openai') && modelName?.includes('gpt-4o')) if (provider?.provider.includes('openai') && modelName?.includes('gpt-4o'))
return <OpenaiBlue className={cn('w-5 h-5', className)}/> return <div className='flex w-6 h-6 items-center justify-center'><OpenaiBlue className={cn('w-5 h-5', className)}/></div>
if (provider?.provider.includes('openai') && modelName?.startsWith('gpt-4')) if (provider?.provider.includes('openai') && modelName?.startsWith('gpt-4'))
return <OpenaiViolet className={cn('w-5 h-5', className)}/> return <div className='flex w-6 h-6 items-center justify-center'><OpenaiViolet className={cn('w-5 h-5', className)}/></div>
if (provider?.icon_small) { if (provider?.icon_small) {
return ( return (
<div className={isDeprecated ? 'opacity-50' : ''}> <div className={`flex w-6 h-6 items-center justify-center ${isDeprecated ? 'opacity-50' : ''}`}>
<img <img
alt='model-icon' alt='model-icon'
src={`${provider.icon_small[language] || provider.icon_small.en_US}`} src={`${provider.icon_small[language] || provider.icon_small.en_US}`}
className={cn('w-4 h-4', className)} className={cn('w-5 h-5', className)}
/> />
</div> </div>
) )
@ -41,10 +41,10 @@ const ModelIcon: FC<ModelIconProps> = ({
return ( return (
<div className={cn( <div className={cn(
'flex items-center justify-center w-5 h-5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle', 'flex items-center justify-center w-6 h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
className, className,
)}> )}>
<div className='flex w-3 h-3 items-center justify-center opacity-35'> <div className='flex w-5 h5 items-center justify-center opacity-35'>
<Group className='text-text-tertiary' /> <Group className='text-text-tertiary' />
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
@ -9,6 +9,7 @@ import type {
import { import {
ConfigurationMethodEnum, ConfigurationMethodEnum,
CustomConfigurationStatusEnum, CustomConfigurationStatusEnum,
ModelTypeEnum,
} from '../declarations' } from '../declarations'
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card' import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card'
import type { PluginInfoFromMarketPlace } from '@/app/components/plugins/types' import type { PluginInfoFromMarketPlace } from '@/app/components/plugins/types'
@ -38,6 +39,7 @@ export type AgentModelTriggerProps = {
providerName?: string providerName?: string
modelId?: string modelId?: string
hasDeprecated?: boolean hasDeprecated?: boolean
scope?: string
} }
const AgentModelTrigger: FC<AgentModelTriggerProps> = ({ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
@ -47,6 +49,7 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
providerName, providerName,
modelId, modelId,
hasDeprecated, hasDeprecated,
scope,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { modelProviders } = useProviderContext() const { modelProviders } = useProviderContext()
@ -54,6 +57,7 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
const updateModelProviders = useUpdateModelProviders() const updateModelProviders = useUpdateModelProviders()
const updateModelList = useUpdateModelList() const updateModelList = useUpdateModelList()
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { modelProvider, needsConfiguration } = useMemo(() => {
const modelProvider = modelProviders.find(item => item.provider === providerName) const modelProvider = modelProviders.find(item => item.provider === providerName)
const needsConfiguration = modelProvider?.custom_configuration.status === CustomConfigurationStatusEnum.noConfigure && !( const needsConfiguration = modelProvider?.custom_configuration.status === CustomConfigurationStatusEnum.noConfigure && !(
modelProvider.system_configuration.enabled === true modelProvider.system_configuration.enabled === true
@ -61,6 +65,11 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
item => item.quota_type === modelProvider.system_configuration.current_quota_type, item => item.quota_type === modelProvider.system_configuration.current_quota_type,
) )
) )
return {
modelProvider,
needsConfiguration,
}
}, [modelProviders, providerName])
const [pluginInfo, setPluginInfo] = useState<PluginInfoFromMarketPlace | null>(null) const [pluginInfo, setPluginInfo] = useState<PluginInfoFromMarketPlace | null>(null)
const [isPluginChecked, setIsPluginChecked] = useState(false) const [isPluginChecked, setIsPluginChecked] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -124,6 +133,34 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
}) })
} }
const handleInstall = async (pluginInfo: PluginInfoFromMarketPlace) => {
setLoading(true)
try {
const { all_installed } = await installPackageFromMarketPlace(pluginInfo.latest_package_identifier)
if (all_installed) {
[
ModelTypeEnum.textGeneration,
ModelTypeEnum.textEmbedding,
ModelTypeEnum.rerank,
ModelTypeEnum.moderation,
ModelTypeEnum.speech2text,
ModelTypeEnum.tts,
].forEach((type: ModelTypeEnum) => {
if (scope?.includes(type))
updateModelList(type)
})
updateModelProviders()
setInstalled(true)
}
}
catch (error) {
console.error('Installation failed:', error)
}
finally {
setLoading(false)
}
}
return ( return (
<div <div
className={cn( className={cn(
@ -158,15 +195,18 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
{!installed && !modelProvider && pluginInfo && ( {!installed && !modelProvider && pluginInfo && (
<InstallButton <InstallButton
loading={loading} loading={loading}
onInstall={async () => { onInstall={(e) => {
setLoading(true) e.stopPropagation()
const { all_installed } = await installPackageFromMarketPlace(pluginInfo.latest_package_identifier) handleInstall(pluginInfo)
if (all_installed)
setInstalled(true)
}} }}
t={t} t={t}
/> />
)} )}
{modelProvider && !disabled && !needsConfiguration && (
<div className="flex pr-1 items-center">
<RiEqualizer2Line className="w-4 h-4 text-text-tertiary group-hover:text-text-secondary" />
</div>
)}
</> </>
) : ( ) : (
<> <>

@ -1,4 +1,5 @@
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Link from 'next/link'
import { RiErrorWarningFill } from '@remixicon/react' import { RiErrorWarningFill } from '@remixicon/react'
type StatusIndicatorsProps = { type StatusIndicatorsProps = {
@ -28,7 +29,16 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, disabled, pluginI
<div className='min-w-[200px] text-text-secondary body-xs-regular'> <div className='min-w-[200px] text-text-secondary body-xs-regular'>
{t('workflow.nodes.agent.modelNotInMarketplace.desc')} {t('workflow.nodes.agent.modelNotInMarketplace.desc')}
</div> </div>
<div className='text-text-accent body-xs-regular'>{t('workflow.nodes.agent.modelNotInMarketplace.manageInPlugins')}</div> <div className='text-text-accent body-xs-regular cursor-pointer z-[100]'>
<Link
href={'/plugins'}
onClick={(e) => {
e.stopPropagation()
}}
>
{t('workflow.nodes.agent.linkToPlugin')}
</Link>
</div>
</div> </div>
} }
asChild={false} asChild={false}

@ -22,19 +22,21 @@ const ModelTrigger: FC<ModelTriggerProps> = ({
return ( return (
<div <div
className={cn('group flex items-center px-2 h-8 rounded-lg bg-components-input-bg-disabled cursor-pointer', className)} className={cn('group flex flex-grow items-center p-[3px] pl-1 h-6 gap-1 rounded-lg bg-components-input-bg-disabled cursor-pointer', className)}
> >
<div className='flex items-center py-[1px] gap-1 grow'>
<ModelIcon <ModelIcon
className='shrink-0 mr-1.5' className="m-0.5 w-4 h-4"
provider={currentProvider} provider={currentProvider}
modelName={modelName} modelName={modelName}
/> />
<div className='mr-1 text-[13px] font-medium text-text-secondary truncate'> <div className='system-sm-regular text-components-input-text-filled truncate'>
{modelName} {modelName}
</div> </div>
</div>
<div className='shrink-0 flex items-center justify-center w-4 h-4'> <div className='shrink-0 flex items-center justify-center w-4 h-4'>
<Tooltip popupContent={t('common.modelProvider.deprecated')}> <Tooltip popupContent={t('common.modelProvider.deprecated')}>
<AlertTriangle className='w-4 h-4 text-[#F79009]' /> <AlertTriangle className='w-4 h-4 text-text-warning-secondary' />
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

@ -14,7 +14,7 @@ const ModelTrigger: FC<ModelTriggerProps> = ({
return ( return (
<div <div
className={cn( className={cn(
'flex items-center px-2 h-8 rounded-lg bg-components-input-bg-normal hover:bg-components-input-bg-hover cursor-pointer', open && 'bg-components-input-bg-hover', 'flex items-center p-1 gap-0.5 rounded-lg bg-components-input-bg-normal hover:bg-components-input-bg-hover cursor-pointer', open && 'bg-components-input-bg-hover',
className, className,
)} )}
> >

@ -104,6 +104,7 @@ const ModelSelector: FC<ModelSelectorProps> = ({
modelList={modelList} modelList={modelList}
onSelect={handleSelect} onSelect={handleSelect}
scopeFeatures={scopeFeatures} scopeFeatures={scopeFeatures}
onHide={() => setOpen(false)}
/> />
</PortalToFollowElemContent> </PortalToFollowElemContent>
</div> </div>

@ -34,7 +34,7 @@ const ModelTrigger: FC<ModelTriggerProps> = ({
return ( return (
<div <div
className={cn( className={cn(
'group flex items-center px-2 h-8 rounded-lg bg-components-input-bg-normal', 'group flex items-center p-1 gap-0.5 h-8 rounded-lg bg-components-input-bg-normal',
!readonly && 'hover:bg-components-input-bg-hover cursor-pointer', !readonly && 'hover:bg-components-input-bg-hover cursor-pointer',
open && 'bg-components-input-bg-hover', open && 'bg-components-input-bg-hover',
model.status !== ModelStatusEnum.active && 'bg-components-input-bg-disabled hover:bg-components-input-bg-disabled', model.status !== ModelStatusEnum.active && 'bg-components-input-bg-disabled hover:bg-components-input-bg-disabled',

@ -1,10 +1,25 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiFileTextLine,
RiFilmAiLine,
RiImageCircleAiLine,
RiVoiceAiFill,
} from '@remixicon/react'
import type { import type {
DefaultModel, DefaultModel,
Model, Model,
ModelItem, ModelItem,
} from '../declarations' } from '../declarations'
import {
ModelFeatureEnum,
ModelFeatureTextEnum,
ModelTypeEnum,
} from '../declarations'
import {
modelTypeFormat,
sizeFormat,
} from '../utils'
import { import {
useLanguage, useLanguage,
useUpdateModelList, useUpdateModelList,
@ -12,15 +27,16 @@ import {
} from '../hooks' } from '../hooks'
import ModelIcon from '../model-icon' import ModelIcon from '../model-icon'
import ModelName from '../model-name' import ModelName from '../model-name'
import ModelBadge from '../model-badge'
import { import {
ConfigurationMethodEnum, ConfigurationMethodEnum,
MODEL_STATUS_TEXT,
ModelStatusEnum, ModelStatusEnum,
} from '../declarations' } from '../declarations'
import { Check } from '@/app/components/base/icons/src/vender/line/general' import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
type PopupItemProps = { type PopupItemProps = {
defaultModel?: DefaultModel defaultModel?: DefaultModel
@ -71,34 +87,86 @@ const PopupItem: FC<PopupItemProps> = ({
model.models.map(modelItem => ( model.models.map(modelItem => (
<Tooltip <Tooltip
key={modelItem.model} key={modelItem.model}
popupContent={modelItem.status !== ModelStatusEnum.active ? MODEL_STATUS_TEXT[modelItem.status][language] : undefined}
position='right' position='right'
popupClassName='p-3 !w-[206px] bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border rounded-xl'
popupContent={
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<ModelIcon
className={cn('shrink-0 w-5 h-5')}
provider={model}
modelName={modelItem.model}
/>
<div className='truncate text-text-primary system-md-medium'>{modelItem.label[language] || modelItem.label.en_US}</div>
</div>
{/* {currentProvider?.description && (
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
)} */}
<div className='flex flex-wrap gap-1'>
{modelItem.model_type && (
<ModelBadge>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)}
{modelItem.model_properties.mode && (
<ModelBadge>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)}
{modelItem.model_properties.context_size && (
<ModelBadge>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)}
</div>
{modelItem.model_type === ModelTypeEnum.textGeneration && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature)) && (
<div className='pt-2'>
<div className='mb-1 text-text-tertiary system-2xs-medium-uppercase'>{t('common.model.capabilities')}</div>
<div className='flex flex-wrap gap-1'>
{modelItem.features?.includes(ModelFeatureEnum.vision) && (
<ModelBadge>
<RiImageCircleAiLine className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.vision}</span>
</ModelBadge>
)}
{modelItem.features?.includes(ModelFeatureEnum.audio) && (
<ModelBadge>
<RiVoiceAiFill className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.audio}</span>
</ModelBadge>
)}
{modelItem.features?.includes(ModelFeatureEnum.video) && (
<ModelBadge>
<RiFilmAiLine className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.video}</span>
</ModelBadge>
)}
{modelItem.features?.includes(ModelFeatureEnum.document) && (
<ModelBadge>
<RiFileTextLine className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.document}</span>
</ModelBadge>
)}
</div>
</div>
)}
</div>
}
> >
<div <div
key={modelItem.model} key={modelItem.model}
className={` className={cn('group relative flex items-center px-3 py-1.5 h-8 rounded-lg gap-1', modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt')}
group relative flex items-center px-3 py-1.5 h-8 rounded-lg gap-1
${modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt'}
`}
onClick={() => handleSelect(model.provider, modelItem)} onClick={() => handleSelect(model.provider, modelItem)}
> >
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<ModelIcon <ModelIcon
className={` className={cn('shrink-0 w-5 h-5')}
shrink-0 w-4 h-4
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`}
provider={model} provider={model}
modelName={modelItem.model} modelName={modelItem.model}
/> />
<ModelName <ModelName
className={` className={cn('text-text-secondary system-sm-medium', modelItem.status !== ModelStatusEnum.active && 'opacity-60')}
text-text-secondary system-sm-medium
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`}
modelItem={modelItem} modelItem={modelItem}
showMode
showFeatures
/> />
</div> </div>
{ {

@ -1,6 +1,8 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { import {
RiArrowRightUpLine,
RiSearchLine, RiSearchLine,
} from '@remixicon/react' } from '@remixicon/react'
import type { import type {
@ -12,21 +14,26 @@ import { ModelFeatureEnum } from '../declarations'
import { useLanguage } from '../hooks' import { useLanguage } from '../hooks'
import PopupItem from './popup-item' import PopupItem from './popup-item'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general' import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { useModalContext } from '@/context/modal-context'
type PopupProps = { type PopupProps = {
defaultModel?: DefaultModel defaultModel?: DefaultModel
modelList: Model[] modelList: Model[]
onSelect: (provider: string, model: ModelItem) => void onSelect: (provider: string, model: ModelItem) => void
scopeFeatures?: string[] scopeFeatures?: string[]
onHide: () => void
} }
const Popup: FC<PopupProps> = ({ const Popup: FC<PopupProps> = ({
defaultModel, defaultModel,
modelList, modelList,
onSelect, onSelect,
scopeFeatures = [], scopeFeatures = [],
onHide,
}) => { }) => {
const { t } = useTranslation()
const language = useLanguage() const language = useLanguage()
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const { setShowAccountSettingModal } = useModalContext()
const filteredModelList = useMemo(() => { const filteredModelList = useMemo(() => {
return modelList.map((model) => { return modelList.map((model) => {
@ -99,6 +106,13 @@ const Popup: FC<PopupProps> = ({
) )
} }
</div> </div>
<div className='sticky bottom-0 px-4 py-2 flex items-center border-t border-divider-subtle cursor-pointer text-text-accent-light-mode-only' onClick={() => {
onHide()
setShowAccountSettingModal({ payload: 'provider' })
}}>
<span className='system-xs-medium'>{t('common.model.settingsLink')}</span>
<RiArrowRightUpLine className='ml-0.5 w-3 h-3' />
</div>
</div> </div>
) )
} }

@ -40,7 +40,7 @@ const SearchBox = ({
locale={locale} locale={locale}
/> />
<div className='mx-1 w-[1px] h-3.5 bg-divider-regular'></div> <div className='mx-1 w-[1px] h-3.5 bg-divider-regular'></div>
<div className='grow flex items-center p-1 pl-2'> <div className='relative grow flex items-center p-1 pl-2'>
<div className='flex items-center mr-2 w-full'> <div className='flex items-center mr-2 w-full'>
<input <input
className={cn( className={cn(
@ -54,9 +54,11 @@ const SearchBox = ({
/> />
{ {
search && ( search && (
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}> <ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='w-4 h-4' /> <RiCloseLine className='w-4 h-4' />
</ActionButton> </ActionButton>
</div>
) )
} }
</div> </div>

@ -190,6 +190,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
currentModel={currentModel} currentModel={currentModel}
providerName={value?.provider} providerName={value?.provider}
modelId={value?.model} modelId={value?.model}
scope={scope}
/> />
: <Trigger : <Trigger
disabled={disabled} disabled={disabled}
@ -204,7 +205,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
) )
} }
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn('z-[60]', portalToFollowElemContentClassName)}> <PortalToFollowElemContent className={cn('z-50', portalToFollowElemContentClassName)}>
<div className={cn(popupClassName, 'w-[389px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg')}> <div className={cn(popupClassName, 'w-[389px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg')}>
<div className={cn('max-h-[420px] p-4 pt-3 overflow-y-auto')}> <div className={cn('max-h-[420px] p-4 pt-3 overflow-y-auto')}>
<div className='relative'> <div className='relative'>

@ -392,6 +392,12 @@ export type StrategyParamItem = {
required: boolean required: boolean
default: any default: any
options: any[] options: any[]
template: {
enabled: boolean
},
auto_generate: {
type: string
}
} }
export type StrategyDetail = { export type StrategyDetail = {

@ -74,11 +74,13 @@ const List = ({
) )
} }
const maxWidthClassName = 'max-w-[300px]'
return ( return (
<> <>
{hasRes && ( {hasRes && (
<div <div
className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName)} className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName, maxWidthClassName)}
onClick={handleHeadClick} onClick={handleHeadClick}
> >
<span>{t('plugin.fromMarketplace')}</span> <span>{t('plugin.fromMarketplace')}</span>
@ -93,7 +95,7 @@ const List = ({
</Link> </Link>
</div> </div>
)} )}
<div className='p-1' ref={nextToStickyELemRef}> <div className={cn('p-1', maxWidthClassName)} ref={nextToStickyELemRef}>
{list.map((item, index) => ( {list.map((item, index) => (
<Item <Item
key={index} key={index}

@ -68,6 +68,7 @@ const Tabs: FC<TabsProps> = ({
{ {
activeTab === TabsEnum.Tools && ( activeTab === TabsEnum.Tools && (
<AllTools <AllTools
className='w-[315px]'
searchText={searchText} searchText={searchText}
onSelect={onSelect} onSelect={onSelect}
tags={tags} tags={tags}

@ -64,7 +64,7 @@ const ToolItem: FC<Props> = ({
}) })
}} }}
> >
<div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{payload.name}</div> <div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{payload.label[language]}</div>
</div> </div>
</Tooltip > </Tooltip >
) )

@ -89,15 +89,15 @@ export const AgentStrategySelector = (props: AgentStrategySelectorProps) => {
const { t } = useTranslation() const { t } = useTranslation()
return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'> return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'>
<PortalToFollowElemTrigger className='w-full'> <PortalToFollowElemTrigger className='w-full'>
<div className='py-2 pl-3 pr-2 flex items-center rounded-lg bg-components-input-bg-normal w-full hover:bg-state-base-hover-alt select-none' onClick={() => setOpen(o => !o)}> <div className='h-8 p-1 gap-0.5 flex items-center rounded-lg bg-components-input-bg-normal w-full hover:bg-state-base-hover-alt select-none' onClick={() => setOpen(o => !o)}>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
{icon && <img {icon && <div className='flex items-center justify-center w-6 h-6'><img
src={icon} src={icon}
width={20} width={20}
height={20} height={20}
className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge' className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
alt='icon' alt='icon'
/>} /></div>}
<p <p
className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')} className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')}
> >
@ -126,8 +126,6 @@ export const AgentStrategySelector = (props: AgentStrategySelectorProps) => {
agent_strategy_provider_name: tool!.provider_name, agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label, agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema, agent_output_schema: tool!.output_schema,
agent_configurations: {},
agent_parameters: {},
}) })
setOpen(false) setOpen(false)
}} }}

@ -1,5 +1,5 @@
import type { CredentialFormSchemaNumberInput } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { CredentialFormSchemaNumberInput } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { type CredentialFormSchema, FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolVarInputs } from '../../tool/types' import type { ToolVarInputs } from '../../tool/types'
import ListEmpty from '@/app/components/base/list-empty' import ListEmpty from '@/app/components/base/list-empty'
import { AgentStrategySelector } from './agent-strategy-selector' import { AgentStrategySelector } from './agent-strategy-selector'
@ -13,14 +13,14 @@ import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-sele
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector' import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import Field from './field' import Field from './field'
import type { ComponentProps } from 'react' import type { ComponentProps } from 'react'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useDefaultModel, useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Editor from './prompt/editor'
import { useWorkflowStore } from '../../../store'
export type Strategy = { export type Strategy = {
agent_strategy_provider_name: string agent_strategy_provider_name: string
agent_strategy_name: string agent_strategy_name: string
agent_strategy_label: string agent_strategy_label: string
agent_configurations?: Record<string, any>
agent_parameters?: Record<string, ToolVarInputs>
agent_output_schema: Record<string, any> agent_output_schema: Record<string, any>
} }
@ -36,85 +36,26 @@ type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { typ
type ToolSelectorSchema = CustomSchema<'tool-selector'> type ToolSelectorSchema = CustomSchema<'tool-selector'>
type MultipleToolSelectorSchema = CustomSchema<'array[tools]'> type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type StringSchema = CustomSchema<'string', {
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema template?: {
enabled: boolean
const devMockForm = [{
name: 'instruction',
label: {
en_US: 'Instruction',
zh_Hans: '指令',
pt_BR: 'Instruction',
ja_JP: 'Instruction',
},
placeholder: null,
scope: null,
auto_generate: {
type: 'prompt_instruction',
},
template: {
enabled: true,
},
required: true,
default: null,
min: null,
max: null,
options: [],
type: 'string',
},
{
name: 'query',
label: {
en_US: 'Query',
zh_Hans: '查询',
pt_BR: 'Query',
ja_JP: 'Query',
}, },
placeholder: null, auto_generate?: {
scope: null, type: string
auto_generate: null,
template: null,
required: true,
default: null,
min: null,
max: null,
options: [],
type: 'string',
},
{
name: 'max iterations',
label: {
en_US: 'Max Iterations',
zh_Hans: '最大迭代次数',
pt_BR: 'Max Iterations',
ja_JP: 'Max Iterations',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: true,
default: '1',
min: 1,
max: 10,
type: FormTypeEnum.textNumber,
tooltip: {
en_US: 'The maximum number of iterations to run',
zh_Hans: '运行的最大迭代次数',
pt_BR: 'The maximum number of iterations to run',
ja_JP: 'The maximum number of iterations to run',
},
}].map((item) => {
return {
...item,
variable: item.name,
} }
}) }>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema | StringSchema
export const AgentStrategy = (props: AgentStrategyProps) => { export const AgentStrategy = (props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange } = props const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange } = props
const { t } = useTranslation() const { t } = useTranslation()
const language = useLanguage() const language = useLanguage()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const workflowStore = useWorkflowStore()
const {
setControlPromptEditorRerenderKey,
} = workflowStore.getState()
const override: ComponentProps<typeof Form<CustomField>>['override'] = [ const override: ComponentProps<typeof Form<CustomField>>['override'] = [
[FormTypeEnum.textNumber], [FormTypeEnum.textNumber],
(schema, props) => { (schema, props) => {
@ -188,6 +129,41 @@ export const AgentStrategy = (props: AgentStrategyProps) => {
/> />
) )
} }
case 'string': {
const value = props.value[schema.variable]
const onChange = (value: string) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
const handleGenerated = (value: string) => {
onChange(value)
setControlPromptEditorRerenderKey(Math.random())
}
return <Editor
value={value}
onChange={onChange}
onGenerated={handleGenerated}
title={schema.label[language]}
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
containerClassName='bg-transparent'
gradientBorder={false}
isSupportPromptGenerator={!!schema.auto_generate?.type}
titleTooltip={schema.tooltip?.[language]}
editorContainerClassName='px-0'
isSupportJinja={schema.template?.enabled}
varList={[]}
modelConfig={
defaultModel.data
? {
mode: 'chat',
name: defaultModel.data.model,
provider: defaultModel.data.provider.provider,
completion_params: {},
} : undefined
}
placeholderClassName='px-2 py-1'
inputClassName='px-2 py-1 bg-components-input-bg-normal focus:bg-components-input-bg-active focus:border-components-input-border-active focus:border rounded-lg'
/>
}
} }
} }
return <div className='space-y-2'> return <div className='space-y-2'>
@ -196,10 +172,7 @@ export const AgentStrategy = (props: AgentStrategyProps) => {
strategy strategy
? <div> ? <div>
<Form<CustomField> <Form<CustomField>
formSchemas={[ formSchemas={formSchema}
...formSchema,
...devMockForm as any,
]}
value={formValue} value={formValue}
onChange={onFormValueChange} onChange={onFormValueChange}
validating={false} validating={false}

@ -1,5 +1,5 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC, ReactNode } from 'react'
import React, { useCallback, useRef } from 'react' import React, { useCallback, useRef } from 'react'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
@ -68,6 +68,12 @@ type Props = {
onEditionTypeChange?: (editionType: EditionType) => void onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[] varList?: Variable[]
handleAddVariable?: (payload: any) => void handleAddVariable?: (payload: any) => void
containerClassName?: string
gradientBorder?: boolean
titleTooltip?: ReactNode
inputClassName?: string
editorContainerClassName?: string
placeholderClassName?: string
} }
const Editor: FC<Props> = ({ const Editor: FC<Props> = ({
@ -96,6 +102,12 @@ const Editor: FC<Props> = ({
handleAddVariable, handleAddVariable,
onGenerated, onGenerated,
modelConfig, modelConfig,
containerClassName,
gradientBorder = true,
titleTooltip,
inputClassName,
placeholderClassName,
editorContainerClassName,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
@ -129,10 +141,13 @@ const Editor: FC<Props> = ({
return ( return (
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}> <Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? s.gradientBorder : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5')}> <div ref={ref} className={cn(isFocus ? (gradientBorder && s.gradientBorder) : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}> <div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg', containerClassName)}>
<div className={cn(headerClassName, 'pt-1 pl-3 pr-2 flex justify-between items-center')}> <div className={cn('pt-1 pl-3 pr-2 flex justify-between items-center', headerClassName)}>
<div className='flex gap-2'>
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div> <div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'> <div className='flex items-center'>
<div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div> <div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div>
{isSupportPromptGenerator && ( {isSupportPromptGenerator && (
@ -201,12 +216,13 @@ const Editor: FC<Props> = ({
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}> <div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
{!(isSupportJinja && editionType === EditionType.jinja2) {!(isSupportJinja && editionType === EditionType.jinja2)
? ( ? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}> <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto', editorContainerClassName)}>
<PromptEditor <PromptEditor
key={controlPromptEditorRerenderKey} key={controlPromptEditorRerenderKey}
placeholderClassName={placeholderClassName}
instanceId={instanceId} instanceId={instanceId}
compact compact
className='min-h-[56px]' className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}} style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value} value={value}
contextBlock={{ contextBlock={{
@ -254,7 +270,7 @@ const Editor: FC<Props> = ({
</div> </div>
) )
: ( : (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}> <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto', editorContainerClassName)}>
<CodeEditor <CodeEditor
availableVars={nodesOutputVars || []} availableVars={nodesOutputVars || []}
varList={varList} varList={varList}
@ -266,6 +282,7 @@ const Editor: FC<Props> = ({
onChange={onChange} onChange={onChange}
noWrapper noWrapper
isExpand={isExpand} isExpand={isExpand}
className={inputClassName}
/> />
</div> </div>
)} )}

@ -12,12 +12,12 @@ export type SettingItemProps = PropsWithChildren<{
export const SettingItem = ({ label, children, status, tooltip }: SettingItemProps) => { export const SettingItem = ({ label, children, status, tooltip }: SettingItemProps) => {
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const needTooltip = ['error', 'warning'].includes(status as any) const needTooltip = ['error', 'warning'].includes(status as any)
return <div className='flex items-center h-6 justify-between bg-workflow-block-parma-bg rounded-md px-1 space-x-1 text-xs font-normal relative'> return <div className='flex items-center justify-between bg-workflow-block-parma-bg rounded-md py-1 px-1.5 space-x-1 text-xs font-normal relative'>
<div className={classNames('shrink-0 truncate text-xs font-medium text-text-tertiary uppercase', !!children && 'max-w-[100px]')}> <div className={classNames('shrink-0 truncate text-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}>
{label} {label}
</div> </div>
<Tooltip popupContent={tooltip} disabled={!needTooltip}> <Tooltip popupContent={tooltip} disabled={!needTooltip}>
<div className='truncate text-right text-xs font-normal text-text-secondary'> <div className='truncate text-right system-xs-medium text-text-secondary'>
{children} {children}
</div> </div>
</Tooltip> </Tooltip>

@ -21,7 +21,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const models = currentStrategy?.parameters const models = currentStrategy?.parameters
.filter(param => param.type === FormTypeEnum.modelSelector) .filter(param => param.type === FormTypeEnum.modelSelector)
.reduce((acc, param) => { .reduce((acc, param) => {
const item = inputs.agent_configurations?.[param.name] const item = inputs.agent_parameters?.[param.name]?.value
if (!item) { if (!item) {
if (param.required) { if (param.required) {
acc.push({ param: param.name }) acc.push({ param: param.name })
@ -40,7 +40,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
currentStrategy?.parameters.forEach((param) => { currentStrategy?.parameters.forEach((param) => {
if (param.type === FormTypeEnum.toolSelector) { if (param.type === FormTypeEnum.toolSelector) {
const field = param.name const field = param.name
const value = inputs.agent_configurations?.[field] const value = inputs.agent_parameters?.[field]?.value
if (value) { if (value) {
tools.push({ tools.push({
providerName: value.provider_name as any, providerName: value.provider_name as any,
@ -49,7 +49,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
} }
if (param.type === FormTypeEnum.multiToolSelector) { if (param.type === FormTypeEnum.multiToolSelector) {
const field = param.name const field = param.name
const value = inputs.agent_configurations?.[field] const value = inputs.agent_parameters?.[field]?.value
if (value) { if (value) {
(value as unknown as any[]).forEach((item) => { (value as unknown as any[]).forEach((item) => {
tools.push({ tools.push({
@ -60,7 +60,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
} }
}) })
return tools return tools
}, [currentStrategy?.parameters, inputs.agent_configurations]) }, [currentStrategy?.parameters, inputs.agent_parameters])
return <div className='mb-1 px-3 py-1 space-y-1'> return <div className='mb-1 px-3 py-1 space-y-1'>
{inputs.agent_strategy_name {inputs.agent_strategy_name
? <SettingItem ? <SettingItem

@ -11,7 +11,7 @@ import type { CredentialFormSchema } from '@/app/components/header/account-setti
const i18nPrefix = 'workflow.nodes.agent' const i18nPrefix = 'workflow.nodes.agent'
function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema { export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema {
return { return {
...param as any, ...param as any,
variable: param.name, variable: param.name,
@ -20,7 +20,7 @@ function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFor
} }
const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
const { inputs, setInputs, currentStrategy } = useConfig(props.id, props.data) const { inputs, setInputs, currentStrategy, formData, onFormChange } = useConfig(props.id, props.data)
const { t } = useTranslation() const { t } = useTranslation()
return <div className='my-2'> return <div className='my-2'>
<Field title={t('workflow.nodes.agent.strategy.label')} className='px-4' > <Field title={t('workflow.nodes.agent.strategy.label')} className='px-4' >
@ -28,28 +28,21 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
strategy={inputs.agent_strategy_name ? { strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!, agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!, agent_strategy_name: inputs.agent_strategy_name!,
agent_configurations: inputs.agent_configurations,
agent_strategy_label: inputs.agent_strategy_label!, agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema, agent_output_schema: inputs.output_schema,
agent_parameters: inputs.agent_parameters,
} : undefined} } : undefined}
onStrategyChange={(strategy) => { onStrategyChange={(strategy) => {
setInputs({ setInputs({
...inputs, ...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name, agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name, agent_strategy_name: strategy?.agent_strategy_name,
agent_configurations: strategy?.agent_configurations,
agent_parameters: strategy?.agent_parameters,
agent_strategy_label: strategy?.agent_strategy_label, agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema, output_schema: strategy!.agent_output_schema,
}) })
}} }}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []} formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={inputs.agent_configurations || {}} formValue={formData}
onFormValueChange={value => setInputs({ onFormValueChange={onFormChange}
...inputs,
agent_configurations: value,
})}
/> />
</Field> </Field>
<div> <div>

@ -5,7 +5,6 @@ export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string agent_strategy_provider_name?: string
agent_strategy_name?: string agent_strategy_name?: string
agent_strategy_label?: string agent_strategy_label?: string
agent_parameters?: Record<string, ToolVarInputs> agent_parameters?: ToolVarInputs
agent_configurations?: Record<string, any>
output_schema: Record<string, any> output_schema: Record<string, any>
} }

@ -1,10 +1,13 @@
import { useStrategyProviderDetail } from '@/service/use-strategy' import { useStrategyProviderDetail } from '@/service/use-strategy'
import useNodeCrud from '../_base/hooks/use-node-crud' import useNodeCrud from '../_base/hooks/use-node-crud'
import useVarList from '../_base/hooks/use-var-list' import useVarList from '../_base/hooks/use-var-list'
import useOneStepRun from '../_base/hooks/use-one-step-run'
import type { AgentNodeType } from './types' import type { AgentNodeType } from './types'
import { import {
useNodesReadOnly, useNodesReadOnly,
} from '@/app/components/workflow/hooks' } from '@/app/components/workflow/hooks'
import { useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
const useConfig = (id: string, payload: AgentNodeType) => { const useConfig = (id: string, payload: AgentNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly() const { nodesReadOnly: readOnly } = useNodesReadOnly()
@ -14,12 +17,59 @@ const useConfig = (id: string, payload: AgentNodeType) => {
inputs, inputs,
setInputs, setInputs,
}) })
const strategies = useStrategyProviderDetail( const strategyProvider = useStrategyProviderDetail(
inputs.agent_strategy_provider_name || '', inputs.agent_strategy_provider_name || '',
) )
const currentStrategy = strategies.data?.declaration.strategies.find(
// single run
const agentInputKey = `${id}.input_selector`
const {
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<AgentNodeType>({
id,
data: inputs,
defaultRunInputData: {
[agentInputKey]: [''],
},
})
const currentStrategy = strategyProvider.data?.declaration.strategies.find(
str => str.identity.name === inputs.agent_strategy_name, str => str.identity.name === inputs.agent_strategy_name,
) )
const currentStrategyStatus = useMemo(() => {
if (strategyProvider.isLoading) return 'loading'
if (strategyProvider.isError) return 'plugin-not-found'
if (!currentStrategy) return 'strategy-not-found'
return 'success'
}, [currentStrategy, strategyProvider])
const formData = useMemo(() => {
return Object.fromEntries(
Object.entries(inputs.agent_parameters || {}).map(([key, value]) => {
return [key, value.value]
}),
)
}, [inputs.agent_parameters])
const onFormChange = (value: Record<string, any>) => {
const res: ToolVarInputs = {}
Object.entries(value).forEach(([key, val]) => {
res[key] = {
type: VarType.constant,
value: val,
}
})
setInputs({
...inputs,
agent_parameters: res,
})
}
return { return {
readOnly, readOnly,
inputs, inputs,
@ -27,6 +77,22 @@ const useConfig = (id: string, payload: AgentNodeType) => {
handleVarListChange, handleVarListChange,
handleAddVariable, handleAddVariable,
currentStrategy, currentStrategy,
formData,
onFormChange,
currentStrategyStatus,
strategyProvider: strategyProvider.data,
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
agentInputKey,
} }
} }

@ -1,5 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Split from '../_base/components/split' import Split from '../_base/components/split'
import type { ToolNodeType } from './types' import type { ToolNodeType } from './types'
@ -15,6 +15,8 @@ import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/befo
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import ResultPanel from '@/app/components/workflow/run/result-panel' import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useToolIcon } from '@/app/components/workflow/hooks' import { useToolIcon } from '@/app/components/workflow/hooks'
import { useLogs } from '@/app/components/workflow/run/hooks'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
const i18nPrefix = 'workflow.nodes.tool' const i18nPrefix = 'workflow.nodes.tool'
@ -51,6 +53,12 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
outputSchema, outputSchema,
} = useConfig(id, data) } = useConfig(id, data)
const toolIcon = useToolIcon(data) const toolIcon = useToolIcon(data)
const logsParams = useLogs()
const nodeInfo = useMemo(() => {
if (!runResult)
return null
return formatToTracingNodeList([runResult], t)[0]
}, [runResult, t])
if (isLoading) { if (isLoading) {
return <div className='flex h-[200px] items-center justify-center'> return <div className='flex h-[200px] items-center justify-center'>
@ -161,7 +169,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
runningStatus={runningStatus} runningStatus={runningStatus}
onRun={handleRun} onRun={handleRun}
onStop={handleStop} onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />} {...logsParams}
result={<ResultPanel {...runResult} showSteps={false} {...logsParams} nodeInfo={nodeInfo} />}
/> />
)} )}
</div> </div>

@ -4,7 +4,7 @@ import Button from '@/app/components/base/button'
import type { AgentLogItemWithChildren } from '@/types/workflow' import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentLogNavProps = { type AgentLogNavProps = {
agentOrToolLogItemStack: { id: string; label: string }[] agentOrToolLogItemStack: AgentLogItemWithChildren[]
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
} }
const AgentLogNav = ({ const AgentLogNav = ({

@ -1,9 +1,10 @@
import { RiAlertFill } from '@remixicon/react'
import AgentLogItem from './agent-log-item' import AgentLogItem from './agent-log-item'
import AgentLogNav from './agent-log-nav' import AgentLogNav from './agent-log-nav'
import type { AgentLogItemWithChildren } from '@/types/workflow' import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentResultPanelProps = { type AgentResultPanelProps = {
agentOrToolLogItemStack: { id: string; label: string }[] agentOrToolLogItemStack: AgentLogItemWithChildren[]
agentOrToolLogListMap: Record<string, AgentLogItemWithChildren[]> agentOrToolLogListMap: Record<string, AgentLogItemWithChildren[]>
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
} }
@ -34,6 +35,22 @@ const AgentResultPanel = ({
} }
</div> </div>
} }
{
top.hasCircle && (
<div className='flex items-center rounded-xl px-3 pr-2 border border-components-panel-border bg-components-panel-bg-blur shadow-md'>
<div
className='absolute inset-0 opacity-[0.4] rounded-xl'
style={{
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
}}
></div>
<RiAlertFill className='mr-1.5 w-4 h-4 text-text-warning-secondary' />
<div className='system-xs-medium text-text-primary'>
There is circular invocation of tools/nodes in the current workflow.
</div>
</div>
)
}
</div> </div>
) )
} }

@ -33,7 +33,7 @@ export const useLogs = () => {
setIterationResultDurationMap(iterDurationMap) setIterationResultDurationMap(iterDurationMap)
}, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap]) }, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap])
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<{ id: string; label: string }[]>([]) const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack) const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack)
const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({}) const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({})
const agentOrToolLogListMapRef = useRef(agentOrToolLogListMap) const agentOrToolLogListMapRef = useRef(agentOrToolLogListMap)
@ -43,14 +43,14 @@ export const useLogs = () => {
agentOrToolLogItemStackRef.current = [] agentOrToolLogItemStackRef.current = []
return return
} }
const { id, label, children } = detail const { id, children } = detail
let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice() let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice()
const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.id === id) const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.id === id)
if (index > -1) if (index > -1)
currentAgentOrToolLogItemStack = currentAgentOrToolLogItemStack.slice(0, index + 1) currentAgentOrToolLogItemStack = currentAgentOrToolLogItemStack.slice(0, index + 1)
else else
currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), { id, label }] currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), detail]
setAgentOrToolLogItemStack(currentAgentOrToolLogItemStack) setAgentOrToolLogItemStack(currentAgentOrToolLogItemStack)
agentOrToolLogItemStackRef.current = currentAgentOrToolLogItemStack agentOrToolLogItemStackRef.current = currentAgentOrToolLogItemStack

@ -81,6 +81,7 @@ const NodePanel: FC<Props> = ({
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent const isAgentNode = nodeInfo.node_type === BlockEnum.Agent
const isToolNode = nodeInfo.node_type === BlockEnum.Tool
return ( return (
<div className={cn('px-2 py-1', className)}> <div className={cn('px-2 py-1', className)}>
@ -144,7 +145,7 @@ const NodePanel: FC<Props> = ({
/> />
)} )}
{ {
isAgentNode && onShowAgentOrToolLog && ( (isAgentNode || isToolNode) && onShowAgentOrToolLog && (
<AgentLogTrigger <AgentLogTrigger
nodeInfo={nodeInfo} nodeInfo={nodeInfo}
onShowAgentOrToolLog={onShowAgentOrToolLog} onShowAgentOrToolLog={onShowAgentOrToolLog}

@ -60,6 +60,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && nodeInfo?.retryDetail const isRetryNode = hasRetryNode(nodeInfo?.node_type) && nodeInfo?.retryDetail
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool
return ( return (
<div className='bg-components-panel-bg py-2'> <div className='bg-components-panel-bg py-2'>
@ -90,7 +91,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
) )
} }
{ {
isAgentNode && handleShowAgentOrToolLog && ( (isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
<AgentLogTrigger <AgentLogTrigger
nodeInfo={nodeInfo} nodeInfo={nodeInfo}
onShowAgentOrToolLog={handleShowAgentOrToolLog} onShowAgentOrToolLog={handleShowAgentOrToolLog}

@ -17,7 +17,7 @@ export type SpecialResultPanelProps = {
iterationResultList?: NodeTracing[][] iterationResultList?: NodeTracing[][]
iterationResultDurationMap?: IterationDurationMap iterationResultDurationMap?: IterationDurationMap
agentOrToolLogItemStack?: { id: string; label: string }[] agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]> agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
} }

@ -1,4 +1,5 @@
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import { has } from 'immer/dist/internal'
export const agentNodeData = (() => { export const agentNodeData = (() => {
const node = { const node = {
@ -89,3 +90,95 @@ export const agentNodeData = (() => {
}], }],
} }
})() })()
export const oneStepCircle = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
{ id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
],
},
}
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Node 1',
hasCircle: true,
children: [],
},
],
}],
}
})()
export const multiStepsCircle = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
// 1 -> [2 -> 4 -> 1, 3]
{ id: '1', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '3', parent_id: '1', label: 'Node 3' },
{ id: '4', parent_id: '2', label: 'Node 4' },
// Loop
{ id: '1', parent_id: '4', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '4', parent_id: '2', label: 'Node 4' },
// { id: '1', parent_id: '4', label: 'Node 1' },
// { id: '2', parent_id: '1', label: 'Node 2' },
// { id: '4', parent_id: '2', label: 'Node 4' },
],
},
}
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Node 1',
children: [
{
id: '2',
parent_id: '1',
label: 'Node 2',
children: [
{
id: '4',
parent_id: '2',
label: 'Node 4',
children: [],
hasCircle: true,
}
],
},
{
id: '3',
parent_id: '1',
label: 'Node 3',
},
],
},
],
}],
}
})()
export const CircleNestCircle = (() => {
})()

@ -1,9 +1,16 @@
import exp from 'constants'
import format from '.' import format from '.'
import { agentNodeData } from './data' import { agentNodeData, oneStepCircle, multiStepsCircle } from './data'
describe('agent', () => { describe('agent', () => {
test('list should transform to tree', () => { test('list should transform to tree', () => {
// console.log(format(agentNodeData.in as any)) // console.log(format(agentNodeData.in as any))
expect(format(agentNodeData.in as any)).toEqual(agentNodeData.expect) expect(format(agentNodeData.in as any)).toEqual(agentNodeData.expect)
}) })
test('list should remove circle log item', () => {
// format(oneStepCircle.in as any)
expect(format(oneStepCircle.in as any)).toEqual(oneStepCircle.expect)
expect(format(multiStepsCircle.in as any)).toEqual(multiStepsCircle.expect)
})
}) })

@ -1,8 +1,62 @@
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow' import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow'
import { cloneDeep } from 'lodash-es'
const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool] const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool]
const remove = (node: AgentLogItemWithChildren, removeId: string) => {
const { children } = node
if (!children || children.length === 0) {
return
}
children.forEach((child, index) => {
if (child.id === removeId) {
node.hasCircle = true
children.splice(index, 1)
return
}
remove(child, removeId)
})
}
const removeRepeatedSiblings = (list: AgentLogItemWithChildren[]) => {
if (!list || list.length === 0) {
return []
}
const result: AgentLogItemWithChildren[] = []
const addedItemIds: string[] = []
list.forEach((item) => {
if (!addedItemIds.includes(item.id)) {
result.push(item)
addedItemIds.push(item.id)
}
})
return result
}
const removeCircleLogItem = (log: AgentLogItemWithChildren) => {
let newLog = cloneDeep(log)
newLog.children = removeRepeatedSiblings(newLog.children)
let { id, children } = newLog
if (!children || children.length === 0) {
return log
}
// check one step circle
const hasOneStepCircle = !!children.find(c => c.id === id)
if (hasOneStepCircle) {
newLog.hasCircle = true
newLog.children = newLog.children.filter(c => c.id !== id)
children = newLog.children
}
children.forEach((child, index) => {
remove(child, id) // check multi steps circle
children[index] = removeCircleLogItem(child)
})
return newLog
}
const listToTree = (logs: AgentLogItem[]) => { const listToTree = (logs: AgentLogItem[]) => {
if (!logs || logs.length === 0) if (!logs || logs.length === 0)
return [] return []
@ -24,10 +78,15 @@ const listToTree = (logs: AgentLogItem[]) => {
}) })
return tree return tree
} }
const format = (list: NodeTracing[]): NodeTracing[] => { const format = (list: NodeTracing[]): NodeTracing[] => {
const result: NodeTracing[] = list.map((item) => { const result: NodeTracing[] = list.map((item) => {
let treeList: AgentLogItemWithChildren[] = []
let removedCircleTree: AgentLogItemWithChildren[] = []
if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0) if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0)
item.agentLog = listToTree(item.execution_metadata.agent_log) treeList = listToTree(item.execution_metadata.agent_log)
removedCircleTree = treeList.length > 0 ? treeList.map(t => removeCircleLogItem(t)) : []
item.agentLog = removedCircleTree
return item return item
}) })

@ -126,6 +126,8 @@ const translation = {
Custom: 'Custom', Custom: 'Custom',
}, },
addMoreModel: 'Go to settings to add more models', addMoreModel: 'Go to settings to add more models',
settingsLink: 'Model Provider Settings',
capabilities: 'MultiModal Capabilities',
}, },
menus: { menus: {
status: 'beta', status: 'beta',

@ -716,7 +716,7 @@ const translation = {
}, },
modelNotInMarketplace: { modelNotInMarketplace: {
title: 'Model not installed', title: 'Model not installed',
desc: 'This model is not installed from the marketplace. Please go to Plugins to reinstall.', desc: 'This model is installed from Local or GitHub repository. Please use after installation.',
manageInPlugins: 'Manage in Plugins', manageInPlugins: 'Manage in Plugins',
}, },
configureModel: 'Configure Model', configureModel: 'Configure Model',

@ -126,6 +126,8 @@ const translation = {
Custom: '自定义', Custom: '自定义',
}, },
addMoreModel: '添加更多模型', addMoreModel: '添加更多模型',
settingsLink: '模型设置',
capabilities: '多模态能力',
}, },
menus: { menus: {
status: 'beta', status: 'beta',

@ -716,7 +716,7 @@ const translation = {
}, },
modelNotInMarketplace: { modelNotInMarketplace: {
title: '模型未安装', title: '模型未安装',
desc: '此模型未从市场安装。请转到插件重新安装。', desc: '此模型安装自本地或 GitHub 仓库。请安装后使用。',
manageInPlugins: '在插件中管理', manageInPlugins: '在插件中管理',
}, },
model: '模型', model: '模型',

@ -20,6 +20,7 @@ export type AgentLogItem = {
} }
export type AgentLogItemWithChildren = AgentLogItem & { export type AgentLogItemWithChildren = AgentLogItem & {
hasCircle?: boolean
children: AgentLogItemWithChildren[] children: AgentLogItemWithChildren[]
} }

Loading…
Cancel
Save