Introduce Plugins (#13836)

Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Signed-off-by: -LAN- <laipz8200@outlook.com>
Signed-off-by: xhe <xw897002528@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: takatost <takatost@gmail.com>
Co-authored-by: kurokobo <kuro664@gmail.com>
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: AkaraChen <akarachen@outlook.com>
Co-authored-by: Yi <yxiaoisme@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com>
Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com>
Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Novice <857526207@qq.com>
Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com>
Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com>
Co-authored-by: eux <euxuuu@gmail.com>
Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com>
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
Co-authored-by: lotsik <lotsik@mail.ru>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com>
Co-authored-by: CN-P5 <heibai2006@gmail.com>
Co-authored-by: CN-P5 <heibai2006@qq.com>
Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Boris Feld <lothiraldan@gmail.com>
Co-authored-by: mbo <himabo@gmail.com>
Co-authored-by: mabo <mabo@aeyes.ai>
Co-authored-by: Warren Chen <warren.chen830@gmail.com>
Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com>
Co-authored-by: jiandanfeng <chenjh3@wangsu.com>
Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com>
Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com>
Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com>
Co-authored-by: Xu Song <xusong.vip@gmail.com>
Co-authored-by: rayshaw001 <396301947@163.com>
Co-authored-by: Ding Jiatong <dingjiatong@gmail.com>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: JasonVV <jasonwangiii@outlook.com>
Co-authored-by: le0zh <newlight@qq.com>
Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com>
Co-authored-by: k-zaku <zaku99@outlook.jp>
Co-authored-by: luckylhb90 <luckylhb90@gmail.com>
Co-authored-by: hobo.l <hobo.l@binance.com>
Co-authored-by: jiangbo721 <365065261@qq.com>
Co-authored-by: 刘江波 <jiangbo721@163.com>
Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com>
Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: sino <sino2322@gmail.com>
Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com>
Co-authored-by: lowell <lowell.hu@zkteco.in>
Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com>
Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com>
Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com>
Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com>
Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com>
Co-authored-by: Jason <ggbbddjm@gmail.com>
Co-authored-by: Xin Zhang <sjhpzx@gmail.com>
Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com>
Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com>
Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com>
Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com>
Co-authored-by: Yingchun Lai <laiyingchun@apache.org>
Co-authored-by: Hash Brown <hi@xzd.me>
Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com>
Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com>
Co-authored-by: aplio <ryo.091219@gmail.com>
Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com>
Co-authored-by: Nam Vu <zuzoovn@gmail.com>
Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com>
Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com>
Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com>
Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com>
Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp>
Co-authored-by: HQidea <HQidea@users.noreply.github.com>
Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com>
Co-authored-by: xhe <xw897002528@gmail.com>
Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com>
Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com>
Co-authored-by: engchina <12236799+engchina@users.noreply.github.com>
Co-authored-by: engchina <atjapan2015@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 呆萌闷油瓶 <253605712@qq.com>
Co-authored-by: Kemal <kemalmeler@outlook.com>
Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com>
Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com>
Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com>
Co-authored-by: steven <sunzwj@digitalchina.com>
Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com>
Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com>
Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com>
Co-authored-by: 胡春东 <gycm520@gmail.com>
Co-authored-by: Junjie.M <118170653@qq.com>
Co-authored-by: MuYu <mr.muzea@gmail.com>
Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com>
Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com>
Co-authored-by: Fei He <droxer.he@gmail.com>
Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com>
Co-authored-by: Yuanbo Li <ybalbert@amazon.com>
Co-authored-by: douxc <7553076+douxc@users.noreply.github.com>
Co-authored-by: liuzhenghua <1090179900@qq.com>
Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com>
Co-authored-by: Your Name <you@example.com>
Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com>
Co-authored-by: AugNSo <song.tiankai@icloud.com>
Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com>
Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com>
Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com>
Co-authored-by: Hundredwz <1808096180@qq.com>
Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
This commit is contained in:
Yeuoly
2025-02-17 17:05:13 +08:00
committed by GitHub
parent 222df44d21
commit 403e2d58b9
3272 changed files with 66339 additions and 281594 deletions

View File

@ -2,6 +2,7 @@ import type { FC } from 'react'
import { memo } from 'react'
import { BlockEnum } from './types'
import {
Agent,
Answer,
Assigner,
Code,
@ -53,6 +54,7 @@ const getIcon = (type: BlockEnum, className: string) => {
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
[BlockEnum.DocExtractor]: <DocsExtractor className={className} />,
[BlockEnum.ListFilter]: <ListFilter className={className} />,
[BlockEnum.Agent]: <Agent className={className} />,
}[type]
}
const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
@ -73,6 +75,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[BlockEnum.ParameterExtractor]: 'bg-util-colors-blue-blue-500',
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
}
const BlockIcon: FC<BlockIconProps> = ({
type,

View File

@ -1,35 +1,61 @@
import {
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import type {
OnSelectBlock,
ToolWithProvider,
} from '../types'
import { useStore } from '../store'
import type { ToolValue } from './types'
import { ToolTypeEnum } from './types'
import Tools from './tools'
import { useToolTabs } from './hooks'
import ViewTypeSelect, { ViewType } from './view-type-select'
import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
import ActionButton from '../../base/action-button'
import { RiAddLine } from '@remixicon/react'
import { PluginType } from '../../plugins/types'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
type AllToolsProps = {
className?: string
toolContentClassName?: string
searchText: string
tags: string[]
buildInTools: ToolWithProvider[]
customTools: ToolWithProvider[]
workflowTools: ToolWithProvider[]
onSelect: OnSelectBlock
supportAddCustomTool?: boolean
onAddedCustomTool?: () => void
onShowAddCustomCollectionModal?: () => void
selectedTools?: ToolValue[]
}
const AllTools = ({
className,
toolContentClassName,
searchText,
tags = [],
onSelect,
buildInTools,
workflowTools,
customTools,
supportAddCustomTool,
onShowAddCustomCollectionModal,
selectedTools,
}: AllToolsProps) => {
const language = useGetLanguage()
const tabs = useToolTabs()
const [activeTab, setActiveTab] = useState(ToolTypeEnum.All)
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const [activeView, setActiveView] = useState<ViewType>(ViewType.flat)
const hasFilter = searchText || tags.length > 0
const isMatchingKeywords = (text: string, keywords: string) => {
return text.toLowerCase().includes(keywords.toLowerCase())
}
const tools = useMemo(() => {
let mergedTools: ToolWithProvider[] = []
if (activeTab === ToolTypeEnum.All)
@ -41,39 +67,91 @@ const AllTools = ({
if (activeTab === ToolTypeEnum.Workflow)
mergedTools = workflowTools
if (!hasFilter)
return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0)
return mergedTools.filter((toolWithProvider) => {
return isMatchingKeywords(toolWithProvider.name, searchText)
|| toolWithProvider.tools.some((tool) => {
return Object.values(tool.label).some((label) => {
return isMatchingKeywords(label, searchText)
})
})
return isMatchingKeywords(toolWithProvider.name, searchText) || toolWithProvider.tools.some((tool) => {
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
})
})
}, [activeTab, buildInTools, customTools, workflowTools, searchText])
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter])
const {
queryPluginsWithDebounced: fetchPlugins,
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
useEffect(() => {
if (searchText || tags.length > 0) {
fetchPlugins({
query: searchText,
tags,
category: PluginType.tool,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchText, tags])
const pluginRef = useRef(null)
const wrapElemRef = useRef<HTMLDivElement>(null)
return (
<div>
<div className='flex items-center px-3 h-8 space-x-1 bg-background-default-hover border-b-[0.5px] border-divider-subtle shadow-xs'>
{
tabs.map(tab => (
<div
className={cn(
'flex items-center px-2 h-6 rounded-md hover:bg-state-base-hover-alt cursor-pointer',
'system-xs-medium text-text-tertiary',
activeTab === tab.key && 'system-xs-semibold bg-state-base-hover-alt text-text-primary',
)}
key={tab.key}
onClick={() => setActiveTab(tab.key)}
<div className={cn(className)}>
<div className='flex items-center justify-between px-3 bg-background-default-hover border-b-[0.5px] border-divider-subtle shadow-xs'>
<div className='flex items-center h-8 space-x-1'>
{
tabs.map(tab => (
<div
className={cn(
'flex items-center px-2 h-6 rounded-md hover:bg-state-base-hover cursor-pointer',
'text-xs font-medium text-text-secondary',
activeTab === tab.key && 'bg-state-base-hover-alt',
)}
key={tab.key}
onClick={() => setActiveTab(tab.key)}
>
{tab.name}
</div>
))
}
</div>
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
{supportAddCustomTool && (
<div className='flex items-center'>
<div className='mr-1.5 w-px h-3.5 bg-divider-regular'></div>
<ActionButton
className='bg-components-button-primary-bg hover:bg-components-button-primary-bg text-components-button-primary-text hover:text-components-button-primary-text'
onClick={onShowAddCustomCollectionModal}
>
{tab.name}
</div>
))
}
<RiAddLine className='w-4 h-4' />
</ActionButton>
</div>
)}
</div>
<div
ref={wrapElemRef}
className='max-h-[464px] overflow-y-auto'
onScroll={(pluginRef.current as any)?.handleScroll}
>
<Tools
className={toolContentClassName}
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
tools={tools}
onSelect={onSelect}
viewType={activeView}
hasSearchText={!!searchText}
selectedTools={selectedTools}
/>
{/* Plugins from marketplace */}
<PluginList
wrapElemRef={wrapElemRef}
list={notInstalledPlugins as any} ref={pluginRef}
searchText={searchText}
toolContentClassName={toolContentClassName}
tags={tags}
/>
</div>
<Tools
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
tools={tools}
onSelect={onSelect}
/>
</div>
)
}

View File

@ -84,6 +84,11 @@ export const BLOCKS: Block[] = [
type: BlockEnum.ListFilter,
title: 'List Filter',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Agent,
title: 'Agent',
},
]
export const BLOCK_CLASSIFICATIONS: string[] = [

View File

@ -41,7 +41,7 @@ export const useToolTabs = () => {
},
{
key: ToolTypeEnum.BuiltIn,
name: t('workflow.tabs.builtInTool'),
name: t('workflow.tabs.plugin'),
},
{
key: ToolTypeEnum.Custom,

View File

@ -1,8 +1,29 @@
import { pinyin } from 'pinyin-pro'
import type { FC, RefObject } from 'react'
import type { ToolWithProvider } from '../types'
import { CollectionType } from '../../tools/types'
import classNames from '@/utils/classnames'
export const groupItems = (items: Array<any>, getFirstChar: (item: string) => string) => {
const groups = items.reduce((acc, item) => {
export const CUSTOM_GROUP_NAME = '@@@custom@@@'
export const WORKFLOW_GROUP_NAME = '@@@workflow@@@'
export const AGENT_GROUP_NAME = '@@@agent@@@'
/*
{
A: {
'google': [ // plugin organize name
...tools
],
'custom': [ // custom tools
...tools
],
'workflow': [ // workflow as tools
...tools
]
}
}
*/
export const groupItems = (items: ToolWithProvider[], getFirstChar: (item: ToolWithProvider) => string) => {
const groups = items.reduce((acc: Record<string, Record<string, ToolWithProvider[]>>, item) => {
const firstChar = getFirstChar(item)
if (!firstChar || firstChar.length === 0)
return acc
@ -19,9 +40,23 @@ export const groupItems = (items: Array<any>, getFirstChar: (item: string) => st
letter = '#'
if (!acc[letter])
acc[letter] = []
acc[letter] = {}
let groupName: string = ''
if (item.type === CollectionType.builtIn)
groupName = item.author
else if (item.type === CollectionType.custom)
groupName = CUSTOM_GROUP_NAME
else if (item.type === CollectionType.workflow)
groupName = WORKFLOW_GROUP_NAME
else
groupName = AGENT_GROUP_NAME
if (!acc[letter][groupName])
acc[letter][groupName] = []
acc[letter][groupName].push(item)
acc[letter].push(item)
return acc
}, {})
@ -38,16 +73,18 @@ export const groupItems = (items: Array<any>, getFirstChar: (item: string) => st
type IndexBarProps = {
letters: string[]
itemRefs: RefObject<{ [key: string]: HTMLElement | null }>
className?: string
}
const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs }) => {
const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className }) => {
const handleIndexClick = (letter: string) => {
const element = itemRefs.current?.[letter]
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}
return (
<div className="index-bar fixed right-4 top-36 flex flex-col items-center text-xs font-medium text-text-quaternary">
<div className={classNames('index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary', className)}>
<div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div>
{letters.map(letter => (
<div className="hover:text-text-secondary cursor-pointer" key={letter} onClick={() => handleIndexClick(letter)}>
{letter}

View File

@ -22,6 +22,8 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Input from '@/app/components/base/input'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import {
Plus02,
} from '@/app/components/base/icons/src/vender/line/general'
@ -61,6 +63,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const [localOpen, setLocalOpen] = useState(false)
const open = openFromProps === undefined ? localOpen : openFromProps
const handleOpenChange = useCallback((newOpen: boolean) => {
@ -126,25 +129,37 @@ const NodeSelector: FC<NodeSelectorProps> = ({
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={
classNames(`rounded-lg border-[0.5px] backdrop-blur-[5px]
border-components-panel-border bg-components-panel-bg-blur shadow-lg`, popupClassName)}>
<div className='p-2 pb-1' onClick={e => e.stopPropagation()}>
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
<div className={`rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
)}
</div>
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
onSelect={handleSelect}
searchText={searchText}
tags={tags}
availableBlocksTypes={availableBlocksTypes}
noBlocks={noBlocks}
/>

View File

@ -0,0 +1,87 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
// import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import { useDownloadPlugin } from '@/service/use-plugins'
import { downloadFile } from '@/utils/format'
type Props = {
open: boolean
onOpenChange: (v: boolean) => void
author: string
name: string
version: string
}
const OperationDropdown: FC<Props> = ({
open,
onOpenChange,
author,
name,
version,
}) => {
const { t } = useTranslation()
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
onOpenChange(v)
openRef.current = v
}, [onOpenChange])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
const [needDownload, setNeedDownload] = useState(false)
const { data: blob, isLoading } = useDownloadPlugin({
organization: author,
pluginName: name,
version,
}, needDownload)
const handleDownload = useCallback(() => {
if (isLoading) return
setNeedDownload(true)
}, [isLoading])
useEffect(() => {
if (blob) {
const fileName = `${author}-${name}_${version}.zip`
downloadFile({ data: blob, fileName })
setNeedDownload(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blob])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 0,
crossAxis: 0,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<ActionButton className={cn(open && 'bg-state-base-hover')}>
<RiMoreFill className='w-4 h-4 text-components-button-secondary-accent-text' />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[9999]'>
<div className='w-[112px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div onClick={handleDownload} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.download')}</div>
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}`} target='_blank' className='block px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(OperationDropdown)

View File

@ -0,0 +1,77 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import Action from './action'
import type { Plugin } from '@/app/components/plugins/types.ts'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
import { useBoolean } from 'ahooks'
enum ActionType {
install = 'install',
download = 'download',
// viewDetail = 'viewDetail', // wait for marketplace api
}
type Props = {
payload: Plugin
onAction: (type: ActionType) => void
}
const Item: FC<Props> = ({
payload,
}) => {
const { t } = useTranslation()
const [open, setOpen] = React.useState(false)
const { locale } = useContext(I18n)
const getLocalizedText = (obj: Record<string, string> | undefined) =>
obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''
const [isShowInstallModal, {
setTrue: showInstallModal,
setFalse: hideInstallModal,
}] = useBoolean(false)
return (
<div className='group/plugin flex rounded-lg py-1 pr-1 pl-3 hover:bg-state-base-hover'>
<div
className='shrink-0 relative w-6 h-6 border-[0.5px] border-components-panel-border-subtle rounded-md bg-center bg-no-repeat bg-contain'
style={{ backgroundImage: `url(${payload.icon})` }}
/>
<div className='ml-2 w-0 grow flex'>
<div className='w-0 grow'>
<div className='h-4 leading-4 text-text-primary system-sm-medium truncate '>{getLocalizedText(payload.label)}</div>
<div className='h-5 leading-5 text-text-tertiary system-xs-regular truncate'>{getLocalizedText(payload.brief)}</div>
<div className='flex text-text-tertiary system-xs-regular space-x-1'>
<div>{payload.org}</div>
<div>·</div>
<div>{t('plugin.install', { num: formatNumber(payload.install_count || 0) })}</div>
</div>
</div>
{/* Action */}
<div className={cn(!open ? 'hidden' : 'flex', 'group-hover/plugin:flex items-center space-x-1 h-4 text-components-button-secondary-accent-text system-xs-medium')}>
<div className='px-1.5 cursor-pointer' onClick={showInstallModal}>{t('plugin.installAction')}</div>
<Action
open={open}
onOpenChange={setOpen}
author={payload.org}
name={payload.name}
version={payload.latest_version}
/>
</div>
{isShowInstallModal && (
<InstallFromMarketplace
uniqueIdentifier={payload.latest_package_identifier}
manifest={payload}
onSuccess={hideInstallModal}
onClose={hideInstallModal}
/>
)}
</div>
</div>
)
}
export default React.memo(Item)

View File

@ -0,0 +1,129 @@
'use client'
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import useStickyScroll, { ScrollPosition } from '../use-sticky-scroll'
import Item from './item'
import type { Plugin } from '@/app/components/plugins/types.ts'
import cn from '@/utils/classnames'
import Link from 'next/link'
import { marketplaceUrlPrefix } from '@/config'
import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react'
// import { RiArrowRightUpLine } from '@remixicon/react'
type Props = {
wrapElemRef: React.RefObject<HTMLElement>
list: Plugin[]
searchText: string
tags: string[]
toolContentClassName?: string
disableMaxWidth?: boolean
}
const List = forwardRef<{ handleScroll: () => void }, Props>(({
wrapElemRef,
searchText,
tags,
list,
toolContentClassName,
disableMaxWidth = false,
}, ref) => {
const { t } = useTranslation()
const hasFilter = !searchText
const hasRes = list.length > 0
const urlWithSearchText = `${marketplaceUrlPrefix}/?q=${searchText}&tags=${tags.join(',')}`
const nextToStickyELemRef = useRef<HTMLDivElement>(null)
const { handleScroll, scrollPosition } = useStickyScroll({
wrapElemRef,
nextToStickyELemRef,
})
const stickyClassName = useMemo(() => {
switch (scrollPosition) {
case ScrollPosition.aboveTheWrap:
return 'top-0 h-9 pt-3 pb-2 shadow-xs bg-components-panel-bg-blur cursor-pointer'
case ScrollPosition.showing:
return 'bottom-0 pt-3 pb-1'
case ScrollPosition.belowTheWrap:
return 'bottom-0 items-center rounded-b-xl border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg rounded-b-lg cursor-pointer'
}
}, [scrollPosition])
useImperativeHandle(ref, () => ({
handleScroll,
}))
useEffect(() => {
handleScroll()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list])
const handleHeadClick = () => {
if (scrollPosition === ScrollPosition.belowTheWrap) {
nextToStickyELemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
return
}
window.open(urlWithSearchText, '_blank')
}
if (hasFilter) {
return (
<Link
className='sticky bottom-0 z-10 flex h-8 px-4 py-1 system-sm-medium items-center border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur rounded-b-lg shadow-lg text-text-accent-light-mode-only cursor-pointer'
href={`${marketplaceUrlPrefix}/`}
target='_blank'
>
<span>{t('plugin.findMoreInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 w-3 h-3' />
</Link>
)
}
const maxWidthClassName = toolContentClassName || 'max-w-[300px]'
return (
<>
{hasRes && (
<div
className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName, !disableMaxWidth && maxWidthClassName)}
onClick={handleHeadClick}
>
<span>{t('plugin.fromMarketplace')}</span>
<Link
href={urlWithSearchText}
target='_blank'
className='flex items-center text-text-accent-light-mode-only'
onClick={e => e.stopPropagation()}
>
<span>{t('plugin.searchInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 w-3 h-3' />
</Link>
</div>
)}
<div className={cn('p-1', !disableMaxWidth && maxWidthClassName)} ref={nextToStickyELemRef}>
{list.map((item, index) => (
<Item
key={index}
payload={item}
onAction={() => { }}
/>
))}
<div className='mt-2 mb-3 flex items-center justify-center space-x-2'>
<div className="w-[90px] h-[2px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div>
<Link
href={urlWithSearchText}
target='_blank'
className='shrink-0 flex items-center h-4 system-sm-medium text-text-accent-light-mode-only'
>
<RiSearchLine className='mr-0.5 w-3 h-3' />
<span>{t('plugin.searchInMarketplace')}</span>
</Link>
<div className="w-[90px] h-[2px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div>
</div>
</div>
</>
)
})
List.displayName = 'List'
export default List

View File

@ -1,5 +1,6 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
@ -12,6 +13,7 @@ export type TabsProps = {
activeTab: TabsEnum
onActiveTabChange: (activeTab: TabsEnum) => void
searchText: string
tags: string[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
noBlocks?: boolean
@ -19,12 +21,16 @@ export type TabsProps = {
const Tabs: FC<TabsProps> = ({
activeTab,
onActiveTabChange,
tags,
searchText,
onSelect,
availableBlocksTypes,
noBlocks,
}) => {
const tabs = useTabs()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
return (
<div onClick={e => e.stopPropagation()}>
@ -62,8 +68,13 @@ const Tabs: FC<TabsProps> = ({
{
activeTab === TabsEnum.Tools && (
<AllTools
className='w-[315px]'
searchText={searchText}
onSelect={onSelect}
tags={tags}
buildInTools={buildInTools || []}
customTools={customTools || []}
workflowTools={workflowTools || []}
/>
)
}

View File

@ -0,0 +1,176 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useMemo, useState } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import AllTools from '@/app/components/workflow/block-selector/all-tools'
import type { ToolDefaultValue, ToolValue } from './types'
import type { BlockEnum } from '@/app/components/workflow/types'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal/modal'
import {
createCustomCollection,
} from '@/service/tools'
import type { CustomCollectionBackend } from '@/app/components/tools/types'
import Toast from '@/app/components/base/toast'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
import cn from '@/utils/classnames'
type Props = {
panelClassName?: string
disabled: boolean
trigger: React.ReactNode
placement?: Placement
offset?: OffsetOptions
isShow: boolean
onShowChange: (isShow: boolean) => void
onSelect: (tool: ToolDefaultValue) => void
supportAddCustomTool?: boolean
scope?: string
selectedTools?: ToolValue[]
}
const ToolPicker: FC<Props> = ({
disabled,
trigger,
placement = 'right-start',
offset = 0,
isShow,
onShowChange,
onSelect,
supportAddCustomTool,
scope = 'all',
selectedTools,
panelClassName,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const invalidateCustomTools = useInvalidateAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
if (scope === 'plugins') {
return {
builtinToolList: buildInTools,
customToolList: [],
workflowToolList: [],
}
}
if (scope === 'custom') {
return {
builtinToolList: [],
customToolList: customTools,
workflowToolList: [],
}
}
if (scope === 'workflow') {
return {
builtinToolList: [],
customToolList: [],
workflowToolList: workflowTools,
}
}
return {
builtinToolList: buildInTools,
customToolList: customTools,
workflowToolList: workflowTools,
}
}, [scope, buildInTools, customTools, workflowTools])
const handleAddedCustomTool = invalidateCustomTools
const handleTriggerClick = () => {
if (disabled) return
onShowChange(true)
}
const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => {
onSelect(tool!)
}
const [isShowEditCollectionToolModal, {
setFalse: hideEditCustomCollectionModal,
setTrue: showEditCustomCollectionModal,
}] = useBoolean(false)
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
await createCustomCollection(data)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
hideEditCustomCollectionModal()
handleAddedCustomTool()
}
if (isShowEditCollectionToolModal) {
return (
<EditCustomToolModal
positionLeft
payload={null}
onHide={hideEditCustomCollectionModal}
onAdd={doCreateCustomToolCollection}
/>
)
}
return (
<PortalToFollowElem
placement={placement}
offset={offset}
open={isShow}
onOpenChange={onShowChange}
>
<PortalToFollowElemTrigger
onClick={handleTriggerClick}
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={cn('relative w-[356px] min-h-20 rounded-xl backdrop-blur-sm bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg', panelClassName)}>
<div className='p-2 pb-1'>
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
</div>
<AllTools
className='mt-1'
toolContentClassName='max-w-[360px]'
tags={tags}
searchText={searchText}
onSelect={handleSelect}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
selectedTools={selectedTools}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(ToolPicker)

View File

@ -0,0 +1,89 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import Tooltip from '@/app/components/base/tooltip'
import type { Tool } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { RiCheckLine } from '@remixicon/react'
import Badge from '@/app/components/base/badge'
type Props = {
provider: ToolWithProvider
payload: Tool
disabled?: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const ToolItem: FC<Props> = ({
provider,
payload,
onSelect,
disabled,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
return (
<Tooltip
key={payload.name}
position='right'
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
popupContent={(
<div>
<BlockIcon
size='md'
className='mb-2'
type={BlockEnum.Tool}
toolIcon={provider.icon}
/>
<div className='mb-1 text-sm leading-5 text-text-primary'>{payload.label[language]}</div>
<div className='text-xs text-text-secondary leading-[18px]'>{payload.description[language]}</div>
</div>
)}
>
<div
key={payload.name}
className='flex justify-between items-center pr-1 rounded-lg pl-[21px] hover:bg-state-base-hover cursor-pointer'
onClick={() => {
if (disabled) return
const params: Record<string, string> = {}
if (payload.parameters) {
payload.parameters.forEach((item) => {
params[item.name] = ''
})
}
onSelect(BlockEnum.Tool, {
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
tool_name: payload.name,
tool_label: payload.label[language],
title: payload.label[language],
is_team_authorization: provider.is_team_authorization,
output_schema: payload.output_schema,
paramSchemas: payload.parameters,
params,
})
}}
>
<div className={cn('h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium', disabled && 'opacity-30')}>{payload.label[language]}</div>
{disabled && <Badge
className='flex items-center h-5 text-text-tertiary space-x-0.5'
uppercase
>
<RiCheckLine className='w-3 h-3 ' />
<div>{t('tools.addToolModal.added')}</div>
</Badge>
}
</div>
</Tooltip >
)
}
export default React.memo(ToolItem)

View File

@ -0,0 +1,64 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../../types'
import type { BlockEnum } from '../../../types'
import type { ToolDefaultValue, ToolValue } from '../../types'
import Tool from '../tool'
import { ViewType } from '../../view-type-select'
import { useMemo } from 'react'
type Props = {
payload: ToolWithProvider[]
isShowLetterIndex: boolean
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
letters: string[]
toolRefs: any
selectedTools?: ToolValue[]
}
const ToolViewFlatView: FC<Props> = ({
letters,
payload,
isShowLetterIndex,
hasSearchText,
onSelect,
toolRefs,
selectedTools,
}) => {
const firstLetterToolIds = useMemo(() => {
const res: Record<string, string> = {}
letters.forEach((letter) => {
const firstToolId = payload.find(tool => tool.letter === letter)?.id
if (firstToolId)
res[firstToolId] = letter
})
return res
}, [payload, letters])
return (
<div>
{payload.map(tool => (
<div
key={tool.id}
ref={(el) => {
const letter = firstLetterToolIds[tool.id]
if (letter)
toolRefs.current[letter] = el
}}
>
<Tool
payload={tool}
viewType={ViewType.flat}
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
selectedTools={selectedTools}
/>
</div>
))}
</div>
)
}
export default React.memo(ToolViewFlatView)

View File

@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../../types'
import Tool from '../tool'
import type { BlockEnum } from '../../../types'
import { ViewType } from '../../view-type-select'
import type { ToolDefaultValue, ToolValue } from '../../types'
type Props = {
groupName: string
toolList: ToolWithProvider[]
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
selectedTools?: ToolValue[]
}
const Item: FC<Props> = ({
groupName,
toolList,
hasSearchText,
onSelect,
selectedTools,
}) => {
return (
<div>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-text-tertiary'>
{groupName}
</div>
<div>
{toolList.map((tool: ToolWithProvider) => (
<Tool
key={tool.id}
payload={tool}
viewType={ViewType.tree}
isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect}
selectedTools={selectedTools}
/>
))}
</div>
</div>
)
}
export default React.memo(Item)

View File

@ -0,0 +1,56 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { ToolWithProvider } from '../../../types'
import type { BlockEnum } from '../../../types'
import type { ToolDefaultValue, ToolValue } from '../../types'
import Item from './item'
import { useTranslation } from 'react-i18next'
import { AGENT_GROUP_NAME, CUSTOM_GROUP_NAME, WORKFLOW_GROUP_NAME } from '../../index-bar'
type Props = {
payload: Record<string, ToolWithProvider[]>
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
selectedTools?: ToolValue[]
}
const ToolListTreeView: FC<Props> = ({
payload,
hasSearchText,
onSelect,
selectedTools,
}) => {
const { t } = useTranslation()
const getI18nGroupName = useCallback((name: string) => {
if (name === CUSTOM_GROUP_NAME)
return t('workflow.tabs.customTool')
if (name === WORKFLOW_GROUP_NAME)
return t('workflow.tabs.workflowTool')
if (name === AGENT_GROUP_NAME)
return t('workflow.tabs.agent')
return name
}, [t])
if (!payload) return null
return (
<div>
{Object.keys(payload).map(groupName => (
<Item
key={groupName}
groupName={getI18nGroupName(groupName)}
toolList={payload[groupName]}
hasSearchText={hasSearchText}
onSelect={onSelect}
selectedTools={selectedTools}
/>
))}
</div>
)
}
export default React.memo(ToolListTreeView)

View File

@ -0,0 +1,134 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n'
import type { Tool as ToolType } from '../../../tools/types'
import { CollectionType } from '../../../tools/types'
import type { ToolWithProvider } from '../../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue, ToolValue } from '../types'
import { ViewType } from '../view-type-select'
import ActonItem from './action-item'
import BlockIcon from '../../block-icon'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
payload: ToolWithProvider
viewType: ViewType
isShowLetterIndex: boolean
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
selectedTools?: ToolValue[]
}
const Tool: FC<Props> = ({
className,
payload,
viewType,
isShowLetterIndex,
hasSearchText,
onSelect,
selectedTools,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat
const actions = payload.tools
const hasAction = true // Now always support actions
const [isFold, setFold] = React.useState<boolean>(true)
const getIsDisabled = (tool: ToolType) => {
if (!selectedTools || !selectedTools.length) return false
return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name)
}
useEffect(() => {
if (hasSearchText && isFold) {
setFold(false)
return
}
if (!hasSearchText && !isFold)
setFold(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasSearchText])
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
const groupName = useMemo(() => {
if (payload.type === CollectionType.builtIn)
return payload.author
if (payload.type === CollectionType.custom)
return t('workflow.tabs.customTool')
if (payload.type === CollectionType.workflow)
return t('workflow.tabs.workflowTool')
return ''
}, [payload.author, payload.type, t])
return (
<div
key={payload.id}
className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')}
>
<div className={cn(className)}>
<div
className='flex items-center justify-between pl-3 pr-1 w-full rounded-lg hover:bg-state-base-hover cursor-pointer select-none'
onClick={() => {
if (hasAction)
setFold(!isFold)
// Now always support actions
// if (payload.parameters) {
// payload.parameters.forEach((item) => {
// params[item.name] = ''
// })
// }
// onSelect(BlockEnum.Tool, {
// provider_id: payload.id,
// provider_type: payload.type,
// provider_name: payload.name,
// tool_name: payload.name,
// tool_label: payload.label[language],
// title: payload.label[language],
// params: {},
// })
}}
>
<div className='flex grow items-center h-8'>
<BlockIcon
className='shrink-0'
type={BlockEnum.Tool}
toolIcon={payload.icon}
/>
<div className='ml-2 text-sm text-text-primary flex-1 w-0 grow truncate'>{payload.label[language]}</div>
</div>
<div className='flex items-center'>
{isFlatView && (
<div className='text-text-tertiary system-xs-regular'>{groupName}</div>
)}
{hasAction && (
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} />
)}
</div>
</div>
{hasAction && !isFold && (
actions.map(action => (
<ActonItem
key={action.name}
provider={payload}
payload={action}
onSelect={onSelect}
disabled={getIsDisabled(action)}
/>
))
)}
</div>
</div>
)
}
export default React.memo(Tool)

View File

@ -1,103 +1,93 @@
import {
memo,
useCallback,
useMemo,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import type { ToolWithProvider } from '../types'
import type { BlockEnum, ToolWithProvider } from '../types'
import IndexBar, { groupItems } from './index-bar'
import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip'
import type { ToolDefaultValue, ToolValue } from './types'
import { ViewType } from './view-type-select'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { useGetLanguage } from '@/context/i18n'
import ToolListTreeView from './tool/tool-list-tree-view/list'
import ToolListFlatView from './tool/tool-list-flat-view/list'
import classNames from '@/utils/classnames'
type ToolsProps = {
showWorkflowEmpty: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
tools: ToolWithProvider[]
viewType: ViewType
hasSearchText: boolean
className?: string
indexBarClassName?: string
selectedTools?: ToolValue[]
}
const Blocks = ({
showWorkflowEmpty,
onSelect,
tools,
viewType,
hasSearchText,
className,
indexBarClassName,
selectedTools,
}: ToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat
const isShowLetterIndex = isFlatView && tools.length > 10
/*
treeViewToolsData:
{
A: {
'google': [ // plugin organize name
...tools
],
'custom': [ // custom tools
...tools
],
'workflow': [ // workflow as tools
...tools
]
}
}
*/
const { letters, groups: withLetterAndGroupViewToolsData } = groupItems(tools, tool => (tool as any).label[language][0])
const treeViewToolsData = useMemo(() => {
const result: Record<string, ToolWithProvider[]> = {}
Object.keys(withLetterAndGroupViewToolsData).forEach((letter) => {
Object.keys(withLetterAndGroupViewToolsData[letter]).forEach((groupName) => {
if (!result[groupName])
result[groupName] = []
result[groupName].push(...withLetterAndGroupViewToolsData[letter][groupName])
})
})
return result
}, [withLetterAndGroupViewToolsData])
const listViewToolData = useMemo(() => {
const result: ToolWithProvider[] = []
letters.forEach((letter) => {
Object.keys(withLetterAndGroupViewToolsData[letter]).forEach((groupName) => {
result.push(...withLetterAndGroupViewToolsData[letter][groupName].map((item) => {
return {
...item,
letter,
}
}))
})
})
return result
}, [withLetterAndGroupViewToolsData, letters])
const { letters, groups: groupedTools } = groupItems(tools, tool => tool.label[language][0])
const toolRefs = useRef({})
const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
const list = toolWithProvider.tools
return (
<div
key={toolWithProvider.id}
className='mb-1 last-of-type:mb-0'
>
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
{toolWithProvider.label[language]}
</div>
{
list.map(tool => (
<Tooltip
key={tool.name}
position='right'
popupClassName='w-[200px]'
popupContent={(
<div>
<BlockIcon
size='md'
className='mb-2'
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className='mb-1 system-md-medium text-text-primary'>{tool.label[language]}</div>
<div className='system-xs-regular text-text-tertiary'>{tool.description[language]}</div>
</div>
)}
>
<div
className='flex items-center px-3 w-full h-8 rounded-lg hover:bg-state-base-hover cursor-pointer'
onClick={() => onSelect(BlockEnum.Tool, {
provider_id: toolWithProvider.id,
provider_type: toolWithProvider.type,
provider_name: toolWithProvider.name,
tool_name: tool.name,
tool_label: tool.label[language],
title: tool.label[language],
})}
>
<BlockIcon
className='mr-2 shrink-0'
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className='text-sm text-text-secondary flex-1 min-w-0 truncate'>{tool.label[language]}</div>
</div>
</Tooltip>
))
}
</div>
)
}, [onSelect, language])
const renderLetterGroup = (letter) => {
const tools = groupedTools[letter]
return (
<div
key={letter}
ref={el => (toolRefs.current[letter] = el)}
>
{tools.map(renderGroup)}
</div>
)
}
return (
<div className='p-1 max-w-[320px] max-h-[464px] overflow-y-auto'>
<div className={classNames('p-1 max-w-[320px]', className)}>
{
!tools.length && !showWorkflowEmpty && (
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>
@ -108,8 +98,28 @@ const Blocks = ({
<Empty />
</div>
)}
{!!tools.length && letters.map(renderLetterGroup)}
{tools.length > 10 && <IndexBar letters={letters} itemRefs={toolRefs} />}
{!!tools.length && (
isFlatView ? (
<ToolListFlatView
toolRefs={toolRefs}
letters={letters}
payload={listViewToolData}
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
selectedTools={selectedTools}
/>
) : (
<ToolListTreeView
payload={treeViewToolsData}
hasSearchText={hasSearchText}
onSelect={onSelect}
selectedTools={selectedTools}
/>
)
)}
{isShowLetterIndex && <IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
</div>
)
}

View File

@ -25,4 +25,18 @@ export type ToolDefaultValue = {
tool_name: string
tool_label: string
title: string
is_team_authorization: boolean
params: Record<string, any>
paramSchemas: Record<string, any>[]
output_schema: Record<string, any>
}
export type ToolValue = {
provider_name: string
tool_name: string
tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any>
enabled?: boolean
extra?: Record<string, any>
}

View File

@ -0,0 +1,45 @@
import React from 'react'
import { useThrottleFn } from 'ahooks'
export enum ScrollPosition {
belowTheWrap = 'belowTheWrap',
showing = 'showing',
aboveTheWrap = 'aboveTheWrap',
}
type Params = {
wrapElemRef: React.RefObject<HTMLElement>
nextToStickyELemRef: React.RefObject<HTMLElement>
}
const useStickyScroll = ({
wrapElemRef,
nextToStickyELemRef,
}: Params) => {
const [scrollPosition, setScrollPosition] = React.useState<ScrollPosition>(ScrollPosition.belowTheWrap)
const { run: handleScroll } = useThrottleFn(() => {
const wrapDom = wrapElemRef.current
const stickyDOM = nextToStickyELemRef.current
if (!wrapDom || !stickyDOM)
return
const { height: wrapHeight, top: wrapTop } = wrapDom.getBoundingClientRect()
const { top: nextToStickyTop } = stickyDOM.getBoundingClientRect()
let scrollPositionNew = ScrollPosition.belowTheWrap
if (nextToStickyTop - wrapTop >= wrapHeight)
scrollPositionNew = ScrollPosition.belowTheWrap
else if (nextToStickyTop <= wrapTop)
scrollPositionNew = ScrollPosition.aboveTheWrap
else
scrollPositionNew = ScrollPosition.showing
if (scrollPosition !== scrollPositionNew)
setScrollPosition(scrollPositionNew)
}, { wait: 100 })
return {
handleScroll,
scrollPosition,
}
}
export default useStickyScroll

View File

@ -0,0 +1,58 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { RiNodeTree, RiSortAlphabetAsc } from '@remixicon/react'
import cn from '@/utils/classnames'
export enum ViewType {
flat = 'flat',
tree = 'tree',
}
type Props = {
viewType: ViewType
onChange: (viewType: ViewType) => void
}
const ViewTypeSelect: FC<Props> = ({
viewType,
onChange,
}) => {
const handleChange = useCallback((nextViewType: ViewType) => {
return () => {
if (nextViewType === viewType)
return
onChange(nextViewType)
}
}, [viewType, onChange])
return (
<div className='flex items-center rounded-lg bg-components-segmented-control-bg-normal p-px'>
<div
className={
cn('p-[3px] rounded-lg',
viewType === ViewType.flat
? 'bg-components-segmented-control-item-active-bg shadow-xs text-text-accent-light-mode-only'
: 'text-text-tertiary cursor-pointer',
)
}
onClick={handleChange(ViewType.flat)}
>
<RiSortAlphabetAsc className='w-4 h-4' />
</div>
<div
className={
cn('p-[3px] rounded-lg',
viewType === ViewType.tree
? 'bg-components-segmented-control-item-active-bg shadow-xs text-text-accent-light-mode-only'
: 'text-text-tertiary cursor-pointer',
)
}
onClick={handleChange(ViewType.tree)}
>
<RiNodeTree className='w-4 h-4 ' />
</div>
</div>
)
}
export default React.memo(ViewTypeSelect)

View File

@ -18,6 +18,7 @@ import IterationDefault from './nodes/iteration/default'
import DocExtractorDefault from './nodes/document-extractor/default'
import ListFilterDefault from './nodes/list-operator/default'
import IterationStartDefault from './nodes/iteration-start/default'
import AgentDefault from './nodes/agent/default'
type NodesExtraData = {
author: string
@ -200,7 +201,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes,
checkValid: ListFilterDefault.checkValid,
},
[BlockEnum.Agent]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: ListFilterDefault.getAvailablePrevNodes,
getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes,
checkValid: AgentDefault.checkValid,
},
}
export const NODES_INITIAL_DATA = {
@ -336,6 +345,12 @@ export const NODES_INITIAL_DATA = {
desc: '',
...ListFilterDefault.defaultValue,
},
[BlockEnum.Agent]: {
type: BlockEnum.Agent,
title: '',
desc: '',
...AgentDefault.defaultValue,
},
}
export const MAX_ITERATION_PARALLEL_NUM = 10
export const MIN_ITERATION_PARALLEL_NUM = 1
@ -386,6 +401,7 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier,
BlockEnum.ParameterExtractor, BlockEnum.Iteration,
BlockEnum.DocExtractor, BlockEnum.ListFilter,
BlockEnum.Agent,
]
export const LLM_OUTPUT_STRUCT: Var[] = [

View File

@ -24,6 +24,8 @@ import { useNodesExtraData } from './use-nodes-data'
import { useToastContext } from '@/app/components/base/toast'
import { CollectionType } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import type { AgentNodeType } from '../nodes/agent/types'
import { useStrategyProviders } from '@/service/use-strategy'
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
@ -33,6 +35,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const { data: strategyProviders } = useStrategyProviders()
const needWarningNodes = useMemo(() => {
const list = []
@ -57,6 +60,19 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon
}
if (node.data.type === BlockEnum.Agent) {
const data = node.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name)
moreDataForCheckValid = {
provider,
strategy,
language,
isReadyForCheckValid,
}
}
if (node.type === CUSTOM_NODE) {
const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
@ -92,7 +108,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
return list
}, [t, nodes, edges, nodesExtraData, buildInTools, customTools, workflowTools, language, isChatMode])
}, [nodes, edges, isChatMode, buildInTools, customTools, workflowTools, language, nodesExtraData, t, strategyProviders])
return needWarningNodes
}
@ -107,6 +123,7 @@ export const useChecklistBeforePublish = () => {
const isChatMode = useIsChatMode()
const store = useStoreApi()
const nodesExtraData = useNodesExtraData()
const { data: strategyProviders } = useStrategyProviders()
const handleCheckBeforePublish = useCallback(() => {
const {
@ -130,6 +147,19 @@ export const useChecklistBeforePublish = () => {
if (node.data.type === BlockEnum.Tool)
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
if (node.data.type === BlockEnum.Agent) {
const data = node.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name)
moreDataForCheckValid = {
provider,
strategy,
language,
isReadyForCheckValid,
}
}
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(node.data, t, moreDataForCheckValid)
if (errorMessage) {
@ -154,7 +184,7 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [nodesExtraData, notify, t, store, isChatMode, buildInTools, customTools, workflowTools, language])
}, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders])
return {
handleCheckBeforePublish,

View File

@ -0,0 +1,12 @@
export * from './use-workflow-started'
export * from './use-workflow-finished'
export * from './use-workflow-failed'
export * from './use-workflow-node-started'
export * from './use-workflow-node-finished'
export * from './use-workflow-node-iteration-started'
export * from './use-workflow-node-iteration-next'
export * from './use-workflow-node-iteration-finished'
export * from './use-workflow-node-retry'
export * from './use-workflow-text-chunk'
export * from './use-workflow-text-replace'
export * from './use-workflow-agent-log'

View File

@ -0,0 +1,50 @@
import { useCallback } from 'react'
import produce from 'immer'
import type { AgentLogResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowAgentLog = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowAgentLog = useCallback((params: AgentLogResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id)
if (currentIndex > -1) {
const current = draft.tracing![currentIndex]
if (current.execution_metadata) {
if (current.execution_metadata.agent_log) {
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.id === data.id)
if (currentLogIndex > -1) {
current.execution_metadata.agent_log[currentLogIndex] = {
...current.execution_metadata.agent_log[currentLogIndex],
...data,
}
}
else {
current.execution_metadata.agent_log.push(data)
}
}
else {
current.execution_metadata.agent_log = [data]
}
}
else {
current.execution_metadata = {
agent_log: [data],
} as any
}
}
}))
}, [workflowStore])
return {
handleWorkflowAgentLog,
}
}

View File

@ -0,0 +1,26 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
export const useWorkflowFailed = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowFailed = useCallback(() => {
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Failed,
}
}))
}, [workflowStore])
return {
handleWorkflowFailed,
}
}

View File

@ -0,0 +1,35 @@
import { useCallback } from 'react'
import produce from 'immer'
import type { WorkflowFinishedResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { getFilesInLogs } from '@/app/components/base/file-uploader/utils'
export const useWorkflowFinished = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowFinished = useCallback((params: WorkflowFinishedResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const isStringOutput = data.outputs && Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
...data,
files: getFilesInLogs(data.outputs),
} as any
if (isStringOutput) {
draft.resultTabActive = true
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
}
}))
}, [workflowStore])
return {
handleWorkflowFinished,
}
}

View File

@ -0,0 +1,72 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import produce from 'immer'
import type { NodeFinishedResponse } from '@/types/workflow'
import {
BlockEnum,
NodeRunningStatus,
} from '@/app/components/workflow/types'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeFinished = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleWorkflowNodeFinished = useCallback((params: NodeFinishedResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex(item => item.id === data.id)
if (currentIndex > -1) {
draft.tracing![currentIndex] = {
...draft.tracing![currentIndex],
...data,
}
}
}))
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._runningStatus = data.status
if (data.status === NodeRunningStatus.Exception) {
if (data.execution_metadata?.error_strategy === ErrorHandleTypeEnum.failBranch)
currentNode.data._runningBranchId = ErrorHandleTypeEnum.failBranch
}
else {
if (data.node_type === BlockEnum.IfElse)
currentNode.data._runningBranchId = data?.outputs?.selected_case_id
if (data.node_type === BlockEnum.QuestionClassifier)
currentNode.data._runningBranchId = data?.outputs?.class_id
}
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter((edge) => {
return edge.target === data.node_id
})
incomeEdges.forEach((edge) => {
edge.data = {
...edge.data,
_targetRunningStatus: data.status as any,
}
})
})
setEdges(newEdges)
}, [store, workflowStore])
return {
handleWorkflowNodeFinished,
}
}

View File

@ -0,0 +1,46 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import produce from 'immer'
import type { IterationFinishedResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { DEFAULT_ITER_TIMES } from '@/app/components/workflow/constants'
export const useWorkflowNodeIterationFinished = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleWorkflowNodeIterationFinished = useCallback((params: IterationFinishedResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
setIterTimes,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex(item => item.id === data.id)
if (currentIndex > -1) {
draft.tracing![currentIndex] = {
...draft.tracing![currentIndex],
...data,
}
}
}))
setIterTimes(DEFAULT_ITER_TIMES)
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._runningStatus = data.status
})
setNodes(newNodes)
}, [workflowStore, store])
return {
handleWorkflowNodeIterationFinished,
}
}

