|
'use client' |
|
import type { FC } from 'react' |
|
import React, { useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import produce from 'immer' |
|
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types' |
|
import cn from '@/utils/classnames' |
|
import Drawer from '@/app/components/base/drawer-plus' |
|
import Input from '@/app/components/base/input' |
|
import Textarea from '@/app/components/base/textarea' |
|
import Button from '@/app/components/base/button' |
|
import Toast from '@/app/components/base/toast' |
|
import EmojiPicker from '@/app/components/base/emoji-picker' |
|
import AppIcon from '@/app/components/base/app-icon' |
|
import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' |
|
import LabelSelector from '@/app/components/tools/labels/selector' |
|
import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' |
|
import Tooltip from '@/app/components/base/tooltip' |
|
|
|
type Props = { |
|
isAdd?: boolean |
|
payload: any |
|
onHide: () => void |
|
onRemove?: () => void |
|
onCreate?: (payload: WorkflowToolProviderRequest & { workflow_app_id: string }) => void |
|
onSave?: (payload: WorkflowToolProviderRequest & Partial<{ |
|
workflow_app_id: string |
|
workflow_tool_id: string |
|
}>) => void |
|
} |
|
|
|
const WorkflowToolAsModal: FC<Props> = ({ |
|
isAdd, |
|
payload, |
|
onHide, |
|
onRemove, |
|
onSave, |
|
onCreate, |
|
}) => { |
|
const { t } = useTranslation() |
|
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState<Boolean>(false) |
|
const [emoji, setEmoji] = useState<Emoji>(payload.icon) |
|
const [label, setLabel] = useState<string>(payload.label) |
|
const [name, setName] = useState(payload.name) |
|
const [description, setDescription] = useState(payload.description) |
|
const [parameters, setParameters] = useState<WorkflowToolProviderParameter[]>(payload.parameters) |
|
const handleParameterChange = (key: string, value: string, index: number) => { |
|
const newData = produce(parameters, (draft: WorkflowToolProviderParameter[]) => { |
|
if (key === 'description') |
|
draft[index].description = value |
|
else |
|
draft[index].form = value |
|
}) |
|
setParameters(newData) |
|
} |
|
const [labels, setLabels] = useState<string[]>(payload.labels) |
|
const handleLabelSelect = (value: string[]) => { |
|
setLabels(value) |
|
} |
|
const [privacyPolicy, setPrivacyPolicy] = useState(payload.privacy_policy) |
|
const [showModal, setShowModal] = useState(false) |
|
|
|
const isNameValid = (name: string) => { |
|
|
|
if (name === '') |
|
return true |
|
|
|
return /^[a-zA-Z0-9_]+$/.test(name) |
|
} |
|
|
|
const onConfirm = () => { |
|
let errorMessage = '' |
|
if (!label) |
|
errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.name') }) |
|
|
|
if (!name) |
|
errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.nameForToolCall') }) |
|
|
|
if (!isNameValid(name)) |
|
errorMessage = t('tools.createTool.nameForToolCall') + t('tools.createTool.nameForToolCallTip') |
|
|
|
if (errorMessage) { |
|
Toast.notify({ |
|
type: 'error', |
|
message: errorMessage, |
|
}) |
|
return |
|
} |
|
|
|
const requestParams = { |
|
name, |
|
description, |
|
icon: emoji, |
|
label, |
|
parameters: parameters.map(item => ({ |
|
name: item.name, |
|
description: item.description, |
|
form: item.form, |
|
})), |
|
labels, |
|
privacy_policy: privacyPolicy, |
|
} |
|
if (!isAdd) { |
|
onSave?.({ |
|
...requestParams, |
|
workflow_tool_id: payload.workflow_tool_id, |
|
}) |
|
} |
|
else { |
|
onCreate?.({ |
|
...requestParams, |
|
workflow_app_id: payload.workflow_app_id, |
|
}) |
|
} |
|
} |
|
|
|
return ( |
|
<> |
|
<Drawer |
|
isShow |
|
onHide={onHide} |
|
title={t('workflow.common.workflowAsTool')!} |
|
panelClassName='mt-2 !w-[640px]' |
|
maxWidthClassName='!max-w-[640px]' |
|
height='calc(100vh - 16px)' |
|
headerClassName='!border-b-black/5' |
|
body={ |
|
<div className='flex flex-col h-full'> |
|
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'> |
|
{/* name & icon */} |
|
<div> |
|
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div> |
|
<div className='flex items-center justify-between gap-3'> |
|
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' iconType='emoji' icon={emoji.content} background={emoji.background} /> |
|
<Input |
|
className='grow h-10' |
|
placeholder={t('tools.createTool.toolNamePlaceHolder')!} |
|
value={label} |
|
onChange={e => setLabel(e.target.value)} |
|
/> |
|
</div> |
|
</div> |
|
{/* name for tool call */} |
|
<div> |
|
<div className='flex items-center py-2 leading-5 text-sm font-medium text-gray-900'> |
|
{t('tools.createTool.nameForToolCall')} <span className='ml-1 text-red-500'>*</span> |
|
<Tooltip |
|
popupContent={ |
|
<div className='w-[180px]'> |
|
{t('tools.createTool.nameForToolCallPlaceHolder')} |
|
</div> |
|
} |
|
/> |
|
</div> |
|
<Input |
|
className='h-10' |
|
placeholder={t('tools.createTool.nameForToolCallPlaceHolder')!} |
|
value={name} |
|
onChange={e => setName(e.target.value)} |
|
/> |
|
{!isNameValid(name) && ( |
|
<div className='text-xs leading-[18px] text-red-500'>{t('tools.createTool.nameForToolCallTip')}</div> |
|
)} |
|
</div> |
|
{/* description */} |
|
<div> |
|
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.description')}</div> |
|
<Textarea |
|
placeholder={t('tools.createTool.descriptionPlaceholder') || ''} |
|
value={description} |
|
onChange={e => setDescription(e.target.value)} |
|
/> |
|
</div> |
|
{/* Tool Input */} |
|
<div> |
|
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.title')}</div> |
|
<div className='rounded-lg border border-gray-200 w-full overflow-x-auto'> |
|
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'> |
|
<thead className='text-gray-500 uppercase'> |
|
<tr className='border-b border-gray-200'> |
|
<th className="p-2 pl-3 font-medium w-[156px]">{t('tools.createTool.toolInput.name')}</th> |
|
<th className="p-2 pl-3 font-medium w-[102px]">{t('tools.createTool.toolInput.method')}</th> |
|
<th className="p-2 pl-3 font-medium">{t('tools.createTool.toolInput.description')}</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
{parameters.map((item, index) => ( |
|
<tr key={index} className='border-b last:border-0 border-gray-200'> |
|
<td className="p-2 pl-3 max-w-[156px]"> |
|
<div className='text-[13px] leading-[18px]'> |
|
<div title={item.name} className='flex'> |
|
<span className='font-medium text-gray-900 truncate'>{item.name}</span> |
|
<span className='shrink-0 pl-1 text-[#ec4a0a] text-xs leading-[18px]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span> |
|
</div> |
|
<div className='text-gray-500'>{item.type}</div> |
|
</div> |
|
</td> |
|
<td> |
|
{item.name === '__image' && ( |
|
<div className={cn( |
|
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-default', |
|
)}> |
|
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}> |
|
{t('tools.createTool.toolInput.methodParameter')} |
|
</div> |
|
</div> |
|
)} |
|
{item.name !== '__image' && ( |
|
<MethodSelector value={item.form} onChange={value => handleParameterChange('form', value, index)} /> |
|
)} |
|
</td> |
|
<td className="p-2 pl-3 text-gray-500 w-[236px]"> |
|
<input |
|
type='text' |
|
className='grow text-gray-700 text-[13px] leading-[18px] font-normal bg-white outline-none appearance-none caret-primary-600 placeholder:text-gray-300' |
|
placeholder={t('tools.createTool.toolInput.descriptionPlaceholder')!} |
|
value={item.description} |
|
onChange={e => handleParameterChange('description', e.target.value, index)} |
|
/> |
|
</td> |
|
</tr> |
|
))} |
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
{/* Tags */} |
|
<div> |
|
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div> |
|
<LabelSelector value={labels} onChange={handleLabelSelect} /> |
|
</div> |
|
{/* Privacy Policy */} |
|
<div> |
|
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.privacyPolicy')}</div> |
|
<Input |
|
className='h-10' |
|
value={privacyPolicy} |
|
onChange={e => setPrivacyPolicy(e.target.value)} |
|
placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} /> |
|
</div> |
|
</div> |
|
<div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} > |
|
{!isAdd && onRemove && ( |
|
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button> |
|
)} |
|
<div className='flex space-x-2 '> |
|
<Button onClick={onHide}>{t('common.operation.cancel')}</Button> |
|
<Button variant='primary' onClick={() => { |
|
if (isAdd) |
|
onConfirm() |
|
else |
|
setShowModal(true) |
|
}}>{t('common.operation.save')}</Button> |
|
</div> |
|
</div> |
|
</div> |
|
} |
|
isShowMask={true} |
|
clickOutsideNotOpen={true} |
|
/> |
|
{showEmojiPicker && <EmojiPicker |
|
onSelect={(icon, icon_background) => { |
|
setEmoji({ content: icon, background: icon_background }) |
|
setShowEmojiPicker(false) |
|
}} |
|
onClose={() => { |
|
setShowEmojiPicker(false) |
|
}} |
|
/>} |
|
{showModal && ( |
|
<ConfirmModal |
|
show={showModal} |
|
onClose={() => setShowModal(false)} |
|
onConfirm={onConfirm} |
|
/> |
|
)} |
|
</> |
|
|
|
) |
|
} |
|
export default React.memo(WorkflowToolAsModal) |
|
|