mirror of https://github.com/langgenius/dify.git
Merge branch 'feat/plugins' into dev/plugin-deploy
This commit is contained in:
commit
6d8b54f1e5
|
|
@ -46,7 +46,7 @@ export default function ChartView({ appId }: IChartViewProps) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
|
||||
<div className='flex flex-row items-center mt-8 mb-4 system-xl-semibold text-text-primary'>
|
||||
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
|
||||
<SimpleSelect
|
||||
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const Overview = async ({
|
|||
params: { appId },
|
||||
}: IDevelopProps) => {
|
||||
return (
|
||||
<div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
|
||||
<div className="h-full px-4 sm:px-12 py-6 overflow-scroll bg-chatbot-bg">
|
||||
<ApikeyInfoPanel />
|
||||
<TracingPanel />
|
||||
<CardView appId={appId} />
|
||||
|
|
|
|||
|
|
@ -60,18 +60,18 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||
return (
|
||||
<div className="flex items-start p-1">
|
||||
{icon && icon_background && iconType === 'app' && (
|
||||
<div className='flex-shrink-0 mr-3'>
|
||||
<div className='shrink-0 mr-3'>
|
||||
<AppIcon icon={icon} background={icon_background} />
|
||||
</div>
|
||||
)}
|
||||
{iconType !== 'app'
|
||||
&& <div className='flex-shrink-0 mr-3'>
|
||||
&& <div className='shrink-0 mr-3'>
|
||||
{ICON_MAP[iconType]}
|
||||
</div>
|
||||
|
||||
}
|
||||
{mode === 'expand' && <div className="group">
|
||||
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 break-all ${textStyle?.main ?? ''}`}>
|
||||
<div className={`flex flex-row items-center text-sm font-semibold text-text-secondary group-hover:text-text-primary break-all ${textStyle?.main ?? ''}`}>
|
||||
{name}
|
||||
{hoverTip
|
||||
&& <Tooltip
|
||||
|
|
@ -86,7 +86,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||
/>
|
||||
}
|
||||
</div>
|
||||
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
|
||||
<div className={`text-xs font-normal text-text-tertiary group-hover:text-text-secondary break-all ${textStyle?.extra ?? ''}`}>{type}</div>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-xs ${className ?? ''}`}>
|
||||
<div className={`flex flex-col w-full px-6 py-4 rounded-xl bg-components-chart-bg shadow-xs ${className ?? ''}`}>
|
||||
<div className='mb-3'>
|
||||
<Basic name={title} type={timePeriod} hoverTip={explanation} />
|
||||
</div>
|
||||
|
|
@ -242,11 +242,11 @@ const Chart: React.FC<IChartProps> = ({
|
|||
type={!CHART_TYPE_CONFIG[chartType].showTokens
|
||||
? ''
|
||||
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
|
||||
<span className='ml-1 text-gray-500'>(</span>
|
||||
<span className='ml-1 text-text-tertiary'>(</span>
|
||||
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
|
||||
<span className='text-gray-500'>)</span>
|
||||
<span className='text-text-tertiary'>)</span>
|
||||
</span></span>}
|
||||
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-gray-300' : ''}` }} />
|
||||
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
|
||||
</div>
|
||||
<ReactECharts option={options} style={{ height: 160 }} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type BadgeProps = {
|
||||
className?: string
|
||||
text?: string
|
||||
children?: React.ReactNode
|
||||
text?: ReactNode
|
||||
children?: ReactNode
|
||||
uppercase?: boolean
|
||||
hasRedCornerMark?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function Modal({
|
|||
}: IModal) {
|
||||
return (
|
||||
<Transition appear show={isShow} as={Fragment}>
|
||||
<Dialog as="div" className={classNames('relative z-50', wrapperClassName)} onClose={onClose}>
|
||||
<Dialog as="div" className={classNames('relative z-[60]', wrapperClassName)} onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ const SimpleSelect: FC<ISelectProps> = ({
|
|||
leaveTo="opacity-0"
|
||||
>
|
||||
|
||||
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
|
||||
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
|
||||
{items.map((item: Item) => (
|
||||
<Listbox.Option
|
||||
key={item.value}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const ModelIcon: FC<ModelIconProps> = ({
|
|||
|
||||
if (provider?.icon_small) {
|
||||
return (
|
||||
|
||||
<div className={`flex items-center justify-center ${isDeprecated ? 'opacity-50' : ''}`}>
|
||||
<img
|
||||
alt='model-icon'
|
||||
|
|
@ -44,8 +43,8 @@ const ModelIcon: FC<ModelIconProps> = ({
|
|||
'flex items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
|
||||
className,
|
||||
)}>
|
||||
<div className='flex w-5 h5 items-center justify-center opacity-35'>
|
||||
<Group className='text-text-tertiary' />
|
||||
<div className='flex w-5 h-5 items-center justify-center opacity-35'>
|
||||
<Group className='text-text-tertiary w-3 h-3' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const PopupItem: FC<PopupItemProps> = ({
|
|||
popupClassName='p-3 !w-[206px] bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border rounded-xl'
|
||||
popupContent={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<ModelIcon
|
||||
className={cn('shrink-0 w-5 h-5')}
|
||||
provider={model}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const Popup: FC<PopupProps> = ({
|
|||
)
|
||||
}
|
||||
</div>
|
||||
<div className='sticky bottom-0 px-4 py-2 flex items-center border-t border-divider-subtle cursor-pointer text-text-accent-light-mode-only' onClick={() => {
|
||||
<div className='sticky bottom-0 px-4 py-2 flex items-center border-t border-divider-subtle cursor-pointer text-text-accent-light-mode-only bg-components-panel-bg rounded-b-lg' onClick={() => {
|
||||
onHide()
|
||||
setShowAccountSettingModal({ payload: 'provider' })
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
|||
{t('common.modelProvider.systemModelSettings')}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-50'>
|
||||
<PortalToFollowElemContent className='z-[60]'>
|
||||
<div className='pt-4 w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'>
|
||||
<div className='px-6 py-1'>
|
||||
<div className='flex items-center h-8 text-[13px] font-medium text-text-primary'>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import type { Plugin, PluginManifestInMarket } from '../../types'
|
||||
import { PluginType } from '../../types'
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const { refreshModelProviders } = useProviderContext()
|
||||
|
||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
||||
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
|
||||
const invalidateStrategyProviders = useInvalidateStrategyProviders()
|
||||
return {
|
||||
refreshPluginList: (manifest: PluginManifestInMarket | Plugin) => {
|
||||
// installed list
|
||||
invalidateInstalledPluginList()
|
||||
|
||||
// tool page, tool select
|
||||
if (PluginType.tool.includes(manifest.category)) {
|
||||
invalidateAllToolProviders()
|
||||
invalidateAllBuiltInTools()
|
||||
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
|
||||
}
|
||||
|
||||
// model select
|
||||
if (PluginType.model.includes(manifest.category)) {
|
||||
updateModelProviders()
|
||||
refreshModelProviders()
|
||||
}
|
||||
|
||||
// agent select
|
||||
if (PluginType.agent.includes(manifest.category))
|
||||
invalidateStrategyProviders()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useRefreshPluginList
|
||||
|
|
@ -3,13 +3,11 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
|
||||
import { InstallStep, PluginType } from '../../types'
|
||||
import { InstallStep } from '../../types'
|
||||
import Install from './steps/install'
|
||||
import Installed from '../base/installed'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
|
@ -35,9 +33,7 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
|
|||
// readyToInstall -> check installed -> installed/failed
|
||||
const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (isBundle && step === InstallStep.installed)
|
||||
|
|
@ -51,12 +47,8 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
|
|||
|
||||
const handleInstalled = useCallback(() => {
|
||||
setStep(InstallStep.installed)
|
||||
invalidateInstalledPluginList()
|
||||
if (PluginType.model.includes(manifest.category))
|
||||
updateModelProviders()
|
||||
if (PluginType.tool.includes(manifest.category))
|
||||
invalidateAllToolProviders()
|
||||
}, [invalidateAllToolProviders, invalidateInstalledPluginList, manifest, updateModelProviders])
|
||||
refreshPluginList(manifest)
|
||||
}, [manifest, refreshPluginList])
|
||||
|
||||
const handleFailed = useCallback((errorMsg?: string) => {
|
||||
setStep(InstallStep.installFailed)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Button from '@/app/components/base/button'
|
|||
import Drawer from '@/app/components/base/drawer'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -24,14 +24,14 @@ const EndpointModal: FC<Props> = ({
|
|||
onCancel,
|
||||
onSaved,
|
||||
}) => {
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const [tempCredential, setTempCredential] = React.useState<any>(defaultValues)
|
||||
|
||||
const handleSave = () => {
|
||||
for (const field of formSchemas) {
|
||||
if (field.required && !tempCredential[field.name]) {
|
||||
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
|
||||
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: getValueFromI18nObject(field.label) }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiCloseLine,
|
||||
|
|
@ -16,8 +15,7 @@ import type {
|
|||
StrategyDetail,
|
||||
} from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -38,8 +36,7 @@ const StrategyDetail: FC<Props> = ({
|
|||
detail,
|
||||
onHide,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const outputSchema = useMemo(() => {
|
||||
|
|
@ -98,10 +95,10 @@ const StrategyDetail: FC<Props> = ({
|
|||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Icon size='tiny' className='w-6 h-6' src={provider.icon} />
|
||||
<div className=''>{provider.label[language]}</div>
|
||||
<div className=''>{getValueFromI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
<div className='mt-1 text-text-primary system-md-semibold'>{detail.identity.label[language]}</div>
|
||||
<Description className='mt-3' text={detail.description[language]} descriptionLineRows={2}></Description>
|
||||
<div className='mt-1 text-text-primary system-md-semibold'>{getValueFromI18nObject(detail.identity.label)}</div>
|
||||
<Description className='mt-3' text={getValueFromI18nObject(detail.description)} descriptionLineRows={2}></Description>
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className='h-full'>
|
||||
|
|
@ -113,7 +110,7 @@ const StrategyDetail: FC<Props> = ({
|
|||
{detail.parameters.map((item: any, index) => (
|
||||
<div key={index} className='py-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-text-secondary code-sm-semibold'>{item.label[language]}</div>
|
||||
<div className='text-text-secondary code-sm-semibold'>{getValueFromI18nObject(item.label)}</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>
|
||||
{getType(item.type)}
|
||||
</div>
|
||||
|
|
@ -123,7 +120,7 @@ const StrategyDetail: FC<Props> = ({
|
|||
</div>
|
||||
{item.human_description && (
|
||||
<div className='mt-0.5 text-text-tertiary system-xs-regular'>
|
||||
{item.human_description?.[language]}
|
||||
{getValueFromI18nObject(item.human_description)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import StrategyDetailPanel from './strategy-detail'
|
||||
import type {
|
||||
StrategyDetail,
|
||||
} from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -26,8 +24,7 @@ const StrategyItem = ({
|
|||
provider,
|
||||
detail,
|
||||
}: Props) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
|
||||
return (
|
||||
|
|
@ -36,8 +33,8 @@ const StrategyItem = ({
|
|||
className={cn('mb-2 px-4 py-3 bg-components-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover')}
|
||||
onClick={() => setShowDetail(true)}
|
||||
>
|
||||
<div className='pb-0.5 text-text-secondary system-md-semibold'>{detail.identity.label[language]}</div>
|
||||
<div className='text-text-tertiary system-xs-regular line-clamp-2' title={detail.description[language]}>{detail.description[language]}</div>
|
||||
<div className='pb-0.5 text-text-secondary system-md-semibold'>{getValueFromI18nObject(detail.identity.label)}</div>
|
||||
<div className='text-text-tertiary system-xs-regular line-clamp-2' title={getValueFromI18nObject(detail.description)}>{getValueFromI18nObject(detail.description)}</div>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<StrategyDetailPanel
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Toast from '@/app/components/base/toast'
|
|||
import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/service/tools'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -26,8 +26,8 @@ const ToolCredentialForm: FC<Props> = ({
|
|||
onCancel,
|
||||
onSaved,
|
||||
}) => {
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const [credentialSchema, setCredentialSchema] = useState<any>(null)
|
||||
const { name: collectionName } = collection
|
||||
const [tempCredential, setTempCredential] = React.useState<any>({})
|
||||
|
|
@ -45,7 +45,7 @@ const ToolCredentialForm: FC<Props> = ({
|
|||
const handleSave = () => {
|
||||
for (const field of credentialSchema) {
|
||||
if (field.required && !tempCredential[field.name]) {
|
||||
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
|
||||
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: getValueFromI18nObject(field.label) }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {
|
|||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
RiErrorWarningFill,
|
||||
RiInstallLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
|
@ -13,7 +15,6 @@ import Button from '@/app/components/base/button'
|
|||
import Indicator from '@/app/components/header/indicator'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -115,10 +116,19 @@ const ToolItem = ({
|
|||
</Button>
|
||||
)}
|
||||
{!isError && uninstalled && (
|
||||
<InstallPluginButton size={'small'} loading={isInstalling} onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onInstall?.()
|
||||
}} />
|
||||
<Button
|
||||
className={cn('flex items-center')}
|
||||
size='small'
|
||||
variant='secondary'
|
||||
disabled={isInstalling}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onInstall?.()
|
||||
}}
|
||||
>
|
||||
{!isInstalling ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
|
||||
{!isInstalling ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
|
||||
</Button>
|
||||
)}
|
||||
{isError && (
|
||||
<Tooltip
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ import Title from '../card/base/title'
|
|||
import Action from './action'
|
||||
import cn from '@/utils/classnames'
|
||||
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
|
||||
import { useLanguage } from '../../header/account-setting/model-provider-page/hooks'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||
import { useCategories } from '../hooks'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
|
|
@ -35,7 +35,6 @@ const PluginItem: FC<Props> = ({
|
|||
className,
|
||||
plugin,
|
||||
}) => {
|
||||
const locale = useLanguage()
|
||||
const { t } = useTranslation()
|
||||
const { categoriesMap } = useCategories()
|
||||
const currentPluginID = usePluginPageContext(v => v.currentPluginID)
|
||||
|
|
@ -66,6 +65,10 @@ const PluginItem: FC<Props> = ({
|
|||
if (PluginType.tool.includes(category))
|
||||
invalidateAllToolProviders()
|
||||
}
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const title = getValueFromI18nObject(label)
|
||||
const descriptionText = getValueFromI18nObject(description)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -92,12 +95,12 @@ const PluginItem: FC<Props> = ({
|
|||
</div>
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex items-center h-5">
|
||||
<Title title={label[locale]} />
|
||||
<Title title={title} />
|
||||
{verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
|
||||
<Badge className='shrink-0 ml-1' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} />
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Description text={description[locale]} descriptionLineRows={1}></Description>
|
||||
<Description text={descriptionText} descriptionLineRows={1}></Description>
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<Action
|
||||
pluginUniqueIdentifier={plugin_unique_identifier}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import Icon from './card/base/card-icon'
|
|||
import Title from './card/base/title'
|
||||
import DownloadCount from './card/base/download-count'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { getPluginLinkInMarketplace } from '@/app/components/plugins/marketplace/utils'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
|
|
@ -26,12 +26,12 @@ const ProviderCard: FC<Props> = ({
|
|||
className,
|
||||
payload,
|
||||
}) => {
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
const [isShowInstallFromMarketplace, {
|
||||
setTrue: showInstallFromMarketplace,
|
||||
setFalse: hideInstallFromMarketplace,
|
||||
}] = useBoolean(false)
|
||||
const language = useGetLanguage()
|
||||
const { org, label } = payload
|
||||
const { locale } = useI18N()
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ const ProviderCard: FC<Props> = ({
|
|||
<Icon src={payload.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex items-center h-5">
|
||||
<Title title={label[language] || label.en_US} />
|
||||
<Title title={getValueFromI18nObject(label)} />
|
||||
{/* <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" /> */}
|
||||
</div>
|
||||
<div className='mb-1 flex justify-between items-center h-4'>
|
||||
|
|
@ -54,7 +54,7 @@ const ProviderCard: FC<Props> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Description className='mt-3' text={payload.brief[language] || payload.brief.en_US} descriptionLineRows={2}></Description>
|
||||
<Description className='mt-3' text={getValueFromI18nObject(payload.brief)} descriptionLineRows={2}></Description>
|
||||
<div className='mt-3 flex space-x-0.5'>
|
||||
{payload.tags.map(tag => (
|
||||
<Badge key={tag.name} text={tag.name} />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ 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()
|
||||
|
|
@ -34,7 +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 agentStrategies = useStore(s => s.agentStrategies)
|
||||
const { data: agentStrategies } = useStrategyProviders()
|
||||
|
||||
const needWarningNodes = useMemo(() => {
|
||||
const list = []
|
||||
|
|
@ -61,9 +62,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
|||
|
||||
if (node.data.type === BlockEnum.Agent) {
|
||||
const data = node.data as AgentNodeType
|
||||
const provider = agentStrategies.find(s => s.plugin_unique_identifier === data.plugin_unique_identifier)
|
||||
const strategy = provider?.declaration.strategies.find(s => s.identity.name === data.agent_strategy_name)
|
||||
// debugger
|
||||
const provider = agentStrategies?.find(s => s.plugin_unique_identifier === data.plugin_unique_identifier)
|
||||
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name)
|
||||
moreDataForCheckValid = {
|
||||
provider,
|
||||
strategy,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ import I18n from '@/context/i18n'
|
|||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||
import { fetchStrategyList } from '@/service/strategy'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
|
@ -460,21 +459,6 @@ export const useFetchToolsData = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export const useFetchAgentStrategy = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const handleFetchAllAgentStrategies = useCallback(async () => {
|
||||
const agentStrategies = await fetchStrategyList()
|
||||
|
||||
workflowStore.setState({
|
||||
agentStrategies: agentStrategies || [],
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleFetchAllAgentStrategies,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowInit = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
|
|
@ -482,7 +466,6 @@ export const useWorkflowInit = () => {
|
|||
edges: edgesTemplate,
|
||||
} = useWorkflowTemplate()
|
||||
const { handleFetchAllTools } = useFetchToolsData()
|
||||
const { handleFetchAllAgentStrategies } = useFetchAgentStrategy()
|
||||
const appDetail = useAppStore(state => state.appDetail)!
|
||||
const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
|
||||
const [data, setData] = useState<FetchWorkflowDraftResponse>()
|
||||
|
|
@ -562,8 +545,7 @@ export const useWorkflowInit = () => {
|
|||
handleFetchAllTools('builtin')
|
||||
handleFetchAllTools('custom')
|
||||
handleFetchAllTools('workflow')
|
||||
handleFetchAllAgentStrategies()
|
||||
}, [handleFetchPreloadData, handleFetchAllTools, handleFetchAllAgentStrategies])
|
||||
}, [handleFetchPreloadData, handleFetchAllTools])
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import type { Strategy } from './agent-strategy'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
|
@ -16,17 +17,32 @@ import 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'
|
||||
|
||||
const NotFoundWarn = (props: {
|
||||
title: ReactNode,
|
||||
description: ReactNode
|
||||
}) => {
|
||||
const { title, description } = props
|
||||
|
||||
const ExternalNotInstallWarn = () => {
|
||||
const { t } = useTranslation()
|
||||
return <Tooltip
|
||||
popupContent={<div className='space-y-1 text-xs'>
|
||||
<h3 className='text-text-primary font-semibold'>{t('workflow.nodes.agent.pluginNotInstalled')}</h3>
|
||||
<p className='text-text-secondary tracking-tight'>{t('workflow.nodes.agent.pluginNotInstalledDesc')}</p>
|
||||
<p>
|
||||
<Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('workflow.nodes.agent.linkToPlugin')}</Link>
|
||||
</p>
|
||||
</div>}
|
||||
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>
|
||||
|
|
@ -81,15 +97,34 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||
if (!list) return []
|
||||
return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase()))
|
||||
}, [query, list])
|
||||
// TODO: should be replaced by real data
|
||||
const isExternalInstalled = true
|
||||
const { strategyStatus } = useStrategyInfo(
|
||||
value?.agent_strategy_provider_name,
|
||||
value?.agent_strategy_name,
|
||||
)
|
||||
|
||||
const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external'
|
||||
&& !strategyStatus.plugin.installed
|
||||
|
||||
const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external'
|
||||
&& !strategyStatus?.isExistInPlugin
|
||||
|
||||
const showSwitchVersion = !strategyStatus?.isExistInPlugin
|
||||
&& strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed
|
||||
|
||||
const showInstallButton = !strategyStatus?.isExistInPlugin
|
||||
&& strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed
|
||||
|
||||
const icon = list?.find(
|
||||
coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name),
|
||||
)?.icon as string | undefined
|
||||
const { t } = useTranslation()
|
||||
|
||||
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)}>
|
||||
<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}
|
||||
|
|
@ -104,8 +139,30 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||
{value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}
|
||||
</p>
|
||||
{value && <div className='ml-auto flex items-center gap-1'>
|
||||
<InstallPluginButton onClick={e => e.stopPropagation()} size={'small'} />
|
||||
{isExternalInstalled ? <ExternalNotInstallWarn /> : <RiArrowDownSLine className='size-4 text-text-tertiary' />}
|
||||
{showInstallButton && <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={'langgenius/openai:12'}
|
||||
tooltip={t('workflow.nodes.agent.switchToNewVersion')}
|
||||
onChange={() => {
|
||||
// TODO: refresh all strategies
|
||||
}}
|
||||
/>}
|
||||
</div>}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
@ -143,9 +200,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/* <div>
|
||||
aaa
|
||||
</div> */}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,16 +1,43 @@
|
|||
import Button from '@/app/components/base/button'
|
||||
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
|
||||
import type { ComponentProps } from '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'>
|
||||
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
|
||||
uniqueIdentifier: string
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
|
||||
const { loading, className, ...rest } = props
|
||||
const { className, uniqueIdentifier, onSuccess, ...rest } = props
|
||||
const { t } = useTranslation()
|
||||
return <Button variant={'secondary'} disabled={loading} className={classNames('flex items-center', className)} {...rest}>
|
||||
{loading ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
|
||||
{!loading ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
|
||||
const manifest = useCheckInstalled({
|
||||
pluginIds: [uniqueIdentifier],
|
||||
enabled: !!uniqueIdentifier,
|
||||
})
|
||||
const install = useInstallPackageFromMarketPlace({
|
||||
onSuccess() {
|
||||
manifest.refetch()
|
||||
onSuccess?.()
|
||||
},
|
||||
})
|
||||
const handleInstall: MouseEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
install.mutate(uniqueIdentifier)
|
||||
}
|
||||
const isLoading = manifest.isLoading || install.isPending
|
||||
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-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
|
||||
</Button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
'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 } from '@remixicon/react'
|
||||
import { type FC, useCallback, useState } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCheckInstalled } from '@/service/use-plugins'
|
||||
|
||||
export type SwitchPluginVersionProps = {
|
||||
uniqueIdentifier: string
|
||||
tooltip?: string
|
||||
onChange?: (version: string) => void
|
||||
}
|
||||
|
||||
export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
|
||||
const { uniqueIdentifier, tooltip, onChange } = props
|
||||
const [pluginId] = uniqueIdentifier.split(':')
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false)
|
||||
const [targetVersion, setTargetVersion] = useState<string>()
|
||||
const pluginDetails = useCheckInstalled({
|
||||
pluginIds: [pluginId],
|
||||
enabled: true,
|
||||
})
|
||||
const pluginDetail = pluginDetails.data?.plugins.at(0)
|
||||
|
||||
const handleUpdatedFromMarketplace = useCallback(() => {
|
||||
hideUpdateModal()
|
||||
pluginDetails.refetch()
|
||||
onChange?.(targetVersion!)
|
||||
}, [hideUpdateModal, onChange, pluginDetails, targetVersion])
|
||||
return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'>
|
||||
<div className='w-fit'>
|
||||
{isShowUpdateModal && pluginDetail && <UpdateFromMarketplace
|
||||
payload={{
|
||||
originalPackageInfo: {
|
||||
id: uniqueIdentifier,
|
||||
payload: pluginDetail.declaration,
|
||||
},
|
||||
targetPackageInfo: {
|
||||
id: uniqueIdentifier,
|
||||
version: targetVersion!,
|
||||
},
|
||||
}}
|
||||
onCancel={hideUpdateModal}
|
||||
onSave={handleUpdatedFromMarketplace}
|
||||
/>}
|
||||
{pluginDetail && <PluginVersionPicker
|
||||
isShow={isShow}
|
||||
onShowChange={setIsShow}
|
||||
pluginID={pluginId}
|
||||
currentVersion={pluginDetail.version}
|
||||
onSelect={(state) => {
|
||||
setTargetVersion(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>
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
|
|||
: ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
},
|
||||
checkValid(payload, t, moreDataForCheckValid: {
|
||||
strategyProvider: StrategyPluginDetail | undefined,
|
||||
strategy: StrategyDetail | undefined
|
||||
strategyProvider?: StrategyPluginDetail,
|
||||
strategy?: StrategyDetail
|
||||
language: string
|
||||
}) {
|
||||
const { strategy, language } = moreDataForCheckValid
|
||||
|
|
|
|||
|
|
@ -89,17 +89,15 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
|
|||
{inputs.agent_strategy_name
|
||||
? <SettingItem
|
||||
label={t('workflow.nodes.agent.strategy.shortLabel')}
|
||||
status={
|
||||
['plugin-not-found', 'strategy-not-found'].includes(currentStrategyStatus)
|
||||
? 'error'
|
||||
: undefined
|
||||
status={!currentStrategyStatus?.isExistInPlugin ? 'error' : undefined}
|
||||
tooltip={
|
||||
!currentStrategyStatus?.isExistInPlugin ? t('workflow.nodes.agent.strategyNotInstallTooltip', {
|
||||
plugin: pluginDetail?.declaration.label
|
||||
? renderI18nObject(pluginDetail?.declaration.label)
|
||||
: undefined,
|
||||
strategy: inputs.agent_strategy_label,
|
||||
}) : undefined
|
||||
}
|
||||
tooltip={t(`workflow.nodes.agent.${currentStrategyStatus === 'plugin-not-found' ? 'strategyNotInstallTooltip' : 'strategyNotFoundInPlugin'}`, {
|
||||
strategy: inputs.agent_strategy_label,
|
||||
plugin: pluginDetail?.declaration.label
|
||||
? renderI18nObject(pluginDetail?.declaration.label)
|
||||
: undefined,
|
||||
})}
|
||||
>
|
||||
{inputs.agent_strategy_label}
|
||||
</SettingItem>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,54 @@ import {
|
|||
} from '@/app/components/workflow/hooks'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { type ToolVarInputs, VarType } from '../tool/types'
|
||||
import { useCheckInstalled } from '@/service/use-plugins'
|
||||
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])
|
||||
return {
|
||||
strategyProvider,
|
||||
strategy,
|
||||
strategyStatus,
|
||||
}
|
||||
}
|
||||
|
||||
const useConfig = (id: string, payload: AgentNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<AgentNodeType>(id, payload)
|
||||
|
|
@ -21,21 +64,17 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
|||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
const strategyProvider = useStrategyProviderDetail(
|
||||
inputs.agent_strategy_provider_name || '',
|
||||
const {
|
||||
strategyStatus: currentStrategyStatus,
|
||||
strategy: currentStrategy,
|
||||
strategyProvider,
|
||||
} = useStrategyInfo(
|
||||
inputs.agent_strategy_provider_name,
|
||||
inputs.agent_strategy_name,
|
||||
)
|
||||
const currentStrategy = strategyProvider.data?.declaration.strategies.find(
|
||||
str => str.identity.name === inputs.agent_strategy_name,
|
||||
)
|
||||
const currentStrategyStatus: 'loading' | 'plugin-not-found' | 'strategy-not-found' | 'success' = useMemo(() => {
|
||||
if (strategyProvider.isLoading) return 'loading'
|
||||
if (strategyProvider.isError) return 'plugin-not-found'
|
||||
if (!currentStrategy) return 'strategy-not-found'
|
||||
return 'success'
|
||||
}, [currentStrategy, strategyProvider])
|
||||
const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/')
|
||||
const pluginDetail = useCheckInstalled({
|
||||
pluginIds: [pluginId || ''],
|
||||
pluginIds: [pluginId!],
|
||||
enabled: Boolean(pluginId),
|
||||
})
|
||||
const formData = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ const NodePanel: FC<Props> = ({
|
|||
setCollapseState(!nodeInfo.expand)
|
||||
}, [nodeInfo.expand, setCollapseState])
|
||||
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && nodeInfo.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail?.length
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && nodeInfo.agentLog?.length
|
||||
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && nodeInfo.agentLog?.length
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
|
||||
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
|
||||
|
||||
return (
|
||||
<div className={cn('px-2 py-1', className)}>
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
|||
handleShowAgentOrToolLog,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && nodeInfo?.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && nodeInfo?.retryDetail?.length
|
||||
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && nodeInfo?.agentLog?.length
|
||||
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && nodeInfo?.agentLog?.length
|
||||
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
|
||||
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
|
||||
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg py-2'>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
import { parseDSL } from './graph-to-log-struct-2'
|
||||
|
||||
describe('parseDSL', () => {
|
||||
it('should parse plain nodes correctly', () => {
|
||||
const dsl = 'plainNode1 -> plainNode2'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: {}, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse retry nodes correctly', () => {
|
||||
const dsl = '(retry, retryNode, 3)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse iteration nodes correctly', () => {
|
||||
const dsl = '(iteration, iterationNode, plainNode1 -> plainNode2)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse parallel nodes correctly', () => {
|
||||
const dsl = '(parallel, parallelNode, nodeA, nodeB -> nodeC)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
|
||||
{ id: 'nodeA', node_id: 'nodeA', title: 'nodeA', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeA' }, status: 'succeeded' },
|
||||
{ id: 'nodeB', node_id: 'nodeB', title: 'nodeB', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
|
||||
{ id: 'nodeC', node_id: 'nodeC', title: 'nodeC', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
// TODO
|
||||
it('should handle nested parallel nodes', () => {
|
||||
const dsl = '(parallel, outerParallel, (parallel, innerParallel, plainNode1 -> plainNode2) -> plainNode3)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'outerParallel',
|
||||
node_id: 'outerParallel',
|
||||
title: 'outerParallel',
|
||||
execution_metadata: { parallel_id: 'outerParallel' },
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'innerParallel',
|
||||
node_id: 'innerParallel',
|
||||
title: 'innerParallel',
|
||||
execution_metadata: { parallel_id: 'outerParallel', parallel_start_node_id: 'innerParallel' },
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'plainNode1',
|
||||
node_id: 'plainNode1',
|
||||
title: 'plainNode1',
|
||||
execution_metadata: {
|
||||
parallel_id: 'innerParallel',
|
||||
parallel_start_node_id: 'plainNode1',
|
||||
parent_parallel_id: 'outerParallel',
|
||||
parent_parallel_start_node_id: 'innerParallel',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'plainNode2',
|
||||
node_id: 'plainNode2',
|
||||
title: 'plainNode2',
|
||||
execution_metadata: {
|
||||
parallel_id: 'innerParallel',
|
||||
parallel_start_node_id: 'plainNode1',
|
||||
parent_parallel_id: 'outerParallel',
|
||||
parent_parallel_start_node_id: 'innerParallel',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'plainNode3',
|
||||
node_id: 'plainNode3',
|
||||
title: 'plainNode3',
|
||||
execution_metadata: {
|
||||
parallel_id: 'outerParallel',
|
||||
parallel_start_node_id: 'plainNode3',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// iterations not support nested iterations
|
||||
// it('should handle nested iterations', () => {
|
||||
// const dsl = '(iteration, outerIteration, (iteration, innerIteration -> plainNode1 -> plainNode2))'
|
||||
// const result = parseDSL(dsl)
|
||||
// expect(result).toEqual([
|
||||
// { id: 'outerIteration', node_id: 'outerIteration', title: 'outerIteration', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
|
||||
// { id: 'innerIteration', node_id: 'innerIteration', title: 'innerIteration', node_type: 'iteration', execution_metadata: { iteration_id: 'outerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// ])
|
||||
// })
|
||||
|
||||
it('should handle nested iterations within parallel nodes', () => {
|
||||
const dsl = '(parallel, parallelNode, (iteration, iterationNode, plainNode1, plainNode2))'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
|
||||
{ id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should throw an error for unknown node types', () => {
|
||||
const dsl = '(unknown, nodeId)'
|
||||
expect(() => parseDSL(dsl)).toThrowError('Unknown nodeType: unknown')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
type IterationInfo = { iterationId: string; iterationIndex: number }
|
||||
type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial<IterationInfo>
|
||||
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial<IterationInfo>) | Node[] | number)[] } & Partial<IterationInfo>
|
||||
type Node = NodePlain | NodeComplex
|
||||
|
||||
/**
|
||||
* Parses a DSL string into an array of node objects.
|
||||
* @param dsl - The input DSL string.
|
||||
* @returns An array of parsed nodes.
|
||||
*/
|
||||
function parseDSL(dsl: string): NodeData[] {
|
||||
return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a top-level flow string by "->", respecting nested structures.
|
||||
* @param dsl - The DSL string to split.
|
||||
* @returns An array of top-level segments.
|
||||
*/
|
||||
function parseTopLevelFlow(dsl: string): string[] {
|
||||
const segments: string[] = []
|
||||
let buffer = ''
|
||||
let nested = 0
|
||||
|
||||
for (let i = 0; i < dsl.length; i++) {
|
||||
const char = dsl[i]
|
||||
if (char === '(') nested++
|
||||
if (char === ')') nested--
|
||||
if (char === '-' && dsl[i + 1] === '>' && nested === 0) {
|
||||
segments.push(buffer.trim())
|
||||
buffer = ''
|
||||
i++ // Skip the ">" character
|
||||
}
|
||||
else {
|
||||
buffer += char
|
||||
}
|
||||
}
|
||||
if (buffer.trim())
|
||||
segments.push(buffer.trim())
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single node string.
|
||||
* If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
|
||||
* @param nodeStr - The node string to parse.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @returns A parsed node object.
|
||||
*/
|
||||
function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
// Check if the node is a complex node
|
||||
if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
|
||||
const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
|
||||
let nested = 0
|
||||
let buffer = ''
|
||||
const parts: string[] = []
|
||||
|
||||
// Split the inner content by commas, respecting nested parentheses
|
||||
for (let i = 0; i < innerContent.length; i++) {
|
||||
const char = innerContent[i]
|
||||
if (char === '(') nested++
|
||||
if (char === ')') nested--
|
||||
|
||||
if (char === ',' && nested === 0) {
|
||||
parts.push(buffer.trim())
|
||||
buffer = ''
|
||||
}
|
||||
else {
|
||||
buffer += char
|
||||
}
|
||||
}
|
||||
parts.push(buffer.trim())
|
||||
|
||||
// Extract nodeType, nodeId, and params
|
||||
const [nodeType, nodeId, ...paramsRaw] = parts
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId)
|
||||
const complexNode = {
|
||||
nodeType: nodeType.trim(),
|
||||
nodeId: nodeId.trim(),
|
||||
params,
|
||||
}
|
||||
if (parentIterationId) {
|
||||
(complexNode as any).iterationId = parentIterationId;
|
||||
(complexNode as any).iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
return complexNode
|
||||
}
|
||||
|
||||
// If it's not a complex node, treat it as a plain node
|
||||
const plainNode: NodePlain = { nodeType: 'plain', nodeId: nodeStr.trim() }
|
||||
if (parentIterationId) {
|
||||
plainNode.iterationId = parentIterationId
|
||||
plainNode.iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
return plainNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses parameters of a complex node.
|
||||
* Supports nested flows and complex sub-nodes.
|
||||
* Adds iteration-specific metadata recursively.
|
||||
* @param paramParts - The parameters string split by commas.
|
||||
* @param iterationId - The ID of the iteration node, if applicable.
|
||||
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
|
||||
*/
|
||||
function parseParams(paramParts: string[], iterationId?: string): (Node | Node[] | number)[] {
|
||||
return paramParts.map((part) => {
|
||||
if (part.includes('->')) {
|
||||
// Parse as a flow and return an array of nodes
|
||||
return parseTopLevelFlow(part).map(node => parseNode(node, iterationId))
|
||||
}
|
||||
else if (part.startsWith('(')) {
|
||||
// Parse as a nested complex node
|
||||
return parseNode(part, iterationId)
|
||||
}
|
||||
else if (!Number.isNaN(Number(part.trim()))) {
|
||||
// Parse as a numeric parameter
|
||||
return Number(part.trim())
|
||||
}
|
||||
else {
|
||||
// Parse as a plain node
|
||||
return parseNode(part, iterationId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type NodeData = {
|
||||
id: string;
|
||||
node_id: string;
|
||||
title: string;
|
||||
node_type?: string;
|
||||
execution_metadata: Record<string, any>;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain node to node data.
|
||||
*/
|
||||
function convertPlainNode(node: Node): NodeData[] {
|
||||
return [
|
||||
{
|
||||
id: node.nodeId,
|
||||
node_id: node.nodeId,
|
||||
title: node.nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a retry node to node data.
|
||||
*/
|
||||
function convertRetryNode(node: Node): NodeData[] {
|
||||
const { nodeId, iterationId, iterationIndex, params } = node as NodeComplex
|
||||
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
|
||||
for (let i = 0; i < retryCount; i++) {
|
||||
result.push({
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: iterationId ? {
|
||||
iteration_id: iterationId,
|
||||
iteration_index: iterationIndex || 0,
|
||||
} : {},
|
||||
status: 'retry',
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an iteration node to node data.
|
||||
*/
|
||||
function convertIterationNode(node: Node): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
node_type: 'iteration',
|
||||
status: 'succeeded',
|
||||
execution_metadata: {},
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param: any) => {
|
||||
if (Array.isArray(param)) {
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
iteration_id: nodeId,
|
||||
iteration_index: 0,
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parallel node to node data.
|
||||
*/
|
||||
function convertParallelNode(node: Node, parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: {
|
||||
parallel_id: nodeId,
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param) => {
|
||||
if (Array.isArray(param)) {
|
||||
const startNodeId = param[0]?.nodeId
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
parallel_id: nodeId,
|
||||
parallel_start_node_id: startNodeId,
|
||||
...(parentParallelId && {
|
||||
parent_parallel_id: parentParallelId,
|
||||
parent_parallel_start_node_id: parentStartNodeId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
else if (param && typeof param === 'object') {
|
||||
const startNodeId = param.nodeId
|
||||
const childData = convertToNodeData([param])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
parallel_id: nodeId,
|
||||
parallel_start_node_id: startNodeId,
|
||||
...(parentParallelId && {
|
||||
parent_parallel_id: parentParallelId,
|
||||
parent_parallel_start_node_id: parentStartNodeId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to convert nodes to node data.
|
||||
*/
|
||||
function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
|
||||
const result: NodeData[] = []
|
||||
|
||||
nodes.forEach((node) => {
|
||||
switch (node.nodeType) {
|
||||
case 'plain':
|
||||
result.push(...convertPlainNode(node))
|
||||
break
|
||||
case 'retry':
|
||||
result.push(...convertRetryNode(node))
|
||||
break
|
||||
case 'iteration':
|
||||
result.push(...convertIterationNode(node))
|
||||
break
|
||||
case 'parallel':
|
||||
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown nodeType: ${node.nodeType}`)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export { parseDSL }
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import graphToLogStruct, { parseNodeString } from './graph-to-log-struct'
|
||||
|
||||
describe('graphToLogStruct', () => {
|
||||
test('parseNodeString', () => {
|
||||
expect(parseNodeString('(node1, param1, (node2, param2, (node3, param1)), param4)')).toEqual({
|
||||
node: 'node1',
|
||||
params: [
|
||||
'param1',
|
||||
{
|
||||
node: 'node2',
|
||||
params: [
|
||||
'param2',
|
||||
{
|
||||
node: 'node3',
|
||||
params: [
|
||||
'param1',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'param4',
|
||||
],
|
||||
})
|
||||
})
|
||||
test('iteration nodes', () => {
|
||||
expect(graphToLogStruct('start -> (iteration, 1, [2, 3])')).toEqual([
|
||||
{
|
||||
id: 'start',
|
||||
node_id: 'start',
|
||||
title: 'start',
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
node_id: '1',
|
||||
title: '1',
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
node_type: 'iteration',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
node_id: '2',
|
||||
title: '2',
|
||||
execution_metadata: { iteration_id: '1', iteration_index: 0 },
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
node_id: '3',
|
||||
title: '3',
|
||||
execution_metadata: { iteration_id: '1', iteration_index: 1 },
|
||||
status: 'succeeded',
|
||||
},
|
||||
])
|
||||
})
|
||||
test('retry nodes', () => {
|
||||
expect(graphToLogStruct('start -> (retry, 1, 3)')).toEqual([
|
||||
{
|
||||
id: 'start',
|
||||
node_id: 'start',
|
||||
title: 'start',
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
node_id: '1',
|
||||
title: '1',
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
node_id: '1',
|
||||
title: '1',
|
||||
execution_metadata: {},
|
||||
status: 'retry',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
node_id: '1',
|
||||
title: '1',
|
||||
execution_metadata: {},
|
||||
status: 'retry',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
node_id: '1',
|
||||
title: '1',
|
||||
execution_metadata: {},
|
||||
status: 'retry',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
const STEP_SPLIT = '->'
|
||||
|
||||
const toNodeData = (step: string, info: Record<string, any> = {}): any => {
|
||||
const [nodeId, title] = step.split('@')
|
||||
|
||||
const data: Record<string, any> = {
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: title || nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
}
|
||||
|
||||
const executionMetadata = data.execution_metadata
|
||||
const { isRetry, isIteration, inIterationInfo } = info
|
||||
if (isRetry)
|
||||
data.status = 'retry'
|
||||
|
||||
if (isIteration)
|
||||
data.node_type = 'iteration'
|
||||
|
||||
if (inIterationInfo) {
|
||||
executionMetadata.iteration_id = inIterationInfo.iterationId
|
||||
executionMetadata.iteration_index = inIterationInfo.iterationIndex
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const toRetryNodeData = ({
|
||||
nodeId,
|
||||
repeatTimes,
|
||||
}: {
|
||||
nodeId: string,
|
||||
repeatTimes: number,
|
||||
}): any => {
|
||||
const res = [toNodeData(nodeId)]
|
||||
for (let i = 0; i < repeatTimes; i++)
|
||||
res.push(toNodeData(nodeId, { isRetry: true }))
|
||||
return res
|
||||
}
|
||||
|
||||
const toIterationNodeData = ({
|
||||
nodeId,
|
||||
children,
|
||||
}: {
|
||||
nodeId: string,
|
||||
children: number[],
|
||||
}) => {
|
||||
const res = [toNodeData(nodeId, { isIteration: true })]
|
||||
// TODO: handle inner node structure
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const step = `${children[i]}`
|
||||
res.push(toNodeData(step, { inIterationInfo: { iterationId: nodeId, iterationIndex: i } }))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type NodeStructure = {
|
||||
node: string;
|
||||
params: Array<string | NodeStructure>;
|
||||
}
|
||||
|
||||
export function parseNodeString(input: string): NodeStructure {
|
||||
input = input.trim()
|
||||
if (input.startsWith('(') && input.endsWith(')'))
|
||||
input = input.slice(1, -1)
|
||||
|
||||
const parts: Array<string | NodeStructure> = []
|
||||
let current = ''
|
||||
let depth = 0
|
||||
let inArrayDepth = 0
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const char = input[i]
|
||||
|
||||
if (char === '(')
|
||||
depth++
|
||||
else if (char === ')')
|
||||
depth--
|
||||
|
||||
if (char === '[')
|
||||
inArrayDepth++
|
||||
else if (char === ']')
|
||||
inArrayDepth--
|
||||
|
||||
const isInArray = inArrayDepth > 0
|
||||
|
||||
if (char === ',' && depth === 0 && !isInArray) {
|
||||
parts.push(current.trim())
|
||||
current = ''
|
||||
}
|
||||
else {
|
||||
current += char
|
||||
}
|
||||
}
|
||||
|
||||
if (current)
|
||||
parts.push(current.trim())
|
||||
|
||||
const result: NodeStructure = {
|
||||
node: '',
|
||||
params: [],
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
|
||||
if (typeof part === 'string') {
|
||||
if (part.startsWith('('))
|
||||
result.params.push(parseNodeString(part))
|
||||
|
||||
if (part.startsWith('[')) {
|
||||
const content = part.slice(1, -1)
|
||||
result.params.push(parseNodeString(content))
|
||||
}
|
||||
}
|
||||
else if (i === 0) {
|
||||
result.node = part as unknown as string
|
||||
}
|
||||
else {
|
||||
result.params.push(part as unknown as string)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const toNodes = (input: string): any[] => {
|
||||
const list = input.split(STEP_SPLIT)
|
||||
.map(step => step.trim())
|
||||
|
||||
const res: any[] = []
|
||||
list.forEach((step) => {
|
||||
const isPlainStep = !step.includes('(')
|
||||
if (isPlainStep) {
|
||||
res.push(toNodeData(step))
|
||||
return
|
||||
}
|
||||
|
||||
const { node, params } = parseNodeString(step)
|
||||
switch (node) {
|
||||
case 'iteration':
|
||||
console.log(params)
|
||||
break
|
||||
res.push(...toIterationNodeData({
|
||||
nodeId: params[0] as string,
|
||||
children: JSON.parse(params[1] as string) as number[],
|
||||
}))
|
||||
break
|
||||
case 'retry':
|
||||
res.push(...toRetryNodeData({
|
||||
nodeId: params[0] as string,
|
||||
repeatTimes: Number.parseInt(params[1] as string),
|
||||
}))
|
||||
break
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/*
|
||||
* : 1 -> 2 -> 3
|
||||
* iteration: (iteration, 1, [2, 3]) -> 4. (1, [2, 3]) means 1 is parent, [2, 3] is children
|
||||
* parallel: 1 -> (parallel, [1,2,3], [4, (parallel: (6,7))]).
|
||||
* retry: (retry, 1, 3). 1 is parent, 3 is retry times
|
||||
*/
|
||||
const graphToLogStruct = (input: string): any[] => {
|
||||
const list = toNodes(input)
|
||||
return list
|
||||
}
|
||||
|
||||
export default graphToLogStruct
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { BlockEnum } from '../../../types'
|
||||
|
||||
type IterationNodeId = string
|
||||
type RunIndex = string
|
||||
type IterationGroupMap = Map<IterationNodeId, Map<RunIndex, NodeTracing[]>>
|
||||
|
||||
const processIterationNode = (item: NodeTracing) => {
|
||||
return {
|
||||
...item,
|
||||
details: [], // to add the sub nodes in the iteration
|
||||
}
|
||||
}
|
||||
|
||||
const updateParallelModeGroup = (nodeGroupMap: IterationGroupMap, runIndex: string, item: NodeTracing, iterationNode: NodeTracing) => {
|
||||
if (!nodeGroupMap.has(iterationNode.node_id))
|
||||
nodeGroupMap.set(iterationNode.node_id, new Map())
|
||||
|
||||
const groupMap = nodeGroupMap.get(iterationNode.node_id)!
|
||||
|
||||
if (!groupMap.has(runIndex))
|
||||
groupMap.set(runIndex, [item])
|
||||
|
||||
else
|
||||
groupMap.get(runIndex)!.push(item)
|
||||
|
||||
if (item.status === 'failed') {
|
||||
iterationNode.status = 'failed'
|
||||
iterationNode.error = item.error
|
||||
}
|
||||
|
||||
iterationNode.details = Array.from(groupMap.values())
|
||||
}
|
||||
|
||||
const updateSequentialModeGroup = (runIndex: number, item: NodeTracing, iterationNode: NodeTracing) => {
|
||||
const { details } = iterationNode
|
||||
if (details) {
|
||||
if (!details[runIndex])
|
||||
details[runIndex] = [item]
|
||||
else
|
||||
details[runIndex].push(item)
|
||||
}
|
||||
|
||||
if (item.status === 'failed') {
|
||||
iterationNode.status = 'failed'
|
||||
iterationNode.error = item.error
|
||||
}
|
||||
}
|
||||
|
||||
const addRetryDetail = (result: NodeTracing[], item: NodeTracing) => {
|
||||
const retryNode = result.find(node => node.node_id === item.node_id)
|
||||
|
||||
if (retryNode) {
|
||||
if (retryNode?.retryDetail)
|
||||
retryNode.retryDetail.push(item)
|
||||
else
|
||||
retryNode.retryDetail = [item]
|
||||
}
|
||||
}
|
||||
|
||||
const processNonIterationNode = (result: NodeTracing[], nodeGroupMap: IterationGroupMap, item: NodeTracing) => {
|
||||
const { execution_metadata } = item
|
||||
if (!execution_metadata?.iteration_id) {
|
||||
if (item.status === 'retry') {
|
||||
addRetryDetail(result, item)
|
||||
return
|
||||
}
|
||||
result.push(item)
|
||||
return
|
||||
}
|
||||
|
||||
const parentIterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
|
||||
const isInIteration = !!parentIterationNode && Array.isArray(parentIterationNode.details)
|
||||
if (!isInIteration)
|
||||
return
|
||||
|
||||
// the parallel in the iteration in mode.
|
||||
const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
|
||||
const isInParallel = !!parallel_mode_run_id
|
||||
|
||||
if (isInParallel)
|
||||
updateParallelModeGroup(nodeGroupMap, parallel_mode_run_id, item, parentIterationNode)
|
||||
else
|
||||
updateSequentialModeGroup(iteration_index, item, parentIterationNode)
|
||||
}
|
||||
|
||||
// list => tree. Put the iteration node's children into the details field.
|
||||
const formatToTracingNodeList = (list: NodeTracing[]) => {
|
||||
const allItems = [...list].reverse()
|
||||
const result: NodeTracing[] = []
|
||||
const iterationGroupMap = new Map<string, Map<string, NodeTracing[]>>()
|
||||
|
||||
allItems.forEach((item) => {
|
||||
item.node_type === BlockEnum.Iteration
|
||||
? result.push(processIterationNode(item))
|
||||
: processNonIterationNode(result, iterationGroupMap, item)
|
||||
})
|
||||
|
||||
// console.log(allItems)
|
||||
// console.log(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default formatToTracingNodeList
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
export const simpleIterationData = (() => {
|
||||
// start -> code(output: [1, 2, 3]) -> iteration(output: ['aaa', 'aaa', 'aaa']) -> end(output: ['aaa', 'aaa', 'aaa'])
|
||||
const startNode = {
|
||||
id: '36c9860a-39e6-4107-b750-655b07895f47',
|
||||
index: 1,
|
||||
predecessor_node_id: null,
|
||||
node_id: '1735023354069',
|
||||
node_type: 'start',
|
||||
title: 'Start',
|
||||
inputs: {
|
||||
'sys.files': [],
|
||||
'sys.user_id': '5ee03762-1d1a-46e8-ba0b-5f419a77da96',
|
||||
'sys.app_id': '8a5e87f8-6433-40f4-a67a-4be78a558dc7',
|
||||
'sys.workflow_id': 'bb5e2b89-40ac-45c9-9ccb-4f2cd926e080',
|
||||
'sys.workflow_run_id': '76adf675-a7d3-4cc1-9282-ed7ecfe4f65d',
|
||||
},
|
||||
process_data: null,
|
||||
outputs: {
|
||||
'sys.files': [],
|
||||
'sys.user_id': '5ee03762-1d1a-46e8-ba0b-5f419a77da96',
|
||||
'sys.app_id': '8a5e87f8-6433-40f4-a67a-4be78a558dc7',
|
||||
'sys.workflow_id': 'bb5e2b89-40ac-45c9-9ccb-4f2cd926e080',
|
||||
'sys.workflow_run_id': '76adf675-a7d3-4cc1-9282-ed7ecfe4f65d',
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.011458,
|
||||
execution_metadata: null,
|
||||
extras: {},
|
||||
created_by_end_user: null,
|
||||
finished_at: 1735023510,
|
||||
}
|
||||
|
||||
const outputArrayNode = {
|
||||
id: 'a3105c5d-ff9e-44ea-9f4c-ab428958af20',
|
||||
index: 2,
|
||||
predecessor_node_id: '1735023354069',
|
||||
node_id: '1735023361224',
|
||||
node_type: 'code',
|
||||
title: 'Code',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: {
|
||||
result: [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.103333,
|
||||
execution_metadata: null,
|
||||
extras: {},
|
||||
finished_at: 1735023511,
|
||||
}
|
||||
|
||||
const iterationNode = {
|
||||
id: 'a823134d-9f1a-45a4-8977-db838d076316',
|
||||
index: 3,
|
||||
predecessor_node_id: '1735023361224',
|
||||
node_id: '1735023391914',
|
||||
node_type: 'iteration',
|
||||
title: 'Iteration',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: {
|
||||
output: [
|
||||
'aaa',
|
||||
'aaa',
|
||||
'aaa',
|
||||
],
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const iterations = [
|
||||
{
|
||||
id: 'a84a22d8-0f08-4006-bee2-fa7a7aef0420',
|
||||
index: 4,
|
||||
predecessor_node_id: '1735023391914start',
|
||||
node_id: '1735023409906',
|
||||
node_type: 'code',
|
||||
title: 'Code 2',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: {
|
||||
result: 'aaa',
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.112688,
|
||||
execution_metadata: {
|
||||
iteration_id: '1735023391914',
|
||||
iteration_index: 0,
|
||||
},
|
||||
extras: {},
|
||||
created_at: 1735023511,
|
||||
finished_at: 1735023511,
|
||||
},
|
||||
{
|
||||
id: 'ff71d773-a916-4513-960f-d7dcc4fadd86',
|
||||
index: 5,
|
||||
predecessor_node_id: '1735023391914start',
|
||||
node_id: '1735023409906',
|
||||
node_type: 'code',
|
||||
title: 'Code 2',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: {
|
||||
result: 'aaa',
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.126034,
|
||||
execution_metadata: {
|
||||
iteration_id: '1735023391914',
|
||||
iteration_index: 1,
|
||||
},
|
||||
extras: {},
|
||||
created_at: 1735023511,
|
||||
finished_at: 1735023511,
|
||||
},
|
||||
{
|
||||
id: 'd91c3ef9-0162-4013-9272-d4cc7fb1f188',
|
||||
index: 6,
|
||||
predecessor_node_id: '1735023391914start',
|
||||
node_id: '1735023409906',
|
||||
node_type: 'code',
|
||||
title: 'Code 2',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: {
|
||||
result: 'aaa',
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.122716,
|
||||
execution_metadata: {
|
||||
iteration_id: '1735023391914',
|
||||
iteration_index: 2,
|
||||
},
|
||||
extras: {},
|
||||
created_at: 1735023511,
|
||||
finished_at: 1735023511,
|
||||
},
|
||||
]
|
||||
|
||||
const endNode = {
|
||||
id: 'e6ad6560-1aa3-43f3-89e3-e5287c9ea272',
|
||||
index: 7,
|
||||
predecessor_node_id: '1735023391914',
|
||||
node_id: '1735023417757',
|
||||
node_type: 'end',
|
||||
title: 'End',
|
||||
inputs: {
|
||||
output: [
|
||||
'aaa',
|
||||
'aaa',
|
||||
'aaa',
|
||||
],
|
||||
},
|
||||
process_data: null,
|
||||
outputs: {
|
||||
output: [
|
||||
'aaa',
|
||||
'aaa',
|
||||
'aaa',
|
||||
],
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.017552,
|
||||
execution_metadata: null,
|
||||
extras: {},
|
||||
finished_at: 1735023511,
|
||||
}
|
||||
|
||||
return {
|
||||
in: [startNode, outputArrayNode, iterationNode, ...iterations, endNode],
|
||||
expect: [startNode, outputArrayNode, {
|
||||
...iterationNode,
|
||||
details: [
|
||||
[iterations[0]],
|
||||
[iterations[1]],
|
||||
[iterations[2]],
|
||||
],
|
||||
}, endNode],
|
||||
}
|
||||
})()
|
||||
|
|
@ -1,11 +1,23 @@
|
|||
import format from '.'
|
||||
import { simpleIterationData } from './data'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('iteration', () => {
|
||||
const list = graphToLogStruct('start -> (iteration, 1, [2, 3])')
|
||||
const [startNode, iterationNode, ...iterations] = graphToLogStruct('start -> (iteration, 1, [2, 3])')
|
||||
const result = format(list as any, () => { })
|
||||
test('result should have no nodes in iteration node', () => {
|
||||
expect(format(simpleIterationData.in as any).find(item => !!(item as any).execution_metadata?.iteration_id)).toBeUndefined()
|
||||
expect((result as any).find((item: any) => !!item.execution_metadata?.iteration_id)).toBeUndefined()
|
||||
})
|
||||
test('iteration should put nodes in details', () => {
|
||||
expect(format(simpleIterationData.in as any)).toEqual(simpleIterationData.expect)
|
||||
expect(result as any).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...iterationNode,
|
||||
details: [
|
||||
[iterations[0]],
|
||||
[iterations[1]],
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
export const simpleRetryData = (() => {
|
||||
const startNode = {
|
||||
id: 'f7938b2b-77cd-43f0-814c-2f0ade7cbc60',
|
||||
index: 1,
|
||||
predecessor_node_id: null,
|
||||
node_id: '1735112903395',
|
||||
node_type: 'start',
|
||||
title: 'Start',
|
||||
inputs: {
|
||||
'sys.files': [],
|
||||
'sys.user_id': '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
||||
'sys.app_id': '6180ead7-2190-4a61-975c-ec3bf29653da',
|
||||
'sys.workflow_id': 'eef6da45-244b-4c79-958e-f3573f7c12bb',
|
||||
'sys.workflow_run_id': 'fc8970ef-1406-484e-afde-8567dc22f34c',
|
||||
},
|
||||
process_data: null,
|
||||
outputs: {
|
||||
'sys.files': [],
|
||||
'sys.user_id': '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
||||
'sys.app_id': '6180ead7-2190-4a61-975c-ec3bf29653da',
|
||||
'sys.workflow_id': 'eef6da45-244b-4c79-958e-f3573f7c12bb',
|
||||
'sys.workflow_run_id': 'fc8970ef-1406-484e-afde-8567dc22f34c',
|
||||
},
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
elapsed_time: 0.008715,
|
||||
execution_metadata: null,
|
||||
extras: {},
|
||||
created_at: 1735112940,
|
||||
created_by_role: 'account',
|
||||
created_by_account: {
|
||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
||||
name: '九彩拼盘',
|
||||
email: 'iamjoel007@gmail.com',
|
||||
},
|
||||
created_by_end_user: null,
|
||||
finished_at: 1735112940,
|
||||
}
|
||||
|
||||
const httpNode = {
|
||||
id: '50220407-3420-4ad4-89da-c6959710d1aa',
|
||||
index: 2,
|
||||
predecessor_node_id: '1735112903395',
|
||||
node_id: '1735112908006',
|
||||
node_type: 'http-request',
|
||||
title: 'HTTP Request',
|
||||
inputs: null,
|
||||
process_data: {
|
||||
request: 'GET / HTTP/1.1\r\nHost: 404\r\n\r\n',
|
||||
},
|
||||
outputs: null,
|
||||
status: 'failed',
|
||||
error: 'timed out',
|
||||
elapsed_time: 30.247757,
|
||||
execution_metadata: null,
|
||||
extras: {},
|
||||
created_at: 1735112940,
|
||||
created_by_role: 'account',
|
||||
created_by_account: {
|
||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
||||
name: '九彩拼盘',
|
||||
email: 'iamjoel007@gmail.com',
|
||||
},
|
||||
created_by_end_user: null,
|
||||
finished_at: 1735112970,
|
||||
}
|
||||
|
||||
const retry1 = {
|
||||
id: 'ed352b36-27fb-49c6-9e8f-cc755bfc25fc',
|
||||
index: 3,
|
||||
predecessor_node_id: '1735112903395',
|
||||
node_id: '1735112908006',
|
||||
node_type: 'http-request',
|
||||
title: 'HTTP Request',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: null,
|
||||
status: 'retry',
|
||||
error: 'timed out',
|
||||
elapsed_time: 10.011833,
|
||||
execution_metadata: {
|
||||
iteration_id: null,
|
||||
parallel_mode_run_id: null,
|
||||
},
|
||||
extras: {},
|
||||
created_at: 1735112940,
|
||||
created_by_role: 'account',
|
||||
created_by_account: {
|
||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
||||
name: '九彩拼盘',
|
||||
email: 'iamjoel007@gmail.com',
|
||||
},
|
||||
created_by_end_user: null,
|
||||
finished_at: 1735112950,
|
||||
}
|
||||
|
||||
const retry2 = {
|
||||
id: '74dfb3d3-dacf-44f2-8784-e36bfa2d6c4e',
|
||||
index: 4,
|
||||
predecessor_node_id: '1735112903395',
|
||||
node_id: '1735112908006',
|
||||
node_type: 'http-request',
|
||||
title: 'HTTP Request',
|
||||
inputs: null,
|
||||
process_data: null,
|
||||
outputs: null,
|
||||
status: 'retry',
|
||||
error: 'timed out',
|
||||
elapsed_time: 10.010368,
|
||||
execution_metadata: {
|
||||
iteration_id: null,
|
||||
parallel_mode_run_id: null,
|
||||
},
|
||||
extras: {},
|
||||
created_at: 1735112950,
|
||||
created_by_role: 'account',
|
||||
created_by_account: {
|
||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
||||
name: '九彩拼盘',
|
||||
email: 'iamjoel007@gmail.com',
|
||||
},
|
||||
created_by_end_user: null,
|
||||
finished_at: 1735112960,
|
||||
}
|
||||
|
||||
return {
|
||||
in: [startNode, httpNode, retry1, retry2],
|
||||
expect: [startNode, {
|
||||
...httpNode,
|
||||
retryDetail: [retry1, retry2],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
|
|
@ -1,11 +1,21 @@
|
|||
import format from '.'
|
||||
import { simpleRetryData } from './data'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('retry', () => {
|
||||
// retry nodeId:1 3 times.
|
||||
const steps = graphToLogStruct('start -> (retry, 1, 3)')
|
||||
const [startNode, retryNode, ...retryDetail] = steps
|
||||
const result = format(steps)
|
||||
test('should have no retry status nodes', () => {
|
||||
expect(format(simpleRetryData.in as any).find(item => (item as any).status === 'retry')).toBeUndefined()
|
||||
expect(result.find(item => (item as any).status === 'retry')).toBeUndefined()
|
||||
})
|
||||
test('should put retry nodes in retryDetail', () => {
|
||||
expect(format(simpleRetryData.in as any)).toEqual(simpleRetryData.expect)
|
||||
expect(result).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...retryNode,
|
||||
retryDetail,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
const STEP_SPLIT = '->'
|
||||
|
||||
/*
|
||||
* : 1 -> 2 -> 3
|
||||
* iteration: (iteration, 1, [2, 3]) -> 4. (1, [2, 3]) means 1 is parent, [2, 3] is children
|
||||
* parallel: 1 -> (parallel, [1,2,3], [4, (parallel: (6,7))]).
|
||||
* retry: (retry, 1, [2,3]). 1 is parent, [2, 3] is retry nodes
|
||||
*/
|
||||
const simpleGraphToLogStruct = (input: string): any[] => {
|
||||
const list = input.split(STEP_SPLIT)
|
||||
return list
|
||||
}
|
||||
|
||||
export default simpleGraphToLogStruct
|
||||
|
|
@ -22,9 +22,6 @@ import type {
|
|||
} from './types'
|
||||
import { WorkflowContext } from './context'
|
||||
import type { NodeTracing, VersionHistory } from '@/types/workflow'
|
||||
import type {
|
||||
StrategyPluginDetail,
|
||||
} from '@/app/components/plugins/types'
|
||||
|
||||
// #TODO chatVar#
|
||||
// const MOCK_DATA = [
|
||||
|
|
@ -101,7 +98,6 @@ type Shape = {
|
|||
setCustomTools: (tools: ToolWithProvider[]) => void
|
||||
workflowTools: ToolWithProvider[]
|
||||
setWorkflowTools: (tools: ToolWithProvider[]) => void
|
||||
agentStrategies: StrategyPluginDetail[],
|
||||
clipboardElements: Node[]
|
||||
setClipboardElements: (clipboardElements: Node[]) => void
|
||||
showDebugAndPreviewPanel: boolean
|
||||
|
|
@ -234,7 +230,6 @@ export const createWorkflowStore = () => {
|
|||
setCustomTools: customTools => set(() => ({ customTools })),
|
||||
workflowTools: [],
|
||||
setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
|
||||
agentStrategies: [],
|
||||
clipboardElements: [],
|
||||
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
|
||||
showDebugAndPreviewPanel: false,
|
||||
|
|
|
|||
|
|
@ -1,76 +1,14 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { FormattedText } from '../components/datasets/formatted-text/formatted'
|
||||
import { PreviewSlice } from '../components/datasets/formatted-text/flavours/preview-slice'
|
||||
import { PreviewContainer } from '../components/datasets/preview/container'
|
||||
import { PreviewHeader } from '../components/datasets/preview/header'
|
||||
import FileIcon from '../components/base/file-icon'
|
||||
import { ChevronDown } from '../components/base/icons/src/vender/solid/arrows'
|
||||
import Badge from '../components/base/badge'
|
||||
import { DividerWithLabel } from '../components/base/divider/with-label'
|
||||
import Button from '../components/base/button'
|
||||
import { ChunkContainer, QAPreview } from '../components/datasets/chunk'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { SwitchPluginVersion } from '../components/workflow/nodes/_base/components/switch-plugin-version'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function Page() {
|
||||
const [parentChild, setParentChild] = useState(false)
|
||||
const [vertical, setVertical] = useState(false)
|
||||
const [qa, setQa] = useState(false)
|
||||
return <div className='p-4'>
|
||||
<div className='flex gap-2 my-4'>
|
||||
<Button onClick={() => setParentChild(!parentChild)}>
|
||||
Parent-Child
|
||||
</Button>
|
||||
<Button onClick={() => setVertical(!vertical)}>Vertical</Button>
|
||||
<Button onClick={() => setQa(!qa)}>QA</Button>
|
||||
</div>
|
||||
<PreviewContainer header={
|
||||
<PreviewHeader title='Preview'>
|
||||
<div className='flex items-center'>
|
||||
<FileIcon type='pdf' className='size-4' />
|
||||
<p
|
||||
className='text-text-primary text-sm font-semibold mx-1'
|
||||
>EOS R3 Tech Sheet.pdf</p>
|
||||
<ChevronDown className='size-[18px]' />
|
||||
<Badge text='276 Estimated chunks' className='ml-1' />
|
||||
</div>
|
||||
</PreviewHeader>
|
||||
}>
|
||||
<div className='space-y-6'>{parentChild
|
||||
? Array.from({ length: 4 }, (_, i) => {
|
||||
return <ChunkContainer
|
||||
label='Parent-Chunk-01'
|
||||
characterCount={521}
|
||||
key={i}
|
||||
>
|
||||
<FormattedText className={classNames(
|
||||
'w-full',
|
||||
vertical && 'flex flex-col gap-2',
|
||||
)}>
|
||||
{Array.from({ length: 4 }, (_, i) => {
|
||||
return <PreviewSlice
|
||||
key={i}
|
||||
label='C-1'
|
||||
text='lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' tooltip={'Child-chunk-2 · 268 Characters'} />
|
||||
})}
|
||||
</FormattedText>
|
||||
</ChunkContainer>
|
||||
})
|
||||
: Array.from({ length: 2 }, (_, i) => {
|
||||
return <ChunkContainer label='Chunk-01' characterCount={521} key={i}>
|
||||
{
|
||||
qa
|
||||
? <QAPreview qa={{
|
||||
question: 'What is the author\'s unconventional approach to writing this book, and how does it challenge the traditional academic mindset of \'publish or perish\'?',
|
||||
answer: 'It is quite natural for academics who are continuously told to “publish or perish” to want to always create something from scratch that is their own fresh creation. This book is an experiment in not starting from scratch, but instead “re-mixing” the book titled Think Python: How to Think Like a Computer Scientist written by Allen B. Downey, Jeff Elkner and others.',
|
||||
}} />
|
||||
: 'In December of 2009, I was preparing to teach SI502 - Networked Programming at the University of Michigan for the fifth semester in a row and decided it was time to write a Python textbook that focused on exploring data instead of understanding algorithms and abstractions. My goal in SI502 is to teach people life-long data handling skills using Python. Few of my students were planning to be professional computer programmers. Instead, they planned be librarians, managers, lawyers, biologists, economists, etc. who happened to want to skillfully use technology in their chosen field.'
|
||||
}
|
||||
</ChunkContainer>
|
||||
})
|
||||
}</div>
|
||||
<DividerWithLabel label='Display previews of up to 10 paragraphs' />
|
||||
</PreviewContainer>
|
||||
const { t } = useTranslation()
|
||||
return <div className="p-20">
|
||||
<SwitchPluginVersion
|
||||
uniqueIdentifier={'langgenius/openai:12'}
|
||||
tooltip={t('workflow.nodes.agent.switchToNewVersion')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -733,7 +733,10 @@ const translation = {
|
|||
toolNotInstallTooltip: '{{tool}} is not installed',
|
||||
toolNotAuthorizedTooltip: '{{tool}} Not Authorized',
|
||||
strategyNotInstallTooltip: '{{strategy}} is not installed',
|
||||
strategyNotFoundInPlugin: '{{strategy}} is not found in {{plugin}}',
|
||||
unsupportedStrategy: 'Unsupported strategy',
|
||||
pluginNotFoundDesc: 'This plugin is installed from GitHub. Please go to Plugins to reinstall',
|
||||
strategyNotFoundDesc: 'The installed plugin version does not provide this strategy.',
|
||||
strategyNotFoundDescAndSwitchVersion: 'The installed plugin version does not provide this strategy. Click to switch version.',
|
||||
modelSelectorTooltips: {
|
||||
deprecated: 'This model is deprecated',
|
||||
},
|
||||
|
|
@ -752,6 +755,7 @@ const translation = {
|
|||
checkList: {
|
||||
strategyNotSelected: 'Strategy not selected',
|
||||
},
|
||||
switchToNewVersion: 'Switch to new version',
|
||||
},
|
||||
tracing: {
|
||||
stopBy: 'Stop by {{user}}',
|
||||
|
|
|
|||
|
|
@ -733,7 +733,10 @@ const translation = {
|
|||
toolNotInstallTooltip: '{{tool}} 未安装',
|
||||
toolNotAuthorizedTooltip: '{{tool}} 未授权',
|
||||
strategyNotInstallTooltip: '{{strategy}} 未安装',
|
||||
strategyNotFoundInPlugin: '在 {{plugin}} 中未找到 {{strategy}}',
|
||||
unsupportedStrategy: '不支持的策略',
|
||||
strategyNotFoundDesc: '安装的插件版本不提供此策略。',
|
||||
pluginNotFoundDesc: '此插件安装自 GitHub。请转到插件重新安装。',
|
||||
strategyNotFoundDescAndSwitchVersion: '安装的插件版本不提供此策略。点击切换版本。',
|
||||
modelSelectorTooltips: {
|
||||
deprecated: '此模型已弃用',
|
||||
},
|
||||
|
|
@ -751,6 +754,7 @@ const translation = {
|
|||
checkList: {
|
||||
strategyNotSelected: '未选择策略',
|
||||
},
|
||||
switchToNewVersion: '切换到新版',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type {
|
|||
PluginsSearchParams,
|
||||
} from '@/app/components/plugins/marketplace/types'
|
||||
import { get, getMarketplace, post, postMarketplace } from './base'
|
||||
import type { MutateOptions, QueryOptions } from '@tanstack/react-query'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
|
|
@ -72,8 +73,9 @@ export const useInvalidateInstalledPluginList = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export const useInstallPackageFromMarketPlace = () => {
|
||||
export const useInstallPackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, string>) => {
|
||||
return useMutation({
|
||||
...options,
|
||||
mutationFn: (uniqueIdentifier: string) => {
|
||||
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } })
|
||||
},
|
||||
|
|
@ -319,8 +321,9 @@ export const useMutationPluginsFromMarketplace = () => {
|
|||
})
|
||||
}
|
||||
|
||||
export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[]) => {
|
||||
export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[], options?: QueryOptions<{ data: PluginsFromMarketplaceResponse }>) => {
|
||||
return useQuery({
|
||||
...options,
|
||||
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByIds', unique_identifiers],
|
||||
queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/identifier/batch', {
|
||||
body: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type {
|
|||
StrategyPluginDetail,
|
||||
} from '@/app/components/plugins/types'
|
||||
import { useInvalid } from './use-base'
|
||||
import type { QueryOptions } from '@tanstack/react-query'
|
||||
import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
|
@ -21,8 +22,9 @@ export const useInvalidateStrategyProviders = () => {
|
|||
return useInvalid(useStrategyListKey)
|
||||
}
|
||||
|
||||
export const useStrategyProviderDetail = (agentProvider: string) => {
|
||||
export const useStrategyProviderDetail = (agentProvider: string, options?: QueryOptions<StrategyPluginDetail>) => {
|
||||
return useQuery<StrategyPluginDetail>({
|
||||
...options,
|
||||
queryKey: [NAME_SPACE, 'detail', agentProvider],
|
||||
queryFn: () => fetchStrategyDetail(agentProvider),
|
||||
enabled: !!agentProvider,
|
||||
|
|
|
|||
Loading…
Reference in New Issue