View File

@ -0,0 +1,35 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import produce from 'immer'
import type { IterationNextResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeIterationNext = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleWorkflowNodeIterationNext = useCallback((params: IterationNextResponse) => {
const {
iterTimes,
setIterTimes,
} = workflowStore.getState()
const { data } = params
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._iterationIndex = iterTimes
setIterTimes(iterTimes + 1)
})
setNodes(newNodes)
}, [workflowStore, store])
return {
handleWorkflowNodeIterationNext,
}
}

View File

@ -0,0 +1,85 @@
import { useCallback } from 'react'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import produce from 'immer'
import { useWorkflowStore } from '@/app/components/workflow/store'
import type { IterationStartedResponse } from '@/types/workflow'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import { DEFAULT_ITER_TIMES } from '@/app/components/workflow/constants'
export const useWorkflowNodeIterationStarted = () => {
const store = useStoreApi()
const reactflow = useReactFlow()
const workflowStore = useWorkflowStore()
const handleWorkflowNodeIterationStarted = useCallback((
params: IterationStartedResponse,
containerParams: {
clientWidth: number,
clientHeight: number,
},
) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
setIterTimes,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}))
setIterTimes(DEFAULT_ITER_TIMES)
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
if (!currentNode.parentId) {
setViewport({
x: (containerParams.clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (containerParams.clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length
draft[currentNodeIndex].data._waitingRun = false
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter(edge => edge.target === data.node_id)
incomeEdges.forEach((edge) => {
edge.data = {
...edge.data,
_sourceRunningStatus: nodes.find(node => node.id === edge.source)!.data._runningStatus,
_targetRunningStatus: NodeRunningStatus.Running,
_waitingRun: false,
}
})
})
setEdges(newEdges)
}, [workflowStore, store, reactflow])
return {
handleWorkflowNodeIterationStarted,
}
}

