From d5cfb26db60a6d310f6520adee464e80e613f518 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 2 Jan 2025 16:28:57 +0800 Subject: [PATCH 01/26] feat: support make retry data --- .../format-log/graph-to-log-struct.spec.ts | 28 ++++ .../utils/format-log/graph-to-log-struct.ts | 122 ++++++++++++++++ .../run/utils/format-log/index.backup.ts | 105 -------------- .../run/utils/format-log/retry/data.ts | 133 ------------------ .../run/utils/format-log/retry/index.spec.ts | 16 ++- .../format-log/simple-graph-to-log-struct.ts | 14 -- 6 files changed, 163 insertions(+), 255 deletions(-) create mode 100644 web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts create mode 100644 web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts delete mode 100644 web/app/components/workflow/run/utils/format-log/index.backup.ts delete mode 100644 web/app/components/workflow/run/utils/format-log/retry/data.ts delete mode 100644 web/app/components/workflow/run/utils/format-log/simple-graph-to-log-struct.ts 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..8a783c9644 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts @@ -0,0 +1,28 @@ +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('retry nodes', () => { + console.log(graphToLogStruct('start -> (retry, 1, 3)')) + }) +}) 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..faa16d53c6 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts @@ -0,0 +1,122 @@ +const STEP_SPLIT = '->' + +const toNodeData = (step: string, info: Record = {}): any => { + const [nodeId, title] = step.split('@') + const data = { + id: nodeId, + node_id: nodeId, + title: title || nodeId, + execution_metadata: {}, + status: 'succeeded', + } + // const executionMetadata = data.execution_metadata + const { isRetry } = info + if (isRetry) + data.status = 'retry' + + 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 +} + +type NodeStructure = { + node: string; + params: Array; +} + +export function parseNodeString(input: string): NodeStructure { + input = input.trim() + if (input.startsWith('(') && input.endsWith(')')) + input = input.slice(1, -1) + + const parts: Array = [] + let current = '' + let depth = 0 + + for (let i = 0; i < input.length; i++) { + const char = input[i] + + if (char === '(') + depth++ + else if (char === ')') + depth-- + + if (char === ',' && depth === 0) { + 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' && part.startsWith('(')) + result.params.push(parseNodeString(part)) + else if (i === 0) + result.node = part as string + else + result.params.push(part 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 '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> - -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>() - - 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/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 From f2eb0959601ab030435ac3067a46737117f41d8e Mon Sep 17 00:00:00 2001 From: AkaraChen Date: Thu, 2 Jan 2025 16:35:58 +0800 Subject: [PATCH 02/26] feat: strategy install status --- .../components/agent-strategy-selector.tsx | 14 +++++----- .../nodes/_base/components/agent-strategy.tsx | 6 +++-- .../components/install-plugin-button.tsx | 27 +++++++++++++++---- .../components/workflow/nodes/agent/panel.tsx | 2 ++ .../workflow/nodes/agent/use-config.ts | 4 ++- web/service/use-plugins.ts | 4 ++- 6 files changed, 40 insertions(+), 17 deletions(-) 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..dddaf1fd40 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 @@ -16,6 +16,7 @@ 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 type { StrategyStatus } from '../../agent/use-config' const ExternalNotInstallWarn = () => { const { t } = useTranslation() @@ -67,10 +68,11 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s export type AgentStrategySelectorProps = { value?: Strategy, onChange: (value?: Strategy) => void, + strategyStatus?: StrategyStatus } export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => { - const { value, onChange } = props + const { value, onChange, strategyStatus } = props const [open, setOpen] = useState(false) const [viewType, setViewType] = useState(ViewType.flat) const [query, setQuery] = useState('') @@ -81,8 +83,7 @@ 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 isShowError = (['plugin-not-found', 'strategy-not-found'] as Array).includes(strategyStatus) const icon = list?.find( coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name), )?.icon as string | undefined @@ -104,8 +105,8 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}

{value &&
- e.stopPropagation()} size={'small'} /> - {isExternalInstalled ? : } + {strategyStatus === 'plugin-not-found' && e.stopPropagation()} size={'small'} />} + {isShowError ? : }
} @@ -143,9 +144,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => - {/*
- aaa -
*/} }) diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 4ec46a6d61..fdd3e4f059 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -19,6 +19,7 @@ import { useWorkflowStore } from '../../../store' import { useRenderI18nObject } from '@/hooks/use-i18n' import type { NodeOutPutVar } from '../../../types' import type { Node } from 'reactflow' +import type { StrategyStatus } from '../../agent/use-config' export type Strategy = { agent_strategy_provider_name: string @@ -36,6 +37,7 @@ export type AgentStrategyProps = { onFormValueChange: (value: ToolVarInputs) => void nodeOutputVars?: NodeOutPutVar[], availableNodes?: Node[], + strategyStatus: StrategyStatus } type CustomSchema = Omit & { type: Type } & Field @@ -54,7 +56,7 @@ type StringSchema = CustomSchema<'string', { type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema | StringSchema export const AgentStrategy = memo((props: AgentStrategyProps) => { - const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes } = props + const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, strategyStatus } = props const { t } = useTranslation() const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) const renderI18nObject = useRenderI18nObject() @@ -176,7 +178,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { } } return
- + { strategy ?
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..992dfc05e4 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 @@ -3,14 +3,31 @@ import { RiInstallLine, RiLoader2Line } from '@remixicon/react' import type { ComponentProps } from 'react' import classNames from '@/utils/classnames' import { useTranslation } from 'react-i18next' +import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins' -type InstallPluginButtonProps = Omit, 'children'> +type InstallPluginButtonProps = Omit, 'children' | 'loading'> & { + uniqueIdentifier: string +} export const InstallPluginButton = (props: InstallPluginButtonProps) => { - const { loading, className, ...rest } = props + const { className, uniqueIdentifier, ...rest } = props const { t } = useTranslation() - return } diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index ab8ff1c4b9..cc6ebf3ca2 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -30,6 +30,7 @@ const AgentPanel: FC> = (props) => { inputs, setInputs, currentStrategy, + currentStrategyStatus, formData, onFormChange, @@ -95,6 +96,7 @@ const AgentPanel: FC> = (props) => { onFormValueChange={onFormChange} nodeOutputVars={availableVars} availableNodes={availableNodesWithParent} + strategyStatus={currentStrategyStatus} />
diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 45dd648989..5f68581ea1 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -13,6 +13,8 @@ import type { Var } from '../../types' import { VarType as VarKindType } from '../../types' import useAvailableVarList from '../_base/hooks/use-available-var-list' +export type StrategyStatus = 'loading' | 'plugin-not-found' | 'strategy-not-found' | 'success' + const useConfig = (id: string, payload: AgentNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() const { inputs, setInputs } = useNodeCrud(id, payload) @@ -27,7 +29,7 @@ const useConfig = (id: string, payload: AgentNodeType) => { 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(() => { + const currentStrategyStatus: StrategyStatus = useMemo(() => { if (strategyProvider.isLoading) return 'loading' if (strategyProvider.isError) return 'plugin-not-found' if (!currentStrategy) return 'strategy-not-found' diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 5ad7d831d9..8a7b565350 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 } from '@tanstack/react-query' import { useMutation, useQuery, @@ -72,8 +73,9 @@ export const useInvalidateInstalledPluginList = () => { } } -export const useInstallPackageFromMarketPlace = () => { +export const useInstallPackageFromMarketPlace = (options?: MutateOptions) => { return useMutation({ + ...options, mutationFn: (uniqueIdentifier: string) => { return post('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } }) }, From c458c28c62cc0074583c95b9a681cb4ce8e50046 Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 2 Jan 2025 17:35:11 +0800 Subject: [PATCH 03/26] feat: enhance plugin item localization with i18n support --- web/app/components/plugins/plugin-item/index.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index d997299844..1b853a4d4c 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -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 = ({ 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 renderI18nObject = useRenderI18nObject() + const title = renderI18nObject(label) + const descriptionText = renderI18nObject(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} From 5fb356fd33cb5333837fe045ff4778ff5740f50b Mon Sep 17 00:00:00 2001 From: twwu <twwu@dify.ai> Date: Thu, 2 Jan 2025 18:07:44 +0800 Subject: [PATCH 04/26] refactor: rename renderI18nObject to getValueFromI18nObject for clarity --- web/app/components/plugins/plugin-item/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 1b853a4d4c..469af683b8 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -65,9 +65,9 @@ const PluginItem: FC<Props> = ({ if (PluginType.tool.includes(category)) invalidateAllToolProviders() } - const renderI18nObject = useRenderI18nObject() - const title = renderI18nObject(label) - const descriptionText = renderI18nObject(description) + const getValueFromI18nObject = useRenderI18nObject() + const title = getValueFromI18nObject(label) + const descriptionText = getValueFromI18nObject(description) return ( <div From 39335b8038f8be0b73c692304a9741ae082a5c1c Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Fri, 3 Jan 2025 10:16:44 +0800 Subject: [PATCH 05/26] refactor I18n render in plugin detail --- .../plugin-detail-panel/endpoint-modal.tsx | 6 +++--- .../plugin-detail-panel/strategy-detail.tsx | 17 +++++++---------- .../plugin-detail-panel/strategy-item.tsx | 11 ++++------- .../tool-selector/tool-credentials-form.tsx | 6 +++--- web/app/components/plugins/provider-card.tsx | 8 ++++---- 5 files changed, 21 insertions(+), 27 deletions(-) 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<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 } } 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<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> 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)} > - <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 diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form.tsx index 857c0678cd..6334e792f9 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form.tsx @@ -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 } } 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} /> From 5ba0b857389baa376dcf05daa639c38f0c17a104 Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Fri, 3 Jan 2025 10:35:06 +0800 Subject: [PATCH 06/26] feat: install plugin button --- .../components/agent-strategy-selector.tsx | 6 +++--- .../components/install-plugin-button.tsx | 18 ++++++++++++----- .../workflow/nodes/agent/use-config.ts | 20 ++++++++++++++----- web/service/use-plugins.ts | 5 +++-- web/service/use-strategy.ts | 4 +++- 5 files changed, 37 insertions(+), 16 deletions(-) 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 dddaf1fd40..685f3386b4 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 @@ -83,7 +83,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => if (!list) return [] return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase())) }, [query, list]) - const isShowError = (['plugin-not-found', 'strategy-not-found'] as Array<undefined | StrategyStatus>).includes(strategyStatus) + const showError = (['plugin-not-found', 'strategy-not-found'] as Array<undefined | StrategyStatus>).includes(strategyStatus) const icon = list?.find( coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name), )?.icon as string | undefined @@ -105,8 +105,8 @@ 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'> - {strategyStatus === 'plugin-not-found' && <InstallPluginButton onClick={e => e.stopPropagation()} size={'small'} />} - {isShowError ? <ExternalNotInstallWarn /> : <RiArrowDownSLine className='size-4 text-text-tertiary' />} + {strategyStatus === 'plugin-not-found' && <InstallPluginButton onClick={e => e.stopPropagation()} size={'small'} uniqueIdentifier={value.plugin_unique_identifier} />} + {showError ? <ExternalNotInstallWarn /> : <RiArrowDownSLine className='size-4 text-text-tertiary' />} </div>} </div> </PortalToFollowElemTrigger> 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 992dfc05e4..f6a3334378 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,6 +1,6 @@ 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' @@ -21,13 +21,21 @@ export const InstallPluginButton = (props: InstallPluginButtonProps) => { manifest.refetch() }, }) - const handleInstall = () => { + 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={install.isPending} {...rest} onClick={handleInstall} className={classNames('flex items-center', className)} > - {install.isPending ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')} - {!install.isPending ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />} + 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/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 5f68581ea1..13daf3a11a 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -8,12 +8,12 @@ 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 = 'loading' | 'plugin-not-found' | 'strategy-not-found' | 'success' +export type StrategyStatus = 'loading' | 'plugin-not-found' | 'plugin-not-found-and-not-in-marketplace' | 'strategy-not-found' | 'success' const useConfig = (id: string, payload: AgentNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -25,16 +25,26 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) const strategyProvider = useStrategyProviderDetail( inputs.agent_strategy_provider_name || '', + { retry: false }, ) const currentStrategy = strategyProvider.data?.declaration.strategies.find( str => str.identity.name === inputs.agent_strategy_name, ) + const marketplace = useFetchPluginsInMarketPlaceByIds([inputs.agent_strategy_provider_name!], { + retry: false, + }) const currentStrategyStatus: StrategyStatus = useMemo(() => { - if (strategyProvider.isLoading) return 'loading' - if (strategyProvider.isError) return 'plugin-not-found' + if (strategyProvider.isLoading || marketplace.isLoading) return 'loading' + if (strategyProvider.isError) { + if (marketplace.data && marketplace.data.data.plugins.length === 0) + return 'plugin-not-found-and-not-in-marketplace' + + return 'plugin-not-found' + } if (!currentStrategy) return 'strategy-not-found' return 'success' - }, [currentStrategy, strategyProvider]) + }, [currentStrategy, marketplace, strategyProvider.isError, strategyProvider.isLoading]) + console.log('currentStrategyStatus', currentStrategyStatus) const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/') const pluginDetail = useCheckInstalled({ pluginIds: [pluginId || ''], diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index be21e82637..f9c8f0a2c2 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -22,7 +22,7 @@ import type { PluginsSearchParams, } from '@/app/components/plugins/marketplace/types' import { get, getMarketplace, post, postMarketplace } from './base' -import type { MutateOptions } from '@tanstack/react-query' +import type { MutateOptions, QueryOptions } from '@tanstack/react-query' import { useMutation, useQuery, @@ -321,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, From 483890b207a8d8e2f7895076b75e3113303befd8 Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Fri, 3 Jan 2025 13:39:23 +0800 Subject: [PATCH 07/26] fix install in tool item --- .../tool-selector/tool-item.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) 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 = ({ </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 From 06f0c3c88647d51f2a8eb91f0c0c797be970183b Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Fri, 3 Jan 2025 13:52:38 +0800 Subject: [PATCH 08/26] refactor: strategy status --- .../components/agent-strategy-selector.tsx | 48 +++++++++++---- .../nodes/_base/components/agent-strategy.tsx | 6 +- .../components/workflow/nodes/agent/node.tsx | 18 +++--- .../components/workflow/nodes/agent/panel.tsx | 2 - .../workflow/nodes/agent/use-config.ts | 59 ++++++++++++------- web/i18n/en-US/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + 7 files changed, 92 insertions(+), 43 deletions(-) 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 685f3386b4..ce502e56c6 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 @@ -17,15 +17,27 @@ 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 type { StrategyStatus } from '../../agent/use-config' +import { useStrategyInfo } from '../../agent/use-config' + +const NotInstallWarn = (props: { + strategyStatus: StrategyStatus +}) => { + // strategyStatus can be 'plugin-not-found-and-not-in-marketplace' | 'strategy-not-found' + const { strategyStatus } = 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> + <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> + <Link href={'/plugins'} className='text-text-accent tracking-tight'> + {t('workflow.nodes.agent.linkToPlugin')} + </Link> </p> </div>} needsDelay @@ -68,11 +80,10 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s export type AgentStrategySelectorProps = { value?: Strategy, onChange: (value?: Strategy) => void, - strategyStatus?: StrategyStatus } export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => { - const { value, onChange, strategyStatus } = props + const { value, onChange } = props const [open, setOpen] = useState(false) const [viewType, setViewType] = useState<ViewType>(ViewType.flat) const [query, setQuery] = useState('') @@ -83,14 +94,23 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => if (!list) return [] return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase())) }, [query, list]) - const showError = (['plugin-not-found', 'strategy-not-found'] as Array<undefined | StrategyStatus>).includes(strategyStatus) + const { strategyStatus } = useStrategyInfo( + value?.agent_strategy_provider_name, + value?.agent_strategy_name, + ) + const showError = ['strategy-not-found', 'plugin-not-found-and-not-in-marketplace'] + .includes(strategyStatus) 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} @@ -105,8 +125,16 @@ 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'> - {strategyStatus === 'plugin-not-found' && <InstallPluginButton onClick={e => e.stopPropagation()} size={'small'} uniqueIdentifier={value.plugin_unique_identifier} />} - {showError ? <ExternalNotInstallWarn /> : <RiArrowDownSLine className='size-4 text-text-tertiary' />} + {strategyStatus === 'plugin-not-found' && <InstallPluginButton + onClick={e => e.stopPropagation()} + size={'small'} + uniqueIdentifier={value.plugin_unique_identifier} + />} + {showError + ? <NotInstallWarn + strategyStatus={strategyStatus} + /> + : <RiArrowDownSLine className='size-4 text-text-tertiary' />} </div>} </div> </PortalToFollowElemTrigger> diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index fdd3e4f059..4ec46a6d61 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -19,7 +19,6 @@ import { useWorkflowStore } from '../../../store' import { useRenderI18nObject } from '@/hooks/use-i18n' import type { NodeOutPutVar } from '../../../types' import type { Node } from 'reactflow' -import type { StrategyStatus } from '../../agent/use-config' export type Strategy = { agent_strategy_provider_name: string @@ -37,7 +36,6 @@ export type AgentStrategyProps = { onFormValueChange: (value: ToolVarInputs) => void nodeOutputVars?: NodeOutPutVar[], availableNodes?: Node[], - strategyStatus: StrategyStatus } type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field @@ -56,7 +54,7 @@ type StringSchema = CustomSchema<'string', { type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema | StringSchema export const AgentStrategy = memo((props: AgentStrategyProps) => { - const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, strategyStatus } = props + const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes } = props const { t } = useTranslation() const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) const renderI18nObject = useRenderI18nObject() @@ -178,7 +176,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { } } return <div className='space-y-2'> - <AgentStrategySelector value={strategy} onChange={onStrategyChange} strategyStatus={strategyStatus} /> + <AgentStrategySelector value={strategy} onChange={onStrategyChange} /> { strategy ? <div> diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index 2cf9c67233..dba01adef5 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -90,16 +90,20 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => { ? <SettingItem label={t('workflow.nodes.agent.strategy.shortLabel')} status={ - ['plugin-not-found', 'strategy-not-found'].includes(currentStrategyStatus) + ['plugin-not-found', 'strategy-not-found', 'plugin-not-found-and-not-in-marketplace'].includes(currentStrategyStatus) ? 'error' : 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, - })} + tooltip={ + ['plugin-not-found', 'strategy-not-found', 'plugin-not-found-and-not-in-marketplace'].includes(currentStrategyStatus) + ? t('workflow.nodes.agent.strategyNotInstallTooltip', { + plugin: pluginDetail?.declaration.label + ? renderI18nObject(pluginDetail?.declaration.label) + : undefined, + strategy: inputs.agent_strategy_label, + }) + : undefined + } > {inputs.agent_strategy_label} </SettingItem> diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index cc6ebf3ca2..ab8ff1c4b9 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -30,7 +30,6 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { inputs, setInputs, currentStrategy, - currentStrategyStatus, formData, onFormChange, @@ -96,7 +95,6 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { onFormValueChange={onFormChange} nodeOutputVars={availableVars} availableNodes={availableNodesWithParent} - strategyStatus={currentStrategyStatus} /> </Field> <div> diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 13daf3a11a..47c297e761 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -15,6 +15,38 @@ import useAvailableVarList from '../_base/hooks/use-available-var-list' export type StrategyStatus = 'loading' | 'plugin-not-found' | 'plugin-not-found-and-not-in-marketplace' | 'strategy-not-found' | 'success' +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 = useMemo(() => { + if (strategyProvider.isLoading || marketplace.isLoading) return 'loading' + if (strategyProvider.isError) { + if (marketplace.data && marketplace.data.data.plugins.length === 0) + return 'plugin-not-found-and-not-in-marketplace' + + return 'plugin-not-found' + } + if (!strategy) return 'strategy-not-found' + return 'success' + }, [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) @@ -23,27 +55,14 @@ const useConfig = (id: string, payload: AgentNodeType) => { inputs, setInputs, }) - const strategyProvider = useStrategyProviderDetail( - inputs.agent_strategy_provider_name || '', - { retry: false }, + 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 marketplace = useFetchPluginsInMarketPlaceByIds([inputs.agent_strategy_provider_name!], { - retry: false, - }) - const currentStrategyStatus: StrategyStatus = useMemo(() => { - if (strategyProvider.isLoading || marketplace.isLoading) return 'loading' - if (strategyProvider.isError) { - if (marketplace.data && marketplace.data.data.plugins.length === 0) - return 'plugin-not-found-and-not-in-marketplace' - - return 'plugin-not-found' - } - if (!currentStrategy) return 'strategy-not-found' - return 'success' - }, [currentStrategy, marketplace, strategyProvider.isError, strategyProvider.isLoading]) console.log('currentStrategyStatus', currentStrategyStatus) const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/') const pluginDetail = useCheckInstalled({ diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index c2f9685036..08e6c3e639 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -734,6 +734,7 @@ const translation = { toolNotAuthorizedTooltip: '{{tool}} Not Authorized', strategyNotInstallTooltip: '{{strategy}} is not installed', strategyNotFoundInPlugin: '{{strategy}} is not found in {{plugin}}', + strategyNotInstallAndNotInMarketplace: '{{strategy}} is not installed and not found in Marketplace', modelSelectorTooltips: { deprecated: 'This model is deprecated', }, diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index c72e97b588..763cbf6276 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -733,6 +733,7 @@ const translation = { toolNotInstallTooltip: '{{tool}} 未安装', toolNotAuthorizedTooltip: '{{tool}} 未授权', strategyNotInstallTooltip: '{{strategy}} 未安装', + strategyNotInstallAndNotInMarketplace: '{{strategy}} 未安装且未在市场中找到', strategyNotFoundInPlugin: '在 {{plugin}} 中未找到 {{strategy}}', modelSelectorTooltips: { deprecated: '此模型已弃用', From fbf9984d8572567b19e4982bc91fe530049c2613 Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Fri, 3 Jan 2025 15:25:10 +0800 Subject: [PATCH 09/26] refactor: strategy status --- .../components/agent-strategy-selector.tsx | 69 ++++++++++++------- .../components/workflow/nodes/agent/node.tsx | 20 ++---- .../workflow/nodes/agent/use-config.ts | 29 +++++--- web/i18n/en-US/workflow.ts | 6 +- web/i18n/zh-Hans/workflow.ts | 6 +- 5 files changed, 78 insertions(+), 52 deletions(-) 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 ce502e56c6..3cd88f7329 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,30 +17,31 @@ 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 type { StrategyStatus } from '../../agent/use-config' import { useStrategyInfo } from '../../agent/use-config' -const NotInstallWarn = (props: { - strategyStatus: StrategyStatus +const NotFoundWarn = (props: { + title: ReactNode, + description: ReactNode }) => { - // strategyStatus can be 'plugin-not-found-and-not-in-marketplace' | 'strategy-not-found' - const { strategyStatus } = props + const { title, description } = props 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> @@ -98,8 +100,18 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => value?.agent_strategy_provider_name, value?.agent_strategy_name, ) - const showError = ['strategy-not-found', 'plugin-not-found-and-not-in-marketplace'] - .includes(strategyStatus) + const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external' + && !strategyStatus.plugin.installed + + const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external' + && strategyStatus.strategy === 'not-found' + + const showSwitchVersion = strategyStatus?.strategy === 'not-found' + && strategyStatus.plugin.source === 'marketplace' && strategyStatus.plugin.installed + + const showInstallButton = strategyStatus?.strategy === 'not-found' + && 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 @@ -125,16 +137,23 @@ 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'> - {strategyStatus === 'plugin-not-found' && <InstallPluginButton + {showInstallButton && <InstallPluginButton onClick={e => e.stopPropagation()} size={'small'} uniqueIdentifier={value.plugin_unique_identifier} />} - {showError - ? <NotInstallWarn - strategyStatus={strategyStatus} + {showPluginNotInstalledWarn + ? <NotFoundWarn + title={t('workflow.nodes.agent.pluginNotInstalled')} + description={t('workflow.nodes.agent.pluginNotInstalledDesc')} /> - : <RiArrowDownSLine className='size-4 text-text-tertiary' />} + : showUnsupportedStrategy + ? <NotFoundWarn + title={t('workflow.nodes.agent.unsupportedStrategy')} + description={t('workflow.nodes.agent.strategyNotFoundDesc')} + /> + : <RiArrowDownSLine className='size-4 text-text-tertiary' /> + } </div>} </div> </PortalToFollowElemTrigger> diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index dba01adef5..df6beb24c0 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -89,20 +89,14 @@ 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', 'plugin-not-found-and-not-in-marketplace'].includes(currentStrategyStatus) - ? 'error' - : undefined - } + status={currentStrategyStatus?.strategy === 'not-found' ? 'error' : undefined} tooltip={ - ['plugin-not-found', 'strategy-not-found', 'plugin-not-found-and-not-in-marketplace'].includes(currentStrategyStatus) - ? t('workflow.nodes.agent.strategyNotInstallTooltip', { - plugin: pluginDetail?.declaration.label - ? renderI18nObject(pluginDetail?.declaration.label) - : undefined, - strategy: inputs.agent_strategy_label, - }) - : undefined + currentStrategyStatus?.strategy === 'not-found' ? t('workflow.nodes.agent.strategyNotInstallTooltip', { + plugin: pluginDetail?.declaration.label + ? renderI18nObject(pluginDetail?.declaration.label) + : undefined, + strategy: inputs.agent_strategy_label, + }) : undefined } > {inputs.agent_strategy_label} diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 47c297e761..aa59a3dc4f 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -13,7 +13,13 @@ import type { Var } from '../../types' import { VarType as VarKindType } from '../../types' import useAvailableVarList from '../_base/hooks/use-available-var-list' -export type StrategyStatus = 'loading' | 'plugin-not-found' | 'plugin-not-found-and-not-in-marketplace' | 'strategy-not-found' | 'success' +export type StrategyStatus = { + plugin: { + source: 'external' | 'marketplace' + installed: boolean + } + strategy: 'not-found' | 'normal' +} export const useStrategyInfo = ( strategyProviderName?: string, @@ -29,16 +35,19 @@ export const useStrategyInfo = ( const marketplace = useFetchPluginsInMarketPlaceByIds([strategyProviderName!], { retry: false, }) - const strategyStatus: StrategyStatus = useMemo(() => { - if (strategyProvider.isLoading || marketplace.isLoading) return 'loading' - if (strategyProvider.isError) { - if (marketplace.data && marketplace.data.data.plugins.length === 0) - return 'plugin-not-found-and-not-in-marketplace' - - return 'plugin-not-found' + 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, + }, + strategy: strategyExist ? 'normal' : 'not-found', } - if (!strategy) return 'strategy-not-found' - return 'success' }, [strategy, marketplace, strategyProvider.isError, strategyProvider.isLoading]) return { strategyProvider, diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 08e6c3e639..a9b0fe5587 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -733,8 +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}}', - strategyNotInstallAndNotInMarketplace: '{{strategy}} is not installed and not found in Marketplace', + 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', }, diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 763cbf6276..11996ae982 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -733,8 +733,10 @@ const translation = { toolNotInstallTooltip: '{{tool}} 未安装', toolNotAuthorizedTooltip: '{{tool}} 未授权', strategyNotInstallTooltip: '{{strategy}} 未安装', - strategyNotInstallAndNotInMarketplace: '{{strategy}} 未安装且未在市场中找到', - strategyNotFoundInPlugin: '在 {{plugin}} 中未找到 {{strategy}}', + unsupportedStrategy: '不支持的策略', + strategyNotFoundDesc: '安装的插件版本不提供此策略。', + pluginNotFoundDesc: '此插件安装自 GitHub。请转到插件重新安装。', + strategyNotFoundDescAndSwitchVersion: '安装的插件版本不提供此策略。点击切换版本。', modelSelectorTooltips: { deprecated: '此模型已弃用', }, From 5fdfba6b0019475ab634fc443d01b1a7cb8d9dce Mon Sep 17 00:00:00 2001 From: Joel <iamjoel007@gmail.com> Date: Fri, 3 Jan 2025 15:40:49 +0800 Subject: [PATCH 10/26] feat: make iteration --- .../format-log/graph-to-log-struct.spec.ts | 71 ++++++- .../utils/format-log/graph-to-log-struct.ts | 46 ++++- .../run/utils/format-log/iteration/data.ts | 190 ------------------ .../utils/format-log/iteration/index.spec.ts | 18 +- 4 files changed, 127 insertions(+), 198 deletions(-) delete mode 100644 web/app/components/workflow/run/utils/format-log/iteration/data.ts 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 index 8a783c9644..f4d78b62f2 100644 --- 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 @@ -22,7 +22,76 @@ describe('graphToLogStruct', () => { ], }) }) + 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', () => { - console.log(graphToLogStruct('start -> (retry, 1, 3)')) + 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 index faa16d53c6..d7092f0642 100644 --- 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 @@ -2,18 +2,27 @@ const STEP_SPLIT = '->' const toNodeData = (step: string, info: Record<string, any> = {}): any => { const [nodeId, title] = step.split('@') - const data = { + const data: Record<string, any> = { id: nodeId, node_id: nodeId, title: title || nodeId, execution_metadata: {}, status: 'succeeded', } - // const executionMetadata = data.execution_metadata - const { isRetry } = info + + 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 } @@ -30,6 +39,21 @@ const toRetryNodeData = ({ 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++) + res.push(toNodeData(`${children[i]}`, { inIterationInfo: { iterationId: nodeId, iterationIndex: i } })) + + return res +} + type NodeStructure = { node: string; params: Array<string | NodeStructure>; @@ -43,6 +67,7 @@ export function parseNodeString(input: string): NodeStructure { 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] @@ -52,7 +77,14 @@ export function parseNodeString(input: string): NodeStructure { else if (char === ')') depth-- - if (char === ',' && depth === 0) { + if (char === '[') + inArrayDepth++ + else if (char === ']') + inArrayDepth-- + + const isInArray = inArrayDepth > 0 + + if (char === ',' && depth === 0 && !isInArray) { parts.push(current.trim()) current = '' } @@ -97,6 +129,12 @@ const toNodes = (input: string): any[] => { const { node, params } = parseNodeString(step) switch (node) { + case 'iteration': + 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, 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]], + ], + }, + ]) }) }) From 07aa2ca9cf7e3d3d2deb8bcd4edb69cf57e9920c Mon Sep 17 00:00:00 2001 From: zxhlyh <jasonapring2015@outlook.com> Date: Fri, 3 Jan 2025 16:34:23 +0800 Subject: [PATCH 11/26] fix: single run log --- web/app/components/workflow/run/node.tsx | 8 ++++---- web/app/components/workflow/run/result-panel.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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'> From e0ed17a2e654d733bb737cdc488e87db68771ff2 Mon Sep 17 00:00:00 2001 From: Joel <iamjoel007@gmail.com> Date: Mon, 6 Jan 2025 15:31:18 +0800 Subject: [PATCH 12/26] chore: can generator middle struct --- .../format-log/graph-to-log-struct-2.spec.ts | 93 ++++++++++++++ .../utils/format-log/graph-to-log-struct-2.ts | 115 ++++++++++++++++++ .../utils/format-log/graph-to-log-struct.ts | 30 +++-- 3 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts create mode 100644 web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts 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..3956e039d0 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts @@ -0,0 +1,93 @@ +import { parseDSL } from './graph-to-log-struct-2' + +describe('parseDSL', () => { + test('parse plain flow', () => { + const dsl = 'a -> b -> c' + const result = parseDSL(dsl) + expect(result).toEqual([ + { nodeType: 'plain', nodeId: 'a' }, + { nodeType: 'plain', nodeId: 'b' }, + { nodeType: 'plain', nodeId: 'c' }, + ]) + }) + + test('parse iteration node with flow', () => { + const dsl = '(iteration, a, b -> c)' + const result = parseDSL(dsl) + expect(result).toEqual([ + { + nodeType: 'iteration', + nodeId: 'a', + params: [ + [ + { nodeType: 'plain', nodeId: 'b' }, + { nodeType: 'plain', nodeId: 'c' }, + ], + ], + }, + ]) + }) + + test('parse parallel node with flow', () => { + const dsl = 'a -> (parallel, b, c -> d, e)' + const result = parseDSL(dsl) + expect(result).toEqual([ + { + nodeType: 'plain', + nodeId: 'a', + }, + { + nodeType: 'parallel', + nodeId: 'b', + params: [ + [ + { nodeType: 'plain', nodeId: 'c' }, + { nodeType: 'plain', nodeId: 'd' }, + ], + // single node don't need to be wrapped in an array + { nodeType: 'plain', nodeId: 'e' }, + ], + }, + ]) + }) + + test('parse retry', () => { + const dsl = '(retry, a, 3)' + const result = parseDSL(dsl) + expect(result).toEqual([ + { + nodeType: 'retry', + nodeId: 'a', + params: [3], + }, + ]) + }) + + test('parse nested complex nodes', () => { + const dsl = '(iteration, a, b -> (parallel, e, f -> g, h))' + const result = parseDSL(dsl) + expect(result).toEqual([ + { + nodeType: 'iteration', + nodeId: 'a', + params: [ + [ + { nodeType: 'plain', nodeId: 'b' }, + { + nodeType: 'parallel', + nodeId: 'e', + params: [ + [ + { nodeType: 'plain', nodeId: 'f' }, + { nodeType: 'plain', nodeId: 'g' }, + ], + // single node don't need to be wrapped in an array + { nodeType: 'plain', nodeId: 'h' }, + ], + }, + ], + ], + }, + ]) + }) +}) 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..a812b0a3c4 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts @@ -0,0 +1,115 @@ +type NodePlain = { nodeType: 'plain'; nodeId: string } +type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | NodeComplex | Node[])[] } +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): Node[] { + return parseTopLevelFlow(dsl).map(parseNode) +} + +/** + * 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. + * @returns A parsed node object. + */ +function parseNode(nodeStr: 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) + + return { + nodeType: nodeType.trim(), + nodeId: nodeId.trim(), + params, + } + } + + // If it's not a complex node, treat it as a plain node + return { nodeType: 'plain', nodeId: nodeStr.trim() } +} + +/** + * Parses parameters of a complex node. + * Supports nested flows and complex sub-nodes. + * @param paramParts - The parameters string split by commas. + * @returns An array of parsed parameters (plain nodes, nested nodes, or flows). + */ +function parseParams(paramParts: string[]): (Node | Node[])[] { + return paramParts.map((part) => { + if (part.includes('->')) { + // Parse as a flow and return an array of nodes + return parseTopLevelFlow(part).map(parseNode) + } + else if (part.startsWith('(')) { + // Parse as a nested complex node + return parseNode(part) + } + else if (!isNaN(Number(part.trim()))) { + // Parse as a numeric parameter + return Number(part.trim()) + } + else { + // Parse as a plain node + return parseNode(part) + } + }) +} + +export { parseDSL } 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 index d7092f0642..0a3a04da09 100644 --- 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 @@ -2,6 +2,7 @@ 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, @@ -48,8 +49,10 @@ const toIterationNodeData = ({ }) => { const res = [toNodeData(nodeId, { isIteration: true })] // TODO: handle inner node structure - for (let i = 0; i < children.length; i++) - res.push(toNodeData(`${children[i]}`, { inIterationInfo: { iterationId: nodeId, iterationIndex: i } })) + for (let i = 0; i < children.length; i++) { + const step = `${children[i]}` + res.push(toNodeData(step, { inIterationInfo: { iterationId: nodeId, iterationIndex: i } })) + } return res } @@ -104,12 +107,21 @@ export function parseNodeString(input: string): NodeStructure { for (let i = 0; i < parts.length; i++) { const part = parts[i] - if (typeof part === 'string' && part.startsWith('(')) - result.params.push(parseNodeString(part)) - else if (i === 0) - result.node = part as string - else - result.params.push(part as string) + 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 @@ -130,6 +142,8 @@ const toNodes = (input: string): any[] => { 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[], From 228cd1cdbe7fd1828bd0c5fefcefdc716ab1882f Mon Sep 17 00:00:00 2001 From: Joel <iamjoel007@gmail.com> Date: Mon, 6 Jan 2025 16:48:24 +0800 Subject: [PATCH 13/26] feat: add iteration id --- .../format-log/graph-to-log-struct-2.spec.ts | 14 ++++--- .../utils/format-log/graph-to-log-struct-2.ts | 39 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) 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 index 3956e039d0..ab40f1e48c 100644 --- 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 @@ -20,8 +20,8 @@ describe('parseDSL', () => { nodeId: 'a', params: [ [ - { nodeType: 'plain', nodeId: 'b' }, - { nodeType: 'plain', nodeId: 'c' }, + { nodeType: 'plain', nodeId: 'b', iterationId: 'a', iterationIndex: 0 }, + { nodeType: 'plain', nodeId: 'c', iterationId: 'a', iterationIndex: 0 }, ], ], }, @@ -72,17 +72,19 @@ describe('parseDSL', () => { nodeId: 'a', params: [ [ - { nodeType: 'plain', nodeId: 'b' }, + { nodeType: 'plain', nodeId: 'b', iterationId: 'a', iterationIndex: 0 }, { nodeType: 'parallel', nodeId: 'e', + iterationId: 'a', + iterationIndex: 0, params: [ [ - { nodeType: 'plain', nodeId: 'f' }, - { nodeType: 'plain', nodeId: 'g' }, + { nodeType: 'plain', nodeId: 'f', iterationId: 'a', iterationIndex: 0 }, + { nodeType: 'plain', nodeId: 'g', iterationId: 'a', iterationIndex: 0 }, ], // single node don't need to be wrapped in an array - { nodeType: 'plain', nodeId: 'h' }, + { nodeType: 'plain', nodeId: 'h', iterationId: 'a', iterationIndex: 0 }, ], }, ], 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 index a812b0a3c4..660db75a5e 100644 --- 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 @@ -1,5 +1,6 @@ -type NodePlain = { nodeType: 'plain'; nodeId: string } -type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | NodeComplex | Node[])[] } +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 /** @@ -8,7 +9,7 @@ type Node = NodePlain | NodeComplex * @returns An array of parsed nodes. */ function parseDSL(dsl: string): Node[] { - return parseTopLevelFlow(dsl).map(parseNode) + return parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)) } /** @@ -44,9 +45,10 @@ function parseTopLevelFlow(dsl: string): string[] { * 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): Node { +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 @@ -72,42 +74,53 @@ function parseNode(nodeStr: string): Node { // Extract nodeType, nodeId, and params const [nodeType, nodeId, ...paramsRaw] = parts - const params = parseParams(paramsRaw) - - return { + const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId) + const complexNode = { nodeType: nodeType.trim(), nodeId: nodeId.trim(), params, } + if (parentIterationId) { + complexNode.iterationId = parentIterationId + complexNode.iterationIndex = 0 // Fixed as 0 + } + return complexNode } // If it's not a complex node, treat it as a plain node - return { nodeType: 'plain', nodeId: nodeStr.trim() } + 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[]): (Node | Node[])[] { +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(parseNode) + return parseTopLevelFlow(part).map(node => parseNode(node, iterationId)) } else if (part.startsWith('(')) { // Parse as a nested complex node - return parseNode(part) + return parseNode(part, iterationId) } - else if (!isNaN(Number(part.trim()))) { + 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) + return parseNode(part, iterationId) } }) } From 61d2f7092714848d4274d986714b556d4fb9ef61 Mon Sep 17 00:00:00 2001 From: Joel <iamjoel007@gmail.com> Date: Mon, 6 Jan 2025 18:36:45 +0800 Subject: [PATCH 14/26] feat: add transform node to node data --- .../format-log/graph-to-log-struct-2.spec.ts | 179 ++++++++++------- .../utils/format-log/graph-to-log-struct-2.ts | 184 +++++++++++++++++- 2 files changed, 286 insertions(+), 77 deletions(-) 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 index ab40f1e48c..5e00cd8ca7 100644 --- 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 @@ -1,95 +1,128 @@ import { parseDSL } from './graph-to-log-struct-2' describe('parseDSL', () => { - test('parse plain flow', () => { - const dsl = 'a -> b -> c' + it('should parse plain nodes correctly', () => { + const dsl = 'plainNode1 -> plainNode2' const result = parseDSL(dsl) expect(result).toEqual([ - { nodeType: 'plain', nodeId: 'a' }, - { nodeType: 'plain', nodeId: 'b' }, - { nodeType: 'plain', nodeId: 'c' }, + { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: {}, status: 'succeeded' }, + { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: {}, status: 'succeeded' }, ]) }) - test('parse iteration node with flow', () => { - const dsl = '(iteration, a, b -> c)' + 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([ { - nodeType: 'iteration', - nodeId: 'a', - params: [ - [ - { nodeType: 'plain', nodeId: 'b', iterationId: 'a', iterationIndex: 0 }, - { nodeType: 'plain', nodeId: 'c', iterationId: 'a', iterationIndex: 0 }, - ], - ], + 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', }, ]) }) - test('parse parallel node with flow', () => { - const dsl = 'a -> (parallel, b, c -> d, e)' + // 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([ - { - nodeType: 'plain', - nodeId: 'a', - }, - { - nodeType: 'parallel', - nodeId: 'b', - params: [ - [ - { nodeType: 'plain', nodeId: 'c' }, - { nodeType: 'plain', nodeId: 'd' }, - ], - // single node don't need to be wrapped in an array - { nodeType: 'plain', nodeId: 'e' }, - ], - }, + { 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' }, ]) }) - test('parse retry', () => { - const dsl = '(retry, a, 3)' - const result = parseDSL(dsl) - expect(result).toEqual([ - { - nodeType: 'retry', - nodeId: 'a', - params: [3], - }, - ]) - }) - - test('parse nested complex nodes', () => { - const dsl = '(iteration, a, b -> (parallel, e, f -> g, h))' - const result = parseDSL(dsl) - expect(result).toEqual([ - { - nodeType: 'iteration', - nodeId: 'a', - params: [ - [ - { nodeType: 'plain', nodeId: 'b', iterationId: 'a', iterationIndex: 0 }, - { - nodeType: 'parallel', - nodeId: 'e', - iterationId: 'a', - iterationIndex: 0, - params: [ - [ - { nodeType: 'plain', nodeId: 'f', iterationId: 'a', iterationIndex: 0 }, - { nodeType: 'plain', nodeId: 'g', iterationId: 'a', iterationIndex: 0 }, - ], - // single node don't need to be wrapped in an array - { nodeType: 'plain', nodeId: 'h', iterationId: 'a', iterationIndex: 0 }, - ], - }, - ], - ], - }, - ]) + 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 index 660db75a5e..9b5a830e98 100644 --- 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 @@ -8,8 +8,8 @@ type Node = NodePlain | NodeComplex * @param dsl - The input DSL string. * @returns An array of parsed nodes. */ -function parseDSL(dsl: string): Node[] { - return parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)) +function parseDSL(dsl: string): NodeData[] { + return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr))) } /** @@ -81,8 +81,8 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node { params, } if (parentIterationId) { - complexNode.iterationId = parentIterationId - complexNode.iterationIndex = 0 // Fixed as 0 + (complexNode as any).iterationId = parentIterationId; + (complexNode as any).iterationIndex = 0 // Fixed as 0 } return complexNode } @@ -125,4 +125,180 @@ function parseParams(paramParts: string[], iterationId?: string): (Node | Node[] }) } +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 } From 15f3e46c491aae24c30c2dd6e87b2bae14e1cf4e Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Tue, 7 Jan 2025 09:30:35 +0800 Subject: [PATCH 15/26] refactor: some field name in strategy status --- web/app/components/base/badge.tsx | 5 +- .../components/agent-strategy-selector.tsx | 17 ++-- .../components/switch-plugin-version.tsx | 80 +++++++++++++++++++ .../components/workflow/nodes/agent/node.tsx | 4 +- .../workflow/nodes/agent/use-config.ts | 4 +- web/app/dev-preview/page.tsx | 16 ++++ 6 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx create mode 100644 web/app/dev-preview/page.tsx 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/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index 3cd88f7329..5eedac69d4 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 @@ -18,6 +18,7 @@ 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, @@ -100,17 +101,18 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => 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.strategy === 'not-found' + && !strategyStatus?.isExistInPlugin - const showSwitchVersion = strategyStatus?.strategy === 'not-found' - && strategyStatus.plugin.source === 'marketplace' && strategyStatus.plugin.installed + const showSwitchVersion = !strategyStatus?.isExistInPlugin + && strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed - const showInstallButton = strategyStatus?.strategy === 'not-found' - && 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), @@ -154,6 +156,11 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => /> : <RiArrowDownSLine className='size-4 text-text-tertiary' /> } + {showSwitchVersion && <SwitchPluginVersion + uniqueIdentifier={'langgenius/openai:12'} + onSelect={console.error} + version={''} + />} </div>} </div> </PortalToFollowElemTrigger> 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..fc310676fe --- /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 + version: string + onSelect: (version: string) => void +} + +export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { + const { uniqueIdentifier, tooltip, onSelect, version } = 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() + onSelect(targetVersion!) + }, [hideUpdateModal, onSelect, targetVersion]) + return <Tooltip popupContent={!isShow && tooltip} triggerMethod='hover'> + <div> + {isShowUpdateModal && pluginDetail && <UpdateFromMarketplace + payload={{ + originalPackageInfo: { + id: uniqueIdentifier, + payload: pluginDetail.declaration, + }, + targetPackageInfo: { + id: uniqueIdentifier, + version: targetVersion!, + }, + }} + onCancel={hideUpdateModal} + onSave={handleUpdatedFromMarketplace} + />} + <PluginVersionPicker + isShow={isShow} + onShowChange={setIsShow} + pluginID={pluginId} + currentVersion={version} + onSelect={(state) => { + setTargetVersion(state.version) + showUpdateModal() + }} + trigger={ + <Badge + className={cn( + 'mx-1 hover:bg-state-base-hover', + isShow && 'bg-state-base-hover', + )} + uppercase={true} + text={ + <> + <div>{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/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index df6beb24c0..033827741d 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -89,9 +89,9 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => { {inputs.agent_strategy_name ? <SettingItem label={t('workflow.nodes.agent.strategy.shortLabel')} - status={currentStrategyStatus?.strategy === 'not-found' ? 'error' : undefined} + status={!currentStrategyStatus?.isExistInPlugin ? 'error' : undefined} tooltip={ - currentStrategyStatus?.strategy === 'not-found' ? t('workflow.nodes.agent.strategyNotInstallTooltip', { + !currentStrategyStatus?.isExistInPlugin ? t('workflow.nodes.agent.strategyNotInstallTooltip', { plugin: pluginDetail?.declaration.label ? renderI18nObject(pluginDetail?.declaration.label) : undefined, diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index aa59a3dc4f..d1446892c2 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -18,7 +18,7 @@ export type StrategyStatus = { source: 'external' | 'marketplace' installed: boolean } - strategy: 'not-found' | 'normal' + isExistInPlugin: boolean } export const useStrategyInfo = ( @@ -46,7 +46,7 @@ export const useStrategyInfo = ( source: isInMarketplace ? 'marketplace' : 'external', installed: isPluginInstalled, }, - strategy: strategyExist ? 'normal' : 'not-found', + isExistInPlugin: strategyExist, } }, [strategy, marketplace, strategyProvider.isError, strategyProvider.isLoading]) return { diff --git a/web/app/dev-preview/page.tsx b/web/app/dev-preview/page.tsx new file mode 100644 index 0000000000..8ab12d63f7 --- /dev/null +++ b/web/app/dev-preview/page.tsx @@ -0,0 +1,16 @@ +'use client' + +import { useState } from 'react' +import { SwitchPluginVersion } from '../components/workflow/nodes/_base/components/switch-plugin-version' + +export default function Page() { + const [version, setVersion] = useState('0.0.1') + return <div className="p-20"> + <SwitchPluginVersion + uniqueIdentifier={'langgenius/openai:12'} + onSelect={setVersion} + version={version} + tooltip='Switch to new version' + /> + </div> +} From 0beebab605f048dc055a592ba7eb83cbb436c93d Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Tue, 7 Jan 2025 09:57:35 +0800 Subject: [PATCH 16/26] fix: workflow store agent strategy not up to date --- .../workflow/hooks/use-checklist.ts | 8 ++++---- .../components/workflow/hooks/use-workflow.ts | 20 +------------------ .../workflow/nodes/agent/default.ts | 4 ++-- .../workflow/nodes/agent/use-config.ts | 1 - web/app/components/workflow/store.ts | 5 ----- 5 files changed, 7 insertions(+), 31 deletions(-) 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/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/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index d1446892c2..fda254be13 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -72,7 +72,6 @@ const useConfig = (id: string, payload: AgentNodeType) => { inputs.agent_strategy_provider_name, inputs.agent_strategy_name, ) - console.log('currentStrategyStatus', currentStrategyStatus) const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/') const pluginDetail = useCheckInstalled({ pluginIds: [pluginId || ''], 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, From bdb9d676b1d774a998935cfa6c47a1fb943629be Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Tue, 7 Jan 2025 10:49:26 +0800 Subject: [PATCH 17/26] chore: update switch plugin i18n --- .../nodes/_base/components/agent-strategy-selector.tsx | 1 + .../workflow/nodes/_base/components/switch-plugin-version.tsx | 4 ++-- web/app/dev-preview/page.tsx | 4 +++- web/i18n/en-US/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) 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 5eedac69d4..ab2498529e 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 @@ -160,6 +160,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => uniqueIdentifier={'langgenius/openai:12'} onSelect={console.error} version={''} + tooltip={t('workflow.nodes.agent.switchToNewVersion')} />} </div>} </div> 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 index fc310676fe..cb61b67310 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -34,7 +34,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { onSelect(targetVersion!) }, [hideUpdateModal, onSelect, targetVersion]) return <Tooltip popupContent={!isShow && tooltip} triggerMethod='hover'> - <div> + <div className='w-fit'> {isShowUpdateModal && pluginDetail && <UpdateFromMarketplace payload={{ originalPackageInfo: { @@ -61,7 +61,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { trigger={ <Badge className={cn( - 'mx-1 hover:bg-state-base-hover', + 'mx-1 hover:bg-state-base-hover flex', isShow && 'bg-state-base-hover', )} uppercase={true} diff --git a/web/app/dev-preview/page.tsx b/web/app/dev-preview/page.tsx index 8ab12d63f7..2644993015 100644 --- a/web/app/dev-preview/page.tsx +++ b/web/app/dev-preview/page.tsx @@ -2,15 +2,17 @@ import { useState } from 'react' import { SwitchPluginVersion } from '../components/workflow/nodes/_base/components/switch-plugin-version' +import { useTranslation } from 'react-i18next' export default function Page() { const [version, setVersion] = useState('0.0.1') + const { t } = useTranslation() return <div className="p-20"> <SwitchPluginVersion uniqueIdentifier={'langgenius/openai:12'} onSelect={setVersion} version={version} - tooltip='Switch to new version' + tooltip={t('workflow.nodes.agent.switchToNewVersion')} /> </div> } diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index a9b0fe5587..858f69db94 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -755,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 11996ae982..b90fec2371 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -754,6 +754,7 @@ const translation = { checkList: { strategyNotSelected: '未选择策略', }, + switchToNewVersion: '切换到新版', }, }, tracing: { From a8c48703492584598ee00255c69a165a808a142b Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Tue, 7 Jan 2025 10:55:42 +0800 Subject: [PATCH 18/26] refactor: switch plugin version component to not accept version --- .../components/agent-strategy-selector.tsx | 5 +++-- .../components/switch-plugin-version.tsx | 19 +++++++++---------- web/app/dev-preview/page.tsx | 4 ---- 3 files changed, 12 insertions(+), 16 deletions(-) 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 ab2498529e..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 @@ -158,9 +158,10 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => } {showSwitchVersion && <SwitchPluginVersion uniqueIdentifier={'langgenius/openai:12'} - onSelect={console.error} - version={''} tooltip={t('workflow.nodes.agent.switchToNewVersion')} + onChange={() => { + // TODO: refresh all strategies + }} />} </div>} </div> 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 index cb61b67310..5c8233ecfb 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -13,12 +13,11 @@ import { useCheckInstalled } from '@/service/use-plugins' export type SwitchPluginVersionProps = { uniqueIdentifier: string tooltip?: string - version: string - onSelect: (version: string) => void + onChange?: (version: string) => void } export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { - const { uniqueIdentifier, tooltip, onSelect, version } = props + const { uniqueIdentifier, tooltip, onChange } = props const [pluginId] = uniqueIdentifier.split(':') const [isShow, setIsShow] = useState(false) const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false) @@ -31,9 +30,9 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { const handleUpdatedFromMarketplace = useCallback(() => { hideUpdateModal() - onSelect(targetVersion!) - }, [hideUpdateModal, onSelect, targetVersion]) - return <Tooltip popupContent={!isShow && tooltip} triggerMethod='hover'> + onChange?.(targetVersion!) + }, [hideUpdateModal, onChange, targetVersion]) + return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'> <div className='w-fit'> {isShowUpdateModal && pluginDetail && <UpdateFromMarketplace payload={{ @@ -49,11 +48,11 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { onCancel={hideUpdateModal} onSave={handleUpdatedFromMarketplace} />} - <PluginVersionPicker + {pluginDetail && <PluginVersionPicker isShow={isShow} onShowChange={setIsShow} pluginID={pluginId} - currentVersion={version} + currentVersion={pluginDetail.version} onSelect={(state) => { setTargetVersion(state.version) showUpdateModal() @@ -67,14 +66,14 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { uppercase={true} text={ <> - <div>{version}</div> + <div>{pluginDetail.version}</div> <RiArrowLeftRightLine className='ml-1 w-3 h-3 text-text-tertiary' /> </> } hasRedCornerMark={true} /> } - /> + />} </div> </Tooltip> } diff --git a/web/app/dev-preview/page.tsx b/web/app/dev-preview/page.tsx index 2644993015..49afe537cd 100644 --- a/web/app/dev-preview/page.tsx +++ b/web/app/dev-preview/page.tsx @@ -1,17 +1,13 @@ 'use client' -import { useState } from 'react' import { SwitchPluginVersion } from '../components/workflow/nodes/_base/components/switch-plugin-version' import { useTranslation } from 'react-i18next' export default function Page() { - const [version, setVersion] = useState('0.0.1') const { t } = useTranslation() return <div className="p-20"> <SwitchPluginVersion uniqueIdentifier={'langgenius/openai:12'} - onSelect={setVersion} - version={version} tooltip={t('workflow.nodes.agent.switchToNewVersion')} /> </div> From e24b04b30ff275b6d3cde9a0dbd2c8f8b741638d Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Tue, 7 Jan 2025 10:58:53 +0800 Subject: [PATCH 19/26] refactor: switch plugin version component to not accept version --- .../workflow/nodes/_base/components/switch-plugin-version.tsx | 3 ++- web/app/components/workflow/nodes/agent/use-config.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 index 5c8233ecfb..ad7414ca0c 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -30,8 +30,9 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { const handleUpdatedFromMarketplace = useCallback(() => { hideUpdateModal() + pluginDetails.refetch() onChange?.(targetVersion!) - }, [hideUpdateModal, onChange, targetVersion]) + }, [hideUpdateModal, onChange, pluginDetails, targetVersion]) return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'> <div className='w-fit'> {isShowUpdateModal && pluginDetail && <UpdateFromMarketplace diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index fda254be13..64a666d82f 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -74,7 +74,7 @@ const useConfig = (id: string, payload: AgentNodeType) => { ) 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(() => { From 5d25643f544812aa4485c268488d6c61f56028d0 Mon Sep 17 00:00:00 2001 From: Yi <yxiaoisme@gmail.com> Date: Tue, 7 Jan 2025 11:18:01 +0800 Subject: [PATCH 20/26] fix: group icon style --- .../account-setting/model-provider-page/model-icon/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx index eda380d2ae..1abaf4b891 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx @@ -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> ) From 1419430015cb0dc8a0f65cc4228ec5dbb568caec Mon Sep 17 00:00:00 2001 From: AkaraChen <akarachen@outlook.com> Date: Tue, 7 Jan 2025 11:25:06 +0800 Subject: [PATCH 21/26] chore: upd --- .../workflow/nodes/_base/components/install-plugin-button.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f6a3334378..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 @@ -7,10 +7,11 @@ import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/u type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & { uniqueIdentifier: string + onSuccess?: () => void } export const InstallPluginButton = (props: InstallPluginButtonProps) => { - const { className, uniqueIdentifier, ...rest } = props + const { className, uniqueIdentifier, onSuccess, ...rest } = props const { t } = useTranslation() const manifest = useCheckInstalled({ pluginIds: [uniqueIdentifier], @@ -19,6 +20,7 @@ export const InstallPluginButton = (props: InstallPluginButtonProps) => { const install = useInstallPackageFromMarketPlace({ onSuccess() { manifest.refetch() + onSuccess?.() }, }) const handleInstall: MouseEventHandler = (e) => { From 0e98794d49aa8edeb45c9d12dd4161ce71e0fe9e Mon Sep 17 00:00:00 2001 From: Joel <iamjoel007@gmail.com> Date: Tue, 7 Jan 2025 11:26:33 +0800 Subject: [PATCH 22/26] feat: use all refresh plugin tools to hooks --- .../hooks/use-refresh-plugin-list.tsx | 43 +++++++++++++++++++ .../install-from-marketplace/index.tsx | 18 +++----- 2 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx 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<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) From 8d39ec1da527ecd970213198612af6f964fe9e7f Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Fri, 3 Jan 2025 15:07:42 +0800 Subject: [PATCH 23/26] chart drak mode --- .../app/(appDetailLayout)/[appId]/overview/chartView.tsx | 2 +- .../app/(appDetailLayout)/[appId]/overview/page.tsx | 2 +- web/app/components/app-sidebar/basic.tsx | 8 ++++---- web/app/components/app/overview/appChart.tsx | 8 ++++---- web/app/components/base/select/index.tsx | 2 +- web/context/app-context.tsx | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) 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 ( <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}`) }))} 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 ( - <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} /> 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 ( <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> 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<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> diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index 687d402582..3332a11abd 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -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} diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 7addfb83d4..25a313a76b 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -127,7 +127,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => setCurrentWorkspace(currentWorkspaceResponse) }, [currentWorkspaceResponse]) - const [theme, setTheme] = useState<Theme>(Theme.light) + const [theme, setTheme] = useState<Theme>(Theme.dark) const handleSetTheme = useCallback((theme: Theme) => { setTheme(theme) globalThis.document.documentElement.setAttribute('data-theme', theme) From f5b2735dd5173addc5cfafac3c80605014034bd9 Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Tue, 7 Jan 2025 11:33:32 +0800 Subject: [PATCH 24/26] theme default light --- web/context/app-context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 25a313a76b..7addfb83d4 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -127,7 +127,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => setCurrentWorkspace(currentWorkspaceResponse) }, [currentWorkspaceResponse]) - const [theme, setTheme] = useState<Theme>(Theme.dark) + const [theme, setTheme] = useState<Theme>(Theme.light) const handleSetTheme = useCallback((theme: Theme) => { setTheme(theme) globalThis.document.documentElement.setAttribute('data-theme', theme) From 1348e320158eca8ab8b48c343c10ef5041513e73 Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Tue, 7 Jan 2025 11:40:41 +0800 Subject: [PATCH 25/26] fix balance model z-index --- web/app/components/base/modal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( <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" From 275696edba86e44240b260c9b7bce0ada3ad4f44 Mon Sep 17 00:00:00 2001 From: JzoNg <jzongcode@gmail.com> Date: Tue, 7 Jan 2025 11:53:20 +0800 Subject: [PATCH 26/26] fix system model selector --- .../model-provider-page/model-selector/popup-item.tsx | 2 +- .../model-provider-page/model-selector/popup.tsx | 2 +- .../model-provider-page/system-model-selector/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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<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} diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx index ad06c3238b..8de3cbd720 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx @@ -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' }) }}> 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<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'>