Merge branch 'zhsama/llm-warning-ui' into feat/pull-a-variable

This commit is contained in:
zhsama 2026-01-16 16:22:07 +08:00
commit e85e31773a
5 changed files with 116 additions and 26 deletions

View File

@ -2,25 +2,36 @@ import type { FC } from 'react'
import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react' import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle'
import { Agent } from '@/app/components/base/icons/src/vender/workflow' import { Agent } from '@/app/components/base/icons/src/vender/workflow'
import { cn } from '@/utils/classnames'
type AgentHeaderBarProps = { type AgentHeaderBarProps = {
agentName: string agentName: string
onRemove: () => void onRemove: () => void
onViewInternals?: () => void onViewInternals?: () => void
hasWarning?: boolean
} }
const AgentHeaderBar: FC<AgentHeaderBarProps> = ({ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
agentName, agentName,
onRemove, onRemove,
onViewInternals, onViewInternals,
hasWarning,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex items-center justify-between px-2 py-1"> <div className="flex items-center justify-between px-2 py-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="flex items-center gap-1 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark px-1.5 py-0.5 shadow-xs"> <div
className={cn(
'flex items-center gap-1 rounded-md border-[0.5px] px-1.5 py-0.5 shadow-xs',
hasWarning
? 'border-text-warning-secondary bg-components-badge-status-light-warning-halo'
: 'border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
>
<div className="flex h-4 w-4 items-center justify-center rounded bg-util-colors-indigo-indigo-500"> <div className="flex h-4 w-4 items-center justify-center rounded bg-util-colors-indigo-indigo-500">
<Agent className="h-3 w-3 text-text-primary-on-surface" /> <Agent className="h-3 w-3 text-text-primary-on-surface" />
</div> </div>
@ -39,11 +50,14 @@ const AgentHeaderBar: FC<AgentHeaderBarProps> = ({
</div> </div>
<button <button
type="button" type="button"
className="flex items-center gap-0.5 text-text-tertiary hover:text-text-secondary" className="flex items-center gap-1 text-text-tertiary hover:text-text-secondary"
onClick={onViewInternals} onClick={onViewInternals}
> >
<RiEqualizer2Line className="h-3.5 w-3.5" /> <RiEqualizer2Line className="h-3.5 w-3.5" />
<span className="system-xs-medium">{t('common.viewInternals', { ns: 'workflow' })}</span> <span className="system-xs-medium">{t('common.viewInternals', { ns: 'workflow' })}</span>
{hasWarning && (
<AlertTriangle className="h-3.5 w-3.5 text-text-warning-secondary" />
)}
</button> </button>
</div> </div>
) )

View File

@ -1,12 +1,15 @@
import type { AgentNode } from '@/app/components/base/prompt-editor/types' import type { AgentNode, WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import type { MentionConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types' import type { MentionConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types'
import type { AgentNodeType } from '@/app/components/workflow/nodes/agent/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { import type {
Node, CommonNodeType,
NodeOutPutVar, NodeOutPutVar,
PromptItem, PromptItem,
PromptTemplateItem, PromptTemplateItem,
ValueSelector, ValueSelector,
Node as WorkflowNode,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import { import {
memo, memo,
@ -15,7 +18,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow' import { useNodes, useStoreApi } from 'reactflow'
import PromptEditor from '@/app/components/base/prompt-editor' import PromptEditor from '@/app/components/base/prompt-editor'
import { useNodesMetaData, useNodesSyncDraft } from '@/app/components/workflow/hooks' import { useNodesMetaData, useNodesSyncDraft } from '@/app/components/workflow/hooks'
import { VarKindType as VarKindTypeEnum } from '@/app/components/workflow/nodes/_base/types' import { VarKindType as VarKindTypeEnum } from '@/app/components/workflow/nodes/_base/types'
@ -23,6 +26,8 @@ import { Type } from '@/app/components/workflow/nodes/llm/types'
import { useStore } from '@/app/components/workflow/store' import { useStore } from '@/app/components/workflow/store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types' import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types'
import { generateNewNode, getNodeCustomTypeByNodeDataType } from '@/app/components/workflow/utils' import { generateNewNode, getNodeCustomTypeByNodeDataType } from '@/app/components/workflow/utils'
import { useGetLanguage } from '@/context/i18n'
import { useStrategyProviders } from '@/service/use-strategy'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import SubGraphModal from '../sub-graph-modal' import SubGraphModal from '../sub-graph-modal'
import AgentHeaderBar from './agent-header-bar' import AgentHeaderBar from './agent-header-bar'
@ -39,6 +44,14 @@ const DEFAULT_MENTION_CONFIG: MentionConfig = {
null_strategy: 'use_default', null_strategy: 'use_default',
default_value: '', default_value: '',
} }
type AgentCheckValidContext = {
provider?: StrategyPluginDetail
strategy?: StrategyDetail
language: string
isReadyForCheckValid: boolean
}
type WorkflowNodesMap = NonNullable<WorkflowVariableBlockType['workflowNodesMap']>
const resolvePromptText = (item?: PromptItem) => { const resolvePromptText = (item?: PromptItem) => {
if (!item) if (!item)
@ -113,7 +126,7 @@ const buildPromptTemplateWithText = (promptTemplate: PromptTemplateItem[] | Prom
type MixedVariableTextInputProps = { type MixedVariableTextInputProps = {
readOnly?: boolean readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[] nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[] availableNodes?: WorkflowNode[]
value?: string value?: string
onChange?: (text: string, type?: VarKindType, mentionConfig?: MentionConfig | null) => void onChange?: (text: string, type?: VarKindType, mentionConfig?: MentionConfig | null) => void
showManageInputField?: boolean showManageInputField?: boolean
@ -136,7 +149,10 @@ const MixedVariableTextInput = ({
paramKey = '', paramKey = '',
}: MixedVariableTextInputProps) => { }: MixedVariableTextInputProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const language = useGetLanguage()
const { data: strategyProviders } = useStrategyProviders()
const reactFlowStore = useStoreApi() const reactFlowStore = useStoreApi()
const nodes = useNodes<CommonNodeType>()
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey) const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey) const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey)
const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
@ -147,7 +163,7 @@ const MixedVariableTextInput = ({
return availableNodes.reduce((acc, node) => { return availableNodes.reduce((acc, node) => {
acc[node.id] = node acc[node.id] = node
return acc return acc
}, {} as Record<string, Node>) }, {} as Record<string, WorkflowNode>)
}, [availableNodes]) }, [availableNodes])
const contextNodeIds = useMemo(() => { const contextNodeIds = useMemo(() => {
@ -159,6 +175,13 @@ const MixedVariableTextInput = ({
return ids return ids
}, [availableNodes]) }, [availableNodes])
const nodesById = useMemo(() => {
return nodes.reduce((acc, node) => {
acc[node.id] = node
return acc
}, {} as Record<string, WorkflowNode>)
}, [nodes])
type DetectedAgent = { type DetectedAgent = {
nodeId: string nodeId: string
name: string name: string
@ -198,6 +221,63 @@ const MixedVariableTextInput = ({
})) }))
}, [availableNodes, contextNodeIds]) }, [availableNodes, contextNodeIds])
const workflowNodesMap = useMemo<WorkflowNodesMap>(() => {
const acc: WorkflowNodesMap = {}
availableNodes.forEach((node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
height: node.data.height,
width: node.data.width,
position: node.data.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('blocks.start', { ns: 'workflow' }),
type: BlockEnum.Start,
height: node.data.height,
width: node.data.width,
position: node.data.position,
}
}
})
return acc
}, [availableNodes, t])
const getNodeWarning = useCallback((node?: WorkflowNode) => {
if (!node)
return true
const validator = nodesMetaDataMap?.[node.data.type as BlockEnum]?.checkValid
if (!validator)
return false
let moreDataForCheckValid: AgentCheckValidContext | undefined
if (node.data.type === BlockEnum.Agent) {
const agentData = node.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === agentData.agent_strategy_provider_name)
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === agentData.agent_strategy_name)
moreDataForCheckValid = {
provider,
strategy,
language,
isReadyForCheckValid,
}
}
const { errorMessage } = validator(node.data, t, moreDataForCheckValid)
return Boolean(errorMessage)
}, [language, nodesMetaDataMap, strategyProviders, t])
const hasAgentWarning = useMemo(() => {
if (!detectedAgentFromValue)
return false
const agentWarning = getNodeWarning(nodesById[detectedAgentFromValue.nodeId])
if (!toolNodeId || !paramKey)
return agentWarning
const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
const extractorWarning = getNodeWarning(nodesById[extractorNodeId])
return agentWarning || extractorWarning
}, [detectedAgentFromValue, getNodeWarning, nodesById, paramKey, toolNodeId])
const syncExtractorPromptFromText = useCallback((text: string) => { const syncExtractorPromptFromText = useCallback((text: string) => {
if (!toolNodeId || !paramKey) if (!toolNodeId || !paramKey)
return return
@ -213,7 +293,7 @@ const MixedVariableTextInput = ({
const extractorNodeId = `${toolNodeId}_ext_${paramKey}` const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
const { getNodes, setNodes } = reactFlowStore.getState() const { getNodes, setNodes } = reactFlowStore.getState()
const nodes = getNodes() const nodes = getNodes()
const extractorNode = nodes.find(node => node.id === extractorNodeId) as Node<LLMNodeType> | undefined const extractorNode = nodes.find(node => node.id === extractorNodeId) as WorkflowNode<LLMNodeType> | undefined
if (!extractorNode?.data?.prompt_template) if (!extractorNode?.data?.prompt_template)
return return
@ -278,7 +358,7 @@ const MixedVariableTextInput = ({
if (toolNodeId && paramKey) { if (toolNodeId && paramKey) {
const extractorNodeId = `${toolNodeId}_ext_${paramKey}` const extractorNodeId = `${toolNodeId}_ext_${paramKey}`
const defaultValue = nodesMetaDataMap?.[BlockEnum.LLM]?.defaultValue const defaultValue = nodesMetaDataMap?.[BlockEnum.LLM]?.defaultValue as Partial<LLMNodeType> | undefined
const { getNodes, setNodes } = reactFlowStore.getState() const { getNodes, setNodes } = reactFlowStore.getState()
const nodes = getNodes() const nodes = getNodes()
const hasExtractorNode = nodes.some(node => node.id === extractorNodeId) const hasExtractorNode = nodes.some(node => node.id === extractorNodeId)
@ -288,8 +368,9 @@ const MixedVariableTextInput = ({
id: extractorNodeId, id: extractorNodeId,
type: getNodeCustomTypeByNodeDataType(BlockEnum.LLM), type: getNodeCustomTypeByNodeDataType(BlockEnum.LLM),
data: { data: {
...(defaultValue as any), ...defaultValue,
title: defaultValue.title, type: BlockEnum.LLM,
title: defaultValue?.title ?? '',
desc: defaultValue.desc || '', desc: defaultValue.desc || '',
parent_node_id: toolNodeId, parent_node_id: toolNodeId,
structured_output_enabled: true, structured_output_enabled: true,
@ -351,6 +432,7 @@ const MixedVariableTextInput = ({
agentName={detectedAgentFromValue.name} agentName={detectedAgentFromValue.name}
onRemove={handleAgentRemove} onRemove={handleAgentRemove}
onViewInternals={handleOpenSubGraphModal} onViewInternals={handleOpenSubGraphModal}
hasWarning={hasAgentWarning}
/> />
)} )}
<PromptEditor <PromptEditor
@ -362,19 +444,7 @@ const MixedVariableTextInput = ({
workflowVariableBlock={{ workflowVariableBlock={{
show: !disableVariableInsertion, show: !disableVariableInsertion,
variables: nodesOutputVars || [], variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => { workflowNodesMap,
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('blocks.start', { ns: 'workflow' }),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
showManageInputField, showManageInputField,
onManageInputField, onManageInputField,
}} }}

View File

@ -197,7 +197,12 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
{referenceItems.map(item => ( {referenceItems.map(item => (
<div <div
key={item.key} key={item.key}
className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary" className={cn(
'flex h-6 items-center justify-between space-x-1 rounded-md border px-1 text-xs font-normal text-text-secondary',
item.hasWarning
? 'border-text-warning-secondary bg-components-badge-status-light-warning-halo'
: 'border-transparent bg-workflow-block-parma-bg',
)}
> >
<div className="flex min-w-0 items-center gap-1"> <div className="flex min-w-0 items-center gap-1">
<BlockIcon <BlockIcon

View File

@ -77,6 +77,7 @@ export type CommonNodeType<T = {}> = {
_isCandidate?: boolean _isCandidate?: boolean
_isBundled?: boolean _isBundled?: boolean
_children?: { nodeId: string, nodeType: BlockEnum }[] _children?: { nodeId: string, nodeType: BlockEnum }[]
parent_node_id?: string
_isEntering?: boolean _isEntering?: boolean
_showAddVariablePopup?: boolean _showAddVariablePopup?: boolean
_holdAddVariablePopup?: boolean _holdAddVariablePopup?: boolean

View File

@ -20,7 +20,7 @@ import {
BlockEnum, BlockEnum,
} from '../types' } from '../types'
export function generateNewNode({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }): { export function generateNewNode<T = {}>({ data, position, id, zIndex, type, ...rest }: Omit<Node<T>, 'id'> & { id?: string }): {
newNode: Node newNode: Node
newIterationStartNode?: Node newIterationStartNode?: Node
newLoopStartNode?: Node newLoopStartNode?: Node