View File

@ -0,0 +1,39 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import produce from 'immer'
import type {
NodeFinishedResponse,
} from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeRetry = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleWorkflowNodeRetry = useCallback((params: NodeFinishedResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push(data)
}))
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._retryIndex = data.retry_index
})
setNodes(newNodes)
}, [workflowStore, store])
return {
handleWorkflowNodeRetry,
}
}

View File

@ -0,0 +1,89 @@
import { useCallback } from 'react'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import produce from 'immer'
import type { NodeStartedResponse } from '@/types/workflow'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeStarted = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const handleWorkflowNodeStarted = useCallback((
params: NodeStartedResponse,
containerParams: {
clientWidth: number,
clientHeight: number,
},
) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}))
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
if (!currentNode.parentId) {
setViewport({
x: (containerParams.clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (containerParams.clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
draft[currentNodeIndex].data._waitingRun = false
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter((edge) => {
return edge.target === data.node_id
})
incomeEdges.forEach((edge) => {
const incomeNode = nodes.find(node => node.id === edge.source)!
if (
(!incomeNode.data._runningBranchId && edge.sourceHandle === 'source')
|| (incomeNode.data._runningBranchId && edge.sourceHandle === incomeNode.data._runningBranchId)
) {
edge.data = {
...edge.data,
_sourceRunningStatus: incomeNode.data._runningStatus,
_targetRunningStatus: NodeRunningStatus.Running,
_waitingRun: false,
}
}
})
})
setEdges(newEdges)
}, [workflowStore, store, reactflow])
return {
handleWorkflowNodeStarted,
}
}

