dify
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
text: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const AddButton: FC<Props> = ({
|
||||
className,
|
||||
text,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
className={cn('w-full', className)}
|
||||
variant='tertiary'
|
||||
size='medium'
|
||||
onClick={onClick}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-3.5 w-3.5' />
|
||||
<div>{text}</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddButton)
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useStore } from '../../../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '../../../hooks'
|
||||
import type {
|
||||
ValueSelector,
|
||||
Var,
|
||||
VarType,
|
||||
} from '../../../types'
|
||||
import { useVariableAssigner } from '../../variable-assigner/hooks'
|
||||
import { filterVar } from '../../variable-assigner/utils'
|
||||
import AddVariablePopup from './add-variable-popup'
|
||||
|
||||
type AddVariablePopupWithPositionProps = {
|
||||
nodeId: string
|
||||
nodeData: any
|
||||
}
|
||||
const AddVariablePopupWithPosition = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
}: AddVariablePopupWithPositionProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const showAssignVariablePopup = useStore(s => s.showAssignVariablePopup)
|
||||
const setShowAssignVariablePopup = useStore(s => s.setShowAssignVariablePopup)
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleAddVariableInAddVariablePopupWithPosition } = useVariableAssigner()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
|
||||
const outputType = useMemo(() => {
|
||||
if (!showAssignVariablePopup)
|
||||
return ''
|
||||
|
||||
const groupEnabled = showAssignVariablePopup.variableAssignerNodeData.advanced_settings?.group_enabled
|
||||
|
||||
if (!groupEnabled)
|
||||
return showAssignVariablePopup.variableAssignerNodeData.output_type
|
||||
|
||||
const group = showAssignVariablePopup.variableAssignerNodeData.advanced_settings?.groups.find(group => group.groupId === showAssignVariablePopup.variableAssignerNodeHandleId)
|
||||
return group?.output_type || ''
|
||||
}, [showAssignVariablePopup])
|
||||
const availableVars = useMemo(() => {
|
||||
if (!showAssignVariablePopup)
|
||||
return []
|
||||
|
||||
return getNodeAvailableVars({
|
||||
parentNode: showAssignVariablePopup.parentNode,
|
||||
beforeNodes: [
|
||||
...getBeforeNodesInSameBranch(showAssignVariablePopup.nodeId),
|
||||
{
|
||||
id: showAssignVariablePopup.nodeId,
|
||||
data: showAssignVariablePopup.nodeData,
|
||||
} as any,
|
||||
],
|
||||
hideEnv: true,
|
||||
hideChatVar: !isChatMode,
|
||||
isChatMode,
|
||||
filterVar: filterVar(outputType as VarType),
|
||||
})
|
||||
.map(node => ({
|
||||
...node,
|
||||
vars: node.isStartNode ? node.vars.filter(v => !v.variable.startsWith('sys.')) : node.vars,
|
||||
}))
|
||||
.filter(item => item.vars.length > 0)
|
||||
}, [showAssignVariablePopup, getNodeAvailableVars, getBeforeNodesInSameBranch, isChatMode, outputType])
|
||||
|
||||
useClickAway(() => {
|
||||
if (nodeData._holdAddVariablePopup) {
|
||||
handleNodeDataUpdate({
|
||||
id: nodeId,
|
||||
data: {
|
||||
_holdAddVariablePopup: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
handleNodeDataUpdate({
|
||||
id: nodeId,
|
||||
data: {
|
||||
_showAddVariablePopup: false,
|
||||
},
|
||||
})
|
||||
setShowAssignVariablePopup(undefined)
|
||||
}
|
||||
}, ref)
|
||||
|
||||
const handleAddVariable = useCallback((value: ValueSelector, varDetail: Var) => {
|
||||
if (showAssignVariablePopup) {
|
||||
handleAddVariableInAddVariablePopupWithPosition(
|
||||
showAssignVariablePopup.nodeId,
|
||||
showAssignVariablePopup.variableAssignerNodeId,
|
||||
showAssignVariablePopup.variableAssignerNodeHandleId,
|
||||
value,
|
||||
varDetail,
|
||||
)
|
||||
}
|
||||
}, [showAssignVariablePopup, handleAddVariableInAddVariablePopupWithPosition])
|
||||
|
||||
if (!showAssignVariablePopup)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-10'
|
||||
style={{
|
||||
left: showAssignVariablePopup.x,
|
||||
top: showAssignVariablePopup.y,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<AddVariablePopup
|
||||
availableVars={availableVars}
|
||||
onSelect={handleAddVariable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddVariablePopupWithPosition)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export type AddVariablePopupProps = {
|
||||
availableVars: NodeOutPutVar[]
|
||||
onSelect: (value: ValueSelector, item: Var) => void
|
||||
}
|
||||
export const AddVariablePopup = ({
|
||||
availableVars,
|
||||
onSelect,
|
||||
}: AddVariablePopupProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'>
|
||||
<div className='flex h-[34px] items-center border-b-[0.5px] border-b-divider-regular px-4 text-[13px] font-semibold text-text-secondary'>
|
||||
{t('workflow.nodes.variableAssigner.setAssignVariable')}
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={availableVars}
|
||||
onChange={onSelect}
|
||||
isSupportFileVar
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddVariablePopup)
|
||||
@@ -0,0 +1,242 @@
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { Strategy } from './agent-strategy'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Link from 'next/link'
|
||||
import { InstallPluginButton } from './install-plugin-button'
|
||||
import ViewTypeSelect, { ViewType } from '../../../block-selector/view-type-select'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import Tools from '../../../block-selector/tools'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { PluginCategoryEnum, type StrategyPluginDetail } from '@/app/components/plugins/types'
|
||||
import type { ToolWithProvider } from '../../../types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { useStrategyInfo } from '../../agent/use-config'
|
||||
import { SwitchPluginVersion } from './switch-plugin-version'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
|
||||
import { ToolTipContent } from '@/app/components/base/tooltip/content'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const DEFAULT_TAGS: ListProps['tags'] = []
|
||||
|
||||
const NotFoundWarn = (props: {
|
||||
title: ReactNode,
|
||||
description: ReactNode
|
||||
}) => {
|
||||
const { title, description } = props
|
||||
|
||||
const { t } = useTranslation()
|
||||
return <Tooltip
|
||||
popupContent={
|
||||
<div className='space-y-1 text-xs'>
|
||||
<h3 className='font-semibold text-text-primary'>
|
||||
{title}
|
||||
</h3>
|
||||
<p className='tracking-tight text-text-secondary'>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<Link href={'/plugins'} className='tracking-tight text-text-accent'>
|
||||
{t('workflow.nodes.agent.linkToPlugin')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<RiErrorWarningFill className='size-4 text-text-destructive' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => string): ToolWithProvider[] {
|
||||
return input.map((item) => {
|
||||
const res: ToolWithProvider = {
|
||||
id: item.plugin_unique_identifier,
|
||||
author: item.declaration.identity.author,
|
||||
name: item.declaration.identity.name,
|
||||
description: item.declaration.identity.description as any,
|
||||
plugin_id: item.plugin_id,
|
||||
icon: getIcon(item.declaration.identity.icon),
|
||||
label: item.declaration.identity.label as any,
|
||||
type: CollectionType.all,
|
||||
meta: item.meta,
|
||||
tools: item.declaration.strategies.map(strategy => ({
|
||||
name: strategy.identity.name,
|
||||
author: strategy.identity.author,
|
||||
label: strategy.identity.label as any,
|
||||
description: strategy.description,
|
||||
parameters: strategy.parameters as any,
|
||||
output_schema: strategy.output_schema,
|
||||
labels: [],
|
||||
})),
|
||||
team_credentials: {},
|
||||
is_team_authorization: true,
|
||||
allow_delete: false,
|
||||
labels: [],
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
export type AgentStrategySelectorProps = {
|
||||
value?: Strategy,
|
||||
onChange: (value?: Strategy) => void,
|
||||
canChooseMCPTool: boolean,
|
||||
}
|
||||
|
||||
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
const { value, onChange, canChooseMCPTool } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
|
||||
const [query, setQuery] = useState('')
|
||||
const stra = useStrategyProviders()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const list = stra.data ? formatStrategy(stra.data, getIconUrl) : undefined
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!list) return []
|
||||
return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase()))
|
||||
}, [query, list])
|
||||
const { strategyStatus, refetch: refetchStrategyInfo } = useStrategyInfo(
|
||||
value?.agent_strategy_provider_name,
|
||||
value?.agent_strategy_name,
|
||||
)
|
||||
|
||||
const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external'
|
||||
&& !strategyStatus.plugin.installed && !!value
|
||||
|
||||
const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external'
|
||||
&& !strategyStatus?.isExistInPlugin && !!value
|
||||
|
||||
const showSwitchVersion = !strategyStatus?.isExistInPlugin
|
||||
&& strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed && !!value
|
||||
|
||||
const showInstallButton = !strategyStatus?.isExistInPlugin
|
||||
&& strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed && !!value
|
||||
|
||||
const icon = list?.find(
|
||||
coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name),
|
||||
)?.icon as string | undefined
|
||||
const { t } = useTranslation()
|
||||
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
queryPluginsWithDebounced: fetchPlugins,
|
||||
plugins: notInstalledPlugins = [],
|
||||
} = useMarketplacePlugins()
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable_marketplace) return
|
||||
if (query) {
|
||||
fetchPlugins({
|
||||
query,
|
||||
category: PluginCategoryEnum.agent,
|
||||
})
|
||||
}
|
||||
}, [query])
|
||||
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
|
||||
return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'>
|
||||
<PortalToFollowElemTrigger className='w-full'>
|
||||
<div
|
||||
className='flex h-8 w-full select-none items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 hover:bg-state-base-hover-alt'
|
||||
onClick={() => setOpen(o => !o)}
|
||||
>
|
||||
{ }
|
||||
{icon && <div className='flex h-6 w-6 items-center justify-center'><img
|
||||
src={icon}
|
||||
width={20}
|
||||
height={20}
|
||||
className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
|
||||
alt='icon'
|
||||
/></div>}
|
||||
<p
|
||||
className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'px-1 text-xs')}
|
||||
>
|
||||
{value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}
|
||||
</p>
|
||||
<div className='ml-auto flex items-center gap-1'>
|
||||
{showInstallButton && value && <InstallPluginButton
|
||||
onClick={e => e.stopPropagation()}
|
||||
size={'small'}
|
||||
uniqueIdentifier={value.plugin_unique_identifier}
|
||||
/>}
|
||||
{showPluginNotInstalledWarn
|
||||
? <NotFoundWarn
|
||||
title={t('workflow.nodes.agent.pluginNotInstalled')}
|
||||
description={t('workflow.nodes.agent.pluginNotInstalledDesc')}
|
||||
/>
|
||||
: showUnsupportedStrategy
|
||||
? <NotFoundWarn
|
||||
title={t('workflow.nodes.agent.unsupportedStrategy')}
|
||||
description={t('workflow.nodes.agent.strategyNotFoundDesc')}
|
||||
/>
|
||||
: <RiArrowDownSLine className='size-4 text-text-tertiary' />
|
||||
}
|
||||
{showSwitchVersion && <SwitchPluginVersion
|
||||
uniqueIdentifier={value.plugin_unique_identifier}
|
||||
tooltip={<ToolTipContent
|
||||
title={t('workflow.nodes.agent.unsupportedStrategy')}>
|
||||
{t('workflow.nodes.agent.strategyNotFoundDescAndSwitchVersion')}
|
||||
</ToolTipContent>}
|
||||
onChange={() => {
|
||||
refetchStrategyInfo()
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='w-[388px] overflow-hidden rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow'>
|
||||
<header className='flex gap-1 p-2'>
|
||||
<SearchInput placeholder={t('workflow.nodes.agent.strategy.searchPlaceholder')} value={query} onChange={setQuery} className={'w-full'} />
|
||||
<ViewTypeSelect viewType={viewType} onChange={setViewType} />
|
||||
</header>
|
||||
<main className="relative flex w-full flex-col overflow-hidden md:max-h-[300px] xl:max-h-[400px] 2xl:max-h-[564px]" ref={wrapElemRef}>
|
||||
<Tools
|
||||
tools={filteredTools}
|
||||
viewType={viewType}
|
||||
onSelect={(_, tool) => {
|
||||
onChange({
|
||||
agent_strategy_name: tool!.tool_name,
|
||||
agent_strategy_provider_name: tool!.provider_name,
|
||||
agent_strategy_label: tool!.tool_label,
|
||||
agent_output_schema: tool!.output_schema || {},
|
||||
plugin_unique_identifier: tool!.provider_id,
|
||||
meta: tool!.meta,
|
||||
})
|
||||
setOpen(false)
|
||||
}}
|
||||
className='h-full max-h-full max-w-none overflow-y-auto'
|
||||
indexBarClassName='top-0 xl:top-36'
|
||||
hasSearchText={false}
|
||||
canNotSelectMultiple
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isAgent
|
||||
/>
|
||||
{enable_marketplace && <PluginList
|
||||
ref={pluginRef}
|
||||
wrapElemRef={wrapElemRef}
|
||||
list={notInstalledPlugins}
|
||||
searchText={query}
|
||||
tags={DEFAULT_TAGS}
|
||||
disableMaxWidth
|
||||
/>}
|
||||
</main>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
})
|
||||
|
||||
AgentStrategySelector.displayName = 'AgentStrategySelector'
|
||||
@@ -0,0 +1,249 @@
|
||||
import type { CredentialFormSchemaNumberInput, CredentialFormSchemaTextInput } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { type CredentialFormSchema, FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ToolVarInputs } from '../../tool/types'
|
||||
import ListEmpty from '@/app/components/base/list-empty'
|
||||
import { AgentStrategySelector } from './agent-strategy-selector'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { InputNumber } from '@/app/components/base/input-number'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
|
||||
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
|
||||
import Field from './field'
|
||||
import { type ComponentProps, memo } from 'react'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import Editor from './prompt/editor'
|
||||
import { useWorkflowStore } from '../../../store'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import type { NodeOutPutVar } from '../../../types'
|
||||
import type { Node } from 'reactflow'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type Strategy = {
|
||||
agent_strategy_provider_name: string
|
||||
agent_strategy_name: string
|
||||
agent_strategy_label: string
|
||||
agent_output_schema: Record<string, any>
|
||||
plugin_unique_identifier: string
|
||||
meta?: PluginMeta
|
||||
}
|
||||
|
||||
export type AgentStrategyProps = {
|
||||
strategy?: Strategy
|
||||
onStrategyChange: (strategy?: Strategy) => void
|
||||
formSchema: CredentialFormSchema[]
|
||||
formValue: ToolVarInputs
|
||||
onFormValueChange: (value: ToolVarInputs) => void
|
||||
nodeOutputVars?: NodeOutPutVar[],
|
||||
availableNodes?: Node[],
|
||||
nodeId?: string
|
||||
canChooseMCPTool: boolean
|
||||
}
|
||||
|
||||
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
|
||||
|
||||
type ToolSelectorSchema = CustomSchema<'tool-selector'>
|
||||
type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
|
||||
|
||||
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
|
||||
|
||||
export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
setControlPromptEditorRerenderKey,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const override: ComponentProps<typeof Form<CustomField>>['override'] = [
|
||||
[FormTypeEnum.textNumber, FormTypeEnum.textInput],
|
||||
(schema, props) => {
|
||||
switch (schema.type) {
|
||||
case FormTypeEnum.textInput: {
|
||||
const def = schema as CredentialFormSchemaTextInput
|
||||
const value = props.value[schema.variable] || schema.default
|
||||
const instanceId = schema.variable
|
||||
const onChange = (value: string) => {
|
||||
props.onChange({ ...props.value, [schema.variable]: value })
|
||||
}
|
||||
const handleGenerated = (value: string) => {
|
||||
onChange(value)
|
||||
setControlPromptEditorRerenderKey(Math.random())
|
||||
}
|
||||
return <Editor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onGenerated={handleGenerated}
|
||||
instanceId={instanceId}
|
||||
key={instanceId}
|
||||
title={renderI18nObject(schema.label)}
|
||||
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
|
||||
containerBackgroundClassName='bg-transparent'
|
||||
gradientBorder={false}
|
||||
nodeId={nodeId}
|
||||
isSupportPromptGenerator={!!def.auto_generate?.type}
|
||||
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
|
||||
editorContainerClassName='px-0'
|
||||
availableNodes={availableNodes}
|
||||
nodesOutputVars={nodeOutputVars}
|
||||
isSupportJinja={def.template?.enabled}
|
||||
required={def.required}
|
||||
varList={[]}
|
||||
modelConfig={
|
||||
defaultModel.data
|
||||
? {
|
||||
mode: AppModeEnum.CHAT,
|
||||
name: defaultModel.data.model,
|
||||
provider: defaultModel.data.provider.provider,
|
||||
completion_params: {},
|
||||
} : undefined
|
||||
}
|
||||
placeholderClassName='px-2 py-1'
|
||||
titleClassName='system-sm-semibold-uppercase text-text-secondary text-[13px]'
|
||||
inputClassName='px-2 py-1 bg-components-input-bg-normal focus:bg-components-input-bg-active focus:border-components-input-border-active focus:border rounded-lg'
|
||||
/>
|
||||
}
|
||||
case FormTypeEnum.textNumber: {
|
||||
const def = schema as CredentialFormSchemaNumberInput
|
||||
if (!def.max || !def.min)
|
||||
return false
|
||||
|
||||
const defaultValue = schema.default ? Number.parseInt(schema.default) : 1
|
||||
const value = props.value[schema.variable] || defaultValue
|
||||
const onChange = (value: number) => {
|
||||
props.onChange({ ...props.value, [schema.variable]: value })
|
||||
}
|
||||
return <Field
|
||||
title={<>
|
||||
{renderI18nObject(def.label)} {def.required && <span className='text-red-500'>*</span>}
|
||||
</>}
|
||||
key={def.variable}
|
||||
tooltip={def.tooltip && renderI18nObject(def.tooltip)}
|
||||
inline
|
||||
>
|
||||
<div className='flex w-[200px] items-center gap-3'>
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className='w-full'
|
||||
min={def.min}
|
||||
max={def.max}
|
||||
/>
|
||||
<InputNumber
|
||||
value={value}
|
||||
// TODO: maybe empty, handle this
|
||||
onChange={onChange as any}
|
||||
defaultValue={defaultValue}
|
||||
size='regular'
|
||||
min={def.min}
|
||||
max={def.max}
|
||||
className='w-12'
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
const renderField: ComponentProps<typeof Form<CustomField>>['customRenderField'] = (schema, props) => {
|
||||
switch (schema.type) {
|
||||
case FormTypeEnum.toolSelector: {
|
||||
const value = props.value[schema.variable]
|
||||
const onChange = (value: any) => {
|
||||
props.onChange({ ...props.value, [schema.variable]: value })
|
||||
}
|
||||
return (
|
||||
<Field
|
||||
title={<>
|
||||
{renderI18nObject(schema.label)} {schema.required && <span className='text-red-500'>*</span>}
|
||||
</>}
|
||||
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
|
||||
>
|
||||
<ToolSelector
|
||||
nodeId={props.nodeId || ''}
|
||||
nodeOutputVars={props.nodeOutputVars || []}
|
||||
availableNodes={props.availableNodes || []}
|
||||
scope={schema.scope}
|
||||
value={value}
|
||||
onSelect={item => onChange(item)}
|
||||
onDelete={() => onChange(null)}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onSelectMultiple={noop}
|
||||
/>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
case FormTypeEnum.multiToolSelector: {
|
||||
const value = props.value[schema.variable]
|
||||
const onChange = (value: any) => {
|
||||
props.onChange({ ...props.value, [schema.variable]: value })
|
||||
}
|
||||
return (
|
||||
<MultipleToolSelector
|
||||
nodeId={props.nodeId || ''}
|
||||
nodeOutputVars={props.nodeOutputVars || []}
|
||||
availableNodes={props.availableNodes || []}
|
||||
scope={schema.scope}
|
||||
value={value || []}
|
||||
label={renderI18nObject(schema.label)}
|
||||
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
|
||||
onChange={onChange}
|
||||
supportCollapse
|
||||
required={schema.required}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return <div className='space-y-2'>
|
||||
<AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} />
|
||||
{
|
||||
strategy
|
||||
? <div>
|
||||
<Form<CustomField>
|
||||
formSchemas={[
|
||||
...formSchema,
|
||||
]}
|
||||
value={formValue}
|
||||
onChange={onFormValueChange}
|
||||
validating={false}
|
||||
showOnVariableMap={{}}
|
||||
isEditMode={true}
|
||||
isAgentStrategy={true}
|
||||
fieldLabelClassName='uppercase'
|
||||
customRenderField={renderField}
|
||||
override={override}
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
: <ListEmpty
|
||||
icon={<Agent className='h-5 w-5 shrink-0 text-text-accent' />}
|
||||
title={t('workflow.nodes.agent.strategy.configureTip')}
|
||||
description={<div className='text-xs text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.strategy.configureTipDesc')} <br />
|
||||
<Link href={docLink('/guides/workflow/node/agent#select-an-agent-strategy', {
|
||||
'zh-Hans': '/guides/workflow/node/agent#选择-agent-策略',
|
||||
'ja-JP': '/guides/workflow/node/agent#エージェント戦略の選択',
|
||||
})}
|
||||
className='text-text-accent-secondary' target='_blank'>
|
||||
{t('workflow.nodes.agent.learnMore')}
|
||||
</Link>
|
||||
</div>}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
AgentStrategy.displayName = 'AgentStrategy'
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
value: boolean
|
||||
required?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const BoolInput: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
required,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleChange = useCallback(() => {
|
||||
onChange(!value)
|
||||
}, [value, onChange])
|
||||
return (
|
||||
<div className='flex h-6 items-center gap-2'>
|
||||
<Checkbox
|
||||
className='!h-4 !w-4'
|
||||
checked={!!value}
|
||||
onCheck={handleChange}
|
||||
/>
|
||||
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
|
||||
{name}
|
||||
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(BoolInput)
|
||||
@@ -0,0 +1,338 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import type { InputVar } from '../../../../types'
|
||||
import { BlockEnum, InputVarType, SupportUploadFileTypes } from '../../../../types'
|
||||
import CodeEditor from '../editor/code-editor'
|
||||
import { CodeLanguage } from '../../../code/types'
|
||||
import TextEditor from '../editor/text-editor'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import BoolInput from './bool-input'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
type Props = {
|
||||
payload: InputVar
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
className?: string
|
||||
autoFocus?: boolean
|
||||
inStepRun?: boolean
|
||||
}
|
||||
|
||||
const FormItem: FC<Props> = ({
|
||||
payload,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
autoFocus,
|
||||
inStepRun = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type } = payload
|
||||
const fileSettings = useHooksStore(s => s.configsMap?.fileSettings)
|
||||
|
||||
const handleArrayItemChange = useCallback((index: number) => {
|
||||
return (newValue: any) => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
draft[index] = newValue
|
||||
})
|
||||
onChange(newValues)
|
||||
}
|
||||
}, [value, onChange])
|
||||
|
||||
const handleArrayItemRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newValues)
|
||||
}
|
||||
}, [value, onChange])
|
||||
const nodeKey = (() => {
|
||||
if (typeof payload.label === 'object') {
|
||||
const { nodeType, nodeName, variable, isChatVar } = payload.label
|
||||
return (
|
||||
<div className='flex h-full items-center'>
|
||||
{!isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon type={nodeType || BlockEnum.Start} />
|
||||
</div>
|
||||
<div className='mx-0.5 max-w-[150px] truncate text-xs font-medium text-gray-700' title={nodeName}>
|
||||
{nodeName}
|
||||
</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!isChatVar && <Variable02 className='h-3.5 w-3.5' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('ml-0.5 max-w-[150px] truncate text-xs font-medium', isChatVar && 'text-text-secondary')} title={variable} >
|
||||
{variable}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
|
||||
const isBooleanType = type === InputVarType.checkbox
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
|
||||
const isContext = type === InputVarType.contexts
|
||||
const isIterator = type === InputVarType.iterator
|
||||
const isIteratorItemFile = isIterator && payload.isFileItem
|
||||
const singleFileValue = useMemo(() => {
|
||||
if (payload.variable === '#files#')
|
||||
return value?.[0] || []
|
||||
|
||||
return value ? [value] : []
|
||||
}, [payload.variable, value])
|
||||
const handleSingleFileChange = useCallback((files: FileEntity[]) => {
|
||||
if (payload.variable === '#files#')
|
||||
onChange(files)
|
||||
else if (files.length)
|
||||
onChange(files[0])
|
||||
else
|
||||
onChange(null)
|
||||
}, [onChange, payload.variable])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
{!isArrayLikeType && !isBooleanType && (
|
||||
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
|
||||
<div className='truncate'>
|
||||
{typeof payload.label === 'object' ? nodeKey : payload.label}
|
||||
</div>
|
||||
{payload.hide === true ? (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.panel.optional_and_hidden')}
|
||||
</span>
|
||||
) : (
|
||||
!payload.required && (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.panel.optional')}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='grow'>
|
||||
{
|
||||
type === InputVarType.textInput && (
|
||||
<Input
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.number && (
|
||||
<Input
|
||||
type="number"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
type === InputVarType.select && (
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={value || payload.default || ''}
|
||||
items={payload.options?.map(option => ({ name: option, value: option })) || []}
|
||||
onSelect={i => onChange(i.value)}
|
||||
allowSearch={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{isBooleanType && (
|
||||
<BoolInput
|
||||
name={payload.label as string}
|
||||
value={!!value}
|
||||
required={payload.required}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
type === InputVarType.json && (
|
||||
<CodeEditor
|
||||
value={value}
|
||||
title={<span>JSON</span>}
|
||||
language={CodeLanguage.json}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{type === InputVarType.jsonObject && (
|
||||
<CodeEditor
|
||||
value={value}
|
||||
language={CodeLanguage.json}
|
||||
onChange={onChange}
|
||||
noWrapper
|
||||
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
|
||||
placeholder={
|
||||
<div className='whitespace-pre'>{payload.json_schema}</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(type === InputVarType.singleFile) && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={singleFileValue}
|
||||
onChange={handleSingleFileChange}
|
||||
fileConfig={{
|
||||
allowed_file_types: inStepRun && (!payload.allowed_file_types || payload.allowed_file_types.length === 0)
|
||||
? [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
SupportUploadFileTypes.audio,
|
||||
SupportUploadFileTypes.video,
|
||||
]
|
||||
: payload.allowed_file_types,
|
||||
allowed_file_extensions: inStepRun && (!payload.allowed_file_extensions || payload.allowed_file_extensions.length === 0)
|
||||
? [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
...FILE_EXTS[SupportUploadFileTypes.audio],
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
]
|
||||
: payload.allowed_file_extensions,
|
||||
allowed_file_upload_methods: inStepRun ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
fileUploadConfig: fileSettings?.fileUploadConfig,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(type === InputVarType.multiFiles || isIteratorItemFile) && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={value}
|
||||
onChange={files => onChange(files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: (inStepRun || isIteratorItemFile) && (!payload.allowed_file_types || payload.allowed_file_types.length === 0)
|
||||
? [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
SupportUploadFileTypes.audio,
|
||||
SupportUploadFileTypes.video,
|
||||
]
|
||||
: payload.allowed_file_types,
|
||||
allowed_file_extensions: (inStepRun || isIteratorItemFile) && (!payload.allowed_file_extensions || payload.allowed_file_extensions.length === 0)
|
||||
? [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
...FILE_EXTS[SupportUploadFileTypes.audio],
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
]
|
||||
: payload.allowed_file_extensions,
|
||||
allowed_file_upload_methods: (inStepRun || isIteratorItemFile) ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods,
|
||||
number_limits: (inStepRun || isIteratorItemFile) ? 5 : payload.max_length,
|
||||
fileUploadConfig: fileSettings?.fileUploadConfig,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
type === InputVarType.files && (
|
||||
<TextGenerationImageUploader
|
||||
settings={{
|
||||
...fileSettings,
|
||||
detail: fileSettings?.image?.detail || Resolution.high,
|
||||
transfer_methods: fileSettings?.allowed_file_upload_methods || [],
|
||||
} as any}
|
||||
onFilesChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
isContext && (
|
||||
<div className='space-y-2'>
|
||||
{(value || []).map((item: any, index: number) => (
|
||||
<CodeEditor
|
||||
key={index}
|
||||
value={item}
|
||||
title={<span>JSON</span>}
|
||||
headerRight={
|
||||
(value as any).length > 1
|
||||
? (<RiDeleteBinLine
|
||||
onClick={handleArrayItemRemove(index)}
|
||||
className='mr-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleArrayItemChange(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(isIterator && !isIteratorItemFile) && (
|
||||
<div className='space-y-2'>
|
||||
{(value || []).map((item: any, index: number) => (
|
||||
<TextEditor
|
||||
key={index}
|
||||
isInNode
|
||||
value={item}
|
||||
title={<span>{t('appDebug.variableConfig.content')} {index + 1} </span>}
|
||||
onChange={handleArrayItemChange(index)}
|
||||
headerRight={
|
||||
(value as any).length > 1
|
||||
? (<RiDeleteBinLine
|
||||
onClick={handleArrayItemRemove(index)}
|
||||
className='mr-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FormItem)
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { InputVar } from '../../../../types'
|
||||
import FormItem from './form-item'
|
||||
import cn from '@/utils/classnames'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
|
||||
|
||||
export type Props = {
|
||||
className?: string
|
||||
label?: string
|
||||
inputs: InputVar[]
|
||||
values: Record<string, string>
|
||||
onChange: (newValues: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const Form: FC<Props> = ({
|
||||
className,
|
||||
label,
|
||||
inputs,
|
||||
values,
|
||||
onChange,
|
||||
}) => {
|
||||
const mapKeysWithSameValueSelector = useMemo(() => {
|
||||
const keysWithSameValueSelector = (key: string) => {
|
||||
const targetValueSelector = inputs.find(
|
||||
item => item.variable === key,
|
||||
)?.value_selector
|
||||
if (!targetValueSelector)
|
||||
return [key]
|
||||
|
||||
const result: string[] = []
|
||||
inputs.forEach((item) => {
|
||||
if (item.value_selector?.join('.') === targetValueSelector.join('.'))
|
||||
result.push(item.variable)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const m = new Map()
|
||||
for (const input of inputs)
|
||||
m.set(input.variable, keysWithSameValueSelector(input.variable))
|
||||
|
||||
return m
|
||||
}, [inputs])
|
||||
const valuesRef = useRef(values)
|
||||
useEffect(() => {
|
||||
valuesRef.current = values
|
||||
}, [values])
|
||||
const handleChange = useCallback((key: string) => {
|
||||
const mKeys = mapKeysWithSameValueSelector.get(key) ?? [key]
|
||||
return (value: any) => {
|
||||
const newValues = produce(valuesRef.current, (draft) => {
|
||||
for (const k of mKeys)
|
||||
draft[k] = value
|
||||
})
|
||||
onChange(newValues)
|
||||
}
|
||||
}, [valuesRef, onChange, mapKeysWithSameValueSelector])
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(inputs[0]?.type)
|
||||
const isIteratorItemFile = inputs[0]?.type === InputVarType.iterator && inputs[0]?.isFileItem
|
||||
|
||||
const isContext = inputs[0]?.type === InputVarType.contexts
|
||||
const handleAddContext = useCallback(() => {
|
||||
const newValues = produce(values, (draft: any) => {
|
||||
const key = inputs[0].variable
|
||||
if (!draft[key])
|
||||
draft[key] = []
|
||||
draft[key].push(isContext ? RETRIEVAL_OUTPUT_STRUCT : '')
|
||||
})
|
||||
onChange(newValues)
|
||||
}, [values, onChange, inputs, isContext])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'space-y-2')}>
|
||||
{label && (
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<div className='system-xs-medium-uppercase flex h-6 items-center text-text-tertiary'>{label}</div>
|
||||
{isArrayLikeType && !isIteratorItemFile && (
|
||||
<AddButton onClick={handleAddContext} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{inputs.map((input, index) => {
|
||||
return (
|
||||
<FormItem
|
||||
inStepRun
|
||||
key={index}
|
||||
payload={input}
|
||||
value={values[input.variable]}
|
||||
onChange={handleChange(input.variable)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Form)
|
||||
@@ -0,0 +1,178 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Props as FormProps } from './form'
|
||||
import Form from './form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import type { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
|
||||
import PanelWrap from './panel-wrap'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
export type BeforeRunFormProps = {
|
||||
nodeName: string
|
||||
nodeType?: BlockEnum
|
||||
toolIcon?: string | Emoji
|
||||
onHide: () => void
|
||||
onRun: (submitData: Record<string, any>) => void
|
||||
onStop: () => void
|
||||
runningStatus: NodeRunningStatus
|
||||
forms: FormProps[]
|
||||
showSpecialResultPanel?: boolean
|
||||
existVarValuesInForms: Record<string, any>[]
|
||||
filteredExistVarForms: FormProps[]
|
||||
} & Partial<SpecialResultPanelProps>
|
||||
|
||||
function formatValue(value: string | any, type: InputVarType) {
|
||||
if(type === InputVarType.checkbox)
|
||||
return !!value
|
||||
if(value === undefined || value === null)
|
||||
return value
|
||||
if (type === InputVarType.number)
|
||||
return Number.parseFloat(value)
|
||||
if (type === InputVarType.json)
|
||||
return JSON.parse(value)
|
||||
if (type === InputVarType.contexts) {
|
||||
return value.map((item: any) => {
|
||||
return JSON.parse(item)
|
||||
})
|
||||
}
|
||||
if (type === InputVarType.multiFiles)
|
||||
return getProcessedFiles(value)
|
||||
|
||||
if (type === InputVarType.singleFile) {
|
||||
if (Array.isArray(value))
|
||||
return getProcessedFiles(value)
|
||||
if (!value)
|
||||
return undefined
|
||||
return getProcessedFiles([value])[0]
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
||||
nodeName,
|
||||
onHide,
|
||||
onRun,
|
||||
forms,
|
||||
filteredExistVarForms,
|
||||
existVarValuesInForms,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isFileLoaded = (() => {
|
||||
if (!forms || forms.length === 0)
|
||||
return true
|
||||
// system files
|
||||
const filesForm = forms.find(item => !!item.values['#files#'])
|
||||
if (!filesForm)
|
||||
return true
|
||||
|
||||
const files = filesForm.values['#files#'] as any
|
||||
if (files?.some((item: any) => item.transfer_method === TransferMethod.local_file && !item.upload_file_id))
|
||||
return false
|
||||
|
||||
return true
|
||||
})()
|
||||
const handleRun = () => {
|
||||
let errMsg = ''
|
||||
forms.forEach((form, i) => {
|
||||
const existVarValuesInForm = existVarValuesInForms[i]
|
||||
|
||||
form.inputs.forEach((input) => {
|
||||
const value = form.values[input.variable] as any
|
||||
if (!errMsg && input.required && (input.type !== InputVarType.checkbox) && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
|
||||
errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label })
|
||||
|
||||
if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) {
|
||||
let fileIsUploading = false
|
||||
if (Array.isArray(value))
|
||||
fileIsUploading = value.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
|
||||
else
|
||||
fileIsUploading = value.transferMethod === TransferMethod.local_file && !value.uploadedId
|
||||
|
||||
if (fileIsUploading)
|
||||
errMsg = t('appDebug.errorMessage.waitForFileUpload')
|
||||
}
|
||||
})
|
||||
})
|
||||
if (errMsg) {
|
||||
Toast.notify({
|
||||
message: errMsg,
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const submitData: Record<string, any> = {}
|
||||
let parseErrorJsonField = ''
|
||||
forms.forEach((form) => {
|
||||
form.inputs.forEach((input) => {
|
||||
try {
|
||||
const value = formatValue(form.values[input.variable], input.type)
|
||||
submitData[input.variable] = value
|
||||
}
|
||||
catch {
|
||||
parseErrorJsonField = input.variable
|
||||
}
|
||||
})
|
||||
})
|
||||
if (parseErrorJsonField) {
|
||||
Toast.notify({
|
||||
message: t('workflow.errorMsg.invalidJson', { field: parseErrorJsonField }),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
onRun(submitData)
|
||||
}
|
||||
const hasRun = useRef(false)
|
||||
useEffect(() => {
|
||||
// React 18 run twice in dev mode
|
||||
if(hasRun.current)
|
||||
return
|
||||
hasRun.current = true
|
||||
if(filteredExistVarForms.length === 0)
|
||||
onRun({})
|
||||
}, [filteredExistVarForms, onRun])
|
||||
|
||||
if(filteredExistVarForms.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<PanelWrap
|
||||
nodeName={nodeName}
|
||||
onHide={onHide}
|
||||
>
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<div className='mt-3 space-y-4 px-4'>
|
||||
{filteredExistVarForms.map((form, index) => (
|
||||
<div key={index}>
|
||||
<Form
|
||||
key={index}
|
||||
className={cn(index < forms.length - 1 && 'mb-4')}
|
||||
{...form}
|
||||
/>
|
||||
{index < forms.length - 1 && <Split />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-4 flex justify-between space-x-2 px-4' >
|
||||
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
|
||||
<div>{t(`${i18nPrefix}.startRun`)}</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PanelWrap>
|
||||
)
|
||||
}
|
||||
export default React.memo(BeforeRunForm)
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
export type Props = {
|
||||
nodeName: string
|
||||
onHide: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const PanelWrap: FC<Props> = ({
|
||||
nodeName,
|
||||
onHide,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt'>
|
||||
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
|
||||
<div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'>
|
||||
<div className='truncate text-base font-semibold text-text-primary'>
|
||||
{t(`${i18nPrefix}.testRun`)} {nodeName}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => {
|
||||
onHide()
|
||||
}}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary ' />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PanelWrap)
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import type { CodeLanguage } from '../../code/types'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import { ActionButton } from '@/app/components/base/action-button'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
currentCode?: string
|
||||
className?: string
|
||||
onGenerated?: (prompt: string) => void
|
||||
codeLanguages: CodeLanguage
|
||||
}
|
||||
|
||||
const CodeGenerateBtn: FC<Props> = ({
|
||||
nodeId,
|
||||
currentCode,
|
||||
className,
|
||||
codeLanguages,
|
||||
onGenerated,
|
||||
}) => {
|
||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||
const handleAutomaticRes = useCallback((res: GenRes) => {
|
||||
onGenerated?.(res.modified)
|
||||
showAutomaticFalse()
|
||||
}, [onGenerated, showAutomaticFalse])
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<ActionButton
|
||||
className='hover:bg-[#155EFF]/8'
|
||||
onClick={showAutomaticTrue}>
|
||||
<Generator className='h-4 w-4 text-primary-600' />
|
||||
</ActionButton>
|
||||
{showAutomatic && (
|
||||
<GetCodeGeneratorResModal
|
||||
mode={AppModeEnum.CHAT}
|
||||
isShow={showAutomatic}
|
||||
codeLanguages={codeLanguages}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
flowId={configsMap?.flowId || ''}
|
||||
nodeId={nodeId}
|
||||
currentCode={currentCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CodeGenerateBtn)
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Collapse from '.'
|
||||
|
||||
type FieldCollapseProps = {
|
||||
title: string
|
||||
children: ReactNode
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
operations?: ReactNode
|
||||
}
|
||||
const FieldCollapse = ({
|
||||
title,
|
||||
children,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
operations,
|
||||
}: FieldCollapseProps) => {
|
||||
return (
|
||||
<div className='py-4'>
|
||||
<Collapse
|
||||
trigger={
|
||||
<div className='system-sm-semibold-uppercase flex h-6 cursor-pointer items-center text-text-secondary'>{title}</div>
|
||||
}
|
||||
operations={operations}
|
||||
collapsed={collapsed}
|
||||
onCollapse={onCollapse}
|
||||
>
|
||||
<div className='px-4'>
|
||||
{children}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FieldCollapse
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export { default as FieldCollapse } from './field-collapse'
|
||||
|
||||
type CollapseProps = {
|
||||
disabled?: boolean
|
||||
trigger: React.JSX.Element | ((collapseIcon: React.JSX.Element | null) => React.JSX.Element)
|
||||
children: React.JSX.Element
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
operations?: ReactNode
|
||||
hideCollapseIcon?: boolean
|
||||
}
|
||||
const Collapse = ({
|
||||
disabled,
|
||||
trigger,
|
||||
children,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
operations,
|
||||
hideCollapseIcon,
|
||||
}: CollapseProps) => {
|
||||
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
||||
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||
const collapseIcon = useMemo(() => {
|
||||
if (disabled)
|
||||
return null
|
||||
|
||||
return (
|
||||
<ArrowDownRoundFill
|
||||
className={cn(
|
||||
'h-4 w-4 cursor-pointer text-text-quaternary group-hover/collapse:text-text-secondary',
|
||||
collapsedMerged && 'rotate-[270deg]',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}, [collapsedMerged, disabled])
|
||||
return (
|
||||
<>
|
||||
<div className='group/collapse flex items-center'>
|
||||
<div
|
||||
className='ml-4 flex grow items-center'
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setCollapsedLocal(!collapsedMerged)
|
||||
onCollapse?.(!collapsedMerged)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{typeof trigger === 'function' ? trigger(collapseIcon) : trigger}
|
||||
{!hideCollapseIcon && (
|
||||
<div className='h-4 w-4 shrink-0'>
|
||||
{collapseIcon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{operations}
|
||||
</div>
|
||||
{
|
||||
!collapsedMerged && children
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Collapse
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import VarReferencePicker from './variable/var-reference-picker'
|
||||
import ResolutionPicker from '@/app/components/workflow/nodes/llm/components/resolution-picker'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { type ValueSelector, type Var, VarType, type VisionSetting } from '@/app/components/workflow/types'
|
||||
import { Resolution } from '@/types/app'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
type Props = {
|
||||
isVisionModel: boolean
|
||||
readOnly: boolean
|
||||
enabled: boolean
|
||||
onEnabledChange: (enabled: boolean) => void
|
||||
nodeId: string
|
||||
config?: VisionSetting
|
||||
onConfigChange: (config: VisionSetting) => void
|
||||
}
|
||||
|
||||
const ConfigVision: FC<Props> = ({
|
||||
isVisionModel,
|
||||
readOnly,
|
||||
enabled,
|
||||
onEnabledChange,
|
||||
nodeId,
|
||||
config = {
|
||||
detail: Resolution.high,
|
||||
variable_selector: [],
|
||||
},
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filterVar = useCallback((payload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(payload.type)
|
||||
}, [])
|
||||
const handleVisionResolutionChange = useCallback((resolution: Resolution) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
draft.detail = resolution
|
||||
})
|
||||
onConfigChange(newConfig)
|
||||
}, [config, onConfigChange])
|
||||
|
||||
const handleVarSelectorChange = useCallback((valueSelector: ValueSelector | string) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
draft.variable_selector = valueSelector as ValueSelector
|
||||
})
|
||||
onConfigChange(newConfig)
|
||||
}, [config, onConfigChange])
|
||||
|
||||
return (
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.vision`)}
|
||||
tooltip={t('appDebug.vision.description')!}
|
||||
operations={
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.vision.onlySupportVisionModelTip')!}
|
||||
disabled={isVisionModel}
|
||||
>
|
||||
<Switch disabled={readOnly || !isVisionModel} size='md' defaultValue={!isVisionModel ? false : enabled} onChange={onEnabledChange} />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(enabled && isVisionModel)
|
||||
? (
|
||||
<div>
|
||||
<VarReferencePicker
|
||||
className='mb-4'
|
||||
filterVar={filterVar}
|
||||
nodeId={nodeId}
|
||||
value={config.variable_selector || []}
|
||||
onChange={handleVarSelectorChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
<ResolutionPicker
|
||||
value={config.detail}
|
||||
onChange={handleVisionResolutionChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigVision)
|
||||
@@ -0,0 +1,138 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import ToggleExpandBtn from '../toggle-expand-btn'
|
||||
import CodeGeneratorButton from '../code-generator-button'
|
||||
import type { CodeLanguage } from '../../../code/types'
|
||||
import Wrap from './wrap'
|
||||
import cn from '@/utils/classnames'
|
||||
import PromptEditorHeightResizeWrap from '@/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap'
|
||||
import {
|
||||
Copy,
|
||||
CopyCheck,
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
nodeId?: string
|
||||
className?: string
|
||||
title: React.JSX.Element | string
|
||||
headerRight?: React.JSX.Element
|
||||
children: React.JSX.Element
|
||||
minHeight?: number
|
||||
value: string
|
||||
isFocus: boolean
|
||||
isInNode?: boolean
|
||||
onGenerated?: (prompt: string) => void
|
||||
codeLanguages?: CodeLanguage
|
||||
fileList?: {
|
||||
varName: string
|
||||
list: FileEntity[]
|
||||
}[]
|
||||
showFileList?: boolean
|
||||
showCodeGenerator?: boolean
|
||||
tip?: React.JSX.Element
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
const Base: FC<Props> = ({
|
||||
nodeId,
|
||||
className,
|
||||
title,
|
||||
headerRight,
|
||||
children,
|
||||
minHeight = 120,
|
||||
value,
|
||||
isFocus,
|
||||
isInNode,
|
||||
onGenerated,
|
||||
codeLanguages,
|
||||
fileList = [],
|
||||
showFileList,
|
||||
showCodeGenerator = false,
|
||||
tip,
|
||||
footer,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
wrapClassName,
|
||||
wrapStyle,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
editorExpandHeight,
|
||||
} = useToggleExpend({ ref, hasFooter: false, isInNode })
|
||||
|
||||
const editorContentMinHeight = minHeight - 28
|
||||
const [editorContentHeight, setEditorContentHeight] = useState(editorContentMinHeight)
|
||||
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, 2000)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<Wrap className={cn(wrapClassName)} style={wrapStyle} isInNode={isInNode} isExpand={isExpand}>
|
||||
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
|
||||
<div className='flex h-7 items-center justify-between pl-3 pr-2 pt-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>{title}</div>
|
||||
<div className='flex items-center' onClick={(e) => {
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
{headerRight}
|
||||
{showCodeGenerator && codeLanguages && (
|
||||
<div className='ml-1'>
|
||||
<CodeGeneratorButton
|
||||
onGenerated={onGenerated}
|
||||
codeLanguages={codeLanguages}
|
||||
currentCode={value}
|
||||
nodeId={nodeId!}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ActionButton className='ml-1' onClick={handleCopy}>
|
||||
{!isCopied
|
||||
? (
|
||||
<Copy className='h-4 w-4 cursor-pointer' />
|
||||
)
|
||||
: (
|
||||
<CopyCheck className='h-4 w-4' />
|
||||
)
|
||||
}
|
||||
</ActionButton>
|
||||
<div className='ml-1'>
|
||||
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tip && <div className='px-1 py-0.5'>{tip}</div>}
|
||||
<PromptEditorHeightResizeWrap
|
||||
height={isExpand ? editorExpandHeight : editorContentHeight}
|
||||
minHeight={editorContentMinHeight}
|
||||
onHeightChange={setEditorContentHeight}
|
||||
hideResize={isExpand}
|
||||
>
|
||||
<div className='h-full pb-2 pl-2'>
|
||||
{children}
|
||||
</div>
|
||||
</PromptEditorHeightResizeWrap>
|
||||
{showFileList && fileList.length > 0 && (
|
||||
<FileListInLog fileList={fileList} />
|
||||
)}
|
||||
{footer}
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
export default React.memo(Base)
|
||||
@@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Props as EditorProps } from '.'
|
||||
import Editor from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
const TO_WINDOW_OFFSET = 8
|
||||
|
||||
type Props = {
|
||||
availableVars: NodeOutPutVar[]
|
||||
varList: Variable[]
|
||||
onAddVar?: (payload: Variable) => void
|
||||
} & EditorProps
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
availableVars,
|
||||
varList,
|
||||
onAddVar,
|
||||
...editorProps
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isLeftBraceRef = useRef(false)
|
||||
|
||||
const editorRef = useRef(null)
|
||||
const monacoRef = useRef(null)
|
||||
|
||||
const popupRef = useRef<HTMLDivElement>(null)
|
||||
const [isShowVarPicker, {
|
||||
setTrue: showVarPicker,
|
||||
setFalse: hideVarPicker,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
// Listen for cursor position changes
|
||||
const handleCursorPositionChange = (event: any) => {
|
||||
const editor: any = editorRef.current
|
||||
const { position } = event
|
||||
const text = editor.getModel().getLineContent(position.lineNumber)
|
||||
const charBefore = text[position.column - 2]
|
||||
if (['/', '{'].includes(charBefore)) {
|
||||
isLeftBraceRef.current = charBefore === '{'
|
||||
const editorRect = editor.getDomNode().getBoundingClientRect()
|
||||
const cursorCoords = editor.getScrolledVisiblePosition(position)
|
||||
|
||||
const popupX = editorRect.left + cursorCoords.left
|
||||
const popupY = editorRect.top + cursorCoords.top + 20 // Adjust the vertical position as needed
|
||||
|
||||
setPopupPosition({ x: popupX, y: popupY })
|
||||
showVarPicker()
|
||||
}
|
||||
else {
|
||||
hideVarPicker()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isShowVarPicker && popupRef.current) {
|
||||
const windowWidth = window.innerWidth
|
||||
const { width, height } = popupRef.current!.getBoundingClientRect()
|
||||
const newPopupPosition = { ...popupPosition }
|
||||
if (popupPosition.x + width > windowWidth - TO_WINDOW_OFFSET)
|
||||
newPopupPosition.x = windowWidth - width - TO_WINDOW_OFFSET
|
||||
|
||||
if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET)
|
||||
newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET
|
||||
|
||||
if (newPopupPosition.x !== popupPosition.x || newPopupPosition.y !== popupPosition.y)
|
||||
setPopupPosition(newPopupPosition)
|
||||
}
|
||||
}, [isShowVarPicker, popupPosition])
|
||||
|
||||
const onEditorMounted = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
editor.onDidChangeCursorPosition(handleCursorPositionChange)
|
||||
}
|
||||
|
||||
const getUniqVarName = (varName: string) => {
|
||||
if (varList.find(v => v.variable === varName)) {
|
||||
const match = varName.match(/_(\d+)$/)
|
||||
|
||||
const index = (() => {
|
||||
if (match)
|
||||
return Number.parseInt(match[1]!) + 1
|
||||
|
||||
return 1
|
||||
})()
|
||||
return getUniqVarName(`${varName.replace(/_(\d+)$/, '')}_${index}`)
|
||||
}
|
||||
return varName
|
||||
}
|
||||
|
||||
const getVarName = (varValue: string[]) => {
|
||||
const existVar = varList.find(v => Array.isArray(v.value_selector) && v.value_selector.join('@@@') === varValue.join('@@@'))
|
||||
if (existVar) {
|
||||
return {
|
||||
name: existVar.variable,
|
||||
isExist: true,
|
||||
}
|
||||
}
|
||||
const varName = varValue.slice(-1)[0]
|
||||
return {
|
||||
name: getUniqVarName(varName),
|
||||
isExist: false,
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectVar = (varValue: string[]) => {
|
||||
const { name, isExist } = getVarName(varValue)
|
||||
if (!isExist) {
|
||||
const newVar: Variable = {
|
||||
variable: name,
|
||||
value_selector: varValue,
|
||||
}
|
||||
|
||||
onAddVar?.(newVar)
|
||||
}
|
||||
const editor: any = editorRef.current
|
||||
const monaco: any = monacoRef.current
|
||||
const position = editor?.getPosition()
|
||||
|
||||
// Insert the content at the cursor position
|
||||
editor?.executeEdits('', [
|
||||
{
|
||||
// position.column - 1 to remove the text before the cursor
|
||||
range: new monaco.Range(position.lineNumber, position.column - 1, position.lineNumber, position.column),
|
||||
text: `{{ ${name} }${!isLeftBraceRef.current ? '}' : ''}`, // left brace would auto add one right brace
|
||||
},
|
||||
])
|
||||
|
||||
hideVarPicker()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(editorProps.isExpand && 'h-full')}>
|
||||
<Editor
|
||||
{...editorProps}
|
||||
onMount={onEditorMounted}
|
||||
placeholder={t('workflow.common.jinjaEditorPlaceholder')!}
|
||||
/>
|
||||
{isShowVarPicker && (
|
||||
<div
|
||||
ref={popupRef}
|
||||
className='w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: popupPosition.y,
|
||||
left: popupPosition.x,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={availableVars}
|
||||
onChange={handleSelectVar}
|
||||
isSupportFileVar={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CodeEditor)
|
||||
@@ -0,0 +1,206 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Editor, { loader } from '@monaco-editor/react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Base from '../base'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import {
|
||||
getFilesInLogs,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
import { Theme } from '@/types/app'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import './style.css'
|
||||
import { noop } from 'lodash-es'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
|
||||
if (typeof window !== 'undefined')
|
||||
loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } })
|
||||
|
||||
const CODE_EDITOR_LINE_HEIGHT = 18
|
||||
|
||||
export type Props = {
|
||||
nodeId?: string
|
||||
value?: string | object
|
||||
placeholder?: React.JSX.Element | string
|
||||
onChange?: (value: string) => void
|
||||
title?: string | React.JSX.Element
|
||||
language: CodeLanguage
|
||||
headerRight?: React.JSX.Element
|
||||
readOnly?: boolean
|
||||
isJSONStringifyBeauty?: boolean
|
||||
height?: number
|
||||
isInNode?: boolean
|
||||
onMount?: (editor: any, monaco: any) => void
|
||||
noWrapper?: boolean
|
||||
isExpand?: boolean
|
||||
showFileList?: boolean
|
||||
onGenerated?: (value: string) => void
|
||||
showCodeGenerator?: boolean
|
||||
className?: string
|
||||
tip?: React.JSX.Element
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
export const languageMap = {
|
||||
[CodeLanguage.javascript]: 'javascript',
|
||||
[CodeLanguage.python3]: 'python',
|
||||
[CodeLanguage.json]: 'json',
|
||||
}
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
nodeId,
|
||||
value = '',
|
||||
placeholder = '',
|
||||
onChange = noop,
|
||||
title = '',
|
||||
headerRight,
|
||||
language,
|
||||
readOnly,
|
||||
isJSONStringifyBeauty,
|
||||
height,
|
||||
isInNode,
|
||||
onMount,
|
||||
noWrapper,
|
||||
isExpand,
|
||||
showFileList,
|
||||
onGenerated,
|
||||
showCodeGenerator = false,
|
||||
className,
|
||||
tip,
|
||||
footer,
|
||||
}) => {
|
||||
const [isFocus, setIsFocus] = React.useState(false)
|
||||
const [isMounted, setIsMounted] = React.useState(false)
|
||||
const minHeight = height || 200
|
||||
const [editorContentHeight, setEditorContentHeight] = useState(56)
|
||||
const { theme: appTheme } = useTheme()
|
||||
const valueRef = useRef(value)
|
||||
useEffect(() => {
|
||||
valueRef.current = value
|
||||
}, [value])
|
||||
|
||||
const fileList = useMemo(() => {
|
||||
if (typeof value === 'object')
|
||||
return getFilesInLogs(value)
|
||||
return []
|
||||
}, [value])
|
||||
|
||||
const editorRef = useRef<any>(null)
|
||||
const resizeEditorToContent = () => {
|
||||
if (editorRef.current) {
|
||||
const contentHeight = editorRef.current.getContentHeight() // Math.max(, minHeight)
|
||||
setEditorContentHeight(contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
onChange(value || '')
|
||||
setTimeout(() => {
|
||||
resizeEditorToContent()
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
resizeEditorToContent()
|
||||
|
||||
editor.onDidFocusEditorText(() => {
|
||||
setIsFocus(true)
|
||||
})
|
||||
editor.onDidBlurEditorText(() => {
|
||||
setIsFocus(false)
|
||||
})
|
||||
|
||||
monaco.editor.setTheme(appTheme === Theme.light ? 'light' : 'vs-dark') // Fix: sometimes not load the default theme
|
||||
|
||||
onMount?.(editor, monaco)
|
||||
setIsMounted(true)
|
||||
}
|
||||
|
||||
const outPutValue = (() => {
|
||||
if (!isJSONStringifyBeauty)
|
||||
return value as string
|
||||
try {
|
||||
return JSON.stringify(value as object, null, 2)
|
||||
}
|
||||
catch {
|
||||
return value as string
|
||||
}
|
||||
})()
|
||||
|
||||
const theme = useMemo(() => {
|
||||
if (appTheme === Theme.light)
|
||||
return 'light'
|
||||
return 'vs-dark'
|
||||
}, [appTheme])
|
||||
|
||||
const main = (
|
||||
<>
|
||||
{/* https://www.npmjs.com/package/@monaco-editor/react */}
|
||||
<Editor
|
||||
// className='min-h-[100%]' // h-full
|
||||
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
|
||||
language={languageMap[language] || 'javascript'}
|
||||
theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
|
||||
value={outPutValue}
|
||||
loading={<span className='text-text-primary'>Loading...</span>}
|
||||
onChange={handleEditorChange}
|
||||
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
|
||||
options={{
|
||||
readOnly,
|
||||
domReadOnly: true,
|
||||
quickSuggestions: false,
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 1, // would change line num width
|
||||
wordWrap: 'on', // auto line wrap
|
||||
// lineNumbers: (num) => {
|
||||
// return <div>{num}</div>
|
||||
// }
|
||||
// hide ambiguousCharacters warning
|
||||
unicodeHighlight: {
|
||||
ambiguousCharacters: false,
|
||||
},
|
||||
stickyScroll: { enabled: false },
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
{!outPutValue && !isFocus && <div className='pointer-events-none absolute left-[36px] top-0 text-[13px] font-normal leading-[18px] text-gray-300'>{placeholder}</div>}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn(isExpand && 'h-full', className)}>
|
||||
{noWrapper
|
||||
? <div className='no-wrapper relative' style={{
|
||||
height: isExpand ? '100%' : (editorContentHeight) / 2 + CODE_EDITOR_LINE_HEIGHT, // In IDE, the last line can always be in lop line. So there is some blank space in the bottom.
|
||||
minHeight: CODE_EDITOR_LINE_HEIGHT,
|
||||
}}>
|
||||
{main}
|
||||
</div>
|
||||
: (
|
||||
<Base
|
||||
nodeId={nodeId}
|
||||
className='relative'
|
||||
title={title}
|
||||
value={outPutValue}
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus && !readOnly}
|
||||
minHeight={minHeight}
|
||||
isInNode={isInNode}
|
||||
onGenerated={onGenerated}
|
||||
codeLanguages={language}
|
||||
fileList={fileList as any}
|
||||
showFileList={showFileList}
|
||||
showCodeGenerator={showCodeGenerator}
|
||||
tip={tip}
|
||||
footer={footer}
|
||||
>
|
||||
{main}
|
||||
</Base>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CodeEditor)
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
.monaco-editor {
|
||||
background-color: transparent !important;
|
||||
outline: none !important;
|
||||
}
|
||||
.monaco-editor .monaco-editor-background {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.monaco-editor .margin {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* hide readonly tooltip */
|
||||
.monaco-editor-overlaymessage {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Base from './base'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
title: React.JSX.Element | string
|
||||
headerRight?: React.JSX.Element
|
||||
minHeight?: number
|
||||
onBlur?: () => void
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
isInNode?: boolean
|
||||
}
|
||||
|
||||
const TextEditor: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
title,
|
||||
headerRight,
|
||||
minHeight,
|
||||
onBlur,
|
||||
placeholder,
|
||||
readonly,
|
||||
isInNode,
|
||||
}) => {
|
||||
const [isFocus, {
|
||||
setTrue: setIsFocus,
|
||||
setFalse: setIsNotFocus,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setIsNotFocus()
|
||||
onBlur?.()
|
||||
}, [setIsNotFocus, onBlur])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Base
|
||||
title={title}
|
||||
value={value}
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus}
|
||||
minHeight={minHeight}
|
||||
isInNode={isInNode}
|
||||
>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onFocus={setIsFocus}
|
||||
onBlur={handleBlur}
|
||||
className='h-full w-full resize-none border-none bg-transparent px-3 text-[13px] font-normal leading-[18px] text-gray-900 placeholder:text-gray-300 focus:outline-none'
|
||||
placeholder={placeholder}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</Base>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TextEditor)
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
type Props = {
|
||||
isInNode?: boolean
|
||||
isExpand: boolean
|
||||
className: string
|
||||
style: React.CSSProperties
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
// It doesn't has workflow store
|
||||
const WrapInWebApp = ({
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
}: Props) => {
|
||||
return <div className={className} style={style}>{children}</div>
|
||||
}
|
||||
|
||||
const Wrap = ({
|
||||
className,
|
||||
style,
|
||||
isExpand,
|
||||
children,
|
||||
}: Props) => {
|
||||
const panelWidth = useStore(state => state.panelWidth)
|
||||
const wrapStyle = (() => {
|
||||
if (isExpand) {
|
||||
return {
|
||||
...style,
|
||||
width: panelWidth - 1,
|
||||
}
|
||||
}
|
||||
return style
|
||||
})()
|
||||
return <div className={className} style={wrapStyle}>{children}</div>
|
||||
}
|
||||
|
||||
const Main: FC<Props> = ({
|
||||
isInNode,
|
||||
...otherProps
|
||||
}: Props) => {
|
||||
return isInNode ? <Wrap {...otherProps} /> : <WrapInWebApp {...otherProps} />
|
||||
}
|
||||
export default React.memo(Main)
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export enum StartNodeTypeEnum {
|
||||
Start = 'start',
|
||||
Trigger = 'trigger',
|
||||
}
|
||||
|
||||
type EntryNodeContainerProps = {
|
||||
children: ReactNode
|
||||
customLabel?: string
|
||||
nodeType?: StartNodeTypeEnum
|
||||
}
|
||||
|
||||
const EntryNodeContainer: FC<EntryNodeContainerProps> = ({
|
||||
children,
|
||||
customLabel,
|
||||
nodeType = StartNodeTypeEnum.Trigger,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const label = useMemo(() => {
|
||||
const translationKey = nodeType === StartNodeTypeEnum.Start ? 'entryNodeStatus' : 'triggerStatus'
|
||||
return customLabel || t(`workflow.${translationKey}.enabled`)
|
||||
}, [customLabel, nodeType, t])
|
||||
|
||||
return (
|
||||
<div className="w-fit min-w-[242px] rounded-2xl bg-workflow-block-wrapper-bg-1 px-0 pb-0 pt-0.5">
|
||||
<div className="mb-0.5 flex items-center px-1.5 pt-0.5">
|
||||
<span className="text-2xs font-semibold uppercase text-text-tertiary">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EntryNodeContainer
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { DefaultValueForm } from './types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type DefaultValueProps = {
|
||||
forms: DefaultValueForm[]
|
||||
onFormChange: (form: DefaultValueForm) => void
|
||||
}
|
||||
const DefaultValue = ({
|
||||
forms,
|
||||
onFormChange,
|
||||
}: DefaultValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => {
|
||||
return (payload: any) => {
|
||||
let value
|
||||
if (type === VarType.string || type === VarType.number)
|
||||
value = payload.target.value
|
||||
|
||||
if (type === VarType.array || type === VarType.arrayNumber || type === VarType.arrayString || type === VarType.arrayObject || type === VarType.arrayFile || type === VarType.object)
|
||||
value = payload
|
||||
|
||||
onFormChange({ key, type, value })
|
||||
}
|
||||
}, [onFormChange])
|
||||
|
||||
return (
|
||||
<div className='px-4 pt-2'>
|
||||
<div className='body-xs-regular mb-2 text-text-tertiary'>
|
||||
{t('workflow.nodes.common.errorHandle.defaultValue.desc')}
|
||||
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/README', {
|
||||
'zh-Hans': '/guides/workflow/error-handling/readme',
|
||||
})}
|
||||
target='_blank'
|
||||
className='text-text-accent'
|
||||
>
|
||||
{t('workflow.common.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
forms.map((form, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='py-1'
|
||||
>
|
||||
<div className='mb-1 flex items-center'>
|
||||
<div className='system-sm-medium mr-1 text-text-primary'>{form.key}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{form.type}</div>
|
||||
</div>
|
||||
{
|
||||
(form.type === VarType.string || form.type === VarType.number) && (
|
||||
<Input
|
||||
type={form.type}
|
||||
value={form.value || (form.type === VarType.string ? '' : 0)}
|
||||
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
(
|
||||
form.type === VarType.array
|
||||
|| form.type === VarType.arrayNumber
|
||||
|| form.type === VarType.arrayString
|
||||
|| form.type === VarType.arrayObject
|
||||
|| form.type === VarType.object
|
||||
) && (
|
||||
<CodeEditor
|
||||
language={CodeLanguage.json}
|
||||
value={form.value}
|
||||
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultValue
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUpdateNodeInternals } from 'reactflow'
|
||||
import { NodeSourceHandle } from '../node-handle'
|
||||
import { ErrorHandleTypeEnum } from './types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ErrorHandleOnNodeProps = Pick<Node, 'id' | 'data'>
|
||||
const ErrorHandleOnNode = ({
|
||||
id,
|
||||
data,
|
||||
}: ErrorHandleOnNodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { error_strategy } = data
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
|
||||
useEffect(() => {
|
||||
if (error_strategy === ErrorHandleTypeEnum.failBranch)
|
||||
updateNodeInternals(id)
|
||||
}, [error_strategy, id, updateNodeInternals])
|
||||
|
||||
if (!error_strategy)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='relative px-3 pb-2 pt-1'>
|
||||
<div className={cn(
|
||||
'relative flex h-6 items-center justify-between rounded-md bg-workflow-block-parma-bg px-[5px]',
|
||||
data._runningStatus === NodeRunningStatus.Exception && 'border-[0.5px] border-components-badge-status-light-warning-halo bg-state-warning-hover',
|
||||
)}>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
{t('workflow.common.onFailure')}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'system-xs-medium text-text-secondary',
|
||||
data._runningStatus === NodeRunningStatus.Exception && 'text-text-warning',
|
||||
)}>
|
||||
{
|
||||
error_strategy === ErrorHandleTypeEnum.defaultValue && (
|
||||
t('workflow.nodes.common.errorHandle.defaultValue.output')
|
||||
)
|
||||
}
|
||||
{
|
||||
error_strategy === ErrorHandleTypeEnum.failBranch && (
|
||||
t('workflow.nodes.common.errorHandle.failBranch.title')
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
error_strategy === ErrorHandleTypeEnum.failBranch && (
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleId={ErrorHandleTypeEnum.failBranch}
|
||||
handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2 after:!bg-workflow-link-line-failure-button-bg'
|
||||
nodeSelectorClassName='!bg-workflow-link-line-failure-button-bg'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandleOnNode
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Collapse from '../collapse'
|
||||
import { ErrorHandleTypeEnum } from './types'
|
||||
import ErrorHandleTypeSelector from './error-handle-type-selector'
|
||||
import FailBranchCard from './fail-branch-card'
|
||||
import DefaultValue from './default-value'
|
||||
import {
|
||||
useDefaultValue,
|
||||
useErrorHandle,
|
||||
} from './hooks'
|
||||
import type { DefaultValueForm } from './types'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type ErrorHandleProps = Pick<Node, 'id' | 'data'>
|
||||
const ErrorHandle = ({
|
||||
id,
|
||||
data,
|
||||
}: ErrorHandleProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { error_strategy, default_value } = data
|
||||
const {
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
handleErrorHandleTypeChange,
|
||||
} = useErrorHandle(id, data)
|
||||
const { handleFormChange } = useDefaultValue(id)
|
||||
|
||||
const getHandleErrorHandleTypeChange = useCallback((data: CommonNodeType) => {
|
||||
return (value: ErrorHandleTypeEnum) => {
|
||||
handleErrorHandleTypeChange(value, data)
|
||||
}
|
||||
}, [handleErrorHandleTypeChange])
|
||||
|
||||
const getHandleFormChange = useCallback((data: CommonNodeType) => {
|
||||
return (v: DefaultValueForm) => {
|
||||
handleFormChange(v, data)
|
||||
}
|
||||
}, [handleFormChange])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='py-4'>
|
||||
<Collapse
|
||||
disabled={!error_strategy}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
hideCollapseIcon
|
||||
trigger={
|
||||
collapseIcon => (
|
||||
<div className='flex grow items-center justify-between pr-4'>
|
||||
<div className='flex items-center'>
|
||||
<div className='system-sm-semibold-uppercase mr-0.5 text-text-secondary'>
|
||||
{t('workflow.nodes.common.errorHandle.title')}
|
||||
</div>
|
||||
<Tooltip popupContent={t('workflow.nodes.common.errorHandle.tip')} />
|
||||
{collapseIcon}
|
||||
</div>
|
||||
<ErrorHandleTypeSelector
|
||||
value={error_strategy || ErrorHandleTypeEnum.none}
|
||||
onSelected={getHandleErrorHandleTypeChange(data)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{
|
||||
error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
|
||||
<FailBranchCard />
|
||||
)
|
||||
}
|
||||
{
|
||||
error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
|
||||
<DefaultValue
|
||||
forms={default_value}
|
||||
onFormChange={getHandleFormChange(data)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
</Collapse>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandle
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import { ErrorHandleTypeEnum } from './types'
|
||||
|
||||
type ErrorHandleTipProps = {
|
||||
type?: ErrorHandleTypeEnum
|
||||
}
|
||||
const ErrorHandleTip = ({
|
||||
type,
|
||||
}: ErrorHandleTipProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const text = useMemo(() => {
|
||||
if (type === ErrorHandleTypeEnum.failBranch)
|
||||
return t('workflow.nodes.common.errorHandle.failBranch.inLog')
|
||||
|
||||
if (type === ErrorHandleTypeEnum.defaultValue)
|
||||
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
|
||||
}, [t, type])
|
||||
|
||||
if (!type)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='relative flex rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 pr-[52px] shadow-xs'
|
||||
>
|
||||
<div
|
||||
className='absolute inset-0 rounded-lg opacity-40'
|
||||
style={{
|
||||
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
|
||||
}}
|
||||
></div>
|
||||
<RiAlertFill className='mr-1 h-4 w-4 shrink-0 text-text-warning-secondary' />
|
||||
<div className='system-xs-medium grow text-text-primary'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandleTip
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiCheckLine,
|
||||
} from '@remixicon/react'
|
||||
import { ErrorHandleTypeEnum } from './types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type ErrorHandleTypeSelectorProps = {
|
||||
value: ErrorHandleTypeEnum
|
||||
onSelected: (value: ErrorHandleTypeEnum) => void
|
||||
}
|
||||
const ErrorHandleTypeSelector = ({
|
||||
value,
|
||||
onSelected,
|
||||
}: ErrorHandleTypeSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const options = [
|
||||
{
|
||||
value: ErrorHandleTypeEnum.none,
|
||||
label: t('workflow.nodes.common.errorHandle.none.title'),
|
||||
description: t('workflow.nodes.common.errorHandle.none.desc'),
|
||||
},
|
||||
{
|
||||
value: ErrorHandleTypeEnum.defaultValue,
|
||||
label: t('workflow.nodes.common.errorHandle.defaultValue.title'),
|
||||
description: t('workflow.nodes.common.errorHandle.defaultValue.desc'),
|
||||
},
|
||||
{
|
||||
value: ErrorHandleTypeEnum.failBranch,
|
||||
label: t('workflow.nodes.common.errorHandle.failBranch.title'),
|
||||
description: t('workflow.nodes.common.errorHandle.failBranch.desc'),
|
||||
},
|
||||
]
|
||||
const selectedOption = options.find(option => option.value === value)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setOpen(v => !v)
|
||||
}}>
|
||||
<Button
|
||||
size='small'
|
||||
>
|
||||
{selectedOption?.label}
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onSelected(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 w-4 shrink-0'>
|
||||
{
|
||||
value === option.value && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandleTypeSelector
|
||||
@@ -0,0 +1,34 @@
|
||||
import { RiMindMap } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
const FailBranchCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
return (
|
||||
<div className='px-4 pt-2'>
|
||||
<div className='rounded-[10px] bg-workflow-process-bg p-4'>
|
||||
<div className='mb-2 flex h-8 w-8 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg'>
|
||||
<RiMindMap className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='system-sm-medium mb-1 text-text-secondary'>
|
||||
{t('workflow.nodes.common.errorHandle.failBranch.customize')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}
|
||||
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/error-type')}
|
||||
target='_blank'
|
||||
className='text-text-accent'
|
||||
>
|
||||
{t('workflow.common.learnMore')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FailBranchCard
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ErrorHandleTypeEnum } from './types'
|
||||
import type { DefaultValueForm } from './types'
|
||||
import { getDefaultValue } from './utils'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
useNodeDataUpdate,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useDefaultValue = (
|
||||
id: string,
|
||||
) => {
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
const handleFormChange = useCallback((
|
||||
{
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
}: DefaultValueForm,
|
||||
data: CommonNodeType,
|
||||
) => {
|
||||
const default_value = data.default_value || []
|
||||
const index = default_value.findIndex(form => form.key === key)
|
||||
|
||||
if (index > -1) {
|
||||
const newDefaultValue = [...default_value]
|
||||
newDefaultValue[index].value = value
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
default_value: newDefaultValue,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
default_value: [
|
||||
...default_value,
|
||||
{
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
return {
|
||||
handleFormChange,
|
||||
}
|
||||
}
|
||||
|
||||
export const useErrorHandle = (
|
||||
id: string,
|
||||
data: CommonNodeType,
|
||||
) => {
|
||||
const initCollapsed = useMemo(() => {
|
||||
if (data.error_strategy === ErrorHandleTypeEnum.none)
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [data.error_strategy])
|
||||
const [collapsed, setCollapsed] = useState(initCollapsed)
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
|
||||
|
||||
const handleErrorHandleTypeChange = useCallback((value: ErrorHandleTypeEnum, data: CommonNodeType) => {
|
||||
if (data.error_strategy === value)
|
||||
return
|
||||
|
||||
if (value === ErrorHandleTypeEnum.none) {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
error_strategy: undefined,
|
||||
default_value: undefined,
|
||||
},
|
||||
})
|
||||
setCollapsed(true)
|
||||
handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
|
||||
}
|
||||
|
||||
if (value === ErrorHandleTypeEnum.failBranch) {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
error_strategy: value,
|
||||
default_value: undefined,
|
||||
},
|
||||
})
|
||||
setCollapsed(false)
|
||||
}
|
||||
|
||||
if (value === ErrorHandleTypeEnum.defaultValue) {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
error_strategy: value,
|
||||
default_value: getDefaultValue(data),
|
||||
},
|
||||
})
|
||||
setCollapsed(false)
|
||||
handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
|
||||
}
|
||||
}, [id, handleNodeDataUpdateWithSyncDraft, handleEdgeDeleteByDeleteBranch])
|
||||
|
||||
return {
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
handleErrorHandleTypeChange,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export enum ErrorHandleTypeEnum {
|
||||
none = 'none',
|
||||
failBranch = 'fail-branch',
|
||||
defaultValue = 'default-value',
|
||||
}
|
||||
|
||||
export type DefaultValueForm = {
|
||||
key: string
|
||||
type: VarType
|
||||
value?: any
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
|
||||
|
||||
const getDefaultValueByType = (type: VarType) => {
|
||||
if (type === VarType.string)
|
||||
return ''
|
||||
|
||||
if (type === VarType.number)
|
||||
return 0
|
||||
|
||||
if (type === VarType.object)
|
||||
return '{}'
|
||||
|
||||
if (type === VarType.arrayObject || type === VarType.arrayString || type === VarType.arrayNumber || type === VarType.arrayFile)
|
||||
return '[]'
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export const getDefaultValue = (data: CommonNodeType) => {
|
||||
const { type } = data
|
||||
|
||||
if (type === BlockEnum.LLM) {
|
||||
return [{
|
||||
key: 'text',
|
||||
type: VarType.string,
|
||||
value: getDefaultValueByType(VarType.string),
|
||||
}]
|
||||
}
|
||||
|
||||
if (type === BlockEnum.HttpRequest) {
|
||||
return [
|
||||
{
|
||||
key: 'body',
|
||||
type: VarType.string,
|
||||
value: getDefaultValueByType(VarType.string),
|
||||
},
|
||||
{
|
||||
key: 'status_code',
|
||||
type: VarType.number,
|
||||
value: getDefaultValueByType(VarType.number),
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
type: VarType.object,
|
||||
value: getDefaultValueByType(VarType.object),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (type === BlockEnum.Tool) {
|
||||
return [
|
||||
{
|
||||
key: 'text',
|
||||
type: VarType.string,
|
||||
value: getDefaultValueByType(VarType.string),
|
||||
},
|
||||
{
|
||||
key: 'json',
|
||||
type: VarType.arrayObject,
|
||||
value: getDefaultValueByType(VarType.arrayObject),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (type === BlockEnum.Code) {
|
||||
const { outputs } = data as CodeNodeType
|
||||
|
||||
return Object.keys(outputs).map((key) => {
|
||||
return {
|
||||
key,
|
||||
type: outputs[key].type,
|
||||
value: getDefaultValueByType(outputs[key].type),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title: ReactNode
|
||||
tooltip?: ReactNode
|
||||
isSubTitle?: boolean
|
||||
supportFold?: boolean
|
||||
children?: React.JSX.Element | string | null
|
||||
operations?: React.JSX.Element
|
||||
inline?: boolean
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
const Field: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
isSubTitle,
|
||||
tooltip,
|
||||
children,
|
||||
operations,
|
||||
inline,
|
||||
supportFold,
|
||||
required,
|
||||
}) => {
|
||||
const [fold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(true)
|
||||
return (
|
||||
<div className={cn(className, inline && 'flex w-full items-center justify-between')}>
|
||||
<div
|
||||
onClick={() => supportFold && toggleFold()}
|
||||
className={cn('flex items-center justify-between', supportFold && 'cursor-pointer')}>
|
||||
<div className='flex h-6 items-center'>
|
||||
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>
|
||||
{title} {required && <span className='text-text-destructive'>*</span>}
|
||||
</div>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
popupContent={tooltip}
|
||||
popupClassName='ml-1'
|
||||
triggerClassName='w-4 h-4 ml-1'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
{operations && <div>{operations}</div>}
|
||||
{supportFold && (
|
||||
<RiArrowDownSLine className='h-4 w-4 cursor-pointer text-text-tertiary transition-transform' style={{ transform: fold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children && (!supportFold || (supportFold && !fold)) && <div className={cn(!inline && 'mt-1')}>{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Field)
|
||||
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SupportUploadFileTypes } from '../../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { FileTypeIcon } from '@/app/components/base/file-uploader'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type Props = {
|
||||
type: SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video | SupportUploadFileTypes.custom
|
||||
selected: boolean
|
||||
onToggle: (type: SupportUploadFileTypes) => void
|
||||
onCustomFileTypesChange?: (customFileTypes: string[]) => void
|
||||
customFileTypes?: string[]
|
||||
}
|
||||
|
||||
const FileTypeItem: FC<Props> = ({
|
||||
type,
|
||||
selected,
|
||||
onToggle,
|
||||
customFileTypes = [],
|
||||
onCustomFileTypesChange = noop,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOnSelect = useCallback(() => {
|
||||
onToggle(type)
|
||||
}, [onToggle, type])
|
||||
|
||||
const isCustomSelected = type === SupportUploadFileTypes.custom && selected
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'cursor-pointer select-none rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg',
|
||||
!isCustomSelected && 'px-3 py-2',
|
||||
selected && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg',
|
||||
!selected && 'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover',
|
||||
)}
|
||||
onClick={handleOnSelect}
|
||||
>
|
||||
{isCustomSelected
|
||||
? (
|
||||
<div>
|
||||
<div className='flex items-center border-b border-divider-subtle p-3 pb-2'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='lg' />
|
||||
<div className='system-sm-medium mx-2 grow text-text-primary'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
<div className='p-3' onClick={e => e.stopPropagation()}>
|
||||
<TagInput
|
||||
items={customFileTypes}
|
||||
onChange={onCustomFileTypesChange}
|
||||
placeholder={t('appDebug.variableConfig.file.custom.createPlaceholder')!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='lg' />
|
||||
<div className='mx-2 grow'>
|
||||
<div className='system-sm-medium text-text-primary'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<div className='system-2xs-regular-uppercase mt-1 text-text-tertiary'>{type !== SupportUploadFileTypes.custom ? FILE_EXTS[type].join(', ') : t('appDebug.variableConfig.file.custom.description')}</div>
|
||||
</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FileTypeItem)
|
||||
@@ -0,0 +1,199 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { produce } from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { UploadFileSetting } from '../../../types'
|
||||
import { SupportUploadFileTypes } from '../../../types'
|
||||
import OptionCard from './option-card'
|
||||
import FileTypeItem from './file-type-item'
|
||||
import InputNumberWithSlider from './input-number-with-slider'
|
||||
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
|
||||
type Props = {
|
||||
payload: UploadFileSetting
|
||||
isMultiple: boolean
|
||||
inFeaturePanel?: boolean
|
||||
hideSupportFileType?: boolean
|
||||
onChange: (payload: UploadFileSetting) => void
|
||||
}
|
||||
|
||||
const FileUploadSetting: FC<Props> = ({
|
||||
payload,
|
||||
isMultiple,
|
||||
inFeaturePanel = false,
|
||||
hideSupportFileType = false,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
allowed_file_upload_methods,
|
||||
max_length,
|
||||
allowed_file_types,
|
||||
allowed_file_extensions,
|
||||
} = payload
|
||||
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
|
||||
const {
|
||||
imgSizeLimit,
|
||||
docSizeLimit,
|
||||
audioSizeLimit,
|
||||
videoSizeLimit,
|
||||
maxFileUploadLimit,
|
||||
} = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
if (type === SupportUploadFileTypes.custom) {
|
||||
if (!draft.allowed_file_types.includes(SupportUploadFileTypes.custom))
|
||||
draft.allowed_file_types = [SupportUploadFileTypes.custom]
|
||||
|
||||
else
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== type)
|
||||
}
|
||||
else {
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== SupportUploadFileTypes.custom)
|
||||
if (draft.allowed_file_types.includes(type))
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== type)
|
||||
else
|
||||
draft.allowed_file_types.push(type)
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleUploadMethodChange = useCallback((method: TransferMethod) => {
|
||||
return () => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
if (method === TransferMethod.all)
|
||||
draft.allowed_file_upload_methods = [TransferMethod.local_file, TransferMethod.remote_url]
|
||||
else
|
||||
draft.allowed_file_upload_methods = [method]
|
||||
})
|
||||
onChange(newPayload)
|
||||
}
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleCustomFileTypesChange = useCallback((customFileTypes: string[]) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.allowed_file_extensions = customFileTypes.map((v) => {
|
||||
return v
|
||||
})
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleMaxUploadNumLimitChange = useCallback((value: number) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.max_length = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!inFeaturePanel && (
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.uploadFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.localUpload')}
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.local_file)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.local_file)}
|
||||
/>
|
||||
<OptionCard
|
||||
title="URL"
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.remote_url)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.both')}
|
||||
selected={allowed_file_upload_methods.includes(TransferMethod.local_file) && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.all)}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
{isMultiple && (
|
||||
<Field
|
||||
className='mt-4'
|
||||
title={t('appDebug.variableConfig.maxNumberOfUploads')!}
|
||||
>
|
||||
<div>
|
||||
<div className='body-xs-regular mb-1.5 text-text-tertiary'>{t('appDebug.variableConfig.maxNumberTip', {
|
||||
imgLimit: formatFileSize(imgSizeLimit),
|
||||
docLimit: formatFileSize(docSizeLimit),
|
||||
audioLimit: formatFileSize(audioSizeLimit),
|
||||
videoLimit: formatFileSize(videoSizeLimit),
|
||||
})}</div>
|
||||
|
||||
<InputNumberWithSlider
|
||||
value={max_length}
|
||||
min={1}
|
||||
max={maxFileUploadLimit}
|
||||
onChange={handleMaxUploadNumLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
{inFeaturePanel && !hideSupportFileType && (
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FileUploadSetting)
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const FormInputBoolean: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex w-full space-x-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
|
||||
!value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
)}
|
||||
onClick={() => onChange(true)}
|
||||
>True</div>
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
|
||||
value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
!value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
)}
|
||||
onClick={() => onChange(false)}
|
||||
>False</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormInputBoolean
|
||||
@@ -0,0 +1,599 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { type ResourceVarInputs, VarKindType } from '../types'
|
||||
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import FormInputTypeSwitch from './form-input-type-switch'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { RiCheckLine, RiLoader4Line } from '@remixicon/react'
|
||||
import type { Event } from '@/app/components/tools/types'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import FormInputBoolean from './form-input-boolean'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema
|
||||
value: ResourceVarInputs
|
||||
onChange: (value: any) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool | Event
|
||||
currentProvider?: ToolWithProvider | TriggerWithProvider
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
extraParams?: Record<string, any>
|
||||
providerType?: string
|
||||
disableVariableInsertion?: boolean
|
||||
}
|
||||
|
||||
const FormInputItem: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
extraParams,
|
||||
providerType,
|
||||
disableVariableInsertion = false,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
variable,
|
||||
type,
|
||||
_type,
|
||||
default: defaultValue,
|
||||
options,
|
||||
multiple,
|
||||
scope,
|
||||
} = schema as any
|
||||
const varInput = value[variable]
|
||||
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isObject = type === FormTypeEnum.object
|
||||
const isArray = type === FormTypeEnum.array
|
||||
const isShowJSONEditor = isObject || isArray
|
||||
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
|
||||
const isBoolean = _type === FormTypeEnum.boolean
|
||||
const isCheckbox = _type === FormTypeEnum.checkbox
|
||||
const isSelect = type === FormTypeEnum.select
|
||||
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect
|
||||
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
|
||||
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
|
||||
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const targetVarType = () => {
|
||||
if (isString)
|
||||
return VarType.string
|
||||
else if (isNumber)
|
||||
return VarType.number
|
||||
else if (type === FormTypeEnum.files)
|
||||
return VarType.arrayFile
|
||||
else if (type === FormTypeEnum.file)
|
||||
return VarType.file
|
||||
else if (isSelect)
|
||||
return VarType.string
|
||||
// else if (isAppSelector)
|
||||
// return VarType.appSelector
|
||||
// else if (isModelSelector)
|
||||
// return VarType.modelSelector
|
||||
else if (isBoolean)
|
||||
return VarType.boolean
|
||||
else if (isObject)
|
||||
return VarType.object
|
||||
else if (isArray)
|
||||
return VarType.arrayObject
|
||||
else
|
||||
return VarType.string
|
||||
}
|
||||
|
||||
const getFilterVar = () => {
|
||||
if (isNumber)
|
||||
return (varPayload: any) => varPayload.type === VarType.number
|
||||
else if (isString)
|
||||
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
else if (isFile)
|
||||
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
else if (isBoolean)
|
||||
return (varPayload: any) => varPayload.type === VarType.boolean
|
||||
else if (isObject)
|
||||
return (varPayload: any) => varPayload.type === VarType.object
|
||||
else if (isArray)
|
||||
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getVarKindType = () => {
|
||||
if (isFile)
|
||||
return VarKindType.variable
|
||||
if (isSelect || isDynamicSelect || isBoolean || isNumber || isArray || isObject)
|
||||
return VarKindType.constant
|
||||
if (isString)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
// Fetch dynamic options hook for tools
|
||||
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
|
||||
currentProvider?.plugin_id || '',
|
||||
currentProvider?.name || '',
|
||||
currentTool?.name || '',
|
||||
variable || '',
|
||||
providerType,
|
||||
extraParams,
|
||||
)
|
||||
|
||||
// Fetch dynamic options hook for triggers
|
||||
const { data: triggerDynamicOptions, isLoading: isTriggerOptionsLoading } = useTriggerPluginDynamicOptions({
|
||||
plugin_id: currentProvider?.plugin_id || '',
|
||||
provider: currentProvider?.name || '',
|
||||
action: currentTool?.name || '',
|
||||
parameter: variable || '',
|
||||
extra: extraParams,
|
||||
credential_id: currentProvider?.credential_id || '',
|
||||
}, isDynamicSelect && providerType === PluginCategoryEnum.trigger && !!currentTool && !!currentProvider)
|
||||
|
||||
// Computed values for dynamic options (unified for triggers and tools)
|
||||
const triggerOptions = triggerDynamicOptions?.options
|
||||
const dynamicOptions = providerType === PluginCategoryEnum.trigger
|
||||
? triggerOptions ?? toolsOptions
|
||||
: toolsOptions
|
||||
const isLoadingOptions = providerType === PluginCategoryEnum.trigger
|
||||
? (isTriggerOptionsLoading || isLoadingToolsOptions)
|
||||
: isLoadingToolsOptions
|
||||
|
||||
// Fetch dynamic options for tools only (triggers use hook directly)
|
||||
useEffect(() => {
|
||||
const fetchPanelDynamicOptions = async () => {
|
||||
if (isDynamicSelect && currentTool && currentProvider && (providerType === PluginCategoryEnum.tool || providerType === PluginCategoryEnum.trigger)) {
|
||||
setIsLoadingToolsOptions(true)
|
||||
try {
|
||||
const data = await fetchDynamicOptions()
|
||||
setToolsOptions(data?.options || [])
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch dynamic options:', error)
|
||||
setToolsOptions([])
|
||||
}
|
||||
finally {
|
||||
setIsLoadingToolsOptions(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchPanelDynamicOptions()
|
||||
}, [
|
||||
isDynamicSelect,
|
||||
currentTool?.name,
|
||||
currentProvider?.name,
|
||||
variable,
|
||||
extraParams,
|
||||
providerType,
|
||||
fetchDynamicOptions,
|
||||
])
|
||||
|
||||
const handleTypeChange = (newType: string) => {
|
||||
if (newType === VarKindType.variable) {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.variable,
|
||||
value: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.constant,
|
||||
value: defaultValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleValueChange = (newValue: any) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: getVarKindType(),
|
||||
value: isNumber ? Number.parseFloat(newValue) : newValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectedLabels = (selectedValues: any[]) => {
|
||||
if (!selectedValues || selectedValues.length === 0)
|
||||
return ''
|
||||
|
||||
const optionsList = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
|
||||
const selectedOptions = optionsList.filter((opt: any) =>
|
||||
selectedValues.includes(opt.value),
|
||||
)
|
||||
|
||||
if (selectedOptions.length <= 2) {
|
||||
return selectedOptions
|
||||
.map((opt: any) => opt.label?.[language] || opt.label?.en_US || opt.value)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
return `${selectedOptions.length} selected`
|
||||
}
|
||||
|
||||
const handleAppOrModelSelect = (newValue: any) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
value: newValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleVariableSelectorChange = (newValue: ValueSelector | string, variable: string) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.variable,
|
||||
value: newValue || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const availableCheckboxOptions = useMemo(() => (
|
||||
(options || []).filter((option: { show_on?: Array<{ variable: string; value: any }> }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable]?.value === showOnItem.value || value[showOnItem.variable] === showOnItem.value)
|
||||
return true
|
||||
})
|
||||
), [options, value])
|
||||
|
||||
const checkboxListOptions = useMemo(() => (
|
||||
availableCheckboxOptions.map((option: { value: string; label: Record<string, string> }) => ({
|
||||
value: option.value,
|
||||
label: option.label?.[language] || option.label?.en_US || option.value,
|
||||
}))
|
||||
), [availableCheckboxOptions, language])
|
||||
|
||||
const checkboxListValue = useMemo(() => {
|
||||
let current: string[] = []
|
||||
if (Array.isArray(varInput?.value))
|
||||
current = varInput.value as string[]
|
||||
else if (typeof varInput?.value === 'string')
|
||||
current = [varInput.value as string]
|
||||
else if (Array.isArray(defaultValue))
|
||||
current = defaultValue as string[]
|
||||
|
||||
const allowedValues = new Set(availableCheckboxOptions.map((option: { value: string }) => option.value))
|
||||
return current.filter(item => allowedValues.has(item))
|
||||
}, [varInput?.value, defaultValue, availableCheckboxOptions])
|
||||
|
||||
const handleCheckboxListChange = (selected: string[]) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.constant,
|
||||
value: selected,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
|
||||
{showTypeSwitch && (
|
||||
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange} />
|
||||
)}
|
||||
{isString && (
|
||||
<MixedVariableTextInput
|
||||
readOnly={readOnly}
|
||||
value={varInput?.value as string || ''}
|
||||
onChange={handleValueChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
showManageInputField={showManageInputField}
|
||||
onManageInputField={onManageInputField}
|
||||
disableVariableInsertion={disableVariableInsertion}
|
||||
/>
|
||||
)}
|
||||
{isNumber && isConstant && (
|
||||
<Input
|
||||
className='h-8 grow'
|
||||
type='number'
|
||||
value={Number.isNaN(varInput?.value) ? '' : varInput?.value}
|
||||
onChange={e => handleValueChange(e.target.value)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isCheckbox && isConstant && (
|
||||
<CheckboxList
|
||||
title={schema.label?.[language] || schema.label?.en_US || variable}
|
||||
value={checkboxListValue}
|
||||
onChange={handleCheckboxListChange}
|
||||
options={checkboxListOptions}
|
||||
disabled={readOnly}
|
||||
maxHeight='200px'
|
||||
/>
|
||||
)}
|
||||
{isBoolean && isConstant && (
|
||||
<FormInputBoolean
|
||||
value={varInput?.value as boolean}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
)}
|
||||
{isSelect && isConstant && !isMultipleSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
disabled={readOnly}
|
||||
defaultValue={varInput?.value}
|
||||
items={options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => ({
|
||||
value: option.value,
|
||||
name: option.label[language] || option.label.en_US,
|
||||
icon: option.icon,
|
||||
}))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
renderOption={options.some((opt: any) => opt.icon) ? ({ item }) => (
|
||||
<div className="flex items-center">
|
||||
{item.icon && (
|
||||
<img src={item.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
) : undefined}
|
||||
/>
|
||||
)}
|
||||
{isSelect && isConstant && isMultipleSelect && (
|
||||
<Listbox
|
||||
multiple
|
||||
value={varInput?.value || []}
|
||||
onChange={handleValueChange}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<div className="group/simple-select relative h-8 grow">
|
||||
<ListboxButton className="flex h-full w-full cursor-pointer items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6">
|
||||
<span className={cn('system-sm-regular block truncate text-left',
|
||||
varInput?.value?.length > 0 ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder',
|
||||
)}>
|
||||
{getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
|
||||
{options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ focus }) =>
|
||||
cn('relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
focus && 'bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
{option.icon && (
|
||||
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className={cn('block truncate', selected && 'font-normal')}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
</span>
|
||||
</div>
|
||||
{selected && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent">
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
)}
|
||||
{isDynamicSelect && !isMultipleSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
disabled={readOnly || isLoadingOptions}
|
||||
defaultValue={varInput?.value}
|
||||
items={(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => ({
|
||||
value: option.value,
|
||||
name: option.label[language] || option.label.en_US,
|
||||
icon: option.icon,
|
||||
}))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
placeholder={isLoadingOptions ? 'Loading...' : (placeholder?.[language] || placeholder?.en_US)}
|
||||
renderOption={({ item }) => (
|
||||
<div className="flex items-center">
|
||||
{item.icon && (
|
||||
<img src={item.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDynamicSelect && isMultipleSelect && (
|
||||
<Listbox
|
||||
multiple
|
||||
value={varInput?.value || []}
|
||||
onChange={handleValueChange}
|
||||
disabled={readOnly || isLoadingOptions}
|
||||
>
|
||||
<div className="group/simple-select relative h-8 grow">
|
||||
<ListboxButton className="flex h-full w-full cursor-pointer items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6">
|
||||
<span className={cn('system-sm-regular block truncate text-left',
|
||||
isLoadingOptions ? 'text-components-input-text-placeholder'
|
||||
: varInput?.value?.length > 0 ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder',
|
||||
)}>
|
||||
{isLoadingOptions
|
||||
? 'Loading...'
|
||||
: getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoadingOptions ? (
|
||||
<RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
|
||||
{(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ focus }) =>
|
||||
cn('relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
focus && 'bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
{option.icon && (
|
||||
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className={cn('block truncate', selected && 'font-normal')}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
</span>
|
||||
</div>
|
||||
{selected && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-text-accent">
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
)}
|
||||
{isShowJSONEditor && isConstant && (
|
||||
<div className='mt-1 w-full'>
|
||||
<CodeEditor
|
||||
title='JSON'
|
||||
value={varInput?.value as any}
|
||||
isExpand
|
||||
isInNode
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleValueChange}
|
||||
className='w-full'
|
||||
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isAppSelector && (
|
||||
<AppSelector
|
||||
disabled={readOnly}
|
||||
scope={scope || 'all'}
|
||||
value={varInput?.value}
|
||||
onSelect={handleAppOrModelSelect}
|
||||
/>
|
||||
)}
|
||||
{isModelSelector && isConstant && (
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isAdvancedMode
|
||||
isInWorkflow
|
||||
value={varInput?.value}
|
||||
setModel={handleAppOrModelSelect}
|
||||
readonly={readOnly}
|
||||
scope={scope}
|
||||
/>
|
||||
)}
|
||||
{showVariableSelector && (
|
||||
<VarReferencePicker
|
||||
zIndex={inPanel ? 1000 : undefined}
|
||||
className='h-8 grow'
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.value || []}
|
||||
onChange={value => handleVariableSelectorChange(value, variable)}
|
||||
filterVar={getFilterVar()}
|
||||
schema={schema}
|
||||
valueTypePlaceHolder={targetVarType()}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
isFilterFileVar={isBoolean}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormInputItem
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { VarType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: VarType
|
||||
onChange: (value: VarType) => void
|
||||
}
|
||||
|
||||
const FormInputTypeSwitch: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='inline-flex h-8 shrink-0 gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5'>
|
||||
<Tooltip
|
||||
popupContent={value === VarType.variable ? '' : t('workflow.nodes.common.typeSwitch.variable')}
|
||||
>
|
||||
<div
|
||||
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.variable && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
|
||||
onClick={() => onChange(VarType.variable)}
|
||||
>
|
||||
<Variable02 className='h-4 w-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={value === VarType.constant ? '' : t('workflow.nodes.common.typeSwitch.input')}
|
||||
>
|
||||
<div
|
||||
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.constant && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
|
||||
onClick={() => onChange(VarType.constant)}
|
||||
>
|
||||
<RiEditLine className='h-4 w-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormInputTypeSwitch
|
||||
@@ -0,0 +1,25 @@
|
||||
import classNames from '@/utils/classnames'
|
||||
import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react'
|
||||
|
||||
export type GroupLabelProps = ComponentProps<'div'>
|
||||
|
||||
export const GroupLabel: FC<GroupLabelProps> = (props) => {
|
||||
const { children, className, ...rest } = props
|
||||
return <div {...rest} className={classNames('system-2xs-medium-uppercase mb-1 text-text-tertiary', className)}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
export type GroupProps = PropsWithChildren<{
|
||||
label: ReactNode
|
||||
}>
|
||||
|
||||
export const Group: FC<GroupProps> = (props) => {
|
||||
const { children, label } = props
|
||||
return <div className={classNames('py-1')}>
|
||||
{label}
|
||||
<div className='space-y-0.5'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiBookOpenLine } from '@remixicon/react'
|
||||
import { useNodeHelpLink } from '../hooks/use-node-help-link'
|
||||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type HelpLinkProps = {
|
||||
nodeType: BlockEnum
|
||||
}
|
||||
const HelpLink = ({
|
||||
nodeType,
|
||||
}: HelpLinkProps) => {
|
||||
const { t } = useTranslation()
|
||||
const link = useNodeHelpLink(nodeType)
|
||||
|
||||
if (!link)
|
||||
return null
|
||||
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.userProfile.helpCenter')}
|
||||
>
|
||||
<a
|
||||
href={link}
|
||||
target='_blank'
|
||||
className='mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
>
|
||||
<RiBookOpenLine className='h-4 w-4 text-gray-500' />
|
||||
</a>
|
||||
</TooltipPlus>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(HelpLink)
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
content: ReactNode
|
||||
}
|
||||
|
||||
const InfoPanel: FC<Props> = ({
|
||||
title,
|
||||
content,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex flex-col gap-y-0.5 rounded-md bg-workflow-block-parma-bg px-[5px] py-[3px]'>
|
||||
<div className='system-2xs-semibold-uppercase uppercase text-text-secondary'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='system-xs-regular break-words text-text-tertiary'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InfoPanel)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
const Add = () => {
|
||||
return (
|
||||
<ActionButton>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Add
|
||||
@@ -0,0 +1,24 @@
|
||||
import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import Add from './add'
|
||||
|
||||
const InputField = () => {
|
||||
return (
|
||||
<BoxGroupField
|
||||
fieldProps={{
|
||||
supportCollapse: true,
|
||||
fieldTitleProps: {
|
||||
title: 'input field',
|
||||
operation: <Add />,
|
||||
},
|
||||
}}
|
||||
boxGroupProps={{
|
||||
boxProps: {
|
||||
withBorderBottom: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
input field
|
||||
</BoxGroupField>
|
||||
)
|
||||
}
|
||||
export default InputField
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
export type InputNumberWithSliderProps = {
|
||||
value: number
|
||||
defaultValue?: number
|
||||
min?: number
|
||||
max?: number
|
||||
readonly?: boolean
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const InputNumberWithSlider: FC<InputNumberWithSliderProps> = ({
|
||||
value,
|
||||
defaultValue = 0,
|
||||
min,
|
||||
max,
|
||||
readonly,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleBlur = useCallback(() => {
|
||||
if (value === undefined || value === null) {
|
||||
onChange(defaultValue)
|
||||
return
|
||||
}
|
||||
if (max !== undefined && value > max) {
|
||||
onChange(max)
|
||||
return
|
||||
}
|
||||
if (min !== undefined && value < min)
|
||||
onChange(min)
|
||||
}, [defaultValue, max, min, onChange, value])
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(Number.parseFloat(e.target.value))
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className='flex h-8 items-center justify-between space-x-2'>
|
||||
<input
|
||||
value={value}
|
||||
className='block h-8 w-12 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 text-[13px] text-components-input-text-filled outline-none'
|
||||
type='number'
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<Slider
|
||||
className='grow'
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={onChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputNumberWithSlider)
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
type Props = {
|
||||
instanceId?: string
|
||||
className?: string
|
||||
placeholder?: string
|
||||
placeholderClassName?: string
|
||||
promptMinHeightClassName?: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onFocusChange?: (value: boolean) => void
|
||||
readOnly?: boolean
|
||||
justVar?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
insertVarTipToLeft?: boolean
|
||||
}
|
||||
|
||||
const Editor: FC<Props> = ({
|
||||
instanceId,
|
||||
className,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
promptMinHeightClassName = 'min-h-[20px]',
|
||||
value,
|
||||
onChange,
|
||||
onFocusChange,
|
||||
readOnly,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
insertVarTipToLeft,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFocus, {
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean(false)
|
||||
|
||||
useEffect(() => {
|
||||
onFocusChange?.(isFocus)
|
||||
}, [isFocus])
|
||||
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'relative')}>
|
||||
<>
|
||||
<PromptEditor
|
||||
instanceId={instanceId}
|
||||
className={cn(promptMinHeightClassName, '!leading-[18px]')}
|
||||
placeholder={placeholder}
|
||||
placeholderClassName={placeholderClassName}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
datasets: [],
|
||||
onAddContext: noop,
|
||||
}}
|
||||
historyBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
history: {
|
||||
user: 'Human',
|
||||
assistant: 'Assistant',
|
||||
},
|
||||
onEditRole: noop,
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false,
|
||||
selectable: false,
|
||||
}}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
position: node.position,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
showManageInputField: !!pipelineId,
|
||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable={!readOnly}
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
{isFocus && (
|
||||
<div className={cn('absolute z-10', insertVarTipToLeft ? 'left-[-12px] top-1.5' : ' right-1 top-[-9px]')}>
|
||||
<Tooltip
|
||||
popupContent={`${t('workflow.common.insertVarTip')}`}
|
||||
>
|
||||
<div className='cursor-pointer rounded-[5px] border-[0.5px] border-divider-regular bg-components-badge-white-to-dark p-0.5 shadow-lg'>
|
||||
<Variable02 className='h-3.5 w-3.5 text-components-button-secondary-accent-text' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(Editor)
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
RiAlignLeft,
|
||||
RiBracesLine,
|
||||
RiCheckboxLine,
|
||||
RiCheckboxMultipleLine,
|
||||
RiFileCopy2Line,
|
||||
RiFileList2Line,
|
||||
RiHashtag,
|
||||
RiTextSnippet,
|
||||
} from '@remixicon/react'
|
||||
import { InputVarType } from '../../../types'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
type: InputVarType
|
||||
}
|
||||
|
||||
const getIcon = (type: InputVarType) => {
|
||||
return ({
|
||||
[InputVarType.textInput]: RiTextSnippet,
|
||||
[InputVarType.paragraph]: RiAlignLeft,
|
||||
[InputVarType.select]: RiCheckboxMultipleLine,
|
||||
[InputVarType.number]: RiHashtag,
|
||||
[InputVarType.checkbox]: RiCheckboxLine,
|
||||
[InputVarType.jsonObject]: RiBracesLine,
|
||||
[InputVarType.singleFile]: RiFileList2Line,
|
||||
[InputVarType.multiFiles]: RiFileCopy2Line,
|
||||
} as any)[type] || RiTextSnippet
|
||||
}
|
||||
|
||||
const InputVarTypeIcon: FC<Props> = ({
|
||||
className,
|
||||
type,
|
||||
}) => {
|
||||
const Icon = getIcon(type)
|
||||
return (
|
||||
<Icon className={className} />
|
||||
)
|
||||
}
|
||||
export default React.memo(InputVarTypeIcon)
|
||||
@@ -0,0 +1,104 @@
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
|
||||
import type { ComponentProps, MouseEventHandler } from 'react'
|
||||
import { useState } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
|
||||
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
|
||||
uniqueIdentifier: string
|
||||
extraIdentifiers?: string[]
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
|
||||
const {
|
||||
className,
|
||||
uniqueIdentifier,
|
||||
extraIdentifiers = [],
|
||||
onSuccess,
|
||||
...rest
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const identifiers = Array.from(new Set(
|
||||
[uniqueIdentifier, ...extraIdentifiers].filter((item): item is string => Boolean(item)),
|
||||
))
|
||||
const manifest = useCheckInstalled({
|
||||
pluginIds: identifiers,
|
||||
enabled: identifiers.length > 0,
|
||||
})
|
||||
const install = useInstallPackageFromMarketPlace()
|
||||
const [isTracking, setIsTracking] = useState(false)
|
||||
const isLoading = manifest.isLoading || install.isPending || isTracking
|
||||
const handleInstall: MouseEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
if (isLoading)
|
||||
return
|
||||
setIsTracking(true)
|
||||
install.mutate(uniqueIdentifier, {
|
||||
onSuccess: async (response) => {
|
||||
const finish = async () => {
|
||||
await manifest.refetch()
|
||||
onSuccess?.()
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
await finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (response.all_installed) {
|
||||
await finish()
|
||||
return
|
||||
}
|
||||
|
||||
const { check } = checkTaskStatus()
|
||||
try {
|
||||
const { status } = await check({
|
||||
taskId: response.task_id,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
})
|
||||
|
||||
if (status === TaskStatus.failed) {
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
return
|
||||
}
|
||||
|
||||
await finish()
|
||||
}
|
||||
catch {
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setIsTracking(false)
|
||||
install.reset()
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!manifest.data) return null
|
||||
const identifierSet = new Set(identifiers)
|
||||
const isInstalled = manifest.data.plugins.some(plugin => (
|
||||
identifierSet.has(plugin.id)
|
||||
|| (plugin.plugin_unique_identifier && identifierSet.has(plugin.plugin_unique_identifier))
|
||||
|| (plugin.plugin_id && identifierSet.has(plugin.plugin_id))
|
||||
))
|
||||
if (isInstalled) return null
|
||||
return <Button
|
||||
variant={'secondary'}
|
||||
disabled={isLoading}
|
||||
{...rest}
|
||||
onClick={handleInstall}
|
||||
className={classNames('flex items-center', className)}
|
||||
>
|
||||
{!isLoading ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
|
||||
{!isLoading ? <RiInstallLine className='ml-1 size-3.5' /> : <RiLoader2Line className='ml-1 size-3.5 animate-spin' />}
|
||||
</Button>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type {
|
||||
BoxGroupProps,
|
||||
FieldProps,
|
||||
} from '.'
|
||||
import {
|
||||
BoxGroup,
|
||||
Field,
|
||||
} from '.'
|
||||
|
||||
type BoxGroupFieldProps = {
|
||||
children?: ReactNode
|
||||
boxGroupProps?: Omit<BoxGroupProps, 'children'>
|
||||
fieldProps?: Omit<FieldProps, 'children'>
|
||||
}
|
||||
export const BoxGroupField = memo(({
|
||||
children,
|
||||
fieldProps,
|
||||
boxGroupProps,
|
||||
}: BoxGroupFieldProps) => {
|
||||
return (
|
||||
<BoxGroup {...boxGroupProps}>
|
||||
<Field {...fieldProps}>
|
||||
{children}
|
||||
</Field>
|
||||
</BoxGroup>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Group,
|
||||
} from '.'
|
||||
import type {
|
||||
BoxProps,
|
||||
GroupProps,
|
||||
} from '.'
|
||||
|
||||
export type BoxGroupProps = {
|
||||
children?: ReactNode
|
||||
boxProps?: Omit<BoxProps, 'children'>
|
||||
groupProps?: Omit<GroupProps, 'children'>
|
||||
}
|
||||
export const BoxGroup = memo(({
|
||||
children,
|
||||
boxProps,
|
||||
groupProps,
|
||||
}: BoxGroupProps) => {
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
<Group {...groupProps}>
|
||||
{children}
|
||||
</Group>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type BoxProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
withBorderBottom?: boolean
|
||||
}
|
||||
export const Box = memo(({
|
||||
className,
|
||||
children,
|
||||
withBorderBottom,
|
||||
}: BoxProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'py-2',
|
||||
withBorderBottom && 'border-b border-divider-subtle',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type FieldTitleProps = {
|
||||
title?: string
|
||||
operation?: ReactNode
|
||||
subTitle?: string | ReactNode
|
||||
tooltip?: string
|
||||
showArrow?: boolean
|
||||
disabled?: boolean
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
}
|
||||
export const FieldTitle = memo(({
|
||||
title,
|
||||
operation,
|
||||
subTitle,
|
||||
tooltip,
|
||||
showArrow,
|
||||
disabled,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
}: FieldTitleProps) => {
|
||||
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
||||
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||
|
||||
return (
|
||||
<div className={cn('mb-0.5', !!subTitle && 'mb-1')}>
|
||||
<div
|
||||
className='group/collapse flex items-center justify-between py-1'
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setCollapsedLocal(!collapsedMerged)
|
||||
onCollapse?.(!collapsedMerged)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='system-sm-semibold-uppercase flex items-center text-text-secondary'>
|
||||
{title}
|
||||
{
|
||||
showArrow && (
|
||||
<ArrowDownRoundFill
|
||||
className={cn(
|
||||
'h-4 w-4 cursor-pointer text-text-quaternary group-hover/collapse:text-text-secondary',
|
||||
collapsedMerged && 'rotate-[270deg]',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
tooltip && (
|
||||
<Tooltip
|
||||
popupContent={tooltip}
|
||||
triggerClassName='w-4 h-4 ml-1'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{operation}
|
||||
</div>
|
||||
{
|
||||
subTitle
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { FieldTitleProps } from '.'
|
||||
import { FieldTitle } from '.'
|
||||
|
||||
export type FieldProps = {
|
||||
fieldTitleProps?: FieldTitleProps
|
||||
children?: ReactNode
|
||||
disabled?: boolean
|
||||
supportCollapse?: boolean
|
||||
}
|
||||
export const Field = memo(({
|
||||
fieldTitleProps,
|
||||
children,
|
||||
supportCollapse,
|
||||
disabled,
|
||||
}: FieldProps) => {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldTitle
|
||||
{...fieldTitleProps}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
showArrow={supportCollapse}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{supportCollapse && !collapsed && children}
|
||||
{!supportCollapse && children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type {
|
||||
FieldProps,
|
||||
GroupProps,
|
||||
} from '.'
|
||||
import {
|
||||
Field,
|
||||
Group,
|
||||
} from '.'
|
||||
|
||||
type GroupFieldProps = {
|
||||
children?: ReactNode
|
||||
groupProps?: Omit<GroupProps, 'children'>
|
||||
fieldProps?: Omit<FieldProps, 'children'>
|
||||
}
|
||||
export const GroupField = memo(({
|
||||
children,
|
||||
fieldProps,
|
||||
groupProps,
|
||||
}: GroupFieldProps) => {
|
||||
return (
|
||||
<Group {...groupProps}>
|
||||
<Field {...fieldProps}>
|
||||
{children}
|
||||
</Field>
|
||||
</Group>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type GroupProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
withBorderBottom?: boolean
|
||||
}
|
||||
export const Group = memo(({
|
||||
className,
|
||||
children,
|
||||
withBorderBottom,
|
||||
}: GroupProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'px-4 py-2',
|
||||
withBorderBottom && 'border-b border-divider-subtle',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './box'
|
||||
export * from './group'
|
||||
export * from './box-group'
|
||||
export * from './field-title'
|
||||
export * from './field'
|
||||
export * from './group-field'
|
||||
export * from './box-group-field'
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ListNoDataPlaceholder: FC<Props> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className='system-xs-regular flex min-h-[42px] w-full items-center justify-center rounded-[10px] bg-background-section text-text-tertiary'>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ListNoDataPlaceholder)
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const McpToolNotSupportTooltip: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[256px]'>
|
||||
{t('plugin.detailPanel.toolSelector.unsupportedMCPTool')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RiAlertFill className='size-4 text-text-warning-secondary' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
export default React.memo(McpToolNotSupportTooltip)
|
||||
@@ -0,0 +1,207 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import type { Memory } from '../../../types'
|
||||
import { MemoryRole } from '../../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.common.memory'
|
||||
const WINDOW_SIZE_MIN = 1
|
||||
const WINDOW_SIZE_MAX = 100
|
||||
const WINDOW_SIZE_DEFAULT = 50
|
||||
type RoleItemProps = {
|
||||
readonly: boolean
|
||||
title: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const RoleItem: FC<RoleItemProps> = ({
|
||||
readonly,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='text-[13px] font-normal text-text-secondary'>{title}</div>
|
||||
<Input
|
||||
readOnly={readonly}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className='h-8 w-[200px]'
|
||||
type='text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
readonly: boolean
|
||||
config: { data?: Memory }
|
||||
onChange: (memory?: Memory) => void
|
||||
canSetRoleName?: boolean
|
||||
}
|
||||
|
||||
const MEMORY_DEFAULT: Memory = {
|
||||
window: { enabled: false, size: WINDOW_SIZE_DEFAULT },
|
||||
query_prompt_template: '{{#sys.query#}}\n\n{{#sys.files#}}',
|
||||
}
|
||||
|
||||
const MemoryConfig: FC<Props> = ({
|
||||
className,
|
||||
readonly,
|
||||
config = { data: MEMORY_DEFAULT },
|
||||
onChange,
|
||||
canSetRoleName = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const payload = config.data
|
||||
const handleMemoryEnabledChange = useCallback((enabled: boolean) => {
|
||||
onChange(enabled ? MEMORY_DEFAULT : undefined)
|
||||
}, [onChange])
|
||||
const handleWindowEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newPayload = produce(config.data || MEMORY_DEFAULT, (draft) => {
|
||||
if (!draft.window)
|
||||
draft.window = { enabled: false, size: WINDOW_SIZE_DEFAULT }
|
||||
|
||||
draft.window.enabled = enabled
|
||||
})
|
||||
|
||||
onChange(newPayload)
|
||||
}, [config, onChange])
|
||||
|
||||
const handleWindowSizeChange = useCallback((size: number | string) => {
|
||||
const newPayload = produce(payload || MEMORY_DEFAULT, (draft) => {
|
||||
if (!draft.window)
|
||||
draft.window = { enabled: true, size: WINDOW_SIZE_DEFAULT }
|
||||
let limitedSize: null | string | number = size
|
||||
if (limitedSize === '') {
|
||||
limitedSize = null
|
||||
}
|
||||
else {
|
||||
limitedSize = Number.parseInt(limitedSize as string, 10)
|
||||
if (isNaN(limitedSize))
|
||||
limitedSize = WINDOW_SIZE_DEFAULT
|
||||
|
||||
if (limitedSize < WINDOW_SIZE_MIN)
|
||||
limitedSize = WINDOW_SIZE_MIN
|
||||
|
||||
if (limitedSize > WINDOW_SIZE_MAX)
|
||||
limitedSize = WINDOW_SIZE_MAX
|
||||
}
|
||||
|
||||
draft.window.size = limitedSize as number
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [payload, onChange])
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
const payload = config.data
|
||||
if (!payload)
|
||||
return
|
||||
|
||||
if (payload.window.size === '' || payload.window.size === null)
|
||||
handleWindowSizeChange(WINDOW_SIZE_DEFAULT)
|
||||
}, [handleWindowSizeChange, config])
|
||||
|
||||
const handleRolePrefixChange = useCallback((role: MemoryRole) => {
|
||||
return (value: string) => {
|
||||
const newPayload = produce(config.data || MEMORY_DEFAULT, (draft) => {
|
||||
if (!draft.role_prefix) {
|
||||
draft.role_prefix = {
|
||||
user: '',
|
||||
assistant: '',
|
||||
}
|
||||
}
|
||||
draft.role_prefix[role] = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
}
|
||||
}, [config, onChange])
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.memory`)}
|
||||
tooltip={t(`${i18nPrefix}.memoryTip`)!}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={!!payload}
|
||||
onChange={handleMemoryEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{payload && (
|
||||
<>
|
||||
{/* window size */}
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex h-8 items-center space-x-2'>
|
||||
<Switch
|
||||
defaultValue={payload?.window?.enabled}
|
||||
onChange={handleWindowEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.windowSize`)}</div>
|
||||
</div>
|
||||
<div className='flex h-8 items-center space-x-2'>
|
||||
<Slider
|
||||
className='w-[144px]'
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={handleWindowSizeChange}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
<Input
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
wrapperClassName='w-12'
|
||||
className='appearance-none pr-0'
|
||||
type='number'
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={e => handleWindowSizeChange(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{canSetRoleName && (
|
||||
<div className='mt-4'>
|
||||
<div className='text-xs font-medium uppercase leading-6 text-text-tertiary'>{t(`${i18nPrefix}.conversationRoleName`)}</div>
|
||||
<div className='mt-1 space-y-2'>
|
||||
<RoleItem
|
||||
readonly={readonly}
|
||||
title={t(`${i18nPrefix}.user`)}
|
||||
value={payload.role_prefix?.user || ''}
|
||||
onChange={handleRolePrefixChange(MemoryRole.user)}
|
||||
/>
|
||||
<RoleItem
|
||||
readonly={readonly}
|
||||
title={t(`${i18nPrefix}.assistant`)}
|
||||
value={payload.role_prefix?.assistant || ''}
|
||||
onChange={handleRolePrefixChange(MemoryRole.assistant)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(MemoryConfig)
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import Placeholder from './placeholder'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type MixedVariableTextInputProps = {
|
||||
readOnly?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
value?: string
|
||||
onChange?: (text: string) => void
|
||||
}
|
||||
const MixedVariableTextInput = ({
|
||||
readOnly = false,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
value = '',
|
||||
onChange,
|
||||
}: MixedVariableTextInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PromptEditor
|
||||
wrapperClassName={cn(
|
||||
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
)}
|
||||
className='caret:text-text-accent'
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
placeholder={<Placeholder />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MixedVariableTextInput)
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { FOCUS_COMMAND } from 'lexical'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
const Placeholder = () => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const handleInsert = useCallback((text: string) => {
|
||||
editor.update(() => {
|
||||
const textNode = new CustomTextNode(text)
|
||||
$insertNodes([textNode])
|
||||
})
|
||||
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('')
|
||||
}}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
{t('workflow.nodes.tool.insertPlaceholder1')}
|
||||
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
|
||||
<div
|
||||
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
|
||||
onMouseDown={((e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleInsert('/')
|
||||
})}
|
||||
>
|
||||
{t('workflow.nodes.tool.insertPlaceholder2')}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text='String'
|
||||
uppercase={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type AddProps = {
|
||||
nodeId: string
|
||||
nodeData: CommonNodeType
|
||||
sourceHandle: string
|
||||
isParallel?: boolean
|
||||
isFailBranch?: boolean
|
||||
}
|
||||
const Add = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
sourceHandle,
|
||||
isParallel,
|
||||
isFailBranch,
|
||||
}: AddProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: nodeId,
|
||||
prevNodeSourceHandle: sourceHandle,
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd])
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}, [])
|
||||
|
||||
const tip = useMemo(() => {
|
||||
if (isFailBranch)
|
||||
return t('workflow.common.addFailureBranch')
|
||||
|
||||
if (isParallel)
|
||||
return t('workflow.common.addParallelNode')
|
||||
|
||||
return t('workflow.panel.selectNextStep')
|
||||
}, [isFailBranch, isParallel, t])
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
bg-dropzone-bg hover:bg-dropzone-bg-hover relative flex h-9 cursor-pointer items-center rounded-lg border border-dashed
|
||||
border-divider-regular px-2 text-xs text-text-placeholder
|
||||
${open && '!bg-components-dropzone-bg-alt'}
|
||||
${nodesReadOnly && '!cursor-not-allowed'}
|
||||
`}
|
||||
>
|
||||
<div className='mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px] bg-background-default-dimmed'>
|
||||
<RiAddLine className='h-3 w-3' />
|
||||
</div>
|
||||
<div className='flex items-center uppercase'>
|
||||
{tip}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [nodesReadOnly, tip])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
placement='top'
|
||||
offset={0}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Add)
|
||||
@@ -0,0 +1,65 @@
|
||||
import Add from './add'
|
||||
import Item from './item'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ContainerProps = {
|
||||
nodeId: string
|
||||
nodeData: CommonNodeType
|
||||
sourceHandle: string
|
||||
nextNodes: Node[]
|
||||
branchName?: string
|
||||
isFailBranch?: boolean
|
||||
}
|
||||
|
||||
const Container = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
sourceHandle,
|
||||
nextNodes,
|
||||
branchName,
|
||||
isFailBranch,
|
||||
}: ContainerProps) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'space-y-0.5 rounded-[10px] bg-background-section-burn p-0.5',
|
||||
isFailBranch && 'border-[0.5px] border-state-warning-hover-alt bg-state-warning-hover',
|
||||
)}>
|
||||
{
|
||||
branchName && (
|
||||
<div
|
||||
className={cn(
|
||||
'system-2xs-semibold-uppercase flex items-center truncate px-2 text-text-tertiary',
|
||||
isFailBranch && 'text-text-warning',
|
||||
)}
|
||||
title={branchName}
|
||||
>
|
||||
{branchName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
nextNodes.map(nextNode => (
|
||||
<Item
|
||||
key={nextNode.id}
|
||||
nodeId={nextNode.id}
|
||||
data={nextNode.data}
|
||||
sourceHandle='source'
|
||||
/>
|
||||
))
|
||||
}
|
||||
<Add
|
||||
isParallel={!!nextNodes.length}
|
||||
isFailBranch={isFailBranch}
|
||||
nodeId={nodeId}
|
||||
nodeData={nodeData}
|
||||
sourceHandle={sourceHandle}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Container
|
||||
@@ -0,0 +1,123 @@
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useStore,
|
||||
} from 'reactflow'
|
||||
import { useToolIcon } from '../../../../hooks'
|
||||
import BlockIcon from '../../../../block-icon'
|
||||
import type {
|
||||
Node,
|
||||
} from '../../../../types'
|
||||
import { BlockEnum } from '../../../../types'
|
||||
import Line from './line'
|
||||
import Container from './container'
|
||||
import { hasErrorHandleNode } from '@/app/components/workflow/utils'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
type NextStepProps = {
|
||||
selectedNode: Node
|
||||
}
|
||||
const NextStep = ({
|
||||
selectedNode,
|
||||
}: NextStepProps) => {
|
||||
const { t } = useTranslation()
|
||||
const data = selectedNode.data
|
||||
const toolIcon = useToolIcon(data)
|
||||
const branches = useMemo(() => {
|
||||
return data._targetBranches || []
|
||||
}, [data])
|
||||
const edges = useStore(s => s.edges.map(edge => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle,
|
||||
})), isEqual)
|
||||
const nodes = useStore(s => s.getNodes().map(node => ({
|
||||
id: node.id,
|
||||
data: node.data,
|
||||
})), isEqual)
|
||||
const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
|
||||
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
|
||||
|
||||
const list = useMemo(() => {
|
||||
let items = []
|
||||
if (branches?.length) {
|
||||
items = branches.map((branch, index) => {
|
||||
const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
|
||||
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
|
||||
|
||||
return {
|
||||
branch: {
|
||||
...branch,
|
||||
name: data.type === BlockEnum.QuestionClassifier ? `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}` : branch.name,
|
||||
},
|
||||
nextNodes,
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
const connected = connectedEdges.filter(edge => edge.sourceHandle === 'source')
|
||||
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
|
||||
|
||||
items = [{
|
||||
branch: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
nextNodes,
|
||||
}]
|
||||
|
||||
if (data.error_strategy === ErrorHandleTypeEnum.failBranch && hasErrorHandleNode(data.type)) {
|
||||
const connected = connectedEdges.filter(edge => edge.sourceHandle === ErrorHandleTypeEnum.failBranch)
|
||||
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
|
||||
|
||||
items.push({
|
||||
branch: {
|
||||
id: ErrorHandleTypeEnum.failBranch,
|
||||
name: t('workflow.common.onFailure'),
|
||||
},
|
||||
nextNodes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}, [branches, connectedEdges, data.error_strategy, data.type, outgoers, t])
|
||||
|
||||
return (
|
||||
<div className='flex py-1'>
|
||||
<div className='relative flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-background-default shadow-xs'>
|
||||
<BlockIcon
|
||||
type={selectedNode!.data.type}
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
</div>
|
||||
<Line
|
||||
list={list.length ? list.map(item => item.nextNodes.length + 1) : [1]}
|
||||
/>
|
||||
<div className='grow space-y-2'>
|
||||
{
|
||||
list.map((item, index) => {
|
||||
return (
|
||||
<Container
|
||||
key={index}
|
||||
nodeId={selectedNode!.id}
|
||||
nodeData={selectedNode!.data}
|
||||
sourceHandle={item.branch.id}
|
||||
nextNodes={item.nextNodes}
|
||||
branchName={item.branch.name}
|
||||
isFailBranch={item.branch.id === ErrorHandleTypeEnum.failBranch}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NextStep)
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Operator from './operator'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import {
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ItemProps = {
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
data: CommonNodeType
|
||||
}
|
||||
const Item = ({
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
data,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='group relative flex h-9 cursor-pointer items-center rounded-lg border-[0.5px] border-divider-regular bg-background-default px-2 text-xs text-text-secondary shadow-xs last-of-type:mb-0 hover:bg-background-default-hover'
|
||||
>
|
||||
<BlockIcon
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
className='mr-1.5 shrink-0'
|
||||
/>
|
||||
<div
|
||||
className='system-xs-medium grow truncate text-text-secondary'
|
||||
title={data.title}
|
||||
>
|
||||
{data.title}
|
||||
</div>
|
||||
{
|
||||
!nodesReadOnly && (
|
||||
<>
|
||||
<Button
|
||||
className='mr-1 hidden shrink-0 group-hover:flex'
|
||||
size='small'
|
||||
onClick={() => handleNodeSelect(nodeId)}
|
||||
>
|
||||
{t('workflow.common.jumpToNode')}
|
||||
</Button>
|
||||
<div
|
||||
className={cn(
|
||||
'hidden shrink-0 items-center group-hover:flex',
|
||||
open && 'flex',
|
||||
)}
|
||||
>
|
||||
<Operator
|
||||
data={data}
|
||||
nodeId={nodeId}
|
||||
sourceHandle={sourceHandle}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Item)
|
||||
@@ -0,0 +1,73 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
type LineProps = {
|
||||
list: number[]
|
||||
}
|
||||
const Line = ({
|
||||
list,
|
||||
}: LineProps) => {
|
||||
const listHeight = list.map((item) => {
|
||||
return item * 36 + (item - 1) * 2 + 12 + 6
|
||||
})
|
||||
const processedList = listHeight.map((item, index) => {
|
||||
if (index === 0)
|
||||
return item
|
||||
|
||||
return listHeight.slice(0, index).reduce((acc, cur) => acc + cur, 0) + item
|
||||
})
|
||||
const processedListLength = processedList.length
|
||||
const svgHeight = processedList[processedListLength - 1] + (processedListLength - 1) * 8
|
||||
|
||||
return (
|
||||
<svg className='w-6 shrink-0' style={{ height: svgHeight }}>
|
||||
{
|
||||
processedList.map((item, index) => {
|
||||
const prevItem = index > 0 ? processedList[index - 1] : 0
|
||||
const space = prevItem + index * 8 + 16
|
||||
return (
|
||||
<g key={index}>
|
||||
{
|
||||
index === 0 && (
|
||||
<>
|
||||
<path
|
||||
d='M0,18 L24,18'
|
||||
strokeWidth={1}
|
||||
fill='none'
|
||||
className='stroke-divider-solid'
|
||||
/>
|
||||
<rect
|
||||
x={0}
|
||||
y={16}
|
||||
width={1}
|
||||
height={4}
|
||||
className='fill-divider-solid-alt'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
index > 0 && (
|
||||
<path
|
||||
d={`M0,18 Q12,18 12,28 L12,${space - 10 + 2} Q12,${space + 2} 24,${space + 2}`}
|
||||
strokeWidth={1}
|
||||
fill='none'
|
||||
className='stroke-divider-solid'
|
||||
/>
|
||||
)
|
||||
}
|
||||
<rect
|
||||
x={23}
|
||||
y={space}
|
||||
width={1}
|
||||
height={4}
|
||||
className='fill-divider-solid-alt'
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})
|
||||
}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Line)
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import { intersection } from 'lodash-es'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ChangeItemProps = {
|
||||
data: CommonNodeType
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
}
|
||||
const ChangeItem = ({
|
||||
data,
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
}: ChangeItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
|
||||
}, [nodeId, sourceHandle, handleNodeChange])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<div className='flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'>
|
||||
{t('workflow.panel.change')}
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
onSelect={handleSelect}
|
||||
placement='top-end'
|
||||
offset={{
|
||||
mainAxis: 6,
|
||||
crossAxis: 8,
|
||||
}}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type OperatorProps = {
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
data: CommonNodeType
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
}
|
||||
const Operator = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
}: OperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
handleNodeDelete,
|
||||
handleNodeDisconnect,
|
||||
} = useNodesInteractions()
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
offset={{ mainAxis: 4, crossAxis: -4 }}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
|
||||
<Button className='h-6 w-6 p-0'>
|
||||
<RiMoreFill className='h-4 w-4' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='system-md-regular min-w-[120px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur text-text-secondary shadow-lg'>
|
||||
<div className='p-1'>
|
||||
<ChangeItem
|
||||
data={data}
|
||||
nodeId={nodeId}
|
||||
sourceHandle={sourceHandle}
|
||||
/>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleNodeDisconnect(nodeId)}
|
||||
>
|
||||
{t('workflow.common.disconnect')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleNodeDelete(nodeId)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default Operator
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useNodesInteractions,
|
||||
} from '../../../hooks'
|
||||
import { type Node, NodeRunningStatus } from '../../../types'
|
||||
import { canRunBySingle } from '../../../utils'
|
||||
import PanelOperator from './panel-operator'
|
||||
import {
|
||||
Stop,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
type NodeControlProps = Pick<Node, 'id' | 'data'>
|
||||
const NodeControl: FC<NodeControlProps> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}, [])
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute -top-7 right-0 hidden h-7 pb-1
|
||||
${!data._pluginInstallLocked && 'group-hover:flex'}
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className='flex h-6 items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg px-0.5 text-text-tertiary shadow-md backdrop-blur-[5px]'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
<div
|
||||
className={`flex h-5 w-5 items-center justify-center rounded-md ${isSingleRunning && 'cursor-pointer hover:bg-state-base-hover'}`}
|
||||
onClick={() => {
|
||||
const action = isSingleRunning ? 'stop' : 'run'
|
||||
|
||||
const store = workflowStore.getState()
|
||||
store.setInitShowLastRunTab(true)
|
||||
store.setPendingSingleRun({
|
||||
nodeId: id,
|
||||
action,
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
}}
|
||||
>
|
||||
{
|
||||
isSingleRunning
|
||||
? <Stop className='h-3 w-3' />
|
||||
: (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.panel.runThisStep')}
|
||||
asChild={false}
|
||||
>
|
||||
<RiPlayLargeLine className='h-3 w-3' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<PanelOperator
|
||||
id={id}
|
||||
data={data}
|
||||
offset={0}
|
||||
onOpenChange={handleOpenChange}
|
||||
triggerClassName='!w-5 !h-5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeControl)
|
||||
@@ -0,0 +1,237 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
BlockEnum,
|
||||
NodeRunningStatus,
|
||||
} from '../../../types'
|
||||
import type { Node } from '../../../types'
|
||||
import BlockSelector from '../../../block-selector'
|
||||
import type { PluginDefaultValue } from '../../../block-selector/types'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '../../../hooks'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../../../store'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type NodeHandleProps = {
|
||||
handleId: string
|
||||
handleClassName?: string
|
||||
nodeSelectorClassName?: string
|
||||
showExceptionStatus?: boolean
|
||||
} & Pick<Node, 'id' | 'data'>
|
||||
|
||||
export const NodeTargetHandle = memo(({
|
||||
id,
|
||||
data,
|
||||
handleId,
|
||||
handleClassName,
|
||||
nodeSelectorClassName,
|
||||
}: NodeHandleProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availablePrevBlocks.length
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!connected)
|
||||
setOpen(v => !v)
|
||||
}, [connected])
|
||||
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: id,
|
||||
nextNodeTargetHandle: handleId,
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
className={cn(
|
||||
'z-[1] !h-4 !w-4 !rounded-none !border-none !bg-transparent !outline-none',
|
||||
'after:absolute after:left-1.5 after:top-1 after:h-2 after:w-0.5 after:bg-workflow-link-line-handle',
|
||||
'transition-all hover:scale-125',
|
||||
data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
|
||||
data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
|
||||
data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
|
||||
!connected && 'after:opacity-0',
|
||||
(data.type === BlockEnum.Start
|
||||
|| data.type === BlockEnum.TriggerWebhook
|
||||
|| data.type === BlockEnum.TriggerSchedule
|
||||
|| data.type === BlockEnum.TriggerPlugin) && 'opacity-0',
|
||||
handleClassName,
|
||||
)}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
placement='left'
|
||||
triggerClassName={open => `
|
||||
hidden absolute left-0 top-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
group-hover:!flex
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availablePrevBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</>
|
||||
)
|
||||
})
|
||||
NodeTargetHandle.displayName = 'NodeTargetHandle'
|
||||
|
||||
export const NodeSourceHandle = memo(({
|
||||
id,
|
||||
data,
|
||||
handleId,
|
||||
handleClassName,
|
||||
nodeSelectorClassName,
|
||||
showExceptionStatus,
|
||||
}: NodeHandleProps) => {
|
||||
const { t } = useTranslation()
|
||||
const shouldAutoOpenStartNodeSelector = useStore(s => s.shouldAutoOpenStartNodeSelector)
|
||||
const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector)
|
||||
const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode)
|
||||
const workflowStoreApi = useWorkflowStore()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availableNextBlocks.length
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const connected = data._connectedSourceHandleIds?.includes(handleId)
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setOpen(v => !v)
|
||||
}, [])
|
||||
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: id,
|
||||
prevNodeSourceHandle: handleId,
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoOpenStartNodeSelector)
|
||||
return
|
||||
|
||||
if (isChatMode) {
|
||||
setShouldAutoOpenStartNodeSelector?.(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === BlockEnum.Start || data.type === BlockEnum.TriggerSchedule || data.type === BlockEnum.TriggerWebhook || data.type === BlockEnum.TriggerPlugin) {
|
||||
setOpen(true)
|
||||
if (setShouldAutoOpenStartNodeSelector)
|
||||
setShouldAutoOpenStartNodeSelector(false)
|
||||
else
|
||||
workflowStoreApi?.setState?.({ shouldAutoOpenStartNodeSelector: false })
|
||||
|
||||
if (setHasSelectedStartNode)
|
||||
setHasSelectedStartNode(false)
|
||||
else
|
||||
workflowStoreApi?.setState?.({ hasSelectedStartNode: false })
|
||||
}
|
||||
}, [shouldAutoOpenStartNodeSelector, data.type, isChatMode, setShouldAutoOpenStartNodeSelector, setHasSelectedStartNode, workflowStoreApi])
|
||||
|
||||
return (
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
className={cn(
|
||||
'group/handle z-[1] !h-4 !w-4 !rounded-none !border-none !bg-transparent !outline-none',
|
||||
'after:absolute after:right-1.5 after:top-1 after:h-2 after:w-0.5 after:bg-workflow-link-line-handle',
|
||||
'transition-all hover:scale-125',
|
||||
data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
|
||||
data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
|
||||
showExceptionStatus && data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
|
||||
!connected && 'after:opacity-0',
|
||||
handleClassName,
|
||||
)}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
<div className='absolute -top-1 left-1/2 hidden -translate-x-1/2 -translate-y-full rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 shadow-lg group-hover/handle:block'>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
<div className=' whitespace-nowrap'>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.click.title')}</span>
|
||||
{t('workflow.common.parallelTip.click.desc')}
|
||||
</div>
|
||||
<div>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.drag.title')}</span>
|
||||
{t('workflow.common.parallelTip.drag.desc')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
hidden absolute top-0 left-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
group-hover:!flex
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
)
|
||||
})
|
||||
NodeSourceHandle.displayName = 'NodeSourceHandle'
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import type { OnResize } from 'reactflow'
|
||||
import { NodeResizeControl } from 'reactflow'
|
||||
import { useNodesInteractions } from '../../../hooks'
|
||||
import type { CommonNodeType } from '../../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Icon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M5.19009 11.8398C8.26416 10.6196 10.7144 8.16562 11.9297 5.08904" stroke="black" strokeOpacity="0.16" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
type NodeResizerProps = {
|
||||
nodeId: string
|
||||
nodeData: CommonNodeType
|
||||
icon?: React.JSX.Element
|
||||
minWidth?: number
|
||||
minHeight?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
const NodeResizer = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
icon = <Icon />,
|
||||
minWidth = 258,
|
||||
minHeight = 152,
|
||||
maxWidth,
|
||||
}: NodeResizerProps) => {
|
||||
const { handleNodeResize } = useNodesInteractions()
|
||||
|
||||
const handleResize = useCallback<OnResize>((_, params) => {
|
||||
handleNodeResize(nodeId, params)
|
||||
}, [nodeId, handleNodeResize])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'hidden group-hover:block',
|
||||
nodeData.selected && '!block',
|
||||
)}>
|
||||
<NodeResizeControl
|
||||
position='bottom-right'
|
||||
className='!border-none !bg-transparent'
|
||||
onResize={handleResize}
|
||||
minWidth={minWidth}
|
||||
minHeight={minHeight}
|
||||
maxWidth={maxWidth}
|
||||
>
|
||||
<div className='absolute bottom-[1px] right-[1px]'>{icon}</div>
|
||||
</NodeResizeControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeResizer)
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type NodeStatusIconProps = {
|
||||
status: string
|
||||
className?: string
|
||||
}
|
||||
const NodeStatusIcon = ({
|
||||
status,
|
||||
className,
|
||||
}: NodeStatusIconProps) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
status === 'succeeded' && (
|
||||
<RiCheckboxCircleFill className={cn('h-4 w-4 shrink-0 text-text-success', className)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
status === 'failed' && (
|
||||
<RiErrorWarningLine className={cn('h-4 w-4 shrink-0 text-text-warning', className)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(status === 'stopped' || status === 'exception') && (
|
||||
<RiAlertFill className={cn('h-4 w-4 shrink-0 text-text-warning-secondary', className)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
status === 'running' && (
|
||||
<RiLoader2Line className={cn('h-4 w-4 shrink-0 animate-spin text-text-accent', className)} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodeStatusIcon
|
||||
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const variants = cva([], {
|
||||
variants: {
|
||||
align: {
|
||||
left: 'justify-start',
|
||||
center: 'justify-center',
|
||||
right: 'justify-end',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title: string
|
||||
onSelect: () => void
|
||||
selected: boolean
|
||||
disabled?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
tooltip?: string
|
||||
} & VariantProps<typeof variants>
|
||||
|
||||
const OptionCard: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
onSelect,
|
||||
selected,
|
||||
disabled,
|
||||
align = 'center',
|
||||
tooltip,
|
||||
}) => {
|
||||
const handleSelect = useCallback(() => {
|
||||
if (selected || disabled)
|
||||
return
|
||||
onSelect()
|
||||
}, [onSelect, selected, disabled])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-regular flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
|
||||
(!selected && !disabled) && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
selected && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
disabled && 'text-text-disabled',
|
||||
variants({ align }),
|
||||
className,
|
||||
)}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<span>{title}</span>
|
||||
{tooltip
|
||||
&& <Tooltip
|
||||
popupContent={
|
||||
<div className='w-[240px]'>
|
||||
{tooltip}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(OptionCard)
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import TreeIndentLine from './variable/object-child-tree-panel/tree-indent-line'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title?: string
|
||||
children: ReactNode
|
||||
operations?: ReactNode
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
}
|
||||
|
||||
const OutputVars: FC<Props> = ({
|
||||
title,
|
||||
children,
|
||||
operations,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<FieldCollapse
|
||||
title={title || t('workflow.nodes.common.outputVars')}
|
||||
operations={operations}
|
||||
collapsed={collapsed}
|
||||
onCollapse={onCollapse}
|
||||
>
|
||||
{children}
|
||||
</FieldCollapse>
|
||||
)
|
||||
}
|
||||
type VarItemProps = {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
subItems?: {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
}[]
|
||||
isIndent?: boolean
|
||||
}
|
||||
|
||||
export const VarItem: FC<VarItemProps> = ({
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
subItems,
|
||||
isIndent,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('flex', isIndent && 'relative left-[-7px]')}>
|
||||
{isIndent && <TreeIndentLine depth={1} />}
|
||||
<div className='py-1'>
|
||||
<div className='flex'>
|
||||
<div className='flex items-center leading-[18px]'>
|
||||
<div className='code-sm-semibold text-text-secondary'>{name}</div>
|
||||
<div className='system-xs-regular ml-2 text-text-tertiary'>{type}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-0.5 text-text-tertiary'>
|
||||
{description}
|
||||
{subItems && (
|
||||
<div className='ml-2 border-l border-gray-200 pl-2'>
|
||||
{subItems.map((item, index) => (
|
||||
<VarItem
|
||||
key={index}
|
||||
name={item.name}
|
||||
type={item.type}
|
||||
description={item.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(OutputVars)
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { intersection } from 'lodash-es'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import type {
|
||||
Node,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types'
|
||||
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
type ChangeBlockProps = {
|
||||
nodeId: string
|
||||
nodeData: Node['data']
|
||||
sourceHandle: string
|
||||
}
|
||||
const ChangeBlock = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
sourceHandle,
|
||||
}: ChangeBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
const isChatMode = useIsChatMode()
|
||||
const flowType = useHooksStore(s => s.configsMap?.flowType)
|
||||
const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode
|
||||
const ignoreNodeIds = useMemo(() => {
|
||||
if (isTriggerNode(nodeData.type as BlockEnum))
|
||||
return [nodeId]
|
||||
return undefined
|
||||
}, [nodeData.type, nodeId])
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
if (availablePrevBlocks.length && availableNextBlocks.length)
|
||||
return intersection(availablePrevBlocks, availableNextBlocks)
|
||||
else if (availablePrevBlocks.length)
|
||||
return availablePrevBlocks
|
||||
else
|
||||
return availableNextBlocks
|
||||
}, [availablePrevBlocks, availableNextBlocks])
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
|
||||
}, [handleNodeChange, nodeId, sourceHandle])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<div className='flex h-8 w-[232px] cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'>
|
||||
{t('workflow.panel.changeBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: -36,
|
||||
crossAxis: 4,
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='min-w-[240px]'
|
||||
availableBlocksTypes={availableNodes}
|
||||
showStartTab={showStartTab}
|
||||
ignoreNodeIds={ignoreNodeIds}
|
||||
forceEnableStartTab={nodeData.type === BlockEnum.Start}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ChangeBlock)
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import PanelOperatorPopup from './panel-operator-popup'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
|
||||
type PanelOperatorProps = {
|
||||
id: string
|
||||
data: Node['data']
|
||||
triggerClassName?: string
|
||||
offset?: OffsetOptions
|
||||
onOpenChange?: (open: boolean) => void
|
||||
inNode?: boolean
|
||||
showHelpLink?: boolean
|
||||
}
|
||||
const PanelOperator = ({
|
||||
id,
|
||||
data,
|
||||
triggerClassName,
|
||||
offset = {
|
||||
mainAxis: 4,
|
||||
crossAxis: 53,
|
||||
},
|
||||
onOpenChange,
|
||||
showHelpLink = true,
|
||||
}: PanelOperatorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
|
||||
if (onOpenChange)
|
||||
onOpenChange(newOpen)
|
||||
}, [onOpenChange])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
offset={offset}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => handleOpenChange(!open)}>
|
||||
<div
|
||||
className={`
|
||||
flex h-6 w-6 cursor-pointer items-center justify-center rounded-md
|
||||
hover:bg-state-base-hover
|
||||
${open && 'bg-state-base-hover'}
|
||||
${triggerClassName}
|
||||
`}
|
||||
>
|
||||
<RiMoreFill className={'h-4 w-4 text-text-tertiary'} />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<PanelOperatorPopup
|
||||
id={id}
|
||||
data={data}
|
||||
onClosePopup={() => setOpen(false)}
|
||||
showHelpLink={showHelpLink}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PanelOperator)
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import ChangeBlock from './change-block'
|
||||
import {
|
||||
canRunBySingle,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodeMetaData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
|
||||
type PanelOperatorPopupProps = {
|
||||
id: string
|
||||
data: Node['data']
|
||||
onClosePopup: () => void
|
||||
showHelpLink?: boolean
|
||||
}
|
||||
const PanelOperatorPopup = ({
|
||||
id,
|
||||
data,
|
||||
onClosePopup,
|
||||
showHelpLink,
|
||||
}: PanelOperatorPopupProps) => {
|
||||
const { t } = useTranslation()
|
||||
const edges = useEdges()
|
||||
const {
|
||||
handleNodeDelete,
|
||||
handleNodesDuplicate,
|
||||
handleNodeSelect,
|
||||
handleNodesCopy,
|
||||
} = useNodesInteractions()
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const nodeMetaData = useNodeMetaData({ id, data } as Node)
|
||||
const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
|
||||
return (
|
||||
<div className='w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'>
|
||||
{
|
||||
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
{
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
<div
|
||||
className={`
|
||||
flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary
|
||||
hover:bg-state-base-hover
|
||||
`}
|
||||
onClick={() => {
|
||||
handleNodeSelect(id)
|
||||
handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
|
||||
handleSyncWorkflowDraft(true)
|
||||
onClosePopup()
|
||||
}}
|
||||
>
|
||||
{t('workflow.panel.runThisStep')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showChangeBlock && (
|
||||
<ChangeBlock
|
||||
nodeId={id}
|
||||
nodeData={data}
|
||||
sourceHandle={edge?.sourceHandle || 'source'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!nodesReadOnly && (
|
||||
<>
|
||||
{
|
||||
!nodeMetaData.isSingleton && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesCopy(id)
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.copy')}
|
||||
<ShortcutsName keys={['ctrl', 'c']} />
|
||||
</div>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesDuplicate(id)
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.duplicate')}
|
||||
<ShortcutsName keys={['ctrl', 'd']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!nodeMetaData.isUndeletable && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary
|
||||
hover:bg-state-destructive-hover hover:text-text-destructive
|
||||
`}
|
||||
onClick={() => handleNodeDelete(id)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
<ShortcutsName keys={['del']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
showHelpLink && nodeMetaData.helpLinkUri && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<a
|
||||
href={nodeMetaData.helpLinkUri}
|
||||
target='_blank'
|
||||
className='flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
>
|
||||
{t('workflow.panel.helpLink')}
|
||||
</a>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
<div className='px-3 py-2 text-xs text-text-tertiary'>
|
||||
<div className='mb-1 flex h-[22px] items-center font-medium'>
|
||||
{t('workflow.panel.about').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='mb-1 leading-[18px] text-text-secondary'>{nodeMetaData.description}</div>
|
||||
<div className='leading-[18px]'>
|
||||
{t('workflow.panel.createdBy')} {nodeMetaData.author}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PanelOperatorPopup)
|
||||
@@ -0,0 +1,323 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { BlockEnum, EditionType } from '../../../../types'
|
||||
import type {
|
||||
ModelConfig,
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
Variable,
|
||||
} from '../../../../types'
|
||||
|
||||
import Wrap from '../editor/wrap'
|
||||
import { CodeLanguage } from '../../../code/types'
|
||||
import PromptGeneratorBtn from '../../../llm/components/prompt-generator-btn'
|
||||
import cn from '@/utils/classnames'
|
||||
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
|
||||
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import {
|
||||
Copy,
|
||||
CopyCheck,
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
headerClassName?: string
|
||||
instanceId?: string
|
||||
nodeId?: string
|
||||
editorId?: string
|
||||
title: string | React.JSX.Element
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
readOnly?: boolean
|
||||
showRemove?: boolean
|
||||
onRemove?: () => void
|
||||
justVar?: boolean
|
||||
isChatModel?: boolean
|
||||
isChatApp?: boolean
|
||||
isShowContext?: boolean
|
||||
hasSetBlockStatus?: {
|
||||
context: boolean
|
||||
history: boolean
|
||||
query: boolean
|
||||
}
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
isSupportFileVar?: boolean
|
||||
isSupportPromptGenerator?: boolean
|
||||
onGenerated?: (prompt: string) => void
|
||||
modelConfig?: ModelConfig
|
||||
// for jinja
|
||||
isSupportJinja?: boolean
|
||||
editionType?: EditionType
|
||||
onEditionTypeChange?: (editionType: EditionType) => void
|
||||
varList?: Variable[]
|
||||
handleAddVariable?: (payload: any) => void
|
||||
containerBackgroundClassName?: string
|
||||
gradientBorder?: boolean
|
||||
titleTooltip?: ReactNode
|
||||
inputClassName?: string
|
||||
editorContainerClassName?: string
|
||||
placeholder?: string
|
||||
placeholderClassName?: string
|
||||
titleClassName?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
const Editor: FC<Props> = ({
|
||||
className,
|
||||
headerClassName,
|
||||
instanceId,
|
||||
nodeId,
|
||||
editorId,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
readOnly,
|
||||
showRemove,
|
||||
onRemove,
|
||||
justVar,
|
||||
isChatModel,
|
||||
isChatApp,
|
||||
isShowContext,
|
||||
hasSetBlockStatus,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
isSupportFileVar,
|
||||
isSupportPromptGenerator,
|
||||
isSupportJinja,
|
||||
editionType,
|
||||
onEditionTypeChange,
|
||||
varList = [],
|
||||
handleAddVariable,
|
||||
onGenerated,
|
||||
modelConfig,
|
||||
containerBackgroundClassName: containerClassName,
|
||||
gradientBorder = true,
|
||||
titleTooltip,
|
||||
inputClassName,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
titleClassName,
|
||||
editorContainerClassName,
|
||||
required,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
|
||||
|
||||
const isShowHistory = !isChatModel && isChatApp
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
wrapClassName,
|
||||
wrapStyle,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
editorExpandHeight,
|
||||
} = useToggleExpend({ ref, isInNode: true })
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}, [value])
|
||||
|
||||
const [isFocus, {
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleInsertVariable = () => {
|
||||
setFocus()
|
||||
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId } as any)
|
||||
}
|
||||
|
||||
const getVarType = useWorkflowVariableType()
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
return (
|
||||
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
|
||||
<div ref={ref} className={cn(isFocus ? (gradientBorder && 'bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2') : 'bg-transparent', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
|
||||
<div className={cn(isFocus ? 'bg-background-default' : 'bg-components-input-bg-normal', isExpand && 'flex h-full flex-col', 'rounded-lg', containerClassName)}>
|
||||
<div className={cn('flex items-center justify-between pl-3 pr-2 pt-1', headerClassName)}>
|
||||
<div className='flex gap-2'>
|
||||
<div className={cn('text-xs font-semibold uppercase leading-4 text-text-secondary', titleClassName)}>{title} {required && <span className='text-text-destructive'>*</span>}</div>
|
||||
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
|
||||
{isSupportPromptGenerator && (
|
||||
<PromptGeneratorBtn
|
||||
nodeId={nodeId!}
|
||||
editorId={editorId}
|
||||
className='ml-[5px]'
|
||||
onGenerated={onGenerated}
|
||||
modelConfig={modelConfig}
|
||||
currentPrompt={value}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>
|
||||
{/* Operations */}
|
||||
<div className='flex items-center space-x-[2px]'>
|
||||
{isSupportJinja && (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div>
|
||||
<div>{t('workflow.common.enableJinja')}</div>
|
||||
<a className='text-text-accent' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
|
||||
<Jinja className='h-3 w-6 text-text-quaternary' />
|
||||
<Switch
|
||||
size='sm'
|
||||
defaultValue={editionType === EditionType.jinja2}
|
||||
onChange={(checked) => {
|
||||
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Tooltip
|
||||
popupContent={`${t('workflow.common.insertVarTip')}`}
|
||||
>
|
||||
<ActionButton onClick={handleInsertVariable}>
|
||||
<Variable02 className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showRemove && (
|
||||
<ActionButton onClick={onRemove}>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{!isCopied
|
||||
? (
|
||||
<ActionButton onClick={handleCopy}>
|
||||
<Copy className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)
|
||||
: (
|
||||
<ActionButton>
|
||||
<CopyCheck className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Min: 80 Max: 560. Header: 24 */}
|
||||
<div className={cn('pb-2', isExpand && 'flex grow flex-col')}>
|
||||
{!(isSupportJinja && editionType === EditionType.jinja2)
|
||||
? (
|
||||
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
|
||||
<PromptEditor
|
||||
key={controlPromptEditorRerenderKey}
|
||||
placeholder={placeholder}
|
||||
placeholderClassName={placeholderClassName}
|
||||
instanceId={instanceId}
|
||||
compact
|
||||
className={cn('min-h-[56px]', inputClassName)}
|
||||
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: justVar ? false : isShowContext,
|
||||
selectable: !hasSetBlockStatus?.context,
|
||||
canNotAddContext: true,
|
||||
}}
|
||||
historyBlock={{
|
||||
show: justVar ? false : isShowHistory,
|
||||
selectable: !hasSetBlockStatus?.history,
|
||||
history: {
|
||||
user: 'Human',
|
||||
assistant: 'Assistant',
|
||||
},
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false, // use [sys.query] instead of query block
|
||||
selectable: false,
|
||||
}}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
getVarType: getVarType as any,
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
position: node.position,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
showManageInputField: !!pipelineId,
|
||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative min-h-[56px] overflow-y-auto px-3', editorContainerClassName)}>
|
||||
<CodeEditor
|
||||
availableVars={nodesOutputVars || []}
|
||||
varList={varList}
|
||||
onAddVar={handleAddVariable}
|
||||
isInNode
|
||||
readOnly={readOnly}
|
||||
language={CodeLanguage.python3}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
noWrapper
|
||||
isExpand={isExpand}
|
||||
className={inputClassName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrap>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(Editor)
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useWorkflow } from '../../../hooks'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import { getNodeInfoById, isSystemVar } from './variable/utils'
|
||||
import {
|
||||
VariableLabelInText,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
type Props = {
|
||||
nodeId: string
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const VAR_PLACEHOLDER = '@#!@#!'
|
||||
|
||||
const ReadonlyInputWithSelectVar: FC<Props> = ({
|
||||
nodeId,
|
||||
value,
|
||||
className,
|
||||
}) => {
|
||||
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranchIncludeParent(nodeId)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const res = (() => {
|
||||
const vars: string[] = []
|
||||
const strWithVarPlaceholder = value.replaceAll(/{{#([^#]*)#}}/g, (_match, p1) => {
|
||||
vars.push(p1)
|
||||
return VAR_PLACEHOLDER
|
||||
})
|
||||
|
||||
const html: React.JSX.Element[] = strWithVarPlaceholder.split(VAR_PLACEHOLDER).map((str, index) => {
|
||||
if (!vars[index])
|
||||
return <span className='relative top-[-3px] leading-[16px]' key={index}>{str}</span>
|
||||
|
||||
const value = vars[index].split('.')
|
||||
const isSystem = isSystemVar(value)
|
||||
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
|
||||
const isShowAPart = value.length > 2
|
||||
|
||||
return (<span key={index}>
|
||||
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
|
||||
<VariableLabelInText
|
||||
nodeTitle={node?.title}
|
||||
nodeType={node?.type}
|
||||
notShowFullPath={isShowAPart}
|
||||
variables={value}
|
||||
/>
|
||||
</span>)
|
||||
})
|
||||
return html
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className={cn('break-all text-xs', className)}>
|
||||
{res}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ReadonlyInputWithSelectVar)
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
const Remove: FC<Props> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<ActionButton size='l' className='group shrink-0 hover:!bg-state-destructive-hover' onClick={onClick}>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary group-hover:text-text-destructive' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
export default React.memo(Remove)
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
const i18nPrefix = 'workflow.common.effectVarConfirm'
|
||||
|
||||
const RemoveVarConfirm: FC<Props> = ({
|
||||
isShow,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Confirm
|
||||
isShow={isShow}
|
||||
title={t(`${i18nPrefix}.title`)}
|
||||
content={t(`${i18nPrefix}.content`)}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(RemoveVarConfirm)
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { WorkflowRetryConfig } from './types'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
export const useRetryConfig = (
|
||||
id: string,
|
||||
) => {
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
|
||||
const handleRetryConfigChange = useCallback((value?: WorkflowRetryConfig) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
retry_config: value,
|
||||
},
|
||||
})
|
||||
}, [id, handleNodeDataUpdateWithSyncDraft])
|
||||
|
||||
return {
|
||||
handleRetryConfigChange,
|
||||
}
|
||||
}
|
||||
|
||||
export const useRetryDetailShowInSingleRun = () => {
|
||||
const [retryDetails, setRetryDetails] = useState<NodeTracing[] | undefined>()
|
||||
|
||||
const handleRetryDetailsChange = useCallback((details: NodeTracing[] | undefined) => {
|
||||
setRetryDetails(details)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
retryDetails,
|
||||
handleRetryDetailsChange,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCheckboxCircleFill,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type RetryOnNodeProps = Pick<Node, 'id' | 'data'>
|
||||
const RetryOnNode = ({
|
||||
data,
|
||||
}: RetryOnNodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { retry_config } = data
|
||||
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
|
||||
const {
|
||||
isRunning,
|
||||
isSuccessful,
|
||||
isException,
|
||||
isFailed,
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
isRunning: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
|
||||
isSuccessful: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
|
||||
isFailed: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
|
||||
isException: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
const showDefault = !isRunning && !isSuccessful && !isException && !isFailed
|
||||
|
||||
if (!retry_config?.retry_enabled)
|
||||
return null
|
||||
|
||||
if (!showDefault && !data._retryIndex)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3'>
|
||||
<div className={cn(
|
||||
'system-xs-medium-uppercase flex items-center justify-between rounded-md border-[0.5px] border-transparent bg-workflow-block-parma-bg px-[5px] py-1 text-text-tertiary',
|
||||
isRunning && 'border-state-accent-active bg-state-accent-hover text-text-accent',
|
||||
isSuccessful && 'border-state-success-active bg-state-success-hover text-text-success',
|
||||
(isException || isFailed) && 'border-state-warning-active bg-state-warning-hover text-text-warning',
|
||||
)}>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
showDefault && (
|
||||
t('workflow.nodes.common.retry.retryTimes', { times: retry_config.max_retries })
|
||||
)
|
||||
}
|
||||
{
|
||||
isRunning && (
|
||||
<>
|
||||
<RiLoader2Line className='mr-1 h-3.5 w-3.5 animate-spin' />
|
||||
{t('workflow.nodes.common.retry.retrying')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isSuccessful && (
|
||||
<>
|
||||
<RiCheckboxCircleFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('workflow.nodes.common.retry.retrySuccessful')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(isFailed || isException) && (
|
||||
<>
|
||||
<RiAlertFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('workflow.nodes.common.retry.retryFailed')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!showDefault && !!data._retryIndex && (
|
||||
<div>
|
||||
{data._retryIndex}/{data.retry_config?.max_retries}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetryOnNode
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRetryConfig } from './hooks'
|
||||
import s from './style.module.css'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
|
||||
type RetryOnPanelProps = Pick<Node, 'id' | 'data'>
|
||||
const RetryOnPanel = ({
|
||||
id,
|
||||
data,
|
||||
}: RetryOnPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleRetryConfigChange } = useRetryConfig(id)
|
||||
const { retry_config } = data
|
||||
|
||||
const handleRetryEnabledChange = (value: boolean) => {
|
||||
handleRetryConfigChange({
|
||||
retry_enabled: value,
|
||||
max_retries: retry_config?.max_retries || 3,
|
||||
retry_interval: retry_config?.retry_interval || 1000,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMaxRetriesChange = (value: number) => {
|
||||
if (value > 10)
|
||||
value = 10
|
||||
else if (value < 1)
|
||||
value = 1
|
||||
handleRetryConfigChange({
|
||||
retry_enabled: true,
|
||||
max_retries: value,
|
||||
retry_interval: retry_config?.retry_interval || 1000,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRetryIntervalChange = (value: number) => {
|
||||
if (value > 5000)
|
||||
value = 5000
|
||||
else if (value < 100)
|
||||
value = 100
|
||||
handleRetryConfigChange({
|
||||
retry_enabled: true,
|
||||
max_retries: retry_config?.max_retries || 3,
|
||||
retry_interval: value,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='pt-2'>
|
||||
<div className='flex h-10 items-center justify-between px-4 py-2'>
|
||||
<div className='flex items-center'>
|
||||
<div className='system-sm-semibold-uppercase mr-0.5 text-text-secondary'>{t('workflow.nodes.common.retry.retryOnFailure')}</div>
|
||||
</div>
|
||||
<Switch
|
||||
defaultValue={retry_config?.retry_enabled}
|
||||
onChange={v => handleRetryEnabledChange(v)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
retry_config?.retry_enabled && (
|
||||
<div className='px-4 pb-2'>
|
||||
<div className='mb-1 flex w-full items-center'>
|
||||
<div className='system-xs-medium-uppercase mr-2 grow text-text-secondary'>{t('workflow.nodes.common.retry.maxRetries')}</div>
|
||||
<Slider
|
||||
className='mr-3 w-[108px]'
|
||||
value={retry_config?.max_retries || 3}
|
||||
onChange={handleMaxRetriesChange}
|
||||
min={1}
|
||||
max={10}
|
||||
/>
|
||||
<Input
|
||||
type='number'
|
||||
wrapperClassName='w-[100px]'
|
||||
value={retry_config?.max_retries || 3}
|
||||
onChange={e =>
|
||||
handleMaxRetriesChange(Number.parseInt(e.currentTarget.value, 10) || 3)
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
unit={t('workflow.nodes.common.retry.times') || ''}
|
||||
className={s.input}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='system-xs-medium-uppercase mr-2 grow text-text-secondary'>{t('workflow.nodes.common.retry.retryInterval')}</div>
|
||||
<Slider
|
||||
className='mr-3 w-[108px]'
|
||||
value={retry_config?.retry_interval || 1000}
|
||||
onChange={handleRetryIntervalChange}
|
||||
min={100}
|
||||
max={5000}
|
||||
/>
|
||||
<Input
|
||||
type='number'
|
||||
wrapperClassName='w-[100px]'
|
||||
value={retry_config?.retry_interval || 1000}
|
||||
onChange={e =>
|
||||
handleRetryIntervalChange(Number.parseInt(e.currentTarget.value, 10) || 1000)
|
||||
}
|
||||
min={100}
|
||||
max={5000}
|
||||
unit={t('workflow.nodes.common.retry.ms') || ''}
|
||||
className={s.input}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Split className='mx-4 mt-2' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetryOnPanel
|
||||
@@ -0,0 +1,5 @@
|
||||
.input::-webkit-inner-spin-button,
|
||||
.input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type WorkflowRetryConfig = {
|
||||
max_retries: number
|
||||
retry_interval: number
|
||||
retry_enabled: boolean
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {}
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
type Item = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
type Props = {
|
||||
className?: string
|
||||
trigger?: React.JSX.Element
|
||||
DropDownIcon?: any
|
||||
noLeft?: boolean
|
||||
options: Item[]
|
||||
allOptions?: Item[]
|
||||
value: string
|
||||
placeholder?: string
|
||||
onChange: (value: any) => void
|
||||
uppercase?: boolean
|
||||
popupClassName?: string
|
||||
triggerClassName?: string
|
||||
itemClassName?: string
|
||||
readonly?: boolean
|
||||
showChecked?: boolean
|
||||
}
|
||||
|
||||
const TypeSelector: FC<Props> = ({
|
||||
className,
|
||||
trigger,
|
||||
DropDownIcon = ChevronSelectorVertical,
|
||||
noLeft,
|
||||
options: list,
|
||||
allOptions,
|
||||
value,
|
||||
placeholder = '',
|
||||
onChange,
|
||||
uppercase,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
itemClassName,
|
||||
readonly,
|
||||
showChecked,
|
||||
}) => {
|
||||
const noValue = value === '' || value === undefined || value === null
|
||||
const item = allOptions ? allOptions.find(item => item.value === value) : list.find(item => item.value === value)
|
||||
const [showOption, { setFalse: setHide, toggle: toggleShow }] = useBoolean(false)
|
||||
const ref = React.useRef(null)
|
||||
useClickAway(() => {
|
||||
setHide()
|
||||
}, ref)
|
||||
return (
|
||||
<div className={cn(!trigger && !noLeft && 'left-[-8px]', 'relative select-none', className)} ref={ref}>
|
||||
{trigger
|
||||
? (
|
||||
<div
|
||||
onClick={toggleShow}
|
||||
className={cn(!readonly && 'cursor-pointer')}
|
||||
>
|
||||
{trigger}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
onClick={toggleShow}
|
||||
className={cn(showOption && 'bg-state-base-hover', 'flex h-5 cursor-pointer items-center rounded-md pl-1 pr-0.5 text-xs font-semibold text-text-secondary hover:bg-state-base-hover')}>
|
||||
<div className={cn('text-sm font-semibold', uppercase && 'uppercase', noValue && 'text-text-tertiary', triggerClassName)}>{!noValue ? item?.label : placeholder}</div>
|
||||
{!readonly && <DropDownIcon className='h-3 w-3 ' />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(showOption && !readonly) && (
|
||||
<div className={cn('absolute top-[24px] z-10 w-[120px] select-none rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg', popupClassName)}>
|
||||
{list.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
setHide()
|
||||
onChange(item.value)
|
||||
}}
|
||||
className={cn(itemClassName, uppercase && 'uppercase', 'flex h-[30px] min-w-[44px] cursor-pointer items-center justify-between rounded-lg px-3 text-[13px] font-medium text-text-secondary hover:bg-state-base-hover')}
|
||||
>
|
||||
<div>{item.label}</div>
|
||||
{showChecked && item.value === value && <Check className='h-4 w-4 text-text-primary' />}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TypeSelector)
|
||||
@@ -0,0 +1,28 @@
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { type ComponentProps, type PropsWithChildren, type ReactNode, memo } from 'react'
|
||||
|
||||
export type SettingItemProps = PropsWithChildren<{
|
||||
label: string
|
||||
status?: 'error' | 'warning'
|
||||
tooltip?: ReactNode
|
||||
}>
|
||||
|
||||
export const SettingItem = memo(({ label, children, status, tooltip }: SettingItemProps) => {
|
||||
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
|
||||
const needTooltip = ['error', 'warning'].includes(status as any)
|
||||
return <div className='relative flex items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1.5 py-1 text-xs font-normal'>
|
||||
<div className={classNames('system-xs-medium-uppercase max-w-full shrink-0 truncate text-text-tertiary', !!children && 'max-w-[100px]')}>
|
||||
{label}
|
||||
</div>
|
||||
<Tooltip popupContent={tooltip} disabled={!needTooltip}>
|
||||
<div className='system-xs-medium truncate text-right text-text-secondary'>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{indicator && <Indicator color={indicator} className='absolute -right-0.5 -top-0.5' />}
|
||||
</div>
|
||||
})
|
||||
|
||||
SettingItem.displayName = 'SettingItem'
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Split: FC<Props> = ({
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, 'h-[0.5px] bg-divider-subtle')}>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Split)
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import VarHighlight from '@/app/components/app/configuration/base/var-highlight'
|
||||
type Props = {
|
||||
isFocus?: boolean
|
||||
onFocus?: () => void
|
||||
value: string
|
||||
children?: React.ReactNode
|
||||
wrapClassName?: string
|
||||
textClassName?: string
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const SupportVarInput: FC<Props> = ({
|
||||
isFocus,
|
||||
onFocus,
|
||||
children,
|
||||
value,
|
||||
wrapClassName,
|
||||
textClassName,
|
||||
readonly,
|
||||
}) => {
|
||||
const renderSafeContent = (inputValue: string) => {
|
||||
const parts = inputValue.split(/(\{\{[^}]+\}\}|\n)/g)
|
||||
return parts.map((part, index) => {
|
||||
const variableMatch = part.match(/^\{\{([^}]+)\}\}$/)
|
||||
if (variableMatch) {
|
||||
return (
|
||||
<VarHighlight
|
||||
key={`var-${index}`}
|
||||
name={variableMatch[1]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (part === '\n')
|
||||
return <br key={`br-${index}`} />
|
||||
|
||||
return <span key={`text-${index}`}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
cn(wrapClassName, 'flex h-full w-full')
|
||||
} onClick={onFocus}
|
||||
>
|
||||
{(isFocus && !readonly && children)
|
||||
? (
|
||||
children
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className={cn(textClassName, 'h-full w-0 grow truncate whitespace-nowrap')}
|
||||
title={value}
|
||||
>
|
||||
{renderSafeContent(value || '')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SupportVarInput)
|
||||
@@ -0,0 +1,132 @@
|
||||
'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, RiExternalLinkLine } from '@remixicon/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { type FC, useCallback, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCheckInstalled, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import cn from '@/utils/classnames'
|
||||
import PluginMutationModel from '@/app/components/plugins/plugin-mutation-model'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils'
|
||||
import { Badge as Badge2, BadgeState } from '@/app/components/base/badge/index'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
export type SwitchPluginVersionProps = {
|
||||
uniqueIdentifier: string
|
||||
tooltip?: ReactNode
|
||||
onChange?: (version: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
|
||||
const { uniqueIdentifier, tooltip, onChange, className } = props
|
||||
|
||||
const [pluginId] = uniqueIdentifier?.split(':') || ['']
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false)
|
||||
const [target, setTarget] = useState<{
|
||||
version: string,
|
||||
pluginUniqueIden: string;
|
||||
}>()
|
||||
const pluginDetails = useCheckInstalled({
|
||||
pluginIds: [pluginId],
|
||||
enabled: true,
|
||||
})
|
||||
const pluginDetail = pluginDetails.data?.plugins.at(0)
|
||||
|
||||
const handleUpdatedFromMarketplace = useCallback(() => {
|
||||
hideUpdateModal()
|
||||
pluginDetails.refetch()
|
||||
onChange?.(target!.version)
|
||||
}, [hideUpdateModal, onChange, pluginDetails, target])
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const icon = pluginDetail?.declaration.icon ? getIconUrl(pluginDetail.declaration.icon) : undefined
|
||||
const mutation = useUpdatePackageFromMarketPlace()
|
||||
const install = () => {
|
||||
mutation.mutate(
|
||||
{
|
||||
new_plugin_unique_identifier: target!.pluginUniqueIden,
|
||||
original_plugin_unique_identifier: uniqueIdentifier,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
handleUpdatedFromMarketplace()
|
||||
},
|
||||
})
|
||||
}
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Guard against null/undefined uniqueIdentifier to prevent app crash
|
||||
if (!uniqueIdentifier || !pluginId)
|
||||
return null
|
||||
|
||||
return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'>
|
||||
<div className={cn('flex w-fit items-center justify-center', className)} onClick={e => e.stopPropagation()}>
|
||||
{isShowUpdateModal && pluginDetail && <PluginMutationModel
|
||||
onCancel={hideUpdateModal}
|
||||
plugin={pluginManifestToCardPluginProps({
|
||||
...pluginDetail.declaration,
|
||||
icon: icon!,
|
||||
})}
|
||||
mutation={mutation}
|
||||
mutate={install}
|
||||
confirmButtonText={t('workflow.nodes.agent.installPlugin.install')}
|
||||
cancelButtonText={t('workflow.nodes.agent.installPlugin.cancel')}
|
||||
modelTitle={t('workflow.nodes.agent.installPlugin.title')}
|
||||
description={t('workflow.nodes.agent.installPlugin.desc')}
|
||||
cardTitleLeft={<>
|
||||
<Badge2 className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${pluginDetail.version} -> ${target!.version}`}
|
||||
</Badge2>
|
||||
</>}
|
||||
modalBottomLeft={
|
||||
<Link
|
||||
className='flex items-center justify-center gap-1'
|
||||
href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)}
|
||||
target='_blank'
|
||||
>
|
||||
<span className='system-xs-regular text-xs text-text-accent'>
|
||||
{t('workflow.nodes.agent.installPlugin.changelog')}
|
||||
</span>
|
||||
<RiExternalLinkLine className='size-3 text-text-accent' />
|
||||
</Link>
|
||||
}
|
||||
/>}
|
||||
{pluginDetail && <PluginVersionPicker
|
||||
isShow={isShow}
|
||||
onShowChange={setIsShow}
|
||||
pluginID={pluginId}
|
||||
currentVersion={pluginDetail.version}
|
||||
onSelect={(state) => {
|
||||
setTarget({
|
||||
pluginUniqueIden: state.unique_identifier,
|
||||
version: state.version,
|
||||
})
|
||||
showUpdateModal()
|
||||
}}
|
||||
trigger={
|
||||
<Badge
|
||||
className={cn(
|
||||
'mx-1 flex hover:bg-state-base-hover',
|
||||
isShow && 'bg-state-base-hover',
|
||||
)}
|
||||
uppercase={true}
|
||||
text={
|
||||
<>
|
||||
<div>{pluginDetail.version}</div>
|
||||
<RiArrowLeftRightLine className='ml-1 h-3 w-3 text-text-tertiary' />
|
||||
</>
|
||||
}
|
||||
hasRedCornerMark={true}
|
||||
/>
|
||||
}
|
||||
/>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type TitleInputProps = {
|
||||
value: string
|
||||
onBlur: (value: string) => void
|
||||
}
|
||||
|
||||
export const TitleInput = memo(({
|
||||
value,
|
||||
onBlur,
|
||||
}: TitleInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
|
||||
const handleBlur = () => {
|
||||
if (!localValue) {
|
||||
setLocalValue(value)
|
||||
onBlur(value)
|
||||
return
|
||||
}
|
||||
|
||||
onBlur(localValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
value={localValue}
|
||||
onChange={e => setLocalValue(e.target.value)}
|
||||
className={`
|
||||
system-xl-semibold mr-2 h-7 min-w-0 grow appearance-none rounded-md border border-transparent bg-transparent px-1 text-text-primary
|
||||
outline-none focus:shadow-xs
|
||||
`}
|
||||
placeholder={t('workflow.common.addTitle') || ''}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)
|
||||
})
|
||||
TitleInput.displayName = 'TitleInput'
|
||||
|
||||
type DescriptionInputProps = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
export const DescriptionInput = memo(({
|
||||
value,
|
||||
onChange,
|
||||
}: DescriptionInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [focus, setFocus] = useState(false)
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocus(true)
|
||||
}, [])
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocus(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
leading-0 group flex max-h-[60px] overflow-y-auto rounded-lg bg-components-panel-bg
|
||||
px-2 py-[5px]
|
||||
${focus && '!shadow-xs'}
|
||||
`}
|
||||
>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
minRows={1}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
className={`
|
||||
w-full resize-none appearance-none bg-transparent text-xs
|
||||
leading-[18px] text-text-primary caret-[#295EFF]
|
||||
outline-none placeholder:text-text-quaternary
|
||||
`}
|
||||
placeholder={t('workflow.common.addDescription') || ''}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
DescriptionInput.displayName = 'DescriptionInput'
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import {
|
||||
RiCollapseDiagonalLine,
|
||||
RiExpandDiagonalLine,
|
||||
} from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type Props = {
|
||||
isExpand: boolean
|
||||
onExpandChange: (isExpand: boolean) => void
|
||||
}
|
||||
|
||||
const ExpandBtn: FC<Props> = ({
|
||||
isExpand,
|
||||
onExpandChange,
|
||||
}) => {
|
||||
const handleToggle = useCallback(() => {
|
||||
onExpandChange(!isExpand)
|
||||
}, [isExpand])
|
||||
|
||||
const Icon = isExpand ? RiCollapseDiagonalLine : RiExpandDiagonalLine
|
||||
return (
|
||||
<ActionButton onClick={handleToggle}>
|
||||
<Icon className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
export default React.memo(ExpandBtn)
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useNodes, useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
ValueSelector,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import {
|
||||
VariableLabelInSelect,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
type VariableTagProps = {
|
||||
valueSelector: ValueSelector
|
||||
varType: VarType
|
||||
isShort?: boolean
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
const VariableTag = ({
|
||||
valueSelector,
|
||||
varType,
|
||||
isShort,
|
||||
availableNodes,
|
||||
}: VariableTagProps) => {
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const isRagVar = isRagVariableVar(valueSelector)
|
||||
const node = useMemo(() => {
|
||||
if (isSystemVar(valueSelector)) {
|
||||
const startNode = availableNodes?.find(n => n.data.type === BlockEnum.Start)
|
||||
if (startNode)
|
||||
return startNode
|
||||
}
|
||||
return getNodeInfoById(availableNodes || nodes, isRagVar ? valueSelector[1] : valueSelector[0])
|
||||
}, [nodes, valueSelector, availableNodes, isRagVar])
|
||||
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const isGlobal = isGlobalVar(valueSelector)
|
||||
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
|
||||
|
||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||
const isException = isExceptionVariable(variableName, node?.data.type)
|
||||
|
||||
const reactflow = useReactFlow()
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleVariableJump = useCallback(() => {
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
const {
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
} = workflowContainer!
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
} = reactflow
|
||||
const { transform } = store.getState()
|
||||
const zoom = transform[2]
|
||||
const position = node.position
|
||||
setViewport({
|
||||
x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom,
|
||||
y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
}, [node, reactflow, store])
|
||||
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<VariableLabelInSelect
|
||||
variables={valueSelector}
|
||||
nodeType={node?.data.type}
|
||||
nodeTitle={node?.data.title}
|
||||
variableType={!isShort ? varType : undefined}
|
||||
onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.stopPropagation()
|
||||
handleVariableJump()
|
||||
}
|
||||
}}
|
||||
errorMsg={!isValid ? t('workflow.errorMsg.invalidVariable') : undefined}
|
||||
isExceptionVariable={isException}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableTag
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from './var-reference-vars'
|
||||
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import ListEmpty from '@/app/components/base/list-empty'
|
||||
|
||||
type Props = {
|
||||
vars: NodeOutPutVar[]
|
||||
onChange: (value: ValueSelector, varDetail: Var) => void
|
||||
itemWidth?: number
|
||||
}
|
||||
const AssignedVarReferencePopup: FC<Props> = ({
|
||||
vars,
|
||||
onChange,
|
||||
itemWidth,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
|
||||
return (
|
||||
<div className='bg-components-panel-bg-bur w-[352px] rounded-lg border-[0.5px] border-components-panel-border p-1 shadow-lg' >
|
||||
{(!vars || vars.length === 0)
|
||||
? <ListEmpty
|
||||
title={t('workflow.nodes.assigner.noAssignedVars') || ''}
|
||||
description={t('workflow.nodes.assigner.assignedVarsDescription')}
|
||||
/>
|
||||
: <VarReferenceVars
|
||||
searchBoxClassName='mt-1'
|
||||
vars={vars}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar
|
||||
/>
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(AssignedVarReferencePopup)
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { CredentialFormSchema, CredentialFormSchemaNumberInput, CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
|
||||
type Props = {
|
||||
schema: Partial<CredentialFormSchema>
|
||||
readonly: boolean
|
||||
value: string
|
||||
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
|
||||
onOpenChange?: (open: boolean) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_SCHEMA = {} as CredentialFormSchema
|
||||
|
||||
const ConstantField: FC<Props> = ({
|
||||
schema = DEFAULT_SCHEMA,
|
||||
readonly,
|
||||
value,
|
||||
onChange,
|
||||
onOpenChange,
|
||||
isLoading,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const placeholder = (schema as CredentialFormSchemaSelect).placeholder
|
||||
const handleStaticChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value === '' ? '' : Number.parseFloat(e.target.value)
|
||||
onChange(value, VarKindType.constant)
|
||||
}, [onChange])
|
||||
const handleSelectChange = useCallback((value: string | number) => {
|
||||
value = value === null ? '' : value
|
||||
onChange(value as string, VarKindType.constant)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<>
|
||||
{(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='w-full !h-8'
|
||||
className='flex items-center'
|
||||
disabled={readonly}
|
||||
defaultValue={value}
|
||||
items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
onSelect={item => handleSelectChange(item.value)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
onOpenChange={onOpenChange}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
{schema.type === FormTypeEnum.textNumber && (
|
||||
<input
|
||||
type='number'
|
||||
className='h-8 w-full overflow-hidden rounded-lg bg-workflow-block-parma-bg p-2 text-[13px] font-normal leading-8 text-text-secondary placeholder:text-gray-400 focus:outline-none'
|
||||
value={value}
|
||||
onChange={handleStaticChange}
|
||||
readOnly={readonly}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
min={(schema as CredentialFormSchemaNumberInput).min}
|
||||
max={(schema as CredentialFormSchemaNumberInput).max}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConstantField)
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
|
||||
type ManageInputFieldProps = {
|
||||
onManage: () => void
|
||||
}
|
||||
|
||||
const ManageInputField = ({
|
||||
onManage,
|
||||
}: ManageInputFieldProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center border-t border-divider-subtle pt-1'>
|
||||
<div
|
||||
className='flex h-8 grow cursor-pointer items-center px-3'
|
||||
onClick={onManage}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4 text-text-tertiary' />
|
||||
<div
|
||||
className='system-xs-medium truncate text-text-tertiary'
|
||||
title='Create user input field'
|
||||
>
|
||||
{t('pipeline.inputField.create')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-1 h-3 w-[1px] shrink-0 bg-divider-regular'></div>
|
||||
<div
|
||||
className='system-xs-medium flex h-8 shrink-0 cursor-pointer items-center justify-center px-3 text-text-tertiary'
|
||||
onClick={onManage}
|
||||
>
|
||||
{t('pipeline.inputField.manage')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManageInputField
|
||||
@@ -0,0 +1,162 @@
|
||||
import matchTheSchemaType from './match-schema-type'
|
||||
|
||||
describe('match the schema type', () => {
|
||||
it('should return true for identical primitive types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string' }, { type: 'string' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'number' }, { type: 'number' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for different primitive types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string' }, { type: 'number' })).toBe(false)
|
||||
})
|
||||
|
||||
it('should ignore values and only compare types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for structural differences but no types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle nested objects with same structure and types', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', value: 'Alice' },
|
||||
age: { type: 'number', value: 30 },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string', value: '123 Main St' },
|
||||
city: { type: 'string', value: 'Wonderland' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(matchTheSchemaType(obj1, obj2)).toBe(true)
|
||||
})
|
||||
it('should return false for nested objects with different structures', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
address: { type: 'string' },
|
||||
},
|
||||
}
|
||||
expect(matchTheSchemaType(obj1, obj2)).toBe(false)
|
||||
})
|
||||
|
||||
it('file struct should match file type', () => {
|
||||
const fileSchema = {
|
||||
$id: 'https://dify.ai/schemas/v1/file.json',
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
version: '1.0.0',
|
||||
type: 'object',
|
||||
title: 'File Schema',
|
||||
description: 'Schema for file objects (v1)',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'file size',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'file extension',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'file type',
|
||||
},
|
||||
mime_type: {
|
||||
type: 'string',
|
||||
description: 'file mime type',
|
||||
},
|
||||
transfer_method: {
|
||||
type: 'string',
|
||||
description: 'file transfer method',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'file url',
|
||||
},
|
||||
related_id: {
|
||||
type: 'string',
|
||||
description: 'file related id',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
],
|
||||
}
|
||||
const file = {
|
||||
type: 'object',
|
||||
title: 'File',
|
||||
description: 'Schema for file objects (v1)',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'file size',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'file extension',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'file type',
|
||||
},
|
||||
mime_type: {
|
||||
type: 'string',
|
||||
description: 'file mime type',
|
||||
},
|
||||
transfer_method: {
|
||||
type: 'string',
|
||||
description: 'file transfer method',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'file url',
|
||||
},
|
||||
related_id: {
|
||||
type: 'string',
|
||||
description: 'file related id',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
],
|
||||
}
|
||||
expect(matchTheSchemaType(fileSchema, file)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
export type AnyObj = Record<string, any> | null
|
||||
|
||||
const isObj = (x: any): x is object => x !== null && typeof x === 'object'
|
||||
|
||||
// only compare type in object
|
||||
function matchTheSchemaType(scheme: AnyObj, target: AnyObj): boolean {
|
||||
const isMatch = (schema: AnyObj, t: AnyObj): boolean => {
|
||||
const oSchema = isObj(schema)
|
||||
const oT = isObj(t)
|
||||
if(!oSchema)
|
||||
return true
|
||||
if (!oT) { // ignore the object without type
|
||||
// deep find oSchema has type
|
||||
for (const key in schema) {
|
||||
if (key === 'type')
|
||||
return false
|
||||
if (isObj((schema as any)[key]) && !isMatch((schema as any)[key], null))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// check current `type`
|
||||
const tx = (schema as any).type
|
||||
const ty = (t as any).type
|
||||
const isTypeValueObj = isObj(tx)
|
||||
|
||||
if(!isTypeValueObj) // caution: type can be object, so that it would not be compare by value
|
||||
if (tx !== ty) return false
|
||||
|
||||
// recurse into all keys
|
||||
const keys = new Set([...Object.keys(schema as object), ...Object.keys(t as object)])
|
||||
for (const k of keys) {
|
||||
if (k === 'type' && !isTypeValueObj) continue // already checked
|
||||
if (!isMatch((schema as any)[k], (t as any)[k])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return isMatch(scheme, target)
|
||||
}
|
||||
|
||||
export default matchTheSchemaType
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { Type } from '../../../../../llm/types'
|
||||
import { getFieldType } from '../../../../../llm/utils'
|
||||
import type { Field as FieldType } from '../../../../../llm/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import TreeIndentLine from '../tree-indent-line'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const MAX_DEPTH = 10
|
||||
|
||||
type Props = {
|
||||
valueSelector: ValueSelector
|
||||
name: string,
|
||||
payload: FieldType,
|
||||
depth?: number
|
||||
readonly?: boolean
|
||||
onSelect?: (valueSelector: ValueSelector) => void
|
||||
}
|
||||
|
||||
const Field: FC<Props> = ({
|
||||
valueSelector,
|
||||
name,
|
||||
payload,
|
||||
depth = 1,
|
||||
readonly,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isLastFieldHighlight = readonly
|
||||
const hasChildren = payload.type === Type.object && payload.properties
|
||||
const isHighlight = isLastFieldHighlight && !hasChildren
|
||||
if (depth > MAX_DEPTH + 1)
|
||||
return null
|
||||
return (
|
||||
<div>
|
||||
<Tooltip popupContent={t('app.structOutput.moreFillTip')} disabled={depth !== MAX_DEPTH + 1}>
|
||||
<div
|
||||
className={cn('flex items-center justify-between rounded-md pr-2', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
|
||||
onMouseDown={() => !readonly && onSelect?.([...valueSelector, name])}
|
||||
>
|
||||
<div className='flex grow items-stretch'>
|
||||
<TreeIndentLine depth={depth} />
|
||||
{depth === MAX_DEPTH + 1 ? (
|
||||
<RiMoreFill className='h-3 w-3 text-text-tertiary' />
|
||||
) : (<div className={cn('system-sm-medium h-6 w-0 grow truncate leading-6 text-text-secondary', isHighlight && 'text-text-accent')}>{name}</div>)}
|
||||
|
||||
</div>
|
||||
{depth < MAX_DEPTH + 1 && (
|
||||
<div className='system-xs-regular ml-2 shrink-0 text-text-tertiary'>{getFieldType(payload)}</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{depth <= MAX_DEPTH && payload.type === Type.object && payload.properties && (
|
||||
<div>
|
||||
{Object.keys(payload.properties).map(propName => (
|
||||
<Field
|
||||
key={propName}
|
||||
name={propName}
|
||||
payload={payload.properties?.[propName] as FieldType}
|
||||
depth={depth + 1}
|
||||
readonly={readonly}
|
||||
valueSelector={[...valueSelector, name]}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Field)
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import type { StructuredOutput } from '../../../../../llm/types'
|
||||
import Field from './field'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useHover } from 'ahooks'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
root: { nodeId?: string, nodeName?: string, attrName: string, attrAlias?: string }
|
||||
payload: StructuredOutput
|
||||
readonly?: boolean
|
||||
onSelect?: (valueSelector: ValueSelector) => void
|
||||
onHovering?: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const PickerPanelMain: FC<Props> = ({
|
||||
className,
|
||||
root,
|
||||
payload,
|
||||
readonly,
|
||||
onHovering,
|
||||
onSelect,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useHover(ref, {
|
||||
onChange: (hovering) => {
|
||||
if (hovering) {
|
||||
onHovering?.(true)
|
||||
}
|
||||
else {
|
||||
setTimeout(() => {
|
||||
onHovering?.(false)
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
})
|
||||
const schema = payload.schema
|
||||
const fieldNames = Object.keys(schema.properties)
|
||||
return (
|
||||
<div className={cn(className)} ref={ref}>
|
||||
{/* Root info */}
|
||||
<div className='flex items-center justify-between px-2 py-1'>
|
||||
<div className='flex'>
|
||||
{root.nodeName && (
|
||||
<>
|
||||
<div className='system-sm-medium max-w-[100px] truncate text-text-tertiary'>{root.nodeName}</div>
|
||||
<div className='system-sm-medium text-text-tertiary'>.</div>
|
||||
</>
|
||||
)}
|
||||
<div className='system-sm-medium text-text-secondary'>{root.attrName}</div>
|
||||
</div>
|
||||
<div className='system-xs-regular ml-2 truncate text-text-tertiary' title={root.attrAlias || 'object'}>{root.attrAlias || 'object'}</div>
|
||||
</div>
|
||||
{fieldNames.map(name => (
|
||||
<Field
|
||||
key={name}
|
||||
name={name}
|
||||
payload={schema.properties[name]}
|
||||
readonly={readonly}
|
||||
valueSelector={[root.nodeId!, root.attrName]}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PickerPanel: FC<Props> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('w-[296px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]', className)}>
|
||||
<PickerPanelMain {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PickerPanel)
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDropDownLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Field as FieldType } from '../../../../../llm/types'
|
||||
import { Type } from '../../../../../llm/types'
|
||||
import { getFieldType } from '../../../../../llm/utils'
|
||||
import TreeIndentLine from '../tree-indent-line'
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
payload: FieldType,
|
||||
required: boolean,
|
||||
depth?: number,
|
||||
rootClassName?: string
|
||||
}
|
||||
|
||||
const Field: FC<Props> = ({
|
||||
name,
|
||||
payload,
|
||||
depth = 1,
|
||||
required,
|
||||
rootClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isRoot = depth === 1
|
||||
const hasChildren = payload.type === Type.object && payload.properties
|
||||
const hasEnum = payload.enum && payload.enum.length > 0
|
||||
const [fold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div>
|
||||
<div className={cn('flex pr-2')}>
|
||||
<TreeIndentLine depth={depth} />
|
||||
<div className='w-0 grow'>
|
||||
<div className='relative flex select-none'>
|
||||
{hasChildren && (
|
||||
<RiArrowDropDownLine
|
||||
className={cn('absolute left-[-18px] top-[50%] h-4 w-4 translate-y-[-50%] cursor-pointer bg-components-panel-bg text-text-tertiary', fold && 'rotate-[270deg] text-text-accent')}
|
||||
onClick={toggleFold}
|
||||
/>
|
||||
)}
|
||||
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>
|
||||
{getFieldType(payload)}
|
||||
{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}
|
||||
</div>
|
||||
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
|
||||
</div>
|
||||
{payload.description && (
|
||||
<div className='ml-[7px] flex'>
|
||||
<div className='system-xs-regular w-0 grow truncate text-text-tertiary'>{payload.description}</div>
|
||||
</div>
|
||||
)}
|
||||
{hasEnum && (
|
||||
<div className='ml-[7px] flex'>
|
||||
<div className='system-xs-regular w-0 grow text-text-quaternary'>
|
||||
{payload.enum!.map((value, index) => (
|
||||
<span key={index}>
|
||||
{typeof value === 'string' ? `"${value}"` : value}
|
||||
{index < payload.enum!.length - 1 && ' | '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasChildren && !fold && (
|
||||
<div>
|
||||
{Object.keys(payload.properties!).map(name => (
|
||||
<Field
|
||||
key={name}
|
||||
name={name}
|
||||
payload={payload.properties?.[name] as FieldType}
|
||||
depth={depth + 1}
|
||||
required={!!payload.required?.includes(name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Field)
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { StructuredOutput } from '../../../../../llm/types'
|
||||
import Field from './field'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
payload: StructuredOutput
|
||||
rootClassName?: string
|
||||
}
|
||||
|
||||
const ShowPanel: FC<Props> = ({
|
||||
payload,
|
||||
rootClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const schema = {
|
||||
...payload,
|
||||
schema: {
|
||||
...payload.schema,
|
||||
description: t('app.structOutput.LLMResponse'),
|
||||
},
|
||||
}
|
||||
return (
|
||||
<div className='relative left-[-7px]'>
|
||||
{Object.keys(schema.schema.properties!).map(name => (
|
||||
<Field
|
||||
key={name}
|
||||
name={name}
|
||||
payload={schema.schema.properties![name]}
|
||||
required={!!schema.schema.required?.includes(name)}
|
||||
rootClassName={rootClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ShowPanel)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user