diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index bb1e4fd95b..00f1190045 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -46,7 +46,7 @@ export default function ChartView({ appId }: IChartViewProps) { return (
-
+
{t('appOverview.analysis.title')} ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx index 137c2c36ee..47dd36eb81 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx @@ -12,7 +12,7 @@ const Overview = async ({ params: { appId }, }: IDevelopProps) => { return ( -
+
diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index 51fc10721e..20777c7b6a 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -60,18 +60,18 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type return (
{icon && icon_background && iconType === 'app' && ( -
+
)} {iconType !== 'app' - &&
+ &&
{ICON_MAP[iconType]}
} {mode === 'expand' &&
-
+
{name} {hoverTip && }
-
{type}
+
{type}
{isExternal ? t('dataset.externalTag') : ''}
}
diff --git a/web/app/components/app/overview/appChart.tsx b/web/app/components/app/overview/appChart.tsx index d1426caa27..6ae6253812 100644 --- a/web/app/components/app/overview/appChart.tsx +++ b/web/app/components/app/overview/appChart.tsx @@ -231,7 +231,7 @@ const Chart: React.FC = ({ const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData) return ( -
+
@@ -242,11 +242,11 @@ const Chart: React.FC = ({ type={!CHART_TYPE_CONFIG[chartType].showTokens ? '' : {t('appOverview.analysis.tokenUsage.consumed')} Tokens - ( + ( ~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })} - ) + ) } - textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-gray-300' : ''}` }} /> + textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
diff --git a/web/app/components/base/badge.tsx b/web/app/components/base/badge.tsx index 0214d46968..78b9a76326 100644 --- a/web/app/components/base/badge.tsx +++ b/web/app/components/base/badge.tsx @@ -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 } diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx index 26cde5fce3..a659ccaac7 100644 --- a/web/app/components/base/modal/index.tsx +++ b/web/app/components/base/modal/index.tsx @@ -29,7 +29,7 @@ export default function Modal({ }: IModal) { return ( - + = ({ leaveTo="opacity-0" > - + {items.map((item: Item) => ( = ({ if (provider?.icon_small) { return ( -
model-icon = ({ 'flex items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle', className, )}> -
- +
+
) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index df6e69193e..d9cdb26431 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -91,7 +91,7 @@ const PopupItem: FC = ({ popupClassName='p-3 !w-[206px] bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border rounded-xl' popupContent={
-
+
= ({ ) }
-
{ +
{ onHide() setShowAccountSettingModal({ payload: 'provider' }) }}> diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx index 46d1fafcc7..ec2bdc265f 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx @@ -139,7 +139,7 @@ const SystemModel: FC = ({ {t('common.modelProvider.systemModelSettings')} - +
diff --git a/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx b/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx new file mode 100644 index 0000000000..6bcb8f0321 --- /dev/null +++ b/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx @@ -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 diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx index 046806ee08..7ac271b83f 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx @@ -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 = ({ // readyToInstall -> check installed -> installed/failed const [step, setStep] = useState(InstallStep.readyToInstall) const [errorMsg, setErrorMsg] = useState(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 = ({ 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) diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx index 2d200cb348..e150d72dc3 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx @@ -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 = ({ onCancel, onSaved, }) => { + const getValueFromI18nObject = useRenderI18nObject() const { t } = useTranslation() - const language = useLanguage() const [tempCredential, setTempCredential] = React.useState(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 } } diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx index a4ec6fe0c1..2b58f620b1 100644 --- a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx +++ b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx @@ -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 = ({ 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 = ({
-
{provider.label[language]}
+
{getValueFromI18nObject(provider.label)}
-
{detail.identity.label[language]}
- +
{getValueFromI18nObject(detail.identity.label)}
+
{/* form */}
@@ -113,7 +110,7 @@ const StrategyDetail: FC = ({ {detail.parameters.map((item: any, index) => (
-
{item.label[language]}
+
{getValueFromI18nObject(item.label)}
{getType(item.type)}
@@ -123,7 +120,7 @@ const StrategyDetail: FC = ({
{item.human_description && (
- {item.human_description?.[language]} + {getValueFromI18nObject(item.human_description)}
)}
diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx index ff1e425fc0..8cdb7315d8 100644 --- a/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx @@ -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)} > -
{detail.identity.label[language]}
-
{detail.description[language]}
+
{getValueFromI18nObject(detail.identity.label)}
+
{getValueFromI18nObject(detail.description)}
{showDetail && ( = ({ onCancel, onSaved, }) => { + const getValueFromI18nObject = useRenderI18nObject() const { t } = useTranslation() - const language = useLanguage() const [credentialSchema, setCredentialSchema] = useState(null) const { name: collectionName } = collection const [tempCredential, setTempCredential] = React.useState({}) @@ -45,7 +45,7 @@ const ToolCredentialForm: FC = ({ 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 } } diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx index c393d70a25..5927318619 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx @@ -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 = ({ )} {!isError && uninstalled && ( - { - e.stopPropagation() - onInstall?.() - }} /> + )} {isError && ( = ({ className, plugin, }) => { - const locale = useLanguage() const { t } = useTranslation() const { categoriesMap } = useCategories() const currentPluginID = usePluginPageContext(v => v.currentPluginID) @@ -66,6 +65,10 @@ const PluginItem: FC = ({ if (PluginType.tool.includes(category)) invalidateAllToolProviders() } + const getValueFromI18nObject = useRenderI18nObject() + const title = getValueFromI18nObject(label) + const descriptionText = getValueFromI18nObject(description) + return (
= ({
- + <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} diff --git a/web/app/components/plugins/provider-card.tsx b/web/app/components/plugins/provider-card.tsx index 140fd24328..ed9ad9769f 100644 --- a/web/app/components/plugins/provider-card.tsx +++ b/web/app/components/plugins/provider-card.tsx @@ -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} /> diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 9646b0da87..722ae5f032 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -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, diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 8ce31b8acf..0f6ae59b6e 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -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) { diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index 1cf9fc23ef..a7fa48ec07 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -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> }) diff --git a/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx b/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx index 1ae5fab864..bdbcdfde5a 100644 --- a/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx +++ b/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx @@ -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> } diff --git a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx new file mode 100644 index 0000000000..ad7414ca0c --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -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> +} diff --git a/web/app/components/workflow/nodes/agent/default.ts b/web/app/components/workflow/nodes/agent/default.ts index da1cba4adc..4d7965c77f 100644 --- a/web/app/components/workflow/nodes/agent/default.ts +++ b/web/app/components/workflow/nodes/agent/default.ts @@ -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 diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index 2cf9c67233..033827741d 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -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> diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 45dd648989..64a666d82f 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -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(() => { diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index 9efd03df7a..33ed05e891 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -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)}> diff --git a/web/app/components/workflow/run/result-panel.tsx b/web/app/components/workflow/run/result-panel.tsx index a198b2ff6d..b05e5cb888 100644 --- a/web/app/components/workflow/run/result-panel.tsx +++ b/web/app/components/workflow/run/result-panel.tsx @@ -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'> diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts new file mode 100644 index 0000000000..5e00cd8ca7 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts @@ -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') + }) +}) diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts new file mode 100644 index 0000000000..9b5a830e98 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts @@ -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 } diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts new file mode 100644 index 0000000000..f4d78b62f2 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts @@ -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', + }, + ]) + }) +}) diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts new file mode 100644 index 0000000000..0a3a04da09 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts @@ -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 diff --git a/web/app/components/workflow/run/utils/format-log/index.backup.ts b/web/app/components/workflow/run/utils/format-log/index.backup.ts deleted file mode 100644 index f35e029490..0000000000 --- a/web/app/components/workflow/run/utils/format-log/index.backup.ts +++ /dev/null @@ -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 diff --git a/web/app/components/workflow/run/utils/format-log/iteration/data.ts b/web/app/components/workflow/run/utils/format-log/iteration/data.ts deleted file mode 100644 index 0d08cd5350..0000000000 --- a/web/app/components/workflow/run/utils/format-log/iteration/data.ts +++ /dev/null @@ -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], - } -})() diff --git a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts index 77b776f12c..4c49c41420 100644 --- a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts @@ -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]], + ], + }, + ]) }) }) diff --git a/web/app/components/workflow/run/utils/format-log/retry/data.ts b/web/app/components/workflow/run/utils/format-log/retry/data.ts deleted file mode 100644 index e22c8b8982..0000000000 --- a/web/app/components/workflow/run/utils/format-log/retry/data.ts +++ /dev/null @@ -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], - }], - } -})() diff --git a/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts b/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts index 5ae6c385fd..099b987843 100644 --- a/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts @@ -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, + }, + ]) }) }) diff --git a/web/app/components/workflow/run/utils/format-log/simple-graph-to-log-struct.ts b/web/app/components/workflow/run/utils/format-log/simple-graph-to-log-struct.ts deleted file mode 100644 index 4aea146a7f..0000000000 --- a/web/app/components/workflow/run/utils/format-log/simple-graph-to-log-struct.ts +++ /dev/null @@ -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 diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index b05c6676c0..6bd47eaa01 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -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, diff --git a/web/app/dev-preview/page.tsx b/web/app/dev-preview/page.tsx index 92263d99a0..49afe537cd 100644 --- a/web/app/dev-preview/page.tsx +++ b/web/app/dev-preview/page.tsx @@ -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> } diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index c2f9685036..858f69db94 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -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}}', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index c72e97b588..b90fec2371 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -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: { diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 785e158261..f9c8f0a2c2 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -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: { diff --git a/web/service/use-strategy.ts b/web/service/use-strategy.ts index 49f852ebf5..af591ac019 100644 --- a/web/service/use-strategy.ts +++ b/web/service/use-strategy.ts @@ -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,