View File

@ -0,0 +1,44 @@
import {
useWorkflowAgentLog,
useWorkflowFailed,
useWorkflowFinished,
useWorkflowNodeFinished,
useWorkflowNodeIterationFinished,
useWorkflowNodeIterationNext,
useWorkflowNodeIterationStarted,
useWorkflowNodeRetry,
useWorkflowNodeStarted,
useWorkflowStarted,
useWorkflowTextChunk,
useWorkflowTextReplace,
} from '.'
export const useWorkflowRunEvent = () => {
const { handleWorkflowStarted } = useWorkflowStarted()
const { handleWorkflowFinished } = useWorkflowFinished()
const { handleWorkflowFailed } = useWorkflowFailed()
const { handleWorkflowNodeStarted } = useWorkflowNodeStarted()
const { handleWorkflowNodeFinished } = useWorkflowNodeFinished()
const { handleWorkflowNodeIterationStarted } = useWorkflowNodeIterationStarted()
const { handleWorkflowNodeIterationNext } = useWorkflowNodeIterationNext()
const { handleWorkflowNodeIterationFinished } = useWorkflowNodeIterationFinished()
const { handleWorkflowNodeRetry } = useWorkflowNodeRetry()
const { handleWorkflowTextChunk } = useWorkflowTextChunk()
const { handleWorkflowTextReplace } = useWorkflowTextReplace()
const { handleWorkflowAgentLog } = useWorkflowAgentLog()
return {
handleWorkflowStarted,
handleWorkflowFinished,
handleWorkflowFailed,
handleWorkflowNodeStarted,
handleWorkflowNodeFinished,
handleWorkflowNodeIterationStarted,
handleWorkflowNodeIterationNext,
handleWorkflowNodeIterationFinished,
handleWorkflowNodeRetry,
handleWorkflowTextChunk,
handleWorkflowTextReplace,
handleWorkflowAgentLog,
}
}

View File

@ -0,0 +1,58 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import produce from 'immer'
import type { WorkflowStartedResponse } from '@/types/workflow'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowStarted = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleWorkflowStarted = useCallback((params: WorkflowStartedResponse) => {
const { task_id, data } = params
const {
workflowRunningData,
setWorkflowRunningData,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
setIterParallelLogMap(new Map())
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.task_id = task_id
draft.result = {
...draft?.result,
...data,
status: WorkflowRunningStatus.Running,
}
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data._waitingRun = true
node.data._runningBranchId = undefined
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data = {
...edge.data,
_sourceRunningStatus: undefined,
_targetRunningStatus: undefined,
_waitingRun: true,
}
})
})
setEdges(newEdges)
}, [workflowStore, store])
return {
handleWorkflowStarted,
}
}

View File

@ -0,0 +1,25 @@
import { useCallback } from 'react'
import produce from 'immer'
import type { TextChunkResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowTextChunk = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowTextChunk = useCallback((params: TextChunkResponse) => {
const { data: { text } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultTabActive = true
draft.resultText += text
}))
}, [workflowStore])
return {
handleWorkflowTextChunk,
}
}

View File

@ -0,0 +1,23 @@
import { useCallback } from 'react'
import produce from 'immer'
import type { TextReplaceResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowTextReplace = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowTextReplace = useCallback((params: TextReplaceResponse) => {
const { data: { text } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultText = text
}))
}, [workflowStore])
return {
handleWorkflowTextReplace,
}
}

View File

@ -8,24 +8,16 @@ import { v4 as uuidV4 } from 'uuid'
import { usePathname } from 'next/navigation'
import { useWorkflowStore } from '../store'
import { useNodesSyncDraft } from '../hooks'
import {
BlockEnum,
NodeRunningStatus,
WorkflowRunningStatus,
} from '../types'
import { DEFAULT_ITER_TIMES } from '../constants'
import { WorkflowRunningStatus } from '../types'
import { useWorkflowUpdate } from './use-workflow-interactions'
import { useWorkflowRunEvent } from './use-workflow-run-event/use-workflow-run-event'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { IOtherOptions } from '@/service/base'
import { ssePost } from '@/service/base'
import { stopWorkflowRun } from '@/service/workflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
import {
getFilesInLogs,
} from '@/app/components/base/file-uploader/utils'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { NodeTracing, VersionHistory } from '@/types/workflow'
import type { VersionHistory } from '@/types/workflow'
export const useWorkflowRun = () => {
const store = useStoreApi()
@ -35,6 +27,20 @@ export const useWorkflowRun = () => {
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
const pathname = usePathname()
const {
handleWorkflowStarted,
handleWorkflowFinished,
handleWorkflowFailed,
handleWorkflowNodeStarted,
handleWorkflowNodeFinished,
handleWorkflowNodeIterationStarted,
handleWorkflowNodeIterationNext,
handleWorkflowNodeIterationFinished,
handleWorkflowNodeRetry,
handleWorkflowAgentLog,
handleWorkflowTextChunk,
handleWorkflowTextReplace,
} = useWorkflowRunEvent()
const handleBackupDraft = useCallback(() => {
const {
@ -113,6 +119,7 @@ export const useWorkflowRun = () => {
onIterationNext,
onIterationFinish,
onNodeRetry,
onAgentLog,
onError,
...restCallback
} = callback || {}
@ -132,8 +139,6 @@ export const useWorkflowRun = () => {
if (appDetail?.mode === 'workflow')
url = `/apps/${appDetail.id}/workflows/draft/run`
let prevNodeId = ''
const {
setWorkflowRunningData,
} = workflowStore.getState()
@ -166,584 +171,96 @@ export const useWorkflowRun = () => {
},
{
onWorkflowStarted: (params) => {
const { task_id, data } = params
const {
workflowRunningData,
setWorkflowRunningData,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
setIterParallelLogMap(new Map())
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.task_id = task_id
draft.result = {
...draft?.result,
...data,
status: WorkflowRunningStatus.Running,
}
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data._waitingRun = true
node.data._runningBranchId = undefined
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data = {
...edge.data,
_sourceRunningStatus: undefined,
_targetRunningStatus: undefined,
_waitingRun: true,
}
})
})
setEdges(newEdges)
handleWorkflowStarted(params)
if (onWorkflowStarted)
onWorkflowStarted(params)
},
onWorkflowFinished: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const isStringOutput = data.outputs && Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
...data,
files: getFilesInLogs(data.outputs),
} as any
if (isStringOutput) {
draft.resultTabActive = true
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
}
}))
prevNodeId = ''
handleWorkflowFinished(params)
if (onWorkflowFinished)
onWorkflowFinished(params)
},
onError: (params) => {
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Failed,
}
}))
handleWorkflowFailed()
if (onError)
onError(params)
},
onNodeStarted: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
iterParallelLogMap,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
const node = nodes.find(node => node.id === data.node_id)
if (node?.parentId) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === node?.parentId)
const currIteration = iterations?.details![node.data.iteration_index] || iterations?.details![iterations.details!.length - 1]
if (!data.parallel_run_id) {
currIteration?.push({
...data,
status: NodeRunningStatus.Running,
} as any)
}
else {
const nodeId = iterations?.node_id as string
if (!iterParallelLogMap.has(nodeId as string))
iterParallelLogMap.set(iterations?.node_id as string, new Map())
handleWorkflowNodeStarted(
params,
{
clientWidth,
clientHeight,
},
)
const currentIterLogMap = iterParallelLogMap.get(nodeId)!
if (!currentIterLogMap.has(data.parallel_run_id))
currentIterLogMap.set(data.parallel_run_id, [{ ...data, status: NodeRunningStatus.Running } as any])
else
currentIterLogMap.get(data.parallel_run_id)!.push({ ...data, status: NodeRunningStatus.Running } as any)
setIterParallelLogMap(iterParallelLogMap)
if (iterations)
iterations.details = Array.from(currentIterLogMap.values())
}
}))
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
} as any)
}))
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
if (!currentNode.parentId) {
setViewport({
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
draft[currentNodeIndex].data._waitingRun = false
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter((edge) => {
return edge.target === data.node_id
})
incomeEdges.forEach((edge) => {
const incomeNode = nodes.find(node => node.id === edge.source)!
if (
(!incomeNode.data._runningBranchId && edge.sourceHandle === 'source')
|| (incomeNode.data._runningBranchId && edge.sourceHandle === incomeNode.data._runningBranchId)
) {
edge.data = {
...edge.data,
_sourceRunningStatus: incomeNode.data._runningStatus,
_targetRunningStatus: NodeRunningStatus.Running,
_waitingRun: false,
}
}
})
})
setEdges(newEdges)
}
if (onNodeStarted)
onNodeStarted(params)
},
onNodeFinished: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
iterParallelLogMap,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const nodes = getNodes()
const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId
if (nodeParentId) {
if (!data.execution_metadata.parallel_mode_run_id) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node
if (iterations && iterations.details) {
const iterationIndex = data.execution_metadata?.iteration_index || 0
if (!iterations.details[iterationIndex])
iterations.details[iterationIndex] = []
const currIteration = iterations.details[iterationIndex]
const nodeIndex = currIteration.findIndex(node =>
node.node_id === data.node_id && (
node.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || node.parallel_id === data.execution_metadata?.parallel_id),
)
if (nodeIndex !== -1) {
currIteration[nodeIndex] = {
...currIteration[nodeIndex],
...(currIteration[nodeIndex].retryDetail
? { retryDetail: currIteration[nodeIndex].retryDetail }
: {}),
...data,
} as any
}
else {
currIteration.push({
...data,
} as any)
}
}
}))
}
else {
// open parallel mode
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node
if (iterations && iterations.details) {
const iterRunID = data.execution_metadata?.parallel_mode_run_id
const currIteration = iterParallelLogMap.get(iterations.node_id)?.get(iterRunID)
const nodeIndex = currIteration?.findIndex(node =>
node.node_id === data.node_id && (
node?.parallel_run_id === data.execution_metadata?.parallel_mode_run_id),
)
if (currIteration) {
if (nodeIndex !== undefined && nodeIndex !== -1) {
currIteration[nodeIndex] = {
...currIteration[nodeIndex],
...data,
} as any
}
else {
currIteration.push({
...data,
} as any)
}
}
setIterParallelLogMap(iterParallelLogMap)
const iterLogMap = iterParallelLogMap.get(iterations.node_id)
if (iterLogMap)
iterations.details = Array.from(iterLogMap.values())
}
}))
}
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex((trace) => {
if (!trace.execution_metadata?.parallel_id)
return trace.node_id === data.node_id
return trace.node_id === data.node_id && trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id
})
if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = {
...data,
...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras }
: {}),
...(draft.tracing[currentIndex].retryDetail
? { retryDetail: draft.tracing[currentIndex].retryDetail }
: {}),
} as any
}
}))
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._runningStatus = data.status as any
if (data.status === NodeRunningStatus.Exception) {
if (data.execution_metadata.error_strategy === ErrorHandleTypeEnum.failBranch)
currentNode.data._runningBranchId = ErrorHandleTypeEnum.failBranch
}
else {
if (data.node_type === BlockEnum.IfElse)
currentNode.data._runningBranchId = data?.outputs?.selected_case_id
if (data.node_type === BlockEnum.QuestionClassifier)
currentNode.data._runningBranchId = data?.outputs?.class_id
}
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter((edge) => {
return edge.target === data.node_id
})
incomeEdges.forEach((edge) => {
edge.data = {
...edge.data,
_targetRunningStatus: data.status as any,
}
})
})
setEdges(newEdges)
prevNodeId = data.node_id
}
handleWorkflowNodeFinished(params)
if (onNodeFinished)
onNodeFinished(params)
},
onIterationStart: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
setIterTimes,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
setIterTimes(DEFAULT_ITER_TIMES)
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
details: [],
iterDurationMap: {},
} as any)
}))
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
if (!currentNode.parentId) {
setViewport({
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length
draft[currentNodeIndex].data._waitingRun = false
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter(edge => edge.target === data.node_id)
incomeEdges.forEach((edge) => {
edge.data = {
...edge.data,
_sourceRunningStatus: nodes.find(node => node.id === edge.source)!.data._runningStatus,
_targetRunningStatus: NodeRunningStatus.Running,
_waitingRun: false,
}
})
})
setEdges(newEdges)
handleWorkflowNodeIterationStarted(
params,
{
clientWidth,
clientHeight,
},
)
if (onIterationStart)
onIterationStart(params)
},
onIterationNext: (params) => {
const {
workflowRunningData,
setWorkflowRunningData,
iterTimes,
setIterTimes,
} = workflowStore.getState()
const { data } = params
const {
getNodes,
setNodes,
} = store.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const iteration = draft.tracing!.find(trace => trace.node_id === data.node_id)
if (iteration) {
if (iteration.iterDurationMap && data.duration)
iteration.iterDurationMap[data.parallel_mode_run_id ?? `${data.index - 1}`] = data.duration
if (iteration.details!.length >= iteration.metadata.iterator_length!)
return
}
if (!data.parallel_mode_run_id)
iteration?.details!.push([])
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._iterationIndex = iterTimes
setIterTimes(iterTimes + 1)
})
setNodes(newNodes)
handleWorkflowNodeIterationNext(params)
if (onIterationNext)
onIterationNext(params)
},
onIterationFinish: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
setIterTimes,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const currIterationNode = tracing.find(trace => trace.node_id === data.node_id)
if (currIterationNode) {
Object.assign(currIterationNode, {
...data,
status: NodeRunningStatus.Succeeded,
})
}
}))
setIterTimes(DEFAULT_ITER_TIMES)
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._runningStatus = data.status
})
setNodes(newNodes)
prevNodeId = data.node_id
handleWorkflowNodeIterationFinished(params)
if (onIterationFinish)
onIterationFinish(params)
},
onNodeRetry: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
iterParallelLogMap,
setIterParallelLogMap,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const currentNode = nodes.find(node => node.id === data.node_id)!
const nodeParent = nodes.find(node => node.id === currentNode.parentId)
if (nodeParent) {
if (!data.execution_metadata.parallel_mode_run_id) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iteration = tracing.find(trace => trace.node_id === nodeParent.id)
if (iteration && iteration.details?.length) {
const currentNodeRetry = iteration.details[nodeParent.data._iterationIndex - 1]?.find(item => item.node_id === data.node_id)
if (currentNodeRetry) {
if (currentNodeRetry?.retryDetail)
currentNodeRetry?.retryDetail.push(data as NodeTracing)
else
currentNodeRetry.retryDetail = [data as NodeTracing]
}
}
}))
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iteration = tracing.find(trace => trace.node_id === nodeParent.id)
if (iteration && iteration.details?.length) {
const iterRunID = data.execution_metadata?.parallel_mode_run_id
const currIteration = iterParallelLogMap.get(iteration.node_id)?.get(iterRunID)
const currentNodeRetry = currIteration?.find(item => item.node_id === data.node_id)
if (currentNodeRetry) {
if (currentNodeRetry?.retryDetail)
currentNodeRetry?.retryDetail.push(data as NodeTracing)
else
currentNodeRetry.retryDetail = [data as NodeTracing]
}
setIterParallelLogMap(iterParallelLogMap)
const iterLogMap = iterParallelLogMap.get(iteration.node_id)
if (iterLogMap)
iteration.details = Array.from(iterLogMap.values())
}
}))
}
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const currentRetryNodeIndex = tracing.findIndex(trace => trace.node_id === data.node_id)
if (currentRetryNodeIndex > -1) {
const currentRetryNode = tracing[currentRetryNodeIndex]
if (currentRetryNode.retryDetail)
draft.tracing![currentRetryNodeIndex].retryDetail!.push(data as NodeTracing)
else
draft.tracing![currentRetryNodeIndex].retryDetail = [data as NodeTracing]
}
}))
}
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._retryIndex = data.retry_index
})
setNodes(newNodes)
handleWorkflowNodeRetry(params)
if (onNodeRetry)
onNodeRetry(params)
},
onParallelBranchStarted: (params) => {
// console.log(params, 'parallel start')
},
onParallelBranchFinished: (params) => {
// console.log(params, 'finished')
onAgentLog: (params) => {
handleWorkflowAgentLog(params)
if (onAgentLog)
onAgentLog(params)
},
onTextChunk: (params) => {
const { data: { text } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultTabActive = true
draft.resultText += text
}))
handleWorkflowTextChunk(params)
},
onTextReplace: (params) => {
const { data: { text } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultText = text
}))
handleWorkflowTextReplace(params)
},
onTTSChunk: (messageId: string, audio: string, audioType?: string) => {
onTTSChunk: (messageId: string, audio: string) => {
if (!audio || audio === '')
return
player.playAudioWithAudio(audio, true)
AudioPlayerManager.getInstance().resetMsgId(messageId)
},
onTTSEnd: (messageId: string, audio: string, audioType?: string) => {
onTTSEnd: (messageId: string, audio: string) => {
player.playAudioWithAudio(audio, false)
},
...restCallback,
},
)
}, [store, reactflow, workflowStore, doSyncWorkflowDraft])
}, [store, workflowStore, doSyncWorkflowDraft, handleWorkflowStarted, handleWorkflowFinished, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeRetry, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowAgentLog, pathname])
const handleStopRun = useCallback((taskId: string) => {
const appId = useAppStore.getState().appDetail?.id

View File

@ -484,7 +484,6 @@ export const useWorkflowInit = () => {
return acc
}, {} as Record<string, string>),
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
// #TODO chatVar sync#
conversationVariables: res.conversation_variables || [],
})
setSyncWorkflowDraftHash(res.hash)

