feat: support enable agent mode show tip in prompt editor

This commit is contained in:
Joel
2026-02-10 17:01:07 +08:00
parent 32fcbcdc62
commit c980f1b2ac
13 changed files with 186 additions and 46 deletions

View File

@ -86,6 +86,7 @@ type Props = {
onBlur?: () => void
onFocus?: () => void
disableToolBlocks?: boolean
footer?: ReactNode
}
const Editor: FC<Props> = ({
@ -131,6 +132,7 @@ const Editor: FC<Props> = ({
onBlur,
onFocus,
disableToolBlocks,
footer,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@ -351,6 +353,11 @@ const Editor: FC<Props> = ({
/>
</div>
)}
{!!footer && (
<div className="px-1 pt-2">
{footer}
</div>
)}
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type Props = {
visible: boolean
onEnable: () => void
}
const ComputerUseTip: FC<Props> = ({
visible,
onEnable,
}) => {
const { t } = useTranslation()
const [dismissed, setDismissed] = React.useState(false)
React.useEffect(() => {
if (!visible)
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
setDismissed(false)
}, [visible])
if (!visible || dismissed)
return null
return (
<div className="relative top-1 rounded-xl border border-state-warning-hover-alt bg-state-warning-hover pb-2 pl-2.5 pr-2 pt-2.5">
<div className="flex items-start gap-1.5">
<span className="i-ri-alert-fill mt-0.5 size-4 shrink-0 text-text-warning-secondary" />
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="text-text-primary system-sm-regular">
{t('nodes.llm.computerUse.enableForPromptTools', { ns: 'workflow' })}
</div>
<div className="flex items-center justify-end gap-1">
<Button
size="small"
onClick={() => setDismissed(true)}
>
{t('nodes.llm.computerUse.dismiss', { ns: 'workflow' })}
</Button>
<Button
size="small"
variant="primary"
onClick={onEnable}
>
{t('nodes.llm.computerUse.enable', { ns: 'workflow' })}
</Button>
</div>
</div>
</div>
</div>
)
}
export default React.memo(ComputerUseTip)

View File

@ -7,9 +7,11 @@ import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import { extractToolConfigIds } from '@/app/components/workflow/utils'
import { PromptRole } from '@/models/debug'
import { useWorkflowStore } from '../../../store'
import { EditionType } from '../../../types'
import ComputerUseTip from './computer-use-tip'
const i18nPrefix = 'nodes.llm'
@ -44,6 +46,8 @@ type Props = {
isSupportSandbox?: boolean
onPromptEditorBlur?: () => void
disableToolBlocks?: boolean
showComputerUseTip?: boolean
onEnableComputerUse?: () => void
}
const roleOptions = [
@ -90,6 +94,8 @@ const ConfigPromptItem: FC<Props> = ({
isSupportSandbox,
onPromptEditorBlur,
disableToolBlocks,
showComputerUseTip,
onEnableComputerUse,
}) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
@ -101,6 +107,11 @@ const ConfigPromptItem: FC<Props> = ({
onPromptChange(prompt)
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
}, [onPromptChange, setControlPromptEditorRerenderKey])
const editorValue = payload.edition_type === EditionType.jinja2
? (payload.jinja2_text || '')
: payload.text
const shouldShowComputerUseTip = !!showComputerUseTip
&& extractToolConfigIds(editorValue || '').size > 0
return (
<Editor
@ -135,7 +146,7 @@ const ConfigPromptItem: FC<Props> = ({
/>
</div>
)}
value={payload.edition_type === EditionType.jinja2 ? (payload.jinja2_text || '') : payload.text}
value={editorValue}
onChange={onPromptChange}
promptMetadata={payload.metadata}
onPromptMetadataChange={onMetadataChange}
@ -162,6 +173,12 @@ const ConfigPromptItem: FC<Props> = ({
isSupportSandbox={isSupportSandbox}
disableToolBlocks={disableToolBlocks}
onBlur={onPromptEditorBlur}
footer={(
<ComputerUseTip
visible={shouldShowComputerUseTip}
onEnable={() => onEnableComputerUse?.()}
/>
)}
/>
)
}

View File

@ -22,6 +22,7 @@ import { cn } from '@/utils/classnames'
import { useWorkflowStore } from '../../../store'
import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '../../../types'
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import ComputerUseTip from './computer-use-tip'
import ConfigContextItem from './config-context-item'
import ConfigPromptItem from './config-prompt-item'
@ -67,6 +68,8 @@ type Props = {
modelConfig: ModelConfig
onPromptEditorBlur?: () => void
disableToolBlocks?: boolean
showComputerUseTip?: boolean
onEnableComputerUse?: () => void
}
const ConfigPrompt: FC<Props> = ({
@ -84,6 +87,8 @@ const ConfigPrompt: FC<Props> = ({
modelConfig,
onPromptEditorBlur,
disableToolBlocks,
showComputerUseTip,
onEnableComputerUse,
}) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
@ -284,6 +289,11 @@ const ConfigPrompt: FC<Props> = ({
}
return false
})()
const completionEditorValue = ((payload as PromptItem).edition_type === EditionType.basic || !(payload as PromptItem).edition_type)
? (payload as PromptItem).text
: ((payload as PromptItem).jinja2_text || '')
const shouldShowCompletionComputerUseTip = !!showComputerUseTip
&& extractToolConfigIds(completionEditorValue || '').size > 0
return (
<div>
@ -364,6 +374,8 @@ const ConfigPrompt: FC<Props> = ({
isSupportSandbox={isSupportSandbox}
onPromptEditorBlur={onPromptEditorBlur}
disableToolBlocks={disableToolBlocks}
showComputerUseTip={showComputerUseTip}
onEnableComputerUse={onEnableComputerUse}
/>
</div>
)
@ -421,7 +433,7 @@ const ConfigPrompt: FC<Props> = ({
instanceId={`${nodeId}-chat-workflow-llm-prompt-editor`}
nodeId={nodeId}
title={<span className="capitalize">{t(`${i18nPrefix}.prompt`, { ns: 'workflow' })}</span>}
value={((payload as PromptItem).edition_type === EditionType.basic || !(payload as PromptItem).edition_type) ? (payload as PromptItem).text : ((payload as PromptItem).jinja2_text || '')}
value={completionEditorValue}
onChange={handleCompletionPromptChange}
promptMetadata={(payload as PromptItem).metadata}
onPromptMetadataChange={handleCompletionMetadataChange}
@ -443,6 +455,12 @@ const ConfigPrompt: FC<Props> = ({
isSupportSandbox={isSupportSandbox}
onBlur={onPromptEditorBlur}
disableToolBlocks={disableToolBlocks}
footer={(
<ComputerUseTip
visible={shouldShowCompletionComputerUseTip}
onEnable={() => onEnableComputerUse?.()}
/>
)}
/>
</div>
)}

View File

@ -102,6 +102,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
isComputerUseBlocked,
isToolsBlocked,
disableToolBlocks,
shouldEnableComputerUseForPromptTools,
structuredOutputDisabledTip,
computerUseDisabledTip,
toolsDisabledTip,
@ -185,6 +186,8 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
modelConfig={model}
onPromptEditorBlur={handlePromptEditorBlur}
disableToolBlocks={disableToolBlocks}
showComputerUseTip={shouldEnableComputerUseForPromptTools}
onEnableComputerUse={() => handleComputerUseChange(true)}
/>
)}

View File

@ -49,7 +49,12 @@ export const useStructuredOutputMutualExclusion = ({
const isStructuredOutputBlocked = readOnly || (hasToolConflict && !isStructuredOutputEnabled)
const isComputerUseBlocked = readOnly || (isStructuredOutputEnabled && !inputs.computer_use)
const isToolsBlocked = readOnly || isStructuredOutputEnabled
const disableToolBlocks = isStructuredOutputEnabled
const shouldEnableComputerUseForPromptTools = isSupportSandbox
&& !readOnly
&& !inputs.computer_use
&& hasToolDependencies
&& !isComputerUseBlocked
const disableToolBlocks = isStructuredOutputEnabled || shouldEnableComputerUseForPromptTools
const structuredOutputDisabledTip = useMemo(() => {
if (readOnly || !isStructuredOutputBlocked)
@ -76,6 +81,7 @@ export const useStructuredOutputMutualExclusion = ({
isComputerUseBlocked,
isToolsBlocked,
disableToolBlocks,
shouldEnableComputerUseForPromptTools,
structuredOutputDisabledTip,
computerUseDisabledTip,
toolsDisabledTip,

View File

@ -133,6 +133,7 @@ const ToolBlockComponent = ({
metadata,
onMetadataChange,
useModal,
disableToolBlocks,
nodeId: contextNodeId,
nodesOutputVars,
availableNodes,
@ -141,6 +142,7 @@ const ToolBlockComponent = ({
metadata: context?.metadata,
onMetadataChange: context?.onMetadataChange,
useModal: context?.useModal,
disableToolBlocks: context?.disableToolBlocks,
nodeId: context?.nodeId,
nodesOutputVars: context?.nodesOutputVars,
availableNodes: context?.availableNodes,
@ -210,8 +212,9 @@ const ToolBlockComponent = ({
return resultMetadata?.tools?.[configId]
}, [activeTabId, configId, fileMetadata, isUsingExternalMetadata, metadata])
const isToolMissing = !currentProvider || !currentTool
const isToolDisabled = Boolean(disableToolBlocks)
const isInteractive = editor.isEditable()
const isTriggerInteractive = isInteractive && !isToolDisabled
const defaultToolValue = useMemo(() => {
if (!currentProvider || !currentTool)
@ -357,6 +360,7 @@ const ToolBlockComponent = ({
return false
return !currentProvider.is_team_authorization
}, [currentProvider])
const isWarningStyle = needAuthorization || isToolMissing || isToolDisabled
const renderIcon = () => {
if (isToolMissing) {
@ -400,19 +404,19 @@ const ToolBlockComponent = ({
className={cn(
'i-ri-equalizer-2-line hidden size-[14px]',
needAuthorization ? 'text-text-warning' : 'text-text-accent',
isInteractive && 'group-hover/tool:block',
isTriggerInteractive && 'group-hover/tool:block',
)}
/>
)
const normalIcon = (
<span className={cn('flex items-center justify-center', isInteractive && 'group-hover/tool:hidden')}>
<span className={cn('flex items-center justify-center', isTriggerInteractive && 'group-hover/tool:hidden')}>
{iconNode}
</span>
)
const iconContent = (
<span className="flex size-4 items-center justify-center">
{normalIcon}
{isInteractive && hoverIcon}
{isTriggerInteractive && hoverIcon}
</span>
)
if (!needAuthorization)
@ -601,7 +605,7 @@ const ToolBlockComponent = ({
<>
<span ref={ref} className="inline-flex">
<Tooltip
disabled={!isToolMissing}
disabled={!isToolMissing || isToolDisabled}
offset={4}
noDecoration
popupClassName="bg-transparent p-0"
@ -610,14 +614,14 @@ const ToolBlockComponent = ({
<span
className={cn(
'group/tool inline-flex items-center gap-[2px] rounded-[5px] border py-px pl-px pr-[3px] shadow-xs',
isInteractive ? 'cursor-pointer' : 'cursor-default',
(needAuthorization || isToolMissing) ? 'border-state-warning-active bg-state-warning-hover' : 'border-state-accent-hover-alt bg-state-accent-hover',
isTriggerInteractive ? 'cursor-pointer' : 'cursor-default',
isWarningStyle ? 'border-state-warning-active bg-state-warning-hover' : 'border-state-accent-hover-alt bg-state-accent-hover',
isSelected && 'border-text-accent',
)}
title={`${provider}.${tool}`}
data-tool-config-id={configId}
onMouseDown={() => {
if (!isInteractive)
if (!isTriggerInteractive)
return
if (!currentProvider || !currentTool)
return
@ -627,17 +631,17 @@ const ToolBlockComponent = ({
}}
>
{renderIcon()}
<span className={cn('max-w-[180px] truncate system-xs-medium', (needAuthorization || isToolMissing) ? 'text-text-warning' : 'text-text-accent')}>
<span className={cn('max-w-[180px] truncate system-xs-medium', isWarningStyle ? 'text-text-warning' : 'text-text-accent')}>
{isToolMissing ? missingDisplayLabel : displayLabel}
</span>
{isToolMissing && (
{(isToolMissing || isToolDisabled) && (
<>
<span className="flex h-4 items-center justify-center p-[2px] text-text-warning">
<span className="i-ri-alert-fill h-3 w-3" />
</span>
</>
)}
{!isToolMissing && needAuthorization && (
{!isToolMissing && !isToolDisabled && needAuthorization && (
<span className="flex h-4 items-center gap-0.5 rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1 text-text-warning system-2xs-medium-uppercase">
{authBadgeLabel}
<span className="i-ri-alert-fill h-3 w-3" />

View File

@ -8,6 +8,7 @@ export type ToolBlockContextValue = {
metadata?: Record<string, unknown>
onMetadataChange?: (metadata: Record<string, unknown>) => void
useModal?: boolean
disableToolBlocks?: boolean
nodeId?: string
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]

View File

@ -118,6 +118,7 @@ const ToolGroupBlockComponent = ({
metadata,
onMetadataChange,
useModal,
disableToolBlocks,
nodeId: contextNodeId,
nodesOutputVars,
availableNodes,
@ -126,6 +127,7 @@ const ToolGroupBlockComponent = ({
metadata: context?.metadata,
onMetadataChange: context?.onMetadataChange,
useModal: context?.useModal,
disableToolBlocks: context?.disableToolBlocks,
nodeId: context?.nodeId,
nodesOutputVars: context?.nodesOutputVars,
availableNodes: context?.availableNodes,
@ -134,6 +136,8 @@ const ToolGroupBlockComponent = ({
const isUsingExternalMetadata = Boolean(onMetadataChange)
const useModalValue = Boolean(useModal)
const isInteractive = editor.isEditable()
const isToolDisabled = Boolean(disableToolBlocks)
const isTriggerInteractive = isInteractive && !isToolDisabled
const [isSettingOpen, setIsSettingOpen] = useState(false)
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null)
const [expandedToolId, setExpandedToolId] = useState<string | null>(null)
@ -339,6 +343,7 @@ const ToolGroupBlockComponent = ({
return false
return !currentProvider.is_team_authorization
}, [currentProvider])
const isWarningStyle = needAuthorization || isToolMissing || isToolDisabled
const readmeEntrance = useMemo(() => {
if (!currentProvider)
@ -588,19 +593,19 @@ const ToolGroupBlockComponent = ({
className={cn(
'i-ri-equalizer-2-line hidden size-[14px]',
(needAuthorization || isToolMissing) ? 'text-text-warning' : 'text-text-accent',
isInteractive && 'group-hover:block',
isTriggerInteractive && 'group-hover:block',
)}
/>
)
const normalIcon = (
<span className={cn('flex items-center justify-center', isInteractive && 'group-hover:hidden')}>
<span className={cn('flex items-center justify-center', isTriggerInteractive && 'group-hover:hidden')}>
{iconNode}
</span>
)
const iconContent = (
<span className="flex size-4 items-center justify-center">
{normalIcon}
{isInteractive && hoverIcon}
{isTriggerInteractive && hoverIcon}
</span>
)
if (!needAuthorization)
@ -865,12 +870,46 @@ const ToolGroupBlockComponent = ({
)
const groupPanelContent = expandedToolId && toolDetailContent ? toolDetailContent : groupListContent
const toolStatusBadge = (() => {
if (isToolMissing) {
return (
<>
<span className="flex h-4 min-w-4 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 text-text-tertiary system-2xs-medium-uppercase">
{missingToolCount}
</span>
<span className="flex h-4 items-center justify-center p-[2px] text-text-warning">
<span className="i-ri-alert-fill h-3 w-3" />
</span>
</>
)
}
if (isToolDisabled) {
return (
<span className="flex h-4 items-center justify-center p-[2px] text-text-warning">
<span className="i-ri-alert-fill h-3 w-3" />
</span>
)
}
if (needAuthorization) {
return (
<span className="flex h-4 items-center gap-0.5 rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1 text-text-warning system-2xs-medium-uppercase">
{authBadgeLabel}
<span className="i-ri-alert-fill h-3 w-3" />
</span>
)
}
return (
<span className="flex h-4 items-center rounded-[5px] border border-text-accent-secondary bg-components-badge-bg-dimm px-1 text-text-accent-secondary system-2xs-medium-uppercase">
{displayEnabledCount}
</span>
)
})()
return (
<>
<span ref={ref} className="inline-flex">
<Tooltip
disabled={!isToolMissing}
disabled={!isToolMissing || isToolDisabled}
offset={4}
noDecoration
popupClassName="bg-transparent p-0"
@ -879,13 +918,13 @@ const ToolGroupBlockComponent = ({
<span
className={cn(
'inline-flex items-center gap-[2px] rounded-[5px] border px-px py-[1px] shadow-xs',
isInteractive ? 'group cursor-pointer' : 'cursor-default',
(needAuthorization || isToolMissing) ? 'border-state-warning-active bg-state-warning-hover' : 'border-state-accent-hover-alt bg-state-accent-hover',
isTriggerInteractive ? 'group cursor-pointer' : 'cursor-default',
isWarningStyle ? 'border-state-warning-active bg-state-warning-hover' : 'border-state-accent-hover-alt bg-state-accent-hover',
isSelected && 'border-text-accent',
)}
title={providerLabel}
onMouseDown={() => {
if (!isInteractive)
if (!isTriggerInteractive)
return
if (!toolItems.length)
return
@ -893,32 +932,10 @@ const ToolGroupBlockComponent = ({
}}
>
{renderIcon()}
<span className={cn('max-w-[160px] truncate system-xs-medium', (needAuthorization || isToolMissing) ? 'text-text-warning' : 'text-text-accent')}>
<span className={cn('max-w-[160px] truncate system-xs-medium', isWarningStyle ? 'text-text-warning' : 'text-text-accent')}>
{isToolMissing ? missingDisplayLabel : providerLabel}
</span>
{isToolMissing
? (
<>
<span className="flex h-4 min-w-4 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 text-text-tertiary system-2xs-medium-uppercase">
{missingToolCount}
</span>
<span className="flex h-4 items-center justify-center p-[2px] text-text-warning">
<span className="i-ri-alert-fill h-3 w-3" />
</span>
</>
)
: needAuthorization
? (
<span className="flex h-4 items-center gap-0.5 rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1 text-text-warning system-2xs-medium-uppercase">
{authBadgeLabel}
<span className="i-ri-alert-fill h-3 w-3" />
</span>
)
: (
<span className="flex h-4 items-center rounded-[5px] border border-text-accent-secondary bg-components-badge-bg-dimm px-1 text-text-accent-secondary system-2xs-medium-uppercase">
{displayEnabledCount}
</span>
)}
{toolStatusBadge}
</span>
</Tooltip>
</span>