| '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) |
|
|