View File

@ -72,6 +72,7 @@ import SyncingDataModal from './syncing-data-modal'
import UpdateDSLModal from './update-dsl-modal'
import DSLExportConfirmModal from './dsl-export-confirm-modal'
import LimitTips from './limit-tips'
import PluginDependency from './plugin-dependency'
import {
useStore,
useWorkflowStore,
@ -327,6 +328,7 @@ const Workflow: FC<WorkflowProps> = memo(({
)
}
<LimitTips />
<PluginDependency />
<ReactFlow
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}

View File

@ -7,7 +7,7 @@ import {
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
type Props = {
interface Props {
className?: string
text: string
onClick: () => void

View File

@ -0,0 +1,228 @@
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import type { ReactNode } from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import type { Strategy } from './agent-strategy'
import classNames from '@/utils/classnames'
import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Link from 'next/link'
import { InstallPluginButton } from './install-plugin-button'
import ViewTypeSelect, { ViewType } from '../../../block-selector/view-type-select'
import SearchInput from '@/app/components/base/search-input'
import Tools from '../../../block-selector/tools'
import { useTranslation } from 'react-i18next'
import { useStrategyProviders } from '@/service/use-strategy'
import { PluginType, type StrategyPluginDetail } from '@/app/components/plugins/types'
import type { ToolWithProvider } from '../../../types'
import { CollectionType } from '@/app/components/tools/types'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useStrategyInfo } from '../../agent/use-config'
import { SwitchPluginVersion } from './switch-plugin-version'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
import { ToolTipContent } from '@/app/components/base/tooltip/content'
const NotFoundWarn = (props: {
title: ReactNode,
description: ReactNode
}) => {
const { title, description } = props
const { t } = useTranslation()
return <Tooltip
popupContent={
<div className='space-y-1 text-xs'>
<h3 className='text-text-primary font-semibold'>
{title}
</h3>
<p className='text-text-secondary tracking-tight'>
{description}
</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>
{t('workflow.nodes.agent.linkToPlugin')}
</Link>
</p>
</div>
}
needsDelay
>
<div>
<RiErrorWarningFill className='text-text-destructive size-4' />
</div>
</Tooltip>
}
function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => string): ToolWithProvider[] {
return input.map((item) => {
const res: ToolWithProvider = {
id: item.plugin_unique_identifier,
author: item.declaration.identity.author,
name: item.declaration.identity.name,
description: item.declaration.identity.description as any,
plugin_id: item.plugin_id,
icon: getIcon(item.declaration.identity.icon),
label: item.declaration.identity.label as any,
type: CollectionType.all,
tools: item.declaration.strategies.map(strategy => ({
name: strategy.identity.name,
author: strategy.identity.author,
label: strategy.identity.label as any,
description: strategy.description,
parameters: strategy.parameters as any,
output_schema: strategy.output_schema,
labels: [],
})),
team_credentials: {},
is_team_authorization: true,
allow_delete: false,
labels: [],
}
return res
})
}
export type AgentStrategySelectorProps = {
value?: Strategy,
onChange: (value?: Strategy) => void,
}
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
const { value, onChange } = props
const [open, setOpen] = useState(false)
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
const [query, setQuery] = useState('')
const stra = useStrategyProviders()
const { getIconUrl } = useGetIcon()
const list = stra.data ? formatStrategy(stra.data, getIconUrl) : undefined
const filteredTools = useMemo(() => {
if (!list) return []
return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase()))
}, [query, list])
const { strategyStatus, refetch: refetchStrategyInfo } = useStrategyInfo(
value?.agent_strategy_provider_name,
value?.agent_strategy_name,
)
const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external'
&& !strategyStatus.plugin.installed && !!value
const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external'
&& !strategyStatus?.isExistInPlugin && !!value
const showSwitchVersion = !strategyStatus?.isExistInPlugin
&& strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed && !!value
const showInstallButton = !strategyStatus?.isExistInPlugin
&& strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed && !!value
const icon = list?.find(
coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name),
)?.icon as string | undefined
const { t } = useTranslation()
const wrapElemRef = useRef<HTMLDivElement>(null)
const {
queryPluginsWithDebounced: fetchPlugins,
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
useEffect(() => {
if (query) {
fetchPlugins({
query,
category: PluginType.agent,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
const pluginRef = useRef(null)
return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'>
<PortalToFollowElemTrigger className='w-full'>
<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 */}
{icon && <div className='flex items-center justify-center w-6 h-6'><img
src={icon}
width={20}
height={20}
className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
alt='icon'
/></div>}
<p
className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')}
>
{value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}
</p>
<div className='ml-auto flex items-center gap-1'>
{showInstallButton && value && <InstallPluginButton
onClick={e => e.stopPropagation()}
size={'small'}
uniqueIdentifier={value.plugin_unique_identifier}
/>}
{showPluginNotInstalledWarn
? <NotFoundWarn
title={t('workflow.nodes.agent.pluginNotInstalled')}
description={t('workflow.nodes.agent.pluginNotInstalledDesc')}
/>
: showUnsupportedStrategy
? <NotFoundWarn
title={t('workflow.nodes.agent.unsupportedStrategy')}
description={t('workflow.nodes.agent.strategyNotFoundDesc')}
/>
: <RiArrowDownSLine className='size-4 text-text-tertiary' />
}
{showSwitchVersion && <SwitchPluginVersion
uniqueIdentifier={value.plugin_unique_identifier}
tooltip={<ToolTipContent
title={t('workflow.nodes.agent.unsupportedStrategy')}>
{t('workflow.nodes.agent.strategyNotFoundDescAndSwitchVersion')}
</ToolTipContent>}
onChange={() => {
refetchStrategyInfo()
}}
/>}
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='bg-components-panel-bg-blur border-components-panel-border border-[0.5px] rounded-md shadow overflow-hidden w-[388px]'>
<header className='p-2 gap-1 flex'>
<SearchInput placeholder={t('workflow.nodes.agent.strategy.searchPlaceholder')} value={query} onChange={setQuery} className={'w-full'} />
<ViewTypeSelect viewType={viewType} onChange={setViewType} />
</header>
<main className="md:max-h-[300px] xl:max-h-[400px] 2xl:max-h-[564px] relative overflow-hidden flex flex-col w-full" ref={wrapElemRef}>
<Tools
tools={filteredTools}
viewType={viewType}
onSelect={(_, tool) => {
onChange({
agent_strategy_name: tool!.tool_name,
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,
plugin_unique_identifier: tool!.provider_id,
})
setOpen(false)
} }
className='max-w-none max-h-full h-full overflow-y-auto'
indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} />
<PluginList
wrapElemRef={wrapElemRef}
list={notInstalledPlugins as any} ref={pluginRef}
searchText={query}
tags={[]}
disableMaxWidth
/>
</main>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
})
AgentStrategySelector.displayName = 'AgentStrategySelector'

View File

@ -0,0 +1,228 @@
import type { CredentialFormSchemaNumberInput, CredentialFormSchemaTextInput } 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 ListEmpty from '@/app/components/base/list-empty'
import { AgentStrategySelector } from './agent-strategy-selector'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import { InputNumber } from '@/app/components/base/input-number'
import Slider from '@/app/components/base/slider'
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import Field from './field'
import { type ComponentProps, memo } from 'react'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Editor from './prompt/editor'
import { useWorkflowStore } from '../../../store'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import type { NodeOutPutVar } from '../../../types'
import type { Node } from 'reactflow'
export type Strategy = {
agent_strategy_provider_name: string
agent_strategy_name: string
agent_strategy_label: string
agent_output_schema: Record<string, any>
plugin_unique_identifier: string
}
export type AgentStrategyProps = {
strategy?: Strategy
onStrategyChange: (strategy?: Strategy) => void
formSchema: CredentialFormSchema[]
formValue: ToolVarInputs
onFormValueChange: (value: ToolVarInputs) => void
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
nodeId?: string
}
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
type ToolSelectorSchema = CustomSchema<'tool-selector'>
type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
const { t } = useTranslation()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const renderI18nObject = useRenderI18nObject()
const workflowStore = useWorkflowStore()
const {
setControlPromptEditorRerenderKey,
} = workflowStore.getState()
const override: ComponentProps<typeof Form<CustomField>>['override'] = [
[FormTypeEnum.textNumber, FormTypeEnum.textInput],
(schema, props) => {
switch (schema.type) {
case FormTypeEnum.textInput: {
const def = schema as CredentialFormSchemaTextInput
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={renderI18nObject(schema.label)}
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
containerBackgroundClassName='bg-transparent'
gradientBorder={false}
isSupportPromptGenerator={!!def.auto_generate?.type}
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
editorContainerClassName='px-0'
availableNodes={availableNodes}
nodesOutputVars={nodeOutputVars}
isSupportJinja={def.template?.enabled}
required={def.required}
varList={[]}
modelConfig={
defaultModel.data
? {
mode: 'chat',
name: defaultModel.data.model,
provider: defaultModel.data.provider.provider,
completion_params: {},
} : undefined
}
placeholderClassName='px-2 py-1'
titleClassName='system-sm-semibold-uppercase text-text-secondary text-[13px]'
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'
/>
}
case FormTypeEnum.textNumber: {
const def = schema as CredentialFormSchemaNumberInput
if (!def.max || !def.min)
return false
const defaultValue = schema.default ? Number.parseInt(schema.default) : 1
const value = props.value[schema.variable] || defaultValue
const onChange = (value: number) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return <Field
title={<>
{renderI18nObject(def.label)} {def.required && <span className='text-red-500'>*</span>}
</>}
tooltip={def.tooltip && renderI18nObject(def.tooltip)}
inline
>
<div className='flex w-[200px] items-center gap-3'>
<Slider
value={value}
onChange={onChange}
className='w-full'
min={def.min}
max={def.max}
/>
<InputNumber
value={value}
// TODO: maybe empty, handle this
onChange={onChange as any}
defaultValue={defaultValue}
size='sm'
min={def.min}
max={def.max}
className='w-12'
/>
</div>
</Field>
}
}
},
]
const renderField: ComponentProps<typeof Form<CustomField>>['customRenderField'] = (schema, props) => {
switch (schema.type) {
case FormTypeEnum.toolSelector: {
const value = props.value[schema.variable]
const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return (
<Field
title={<>
{renderI18nObject(schema.label)} {schema.required && <span className='text-red-500'>*</span>}
</>}
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
>
<ToolSelector
nodeId={props.nodeId || ''}
nodeOutputVars={props.nodeOutputVars || []}
availableNodes={props.availableNodes || []}
scope={schema.scope}
value={value}
onSelect={item => onChange(item)}
onDelete={() => onChange(null)}
/>
</Field>
)
}
case FormTypeEnum.multiToolSelector: {
const value = props.value[schema.variable]
const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return (
<MultipleToolSelector
nodeId={props.nodeId || ''}
nodeOutputVars={props.nodeOutputVars || []}
availableNodes={props.availableNodes || []}
scope={schema.scope}
value={value || []}
label={renderI18nObject(schema.label)}
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
onChange={onChange}
supportCollapse
required={schema.required}
/>
)
}
}
}
return <div className='space-y-2'>
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
{
strategy
? <div>
<Form<CustomField>
formSchemas={[
...formSchema,
]}
value={formValue}
onChange={onFormValueChange}
validating={false}
showOnVariableMap={{}}
isEditMode={true}
isAgentStrategy={true}
fieldLabelClassName='uppercase'
customRenderField={renderField}
override={override}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
/>
</div>
: <ListEmpty
icon={<Agent className='w-5 h-5 shrink-0 text-text-accent' />}
title={t('workflow.nodes.agent.strategy.configureTip')}
description={<div className='text-text-tertiary text-xs'>
{t('workflow.nodes.agent.strategy.configureTipDesc')} <br />
<Link href={'/'} className='text-text-accent-secondary'>
{t('workflow.nodes.agent.learnMore')}
</Link>
</div>}
/>
}
</div>
})
AgentStrategy.displayName = 'AgentStrategy'

View File

@ -25,7 +25,7 @@ import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import cn from '@/utils/classnames'
type Props = {
interface Props {
payload: InputVar
value: any
onChange: (value: any) => void

View File

@ -9,7 +9,7 @@ import { InputVarType } from '@/app/components/workflow/types'
import AddButton from '@/app/components/base/button/add-button'
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
export type Props = {
export interface Props {
className?: string
label?: string
inputs: InputVar[]

View File

@ -17,10 +17,10 @@ import ResultPanel from '@/app/components/workflow/run/result-panel'
import Toast from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import type { NodeTracing } from '@/types/workflow'
import RetryResultPanel from '@/app/components/workflow/run/retry-result-panel'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { Emoji } from '@/app/components/tools/types'
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
const i18nPrefix = 'workflow.singleRun'
@ -34,13 +34,12 @@ type BeforeRunFormProps = {
runningStatus: NodeRunningStatus
result?: JSX.Element
forms: FormProps[]
retryDetails?: NodeTracing[]
onRetryDetailBack?: any
}
showSpecialResultPanel?: boolean
} & Partial<SpecialResultPanelProps>
function formatValue(value: string | any, type: InputVarType) {
if (type === InputVarType.number)
return parseFloat(value)
return Number.parseFloat(value)
if (type === InputVarType.json)
return JSON.parse(value)
if (type === InputVarType.contexts) {
@ -66,8 +65,8 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
runningStatus,
result,
forms,
retryDetails,
onRetryDetailBack = () => { },
showSpecialResultPanel,
...restResultPanelParams
}) => {
const { t } = useTranslation()
@ -141,24 +140,14 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
</div>
</div>
{
retryDetails?.length && (
showSpecialResultPanel && (
<div className='h-0 grow overflow-y-auto pb-4'>
<RetryResultPanel
list={retryDetails.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
node_type: nodeType!,
extras: {
icon: toolIcon!,
},
}))}
onBack={onRetryDetailBack}
/>
<SpecialResultPanel {...restResultPanelParams} />
</div>
)
}
{
!retryDetails?.length && (
!showSpecialResultPanel && (
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => (

View File

@ -1,8 +1,9 @@
import type { ReactNode } from 'react'
import Collapse from '.'
type FieldCollapseProps = {
title: string
children: JSX.Element
children: ReactNode
}
const FieldCollapse = ({
title,

View File

@ -16,7 +16,7 @@ import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-tog
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
type Props = {
interface Props {
className?: string
title: JSX.Element | string
headerRight?: JSX.Element

View File

@ -88,7 +88,7 @@ const CodeEditor: FC<Props> = ({
const index = (() => {
if (match)
return parseInt(match[1]!) + 1
return Number.parseInt(match[1]!) + 1
return 1
})()

View File

@ -1,18 +1,17 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { useBoolean } from 'ahooks'
import type { DefaultTFuncReturn } from 'i18next'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
title: JSX.Element | string | DefaultTFuncReturn
tooltip?: React.ReactNode
title: ReactNode
tooltip?: ReactNode
isSubTitle?: boolean
supportFold?: boolean
children?: JSX.Element | string | null
@ -51,7 +50,7 @@ const Field: FC<Props> = ({
<div className='flex'>
{operations && <div>{operations}</div>}
{supportFold && (
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary cursor-pointer transform transition-transform' style={{ transform: fold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary cursor-pointer transition-transform' style={{ transform: fold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
)}
</div>
</div>

View File

@ -0,0 +1,25 @@
import classNames from '@/utils/classnames'
import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react'
export type GroupLabelProps = ComponentProps<'div'>
export const GroupLabel: FC<GroupLabelProps> = (props) => {
const { children, className, ...rest } = props
return <div {...rest} className={classNames('mb-1 system-2xs-medium-uppercase text-text-tertiary', className)}>
{children}
</div>
}
export type Group = PropsWithChildren<{
label: ReactNode
}>
export const Group: FC<Group> = (props) => {
const { children, label } = props
return <div className={classNames('py-1')}>
{label}
<div className='space-y-0.5'>
{children}
</div>
</div>
}

View File

@ -1,10 +1,10 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
type Props = {
title: string
content: string | JSX.Element
content: ReactNode
}
const InfoPanel: FC<Props> = ({

View File

@ -34,7 +34,7 @@ const InputNumberWithSlider: FC<Props> = ({
}, [defaultValue, max, min, onChange, value])
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(parseFloat(e.target.value))
onChange(Number.parseFloat(e.target.value))
}, [onChange])
return (

View File

@ -0,0 +1,45 @@
import Button from '@/app/components/base/button'
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
import type { ComponentProps, MouseEventHandler } from 'react'
import classNames from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
uniqueIdentifier: string
onSuccess?: () => void
}
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
const { className, uniqueIdentifier, onSuccess, ...rest } = props
const { t } = useTranslation()
const manifest = useCheckInstalled({
pluginIds: [uniqueIdentifier],
enabled: !!uniqueIdentifier,
})
const install = useInstallPackageFromMarketPlace()
const isLoading = manifest.isLoading || install.isPending
// await for refetch to get the new installed plugin, when manifest refetch, this component will unmount
|| install.isSuccess
const handleInstall: MouseEventHandler = (e) => {
e.stopPropagation()
install.mutate(uniqueIdentifier, {
onSuccess: async () => {
await manifest.refetch()
onSuccess?.()
},
})
}
if (!manifest.data) return null
if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null
return <Button
variant={'secondary'}
disabled={isLoading}
{...rest}
onClick={handleInstall}
className={classNames('flex items-center', className)}
>
{!isLoading ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
{!isLoading ? <RiInstallLine className='size-3.5 ml-1' /> : <RiLoader2Line className='size-3.5 ml-1 animate-spin' />}
</Button>
}

View File

@ -88,7 +88,7 @@ const MemoryConfig: FC<Props> = ({
limitedSize = null
}
else {
limitedSize = parseInt(limitedSize as string, 10)
limitedSize = Number.parseInt(limitedSize as string, 10)
if (isNaN(limitedSize))
limitedSize = WINDOW_SIZE_DEFAULT

View File

@ -0,0 +1,43 @@
import {
RiAlertFill,
RiCheckboxCircleFill,
RiErrorWarningLine,
RiLoader2Line,
} from '@remixicon/react'
import cn from '@/utils/classnames'
type NodeStatusIconProps = {
status: string
className?: string
}
const NodeStatusIcon = ({
status,
className,
}: NodeStatusIconProps) => {
return (
<>
{
status === 'succeeded' && (
<RiCheckboxCircleFill className={cn('shrink-0 w-4 h-4 text-text-success', className)} />
)
}
{
status === 'failed' && (
<RiErrorWarningLine className={cn('shrink-0 w-4 h-4 text-text-warning', className)} />
)
}
{
(status === 'stopped' || status === 'exception') && (
<RiAlertFill className={cn('shrink-0 w-4 h-4 text-text-warning-secondary', className)} />
)
}
{
status === 'running' && (
<RiLoader2Line className={cn('shrink-0 w-4 h-4 text-text-accent animate-spin', className)} />
)
}
</>
)
}
export default NodeStatusIcon

View File

@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
@ -7,7 +7,7 @@ import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/
type Props = {
className?: string
title?: string
children: JSX.Element
children: ReactNode
}
const OutputVars: FC<Props> = ({

View File

@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React, { useCallback, useRef } from 'react'
import {
RiDeleteBinLine,
@ -68,6 +68,14 @@ type Props = {
onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[]
handleAddVariable?: (payload: any) => void
containerBackgroundClassName?: string
gradientBorder?: boolean
titleTooltip?: ReactNode
inputClassName?: string
editorContainerClassName?: string
placeholderClassName?: string
titleClassName?: string
required?: boolean
}
const Editor: FC<Props> = ({
@ -96,6 +104,14 @@ const Editor: FC<Props> = ({
handleAddVariable,
onGenerated,
modelConfig,
containerBackgroundClassName: containerClassName,
gradientBorder = true,
titleTooltip,
inputClassName,
placeholderClassName,
titleClassName,
editorContainerClassName,
required,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@ -129,10 +145,13 @@ const Editor: FC<Props> = ({
return (
<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 className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
<div className={cn(headerClassName, 'pt-1 pl-3 pr-2 flex justify-between items-center')}>
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
<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', containerClassName)}>
<div className={cn('pt-1 pl-3 pr-2 flex justify-between items-center', headerClassName)}>
<div className='flex gap-2'>
<div className={cn('leading-4 text-xs font-semibold text-gray-700 uppercase', titleClassName)}>{title} {required && <span className='text-red-500'>*</span>}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'>
<div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
@ -201,12 +220,13 @@ const Editor: FC<Props> = ({
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
{!(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
key={controlPromptEditorRerenderKey}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className='min-h-[56px]'
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
@ -254,7 +274,7 @@ const Editor: FC<Props> = ({
</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
availableVars={nodesOutputVars || []}
varList={varList}
@ -266,6 +286,7 @@ const Editor: FC<Props> = ({
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
/>
</div>
)}

View File

@ -0,0 +1,28 @@
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames'
import { type ComponentProps, type PropsWithChildren, type ReactNode, memo } from 'react'
export type SettingItemProps = PropsWithChildren<{
label: string
status?: 'error' | 'warning'
tooltip?: ReactNode
}>
export const SettingItem = memo(({ label, children, status, tooltip }: SettingItemProps) => {
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const needTooltip = ['error', 'warning'].includes(status as any)
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-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}>
{label}
</div>
<Tooltip popupContent={tooltip} disabled={!needTooltip}>
<div className='truncate text-right system-xs-medium text-text-secondary'>
{children}
</div>
</Tooltip>
{indicator && <Indicator color={indicator} className='absolute -right-0.5 -top-0.5' />}
</div>
})
SettingItem.displayName = 'SettingItem'

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
interface Props {
className?: string
}

View File

@ -0,0 +1,126 @@
'use client'
import Badge from '@/app/components/base/badge'
import Tooltip from '@/app/components/base/tooltip'
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
import { RiArrowLeftRightLine, RiExternalLinkLine } from '@remixicon/react'
import type { ReactNode } from 'react'
import { type FC, useCallback, useState } from 'react'
import { useBoolean } from 'ahooks'
import { useCheckInstalled, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
import cn from '@/utils/classnames'
import PluginMutationModel from '@/app/components/plugins/plugin-mutation-model'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils'
import { Badge as Badge2, BadgeState } from '@/app/components/base/badge/index'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { marketplaceUrlPrefix } from '@/config'
export type SwitchPluginVersionProps = {
uniqueIdentifier: string
tooltip?: ReactNode
onChange?: (version: string) => void
className?: string
}
export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
const { uniqueIdentifier, tooltip, onChange, className } = props
const [pluginId] = uniqueIdentifier.split(':')
const [isShow, setIsShow] = useState(false)
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false)
const [target, setTarget] = useState<{
version: string,
pluginUniqueIden: string;
}>()
const pluginDetails = useCheckInstalled({
pluginIds: [pluginId],
enabled: true,
})
const pluginDetail = pluginDetails.data?.plugins.at(0)
const handleUpdatedFromMarketplace = useCallback(() => {
hideUpdateModal()
pluginDetails.refetch()
onChange?.(target!.version)
}, [hideUpdateModal, onChange, pluginDetails, target])
const { getIconUrl } = useGetIcon()
const icon = pluginDetail?.declaration.icon ? getIconUrl(pluginDetail.declaration.icon) : undefined
const mutation = useUpdatePackageFromMarketPlace()
const install = () => {
mutation.mutate(
{
new_plugin_unique_identifier: target!.pluginUniqueIden,
original_plugin_unique_identifier: uniqueIdentifier,
},
{
onSuccess() {
handleUpdatedFromMarketplace()
},
})
}
const { t } = useTranslation()
return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'>
<div className={cn('w-fit flex items-center justify-center', className)} onClick={e => e.stopPropagation()}>
{isShowUpdateModal && pluginDetail && <PluginMutationModel
onCancel={hideUpdateModal}
plugin={pluginManifestToCardPluginProps({
...pluginDetail.declaration,
icon: icon!,
})}
mutation={mutation}
mutate={install}
confirmButtonText={t('workflow.nodes.agent.installPlugin.install')}
cancelButtonText={t('workflow.nodes.agent.installPlugin.cancel')}
modelTitle={t('workflow.nodes.agent.installPlugin.title')}
description={t('workflow.nodes.agent.installPlugin.desc')}
cardTitleLeft={<>
<Badge2 className='mx-1' size="s" state={BadgeState.Warning}>
{`${pluginDetail.version} -> ${target!.version}`}
</Badge2>
</>}
modalBottomLeft={
<Link
className='flex justify-center items-center gap-1'
href={`${marketplaceUrlPrefix}/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`}
target='_blank'
>
<span className='text-text-accent system-xs-regular text-xs'>
{t('workflow.nodes.agent.installPlugin.changelog')}
</span>
<RiExternalLinkLine className='text-text-accent size-3' />
</Link>
}
/>}
{pluginDetail && <PluginVersionPicker
isShow={isShow}
onShowChange={setIsShow}
pluginID={pluginId}
currentVersion={pluginDetail.version}
onSelect={(state) => {
setTarget({
pluginUniqueIden: state.unique_identifier,
version: state.version,
})
showUpdateModal()
}}
trigger={
<Badge
className={cn(
'mx-1 hover:bg-state-base-hover flex',
isShow && 'bg-state-base-hover',
)}
uppercase={true}
text={
<>
<div>{pluginDetail.version}</div>
<RiArrowLeftRightLine className='ml-1 w-3 h-3 text-text-tertiary' />
</>
}
hasRedCornerMark={true}
/>
}
/>}
</div>
</Tooltip>
}

View File

@ -19,7 +19,7 @@ import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import { isExceptionVariable } from '@/app/components/workflow/utils'
type VariableTagProps = {
interface VariableTagProps {
valueSelector: ValueSelector
varType: VarType
isShort?: boolean

View File

@ -24,7 +24,7 @@ const ConstantField: FC<Props> = ({
const language = useLanguage()
const placeholder = (schema as CredentialFormSchemaSelect).placeholder
const handleStaticChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value === '' ? '' : parseFloat(e.target.value)
const value = e.target.value === '' ? '' : Number.parseFloat(e.target.value)
onChange(value, VarKindType.constant)
}, [onChange])
const handleSelectChange = useCallback((value: string | number) => {
@ -39,6 +39,7 @@ const ConstantField: FC<Props> = ({
wrapperClassName='w-full !h-8'
className='flex items-center'
disabled={readonly}
defaultValue={value}
items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleSelectChange(item.value)}
placeholder={placeholder?.[language] || placeholder?.en_US}

View File

@ -11,7 +11,7 @@ import type { VarType } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
import Toast from '@/app/components/base/toast'
type Props = {
interface Props {
readonly: boolean
outputs: OutputVar
outputKeyOrders: string[]

View File

@ -14,7 +14,7 @@ import type { ToolNodeType } from '../../../tool/types'
import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types'
import type { IterationNodeType } from '../../../iteration/types'
import type { ListFilterNodeType } from '../../../list-operator/types'
import { OUTPUT_FILE_SUB_VARIABLES } from '../../../if-else/default'
import { OUTPUT_FILE_SUB_VARIABLES } from '../../../constants'
import type { DocExtractorNodeType } from '../../../document-extractor/types'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
@ -32,6 +32,7 @@ import {
} from '@/app/components/workflow/constants'
import type { PromptItem } from '@/models/debug'
import { VAR_REGEX } from '@/config'
import type { AgentNodeType } from '../../../agent/types'
export const isSystemVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
@ -235,7 +236,29 @@ const formatItem = (
}
case BlockEnum.Tool: {
res.vars = TOOL_OUTPUT_STRUCT
const {
output_schema,
} = data as ToolNodeType
if (!output_schema) {
res.vars = TOOL_OUTPUT_STRUCT
}
else {
const outputSchema: any[] = []
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
outputSchema.push({
variable: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
})
res.vars = [
...TOOL_OUTPUT_STRUCT,
...outputSchema,
]
}
break
}
@ -293,6 +316,25 @@ const formatItem = (
break
}
case BlockEnum.Agent: {
const payload = data as AgentNodeType
const outputs: Var[] = []
Object.keys(payload.output_schema?.properties || {}).forEach((outputKey) => {
const output = payload.output_schema.properties[outputKey]
outputs.push({
variable: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]` as VarType
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}` as VarType,
})
})
res.vars = [
...outputs,
...TOOL_OUTPUT_STRUCT,
]
break
}
case 'env': {
res.vars = data.envList.map((env: EnvironmentVariable) => {
return {
@ -680,7 +722,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
break
}
case BlockEnum.LLM: {
const payload = (data as LLMNodeType)
const payload = data as LLMNodeType
const isChatModel = payload.model?.mode === 'chat'
let prompts: string[] = []
if (isChatModel) {
@ -718,19 +760,19 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
break
}
case BlockEnum.QuestionClassifier: {
const payload = (data as QuestionClassifierNodeType)
const payload = data as QuestionClassifierNodeType
res = [payload.query_variable_selector]
const varInInstructions = matchNotSystemVars([payload.instruction || ''])
res.push(...varInInstructions)
break
}
case BlockEnum.HttpRequest: {
const payload = (data as HttpNodeType)
const payload = data as HttpNodeType
res = matchNotSystemVars([payload.url, payload.headers, payload.params, typeof payload.body.data === 'string' ? payload.body.data : payload.body.data.map(d => d.value).join('')])
break
}
case BlockEnum.Tool: {
const payload = (data as ToolNodeType)
const payload = data as ToolNodeType
const mixVars = matchNotSystemVars(Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type === ToolVarType.mixed).map(key => payload.tool_parameters[key].value) as string[])
const vars = Object.keys(payload.tool_parameters).filter(key => payload.tool_parameters[key].type === ToolVarType.variable).map(key => payload.tool_parameters[key].value as string) || []
res = [...(mixVars as ValueSelector[]), ...(vars as any)]
@ -748,7 +790,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
}
case BlockEnum.ParameterExtractor: {
const payload = (data as ParameterExtractorNodeType)
const payload = data as ParameterExtractorNodeType
res = [payload.query]
const varInInstructions = matchNotSystemVars([payload.instruction || ''])
res.push(...varInInstructions)
@ -764,6 +806,21 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
res = [(data as ListFilterNodeType).variable]
break
}
case BlockEnum.Agent: {
const payload = data as AgentNodeType
const valueSelectors: ValueSelector[] = []
if (!payload.agent_parameters)
break
Object.keys(payload.agent_parameters || {}).forEach((key) => {
const { value } = payload.agent_parameters![key]
if (typeof value === 'string')
valueSelectors.push(...matchNotSystemVars([value]))
})
res = valueSelectors
break
}
}
return res || []
}
@ -775,7 +832,7 @@ export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSe
let res: string | string[] = ''
switch (type) {
case BlockEnum.LLM: {
const payload = (data as LLMNodeType)
const payload = data as LLMNodeType
res = [`#${valueSelector.join('.')}#`]
if (payload.context?.variable_selector.join('.') === valueSelector.join('.'))
res.push('#context#')

View File

@ -64,6 +64,7 @@ type Props = {
placeholder?: string
minWidth?: number
popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
}
const VarReferencePicker: FC<Props> = ({
@ -90,6 +91,7 @@ const VarReferencePicker: FC<Props> = ({
placeholder,
minWidth,
popupFor,
zIndex,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@ -386,7 +388,7 @@ const VarReferencePicker: FC<Props> = ({
</>
</WrapElem>
<PortalToFollowElemContent style={{
zIndex: 100,
zIndex: zIndex || 100,
}} className='mt-1'>
{!isConstant && (
<VarReferencePopup

View File

@ -17,7 +17,7 @@ import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others
import { checkKeys } from '@/utils/var'
import { FILE_STRUCT } from '@/app/components/workflow/constants'
type ObjectChildrenProps = {
interface ObjectChildrenProps {
nodeId: string
title: string
data: Var[]
@ -28,7 +28,7 @@ type ObjectChildrenProps = {
isSupportFileVar?: boolean
}
type ItemProps = {
interface ItemProps {
nodeId: string
title: string
objPath: string[]
@ -134,7 +134,7 @@ const Item: FC<ItemProps> = ({
zIndex: 100,
}}>
{(isObj && !isFile) && (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
// eslint-disable-next-line ts/no-use-before-define
<ObjectChildren
nodeId={nodeId}
title={title}
@ -147,7 +147,7 @@ const Item: FC<ItemProps> = ({
/>
)}
{isFile && (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
// eslint-disable-next-line ts/no-use-before-define
<ObjectChildren
nodeId={nodeId}
title={title}
@ -226,7 +226,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
)
}
type Props = {
interface Props {
hideSearch?: boolean
searchBoxClassName?: string
vars: NodeOutPutVar[]

View File

@ -46,6 +46,7 @@ const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
const { checkValid: checkIterationValid } = IterationDefault
const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.LLM]: checkLLMValid,
[BlockEnum.KnowledgeRetrieval]: checkKnowledgeRetrievalValid,
@ -144,7 +145,7 @@ const useOneStepRun = <T>({
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
const isShowSingleRun = data._isSingleRun && canShowSingleRun
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([])
useEffect(() => {
if (!checkValid) {
@ -175,7 +176,7 @@ const useOneStepRun = <T>({
const workflowStore = useWorkflowStore()
useEffect(() => {
workflowStore.getState().setShowSingleRunPanel(!!isShowSingleRun)
}, [isShowSingleRun])
}, [isShowSingleRun, workflowStore])
const hideSingleRun = () => {
handleNodeDataUpdate({
@ -213,7 +214,7 @@ const useOneStepRun = <T>({
}
else {
setIterationRunResult([])
let _iterationResult: NodeTracing[][] = []
let _iterationResult: NodeTracing[] = []
let _runResult: any = null
ssePost(
getIterationSingleNodeRunUrl(isChatMode, appId!, id),
@ -233,27 +234,43 @@ const useOneStepRun = <T>({
_runResult.created_by = iterationData.created_by.name
setRunResult(_runResult)
},
onIterationNext: () => {
// iteration next trigger time is triggered one more time than iterationTimes
if (_iterationResult.length >= iterationTimes!)
return
onIterationStart: (params) => {
const newIterationRunResult = produce(_iterationResult, (draft) => {
draft.push([])
draft.push({
...params.data,
status: NodeRunningStatus.Running,
})
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onIterationNext: () => {
// iteration next trigger time is triggered one more time than iterationTimes
if (_iterationResult.length >= iterationTimes!)
return _iterationResult.length >= iterationTimes!
},
onIterationFinish: (params) => {
_runResult = params.data
setRunResult(_runResult)
const iterationRunResult = _iterationResult
const currentIndex = iterationRunResult.findIndex(trace => trace.id === params.data.id)
const newIterationRunResult = produce(iterationRunResult, (draft) => {
if (currentIndex > -1) {
draft[currentIndex] = {
...draft[currentIndex],
...data,
}
}
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onNodeStarted: (params) => {
const newIterationRunResult = produce(_iterationResult, (draft) => {
draft[draft.length - 1].push({
draft.push({
...params.data,
status: NodeRunningStatus.Running,
} as NodeTracing)
})
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
@ -262,18 +279,25 @@ const useOneStepRun = <T>({
const iterationRunResult = _iterationResult
const { data } = params
const currentIndex = iterationRunResult[iterationRunResult.length - 1].findIndex(trace => trace.node_id === data.node_id)
const currentIndex = iterationRunResult.findIndex(trace => trace.id === data.id)
const newIterationRunResult = produce(iterationRunResult, (draft) => {
if (currentIndex > -1) {
draft[draft.length - 1][currentIndex] = {
draft[currentIndex] = {
...draft[currentIndex],
...data,
status: NodeRunningStatus.Succeeded,
} as NodeTracing
}
}
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onNodeRetry: (params) => {
const newIterationRunResult = produce(_iterationResult, (draft) => {
draft.push(params.data)
})
_iterationResult = newIterationRunResult
setIterationRunResult(newIterationRunResult)
},
onError: () => {
handleNodeDataUpdate({
id,

View File

@ -0,0 +1,75 @@
import Tooltip from '@/app/components/base/tooltip'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import Indicator from '@/app/components/header/indicator'
import { type FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export type ModelBarProps = {
provider: string
model: string
} | {}
const useAllModel = () => {
const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration)
const { data: moderation } = useModelList(ModelTypeEnum.moderation)
const { data: rerank } = useModelList(ModelTypeEnum.rerank)
const { data: speech2text } = useModelList(ModelTypeEnum.speech2text)
const { data: textEmbedding } = useModelList(ModelTypeEnum.textEmbedding)
const { data: tts } = useModelList(ModelTypeEnum.tts)
const models = useMemo(() => {
return textGeneration
.concat(moderation)
.concat(rerank)
.concat(speech2text)
.concat(textEmbedding)
.concat(tts)
}, [textGeneration, moderation, rerank, speech2text, textEmbedding, tts])
if (!textGeneration || !moderation || !rerank || !speech2text || !textEmbedding || !tts)
return undefined
return models
}
export const ModelBar: FC<ModelBarProps> = (props) => {
const { t } = useTranslation()
const modelList = useAllModel()
if (!('provider' in props)) {
return <Tooltip
popupContent={t('workflow.nodes.agent.modelNotSelected')}
triggerMethod='hover'
>
<div className='relative'>
<ModelSelector
modelList={[]}
triggerClassName='bg-workflow-block-parma-bg !h-6 !rounded-md'
defaultModel={undefined}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName='opacity-50'
/>
<Indicator color={'red'} className='absolute -right-0.5 -top-0.5' />
</div>
</Tooltip>
}
const modelInstalled = modelList?.some(
provider => provider.provider === props.provider && provider.models.some(model => model.model === props.model))
const showWarn = modelList && !modelInstalled
return modelList && <Tooltip
popupContent={t('workflow.nodes.agent.modelNotInstallTooltip')}
triggerMethod='hover'
disabled={!modelList || modelInstalled}
>
<div className='relative'>
<ModelSelector
modelList={modelList}
triggerClassName='bg-workflow-block-parma-bg !h-6 !rounded-md'
defaultModel={props}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName='opacity-50'
/>
{showWarn && <Indicator color={'red'} className='absolute -right-0.5 -top-0.5' />}
</div>
</Tooltip>
}

View File

@ -0,0 +1,81 @@
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames'
import { memo, useMemo, useRef, useState } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { useTranslation } from 'react-i18next'
import { Group } from '@/app/components/base/icons/src/vender/other'
type Status = 'not-installed' | 'not-authorized' | undefined
export type ToolIconProps = {
providerName: string
}
export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const isDataReady = !!buildInTools && !!customTools && !!workflowTools
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.name === providerName
})
}, [buildInTools, customTools, providerName, workflowTools])
const providerNameParts = providerName.split('/')
const author = providerNameParts[0]
const name = providerNameParts[1]
const icon = useMemo(() => {
if (currentProvider) return currentProvider.icon as string
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
return iconFromMarketPlace
}, [author, currentProvider, name])
const status: Status = useMemo(() => {
if (!isDataReady) return undefined
if (!currentProvider) return 'not-installed'
if (currentProvider.is_team_authorization === false) return 'not-authorized'
return undefined
}, [currentProvider, isDataReady])
const indicator = status === 'not-installed' ? 'red' : status === 'not-authorized' ? 'yellow' : undefined
const notSuccess = (['not-installed', 'not-authorized'] as Array<Status>).includes(status)
const { t } = useTranslation()
const tooltip = useMemo(() => {
if (!notSuccess) return undefined
if (status === 'not-installed') return t('workflow.nodes.agent.toolNotInstallTooltip', { tool: name })
if (status === 'not-authorized') return t('workflow.nodes.agent.toolNotAuthorizedTooltip', { tool: name })
throw new Error('Unknown status')
}, [name, notSuccess, status, t])
const [iconFetchError, setIconFetchError] = useState(false)
return <Tooltip
triggerMethod='hover'
popupContent={tooltip}
disabled={!notSuccess}
>
<div
className={classNames(
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]',
)}
ref={containerRef}
>
{!iconFetchError
// eslint-disable-next-line @next/next/no-img-element
? <img
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
/>
: <Group className="w-3 h-3 opacity-35" />
}
{indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />}
</div>
</Tooltip>
})
ToolIcon.displayName = 'ToolIcon'

View File

@ -0,0 +1,143 @@
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import type { NodeDefault } from '../../types'
import type { AgentNodeType } from './types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { renderI18nObject } from '@/hooks/use-i18n'
const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: {
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
getAvailableNextNodes(isChatMode) {
return isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
checkValid(payload, t, moreDataForCheckValid: {
strategyProvider?: StrategyPluginDetail,
strategy?: StrategyDetail
language: string
isReadyForCheckValid: boolean
}) {
const { strategy, language, isReadyForCheckValid } = moreDataForCheckValid
if (!isReadyForCheckValid) {
return {
isValid: true,
errorMessage: '',
}
}
if (!strategy) {
return {
isValid: false,
errorMessage: t('workflow.nodes.agent.checkList.strategyNotSelected'),
}
}
for (const param of strategy.parameters) {
// single tool
if (param.required && param.type === FormTypeEnum.toolSelector) {
// no value
const toolValue = payload.agent_parameters?.[param.name]?.value
if (!toolValue) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
// not enabled
else if (!toolValue.enabled) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
}
}
// check form of tool
else {
const schemas = toolValue.schemas || []
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
}
})
}
}
// multiple tools
if (param.required && param.type === FormTypeEnum.multiToolSelector) {
const tools = payload.agent_parameters?.[param.name]?.value || []
// no value
if (!tools.length) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
// not enabled
else if (tools.every((tool: any) => !tool.enabled)) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
}
}
// check form of tools
else {
let validState = {
isValid: true,
errorMessage: '',
}
for (const tool of tools) {
const schemas = tool.schemas || []
const userSettings = tool.settings
const reasoningConfig = tool.parameters
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
return validState = {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name]?.auto === 0 && !reasoningConfig[schema.name]?.value) {
return validState = {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
}
})
}
return validState
}
}
// common params
if (param.required && !payload.agent_parameters?.[param.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
}
return {
isValid: true,
errorMessage: '',
}
},
}
export default nodeDefault

View File

@ -0,0 +1,113 @@
import { type FC, memo, useMemo } from 'react'
import type { NodeProps } from '../../types'
import type { AgentNodeType } from './types'
import { SettingItem } from '../_base/components/setting-item'
import { Group, GroupLabel } from '../_base/components/group'
import type { ToolIconProps } from './components/tool-icon'
import { ToolIcon } from './components/tool-icon'
import useConfig from './use-config'
import { useTranslation } from 'react-i18next'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { ModelBar } from './components/model-bar'
const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const { inputs, currentStrategy, currentStrategyStatus, pluginDetail } = useConfig(props.id, props.data)
const renderI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const models = useMemo(() => {
if (!inputs) return []
// if selected, show in node
// if required and not selected, show empty selector
// if not required and not selected, show nothing
const models = currentStrategy?.parameters
.filter(param => param.type === FormTypeEnum.modelSelector)
.reduce((acc, param) => {
const item = inputs.agent_parameters?.[param.name]?.value
if (!item) {
if (param.required) {
acc.push({ param: param.name })
return acc
}
else { return acc }
}
acc.push({ provider: item.provider, model: item.model, param: param.name })
return acc
}, [] as Array<{ param: string } | { provider: string, model: string, param: string }>) || []
return models
}, [currentStrategy, inputs])
const tools = useMemo(() => {
const tools: Array<ToolIconProps> = []
currentStrategy?.parameters.forEach((param) => {
if (param.type === FormTypeEnum.toolSelector) {
const field = param.name
const value = inputs.agent_parameters?.[field]?.value
if (value) {
tools.push({
providerName: value.provider_name as any,
})
}
}
if (param.type === FormTypeEnum.multiToolSelector) {
const field = param.name
const value = inputs.agent_parameters?.[field]?.value
if (value) {
(value as unknown as any[]).forEach((item) => {
tools.push({
providerName: item.provider_name,
})
})
}
}
})
return tools
}, [currentStrategy?.parameters, inputs.agent_parameters])
return <div className='mb-1 px-3 py-1 space-y-1'>
{inputs.agent_strategy_name
? <SettingItem
label={t('workflow.nodes.agent.strategy.shortLabel')}
status={
currentStrategyStatus && !currentStrategyStatus.isExistInPlugin
? 'error'
: undefined
}
tooltip={
(currentStrategyStatus && !currentStrategyStatus.isExistInPlugin)
? t('workflow.nodes.agent.strategyNotInstallTooltip', {
plugin: pluginDetail?.declaration.label
? renderI18nObject(pluginDetail?.declaration.label)
: undefined,
strategy: inputs.agent_strategy_label,
})
: undefined
}
>
{inputs.agent_strategy_label}
</SettingItem>
: <SettingItem label={t('workflow.nodes.agent.strategyNotSet')} />}
{models.length > 0 && <Group
label={<GroupLabel className='mt-1'>
{t('workflow.nodes.agent.model')}
</GroupLabel>}
>
{models.map((model) => {
return <ModelBar
{...model}
key={model.param}
/>
})}
</Group>}
{tools.length > 0 && <Group label={<GroupLabel className='mt-1'>
{t('workflow.nodes.agent.toolbox')}
</GroupLabel>}>
<div className='grid grid-cols-10 gap-0.5'>
{tools.map(tool => <ToolIcon {...tool} key={Math.random()} />)}
</div>
</Group>}
</div>
}
AgentNode.displayName = 'AgentNode'
export default memo(AgentNode)

View File

@ -0,0 +1,156 @@
import type { FC } from 'react'
import { memo, useMemo } from 'react'
import type { NodePanelProps } from '../../types'
import type { AgentNodeType } from './types'
import Field from '../_base/components/field'
import { AgentStrategy } from '../_base/components/agent-strategy'
import useConfig from './use-config'
import { useTranslation } from 'react-i18next'
import OutputVars, { VarItem } from '../_base/components/output-vars'
import type { StrategyParamItem } from '@/app/components/plugins/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import formatTracing from '@/app/components/workflow/run/utils/format-log'
import { useLogs } from '@/app/components/workflow/run/hooks'
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
import { toType } from '@/app/components/tools/utils/to-form-schema'
import { useStore } from '../../store'
const i18nPrefix = 'workflow.nodes.agent'
export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema {
return {
...param as any,
variable: param.name,
show_on: [],
type: toType(param.type),
}
}
const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
const {
inputs,
setInputs,
currentStrategy,
formData,
onFormChange,
availableNodesWithParent,
availableVars,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
runInputData,
setRunInputData,
varInputs,
outputSchema,
} = useConfig(props.id, props.data)
const { t } = useTranslation()
const nodeInfo = useMemo(() => {
if (!runResult)
return
return formatTracing([runResult], t)[0]
}, [runResult, t])
const logsParams = useLogs()
const singleRunForms = (() => {
const forms: FormProps[] = []
if (varInputs.length > 0) {
forms.push(
{
label: t(`${i18nPrefix}.singleRun.variable`)!,
inputs: varInputs,
values: runInputData,
onChange: setRunInputData,
},
)
}
return forms
})()
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
return <div className='my-2'>
<Field title={t('workflow.nodes.agent.strategy.label')} className='px-4 py-2' tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
<AgentStrategy
strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!,
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
plugin_unique_identifier: inputs.plugin_unique_identifier!,
} : undefined}
onStrategyChange={(strategy) => {
setInputs({
...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name,
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
plugin_unique_identifier: strategy!.plugin_unique_identifier,
agent_parameters: {},
})
resetEditor(Date.now())
}}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={formData}
onFormValueChange={onFormChange}
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodeId={props.id}
/>
</Field>
<div>
<OutputVars>
<VarItem
name='text'
type='String'
description={t(`${i18nPrefix}.outputVars.text`)}
/>
<VarItem
name='files'
type='Array[File]'
description={t(`${i18nPrefix}.outputVars.files.title`)}
/>
<VarItem
name='json'
type='Array[Object]'
description={t(`${i18nPrefix}.outputVars.json`)}
/>
{outputSchema.map(({ name, type, description }) => (
<VarItem
key={name}
name={name}
type={type}
description={description}
/>
))}
</OutputVars>
</div>
{
isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={<ResultPanel {...runResult} nodeInfo={nodeInfo} showSteps={false} {...logsParams} />}
/>
)
}
</div>
}
AgentPanel.displayName = 'AgentPanel'
export default memo(AgentPanel)

View File

@ -0,0 +1,11 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { ToolVarInputs } from '../tool/types'
export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string
agent_strategy_name?: string
agent_strategy_label?: string
agent_parameters?: ToolVarInputs
output_schema: Record<string, any>
plugin_unique_identifier?: string
}

View File

@ -0,0 +1,208 @@
import { useStrategyProviderDetail } from '@/service/use-strategy'
import useNodeCrud from '../_base/hooks/use-node-crud'
import useVarList from '../_base/hooks/use-var-list'
import useOneStepRun from '../_base/hooks/use-one-step-run'
import type { AgentNodeType } from './types'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useCallback, useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import type { Var } from '../../types'
import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list'
export type StrategyStatus = {
plugin: {
source: 'external' | 'marketplace'
installed: boolean
}
isExistInPlugin: boolean
}
export const useStrategyInfo = (
strategyProviderName?: string,
strategyName?: string,
) => {
const strategyProvider = useStrategyProviderDetail(
strategyProviderName || '',
{ retry: false },
)
const strategy = strategyProvider.data?.declaration.strategies.find(
str => str.identity.name === strategyName,
)
const marketplace = useFetchPluginsInMarketPlaceByIds([strategyProviderName!], {
retry: false,
})
const strategyStatus: StrategyStatus | undefined = useMemo(() => {
if (strategyProvider.isLoading || marketplace.isLoading)
return undefined
const strategyExist = !!strategy
const isPluginInstalled = !strategyProvider.isError
const isInMarketplace = !!marketplace.data?.data.plugins.at(0)
return {
plugin: {
source: isInMarketplace ? 'marketplace' : 'external',
installed: isPluginInstalled,
},
isExistInPlugin: strategyExist,
}
}, [strategy, marketplace, strategyProvider.isError, strategyProvider.isLoading])
const refetch = useCallback(() => {
strategyProvider.refetch()
marketplace.refetch()
}, [marketplace, strategyProvider])
return {
strategyProvider,
strategy,
strategyStatus,
refetch,
}
}
const useConfig = (id: string, payload: AgentNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<AgentNodeType>(id, payload)
// variables
const { handleVarListChange, handleAddVariable } = useVarList<AgentNodeType>({
inputs,
setInputs,
})
const {
strategyStatus: currentStrategyStatus,
strategy: currentStrategy,
strategyProvider,
} = useStrategyInfo(
inputs.agent_strategy_provider_name,
inputs.agent_strategy_name,
)
const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/')
const pluginDetail = useCheckInstalled({
pluginIds: [pluginId!],
enabled: Boolean(pluginId),
})
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,
})
}
// vars
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
return [
VarKindType.arrayObject,
VarKindType.array,
VarKindType.number,
VarKindType.string,
VarKindType.secret,
VarKindType.arrayString,
VarKindType.arrayNumber,
VarKindType.file,
VarKindType.arrayFile,
].includes(varPayload.type)
}, [])
const {
availableVars,
availableNodesWithParent,
} = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: filterMemoryPromptVar,
})
// single run
const {
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
getInputVars,
} = useOneStepRun<AgentNodeType>({
id,
data: inputs,
defaultRunInputData: {},
})
const allVarStrArr = (() => {
const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => {
return formData[item.name]
}) || []
return arr
})()
const varInputs = (() => {
const vars = getInputVars(allVarStrArr)
return vars
})()
const outputSchema = useMemo(() => {
const res: any[] = []
if (!inputs.output_schema)
return []
Object.keys(inputs.output_schema.properties).forEach((outputKey) => {
const output = inputs.output_schema.properties[outputKey]
res.push({
name: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
})
return res
}, [inputs.output_schema])
return {
readOnly,
inputs,
setInputs,
handleVarListChange,
handleAddVariable,
currentStrategy,
formData,
onFormChange,
currentStrategyStatus,
strategyProvider: strategyProvider.data,
pluginDetail: pluginDetail.data?.plugins.at(0),
availableVars,
availableNodesWithParent,
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
varInputs,
outputSchema,
}
}
export default useConfig

View File

@ -108,7 +108,7 @@ const OperationSelector: FC<OperationSelectorProps> = ({
}}
>
<div className='flex min-h-5 px-1 items-center gap-1 grow'>
<span className={'flex flex-grow text-text-secondary system-sm-medium'}>{t(`${i18nPrefix}.operations.${item.name}`)}</span>
<span className={'flex grow text-text-secondary system-sm-medium'}>{t(`${i18nPrefix}.operations.${item.name}`)}</span>
</div>
{item.value === value && (
<div className='flex justify-center items-center'>

View File

@ -128,7 +128,7 @@ const VarList: FC<Props> = ({
return (
<div className='flex items-start gap-1 self-stretch' key={index}>
<div className='flex flex-col items-start gap-1 flex-grow'>
<div className='flex flex-col items-start gap-1 grow'>
<div className='flex items-center gap-1 self-stretch'>
<VarReferencePicker
readonly={readonly}
@ -212,7 +212,7 @@ const VarList: FC<Props> = ({
</div>
<ActionButton
size='l'
className='flex-shrink-0 group hover:!bg-state-destructive-hover'
className='shrink-0 group hover:!bg-state-destructive-hover'
onClick={handleVarRemove(index)}
>
<RiDeleteBinLine className='text-text-tertiary w-4 h-4 group-hover:text-text-destructive' />

View File

@ -3,7 +3,7 @@ import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type AssignerNodeType } from './types'
import type { AssignerNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'

View File

@ -7,9 +7,9 @@ import {
import VarList from './components/var-list'
import useConfig from './use-config'
import type { AssignerNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import { useHandleAddOperationItem } from './hooks'
import ActionButton from '@/app/components/base/action-button'
import { type NodePanelProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.assigner'
@ -40,7 +40,7 @@ const Panel: FC<NodePanelProps<AssignerNodeType>> = ({
<div className='flex py-2 flex-col items-start self-stretch'>
<div className='flex flex-col justify-center items-start gap-1 px-4 py-2 w-full self-stretch'>
<div className='flex items-start gap-2 self-stretch'>
<div className='flex flex-col justify-center items-start flex-grow text-text-secondary system-sm-semibold-uppercase'>{t(`${i18nPrefix}.variables`)}</div>
<div className='flex flex-col justify-center items-start grow text-text-secondary system-sm-semibold-uppercase'>{t(`${i18nPrefix}.variables`)}</div>
<ActionButton onClick={handleAddOperation}>
<RiAddLine className='w-4 h-4 shrink-0 text-text-tertiary' />
</ActionButton>

View File

@ -13,7 +13,7 @@ import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import { type NodePanelProps } from '@/app/components/workflow/types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
const i18nPrefix = 'workflow.nodes.code'

View File

@ -34,6 +34,9 @@ import DocExtractorNode from './document-extractor/node'
import DocExtractorPanel from './document-extractor/panel'
import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
import { TransferMethod } from '@/types/app'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode,
@ -54,6 +57,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
@ -75,6 +79,22 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Iteration]: IterationPanel,
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
}
export const CUSTOM_NODE_TYPE = 'custom'
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },
{ value: 'audio', i18nKey: 'audio' },
{ value: 'video', i18nKey: 'video' },
]
export const TRANSFER_METHOD = [
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')

View File

@ -1,6 +1,6 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type DocExtractorNodeType } from './types'
import type { DocExtractorNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'

View File

@ -3,7 +3,7 @@ import React from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
import { type DocExtractorNodeType } from './types'
import type { DocExtractorNodeType } from './types'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'

View File

@ -4,7 +4,7 @@ import { useStoreApi } from 'reactflow'
import type { ValueSelector, Var } from '../../types'
import { InputVarType, VarType } from '../../types'
import { type DocExtractorNodeType } from './types'
import type { DocExtractorNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import {

View File

@ -1,6 +1,6 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type EndNodeType } from './types'
import type { EndNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<EndNodeType> = {

View File

@ -1,4 +1,4 @@
import { type FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import useConfig from './use-config'
@ -6,7 +6,7 @@ import type { EndNodeType } from './types'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import AddButton from '@/app/components/base/button/add-button'
import { type NodePanelProps } from '@/app/components/workflow/types'
import type { NodePanelProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.end'

View File

@ -17,7 +17,7 @@ import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.http.authorization'
type Props = {
interface Props {
nodeId: string
payload: AuthorizationPayloadType
onChange: (payload: AuthorizationPayloadType) => void

View File

@ -15,7 +15,7 @@ import { VarType } from '@/app/components/workflow/types'
const UNIQUE_ID_PREFIX = 'key-value-'
type Props = {
interface Props {
readonly: boolean
nodeId: string
payload: Body

View File

@ -4,7 +4,7 @@ import React from 'react'
import type { KeyValue } from '../../types'
import KeyValueEdit from './key-value-edit'
type Props = {
interface Props {
readonly: boolean
nodeId: string
list: KeyValue[]

View File

@ -9,7 +9,7 @@ import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
interface Props {
readonly: boolean
nodeId: string
list: KeyValue[]

View File

@ -8,7 +8,7 @@ import RemoveButton from '@/app/components/workflow/nodes/_base/components/remov
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
type Props = {
interface Props {
className?: string
instanceId?: string
nodeId: string

View File

@ -14,7 +14,7 @@ import { VarType } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
interface Props {
instanceId: string
className?: string
nodeId: string

View File

@ -6,7 +6,7 @@ import type { Timeout as TimeoutPayloadType } from '../../types'
import Input from '@/app/components/base/input'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
type Props = {
interface Props {
readonly: boolean
nodeId: string
payload: TimeoutPayloadType
@ -35,7 +35,7 @@ const InputField: FC<{
type='number'
value={value}
onChange={(e) => {
const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10)))
const value = Math.max(min, Math.min(max, Number.parseInt(e.target.value, 10)))
onChange(value)
}}
placeholder={placeholder}

View File

@ -18,7 +18,6 @@ import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.http'
@ -61,10 +60,6 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
hideCurlPanel,
handleCurlImport,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
// To prevent prompt editor in body not update data.
if (!isDataReady)
return null
@ -198,9 +193,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
{(isShowCurlPanel && !readOnly) && (

View File

@ -18,7 +18,7 @@ export enum BodyType {
binary = 'binary',
}
export type KeyValue = {
export interface KeyValue {
id?: string
key: string
value: string
@ -38,7 +38,7 @@ export type BodyPayload = {
file?: ValueSelector // when type is file
value?: string // when type is text
}[]
export type Body = {
export interface Body {
type: BodyType
data: string | BodyPayload // string is deprecated, it would convert to BodyPayload after loaded
}
@ -54,7 +54,7 @@ export enum APIType {
custom = 'custom',
}
export type Authorization = {
export interface Authorization {
type: AuthorizationType
config?: {
type: APIType
@ -63,7 +63,7 @@ export type Authorization = {
} | null
}
export type Timeout = {
export interface Timeout {
connect?: number
read?: number
write?: number

View File

@ -18,7 +18,7 @@ import type {
Var,
} from '@/app/components/workflow/types'
type ConditionAddProps = {
interface ConditionAddProps {
className?: string
caseId: string
variables: NodeOutPutVar[]

View File

@ -9,7 +9,7 @@ import {
isComparisonOperatorNeedTranslate,
isEmptyRelatedOperator,
} from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../../constants'
import type { ValueSelector } from '../../../types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'

View File

@ -21,7 +21,7 @@ import {
} from '../../types'
import { comparisonOperatorNotRequireValue, getOperators } from '../../utils'
import ConditionNumberInput from '../condition-number-input'
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default'
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../../constants'
import ConditionWrap from '../condition-wrap'
import ConditionOperator from './condition-operator'
import ConditionInput from './condition-input'

View File

@ -16,7 +16,7 @@ import type { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.ifElse'
type ConditionOperatorProps = {
interface ConditionOperatorProps {
className?: string
disabled?: boolean
varType: VarType

View File

@ -19,7 +19,7 @@ import type {
} from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type ConditionListProps = {
interface ConditionListProps {
isSubVariable?: boolean
disabled?: boolean
caseId: string

View File

@ -30,7 +30,7 @@ const options = [
NumberVarType.constant,
]
type ConditionNumberInputProps = {
interface ConditionNumberInputProps {
numberVarType?: NumberVarType
onNumberVarTypeChange: (v: NumberVarType) => void
value: string

Some files were not shown because too many files have changed in this diff Show More