Merge pull request #585 from SuanmoSuanyangTechnology/feature/app_features_zy
feat(web): app support features
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:11:51
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:11:14
|
||||
* @Last Modified time: 2026-03-16 18:06:00
|
||||
*/
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import RecordRTC from 'recordrtc'
|
||||
@@ -19,13 +19,15 @@ interface AudioRecorderProps {
|
||||
action?: string;
|
||||
/** Additional config passed to the upload request */
|
||||
requestConfig?: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
onRecordingComplete,
|
||||
className = '',
|
||||
action = fileUploadUrlWithoutApiPrefix,
|
||||
requestConfig = {}
|
||||
requestConfig = {},
|
||||
disabled = false
|
||||
}) => {
|
||||
// Whether the recorder is currently capturing audio
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
@@ -34,6 +36,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
|
||||
/** Request microphone access and start recording */
|
||||
const startRecording = async () => {
|
||||
if (disabled) return
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
recorderRef.current = new RecordRTC(stream, {
|
||||
@@ -49,6 +52,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
|
||||
/** Stop recording, upload the audio blob, then invoke the completion callback */
|
||||
const stopRecording = () => {
|
||||
if (disabled) return
|
||||
if (recorderRef.current) {
|
||||
recorderRef.current.stopRecording(() => {
|
||||
const blob = recorderRef.current!.getBlob()
|
||||
@@ -76,7 +80,7 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
// swap background image to reflect current state
|
||||
return (
|
||||
<div
|
||||
className={`rb:size-5.5 rb:cursor-pointer rb:bg-cover ${className} ${
|
||||
className={`rb:size-5.5 rb:bg-cover ${disabled ? 'rb:opacity-65 rb:cursor-not-allowed' : 'rb:cursor-pointer'} ${className} ${
|
||||
isRecording
|
||||
? `rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]`
|
||||
: `rb:bg-[url('@/assets/images/conversation/audio.svg')]`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:01:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-12 14:59:38
|
||||
* @Last Modified time: 2026-03-17 15:35:34
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -63,9 +63,9 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
|
||||
align="center"
|
||||
justify={cicle ? 'center' : 'start'}
|
||||
gap={4}
|
||||
className={clsx("rb:flex rb:items-center rb:cursor-pointer rb:border rb:hover:bg-[#F6F6F6]", {
|
||||
className={clsx("rb:flex rb:items-center rb:cursor-pointer rb:px-2! rb:border rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:size-7 rb:rounded-[14px] rb:border-[0.5px] rb:border-[#EBEBEB]': cicle,
|
||||
'rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6': !cicle,
|
||||
'rb:rounded-lg rb:text-[12px] rb:h-6': !cicle,
|
||||
// Checked state: blue background and border
|
||||
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)] rb:hover:bg-[rgba(21,94,239,0.06)] rb:text-[#155EEF]": checked,
|
||||
// Unchecked state: gray border and dark text
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:17
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-06 21:05:52
|
||||
* @Last Modified time: 2026-03-17 14:11:24
|
||||
*/
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import { type FC, useRef, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import type { ChatContentProps } from './types'
|
||||
import { Spin } from 'antd'
|
||||
import { Spin, Divider, Space } from 'antd'
|
||||
import { SoundOutlined } from '@ant-design/icons'
|
||||
|
||||
/**
|
||||
* Chat Content Display Component
|
||||
@@ -28,7 +29,25 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
// Scroll container reference for controlling auto-scroll to bottom
|
||||
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
|
||||
const prevDataLengthRef = useRef(data.length);
|
||||
const isScrolledToBottomRef = useRef(true); // Track if user is scrolled to bottom
|
||||
const isScrolledToBottomRef = useRef(true);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const [playingIndex, setPlayingIndex] = useState<number | null>(null)
|
||||
|
||||
const handlePlay = (index: number, audioUrl: string) => {
|
||||
if (playingIndex === index) {
|
||||
audioRef.current?.pause()
|
||||
setPlayingIndex(null)
|
||||
return
|
||||
}
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause()
|
||||
}
|
||||
const audio = new Audio(audioUrl)
|
||||
audioRef.current = audio
|
||||
audio.play()
|
||||
setPlayingIndex(index)
|
||||
audio.onended = () => setPlayingIndex(null)
|
||||
}
|
||||
|
||||
// Track scroll position to determine if user is at bottom
|
||||
useEffect(() => {
|
||||
@@ -101,6 +120,19 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
{item.subContent && renderRuntime && renderRuntime(item, index)}
|
||||
{/* Render message content using Markdown component */}
|
||||
<Markdown content={renderRuntime ? item.content ?? '' : item.content ?? errorDesc ?? ''} />
|
||||
|
||||
{item.audioUrl && <>
|
||||
<Divider className="rb:my-3!" />
|
||||
<Space size={12} className="rb:pb-2 rb:pl-1">
|
||||
{playingIndex !== index
|
||||
? <SoundOutlined className="rb:cursor-pointer rb:hover:text-[#155EEF]! rb:size-5.5" onClick={() => handlePlay(index, item.audioUrl!)} />
|
||||
: <div
|
||||
className="rb:size-5.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]"
|
||||
onClick={() => handlePlay(index, item.audioUrl!)}
|
||||
/>
|
||||
}
|
||||
</Space>
|
||||
</>}
|
||||
</div>
|
||||
{/* Bottom label (such as timestamp, username, etc.) */}
|
||||
{labelPosition === 'bottom' &&
|
||||
|
||||
205
web/src/components/Chat/ChatToolbar.tsx
Normal file
205
web/src/components/Chat/ChatToolbar.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-17 14:22:25
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-17 14:22:25
|
||||
*/
|
||||
// Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration
|
||||
import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react'
|
||||
import { Flex, Dropdown, Divider, App, Form, type MenuProps } from 'antd'
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import AudioRecorder from '@/components/AudioRecorder'
|
||||
import UploadFiles from '@/views/Conversation/components/FileUpload'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal'
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import type { VariableConfigModalRef } from '@/views/Workflow/types'
|
||||
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
|
||||
|
||||
// Exposed methods via ref for parent components to access/set form state
|
||||
export interface ChatToolbarRef {
|
||||
getFiles: () => any[]
|
||||
getVariables: () => Variable[]
|
||||
setFiles: (files: any[]) => void
|
||||
setVariables: (variables: Variable[]) => void
|
||||
}
|
||||
|
||||
// Props for configuring toolbar features, upload settings, and event callbacks
|
||||
export interface ChatToolbarProps {
|
||||
features: FeaturesConfigForm
|
||||
extra?: ReactNode
|
||||
uploadAction?: string
|
||||
uploadRequestConfig?: {
|
||||
data?: Record<string, string | number | boolean>
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
onFilesChange?: (files: any[]) => void
|
||||
onVariablesChange?: (variables: Variable[]) => void
|
||||
onRecordingComplete?: (file: any) => void;
|
||||
defaultValue?: { memory: boolean }
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
files: any[]
|
||||
variables: Variable[];
|
||||
memory?: boolean;
|
||||
}
|
||||
|
||||
const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
|
||||
features,
|
||||
extra,
|
||||
uploadAction,
|
||||
uploadRequestConfig,
|
||||
onFilesChange,
|
||||
onVariablesChange,
|
||||
onRecordingComplete,
|
||||
defaultValue,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
|
||||
const [form] = Form.useForm<FormValues>()
|
||||
const queryValues = Form.useWatch([], form)
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultValue) return
|
||||
form.setFieldsValue(defaultValue)
|
||||
}, [defaultValue])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getFiles: () => form.getFieldValue('files') || [],
|
||||
getVariables: () => form.getFieldValue('variables') || [],
|
||||
setFiles: (files) => form.setFieldValue('files', files),
|
||||
setVariables: (variables) => {
|
||||
console.log('variables', variables)
|
||||
form.setFieldValue('variables', variables)
|
||||
},
|
||||
}))
|
||||
|
||||
const { file_upload } = features || {}
|
||||
|
||||
// Append newly uploaded file to the file list when upload is complete
|
||||
const fileChange = (file?: any) => {
|
||||
if (file?.status !== 'done') return
|
||||
const files = [...(queryValues?.files || []), file]
|
||||
form.setFieldValue('files', files)
|
||||
onFilesChange?.(files)
|
||||
}
|
||||
|
||||
// Append recorded audio file to the file list and notify parent
|
||||
const handleRecordingComplete = (file: any) => {
|
||||
const files = [...(queryValues?.files || []), file]
|
||||
form.setFieldValue('files', files)
|
||||
onFilesChange?.(files)
|
||||
onRecordingComplete?.(file)
|
||||
}
|
||||
|
||||
// Merge a batch of files (e.g. from remote URL modal) into the file list
|
||||
const addFileList = (list?: any[]) => {
|
||||
if (!list?.length) return
|
||||
const files = [...(queryValues?.files || []), ...list]
|
||||
form.setFieldValue('files', files)
|
||||
onFilesChange?.(files)
|
||||
}
|
||||
|
||||
// Persist variable values from the config modal and notify parent
|
||||
const handleVariablesSave = (values: Variable[]) => {
|
||||
form.setFieldValue('variables', values)
|
||||
onVariablesChange?.(values)
|
||||
}
|
||||
|
||||
// True when any required variable is missing a value, used to highlight the config button
|
||||
const isNeedVariableConfig = queryValues?.variables?.some(
|
||||
vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === '')
|
||||
)
|
||||
|
||||
// Build dropdown menu items based on allowed transfer methods
|
||||
const fileMenus: MenuProps['items'] = []
|
||||
if (file_upload?.allowed_transfer_methods?.includes('remote_url')) {
|
||||
fileMenus.push({
|
||||
key: 'url',
|
||||
label: t('memoryConversation.addRemoteFile'),
|
||||
onClick: () => {
|
||||
if ((queryValues?.files?.length || 0) >= file_upload.max_file_count) {
|
||||
messageApi.warning(t('common.fileNumTip', { num: file_upload.max_file_count }))
|
||||
return
|
||||
}
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
}
|
||||
})
|
||||
}
|
||||
const enabledTypes = ['image', 'document', 'video', 'audio'].filter(
|
||||
type => file_upload?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']]
|
||||
)
|
||||
if (file_upload?.allowed_transfer_methods?.includes('local_file') && enabledTypes.length > 0) {
|
||||
fileMenus.push({
|
||||
key: 'upload',
|
||||
label: (
|
||||
<UploadFiles
|
||||
action={uploadAction}
|
||||
onChange={fileChange}
|
||||
requestConfig={uploadRequestConfig}
|
||||
featureConfig={file_upload}
|
||||
disabled={(queryValues?.files?.length || 0) >= file_upload.max_file_count}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
console.log('queryValues', queryValues)
|
||||
|
||||
return (
|
||||
<Form form={form} initialValues={{ files: [], variables: [] }}>
|
||||
<Flex justify="space-between" className="rb:flex-1">
|
||||
<Flex gap={8} align="center">
|
||||
<Form.Item name="files" noStyle hidden={!file_upload?.enabled}>
|
||||
<Dropdown menu={{ items: fileMenus }}>
|
||||
<div className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]" />
|
||||
</Dropdown>
|
||||
</Form.Item>
|
||||
{extra}
|
||||
<Form.Item name="variables" className="rb:mb-0!" hidden={queryValues?.variables?.length < 1}>
|
||||
<div
|
||||
className={clsx('rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]', {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
|
||||
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
|
||||
})}
|
||||
onClick={() => variableConfigModalRef.current?.handleOpen(queryValues.variables)}
|
||||
>
|
||||
<SettingOutlined className="rb:mr-1" />
|
||||
{t('memoryConversation.variableConfig')}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
{file_upload?.audio_enabled && file_upload?.allowed_transfer_methods?.includes('local_file') && (
|
||||
<Flex align="center">
|
||||
<AudioRecorder
|
||||
disabled={(queryValues?.files?.length || 0) >= file_upload.max_file_count}
|
||||
action={uploadAction}
|
||||
requestConfig={uploadRequestConfig}
|
||||
onRecordingComplete={handleRecordingComplete}
|
||||
/>
|
||||
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<UploadFileListModal
|
||||
ref={uploadFileListModalRef}
|
||||
refresh={addFileList}
|
||||
featureConfig={file_upload}
|
||||
/>
|
||||
<VariableConfigModal
|
||||
ref={variableConfigModalRef}
|
||||
refresh={handleVariablesSave}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default ChatToolbar
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:45:54
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-12 13:57:51
|
||||
* @Last Modified time: 2026-03-17 13:46:24
|
||||
*/
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ChatItem {
|
||||
subContent?: Record<string, any>[];
|
||||
files?: any[];
|
||||
error?: string;
|
||||
audioUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -449,6 +449,7 @@ export const en = {
|
||||
|
||||
fileSizeTip: 'File size cannot exceed {{size}}MB',
|
||||
fileAcceptTip: 'Unsupported file type:',
|
||||
fileNumTip: 'File count cannot exceed {{num}}',
|
||||
nextStep: 'Next Step',
|
||||
prevStep: 'Previous Step',
|
||||
exportSuccess: 'Export successful',
|
||||
@@ -1373,9 +1374,9 @@ export const en = {
|
||||
dify: 'Dify',
|
||||
pleaseUploadFile: 'Please upload file',
|
||||
setting: 'Settings',
|
||||
funConfig: 'Features',
|
||||
fileUpload: 'File Upload',
|
||||
fileUploadDesc: 'The chat input box supports file uploads. Types include images, documents, and other types',
|
||||
features: 'Conversation Features',
|
||||
file_upload: 'File Upload',
|
||||
file_upload_desc: 'The chat input box supports file uploads. Types include images, documents, and other types',
|
||||
settings: 'File Upload Settings',
|
||||
uploadType: 'Upload Type',
|
||||
local: 'Local Upload',
|
||||
@@ -1392,8 +1393,8 @@ export const en = {
|
||||
maxCount: 'Max Files',
|
||||
singleMaxSize: 'Max Size',
|
||||
unix: 'items',
|
||||
textTranfer: 'Text to Speech',
|
||||
textTranferDesc: 'Text can be converted to speech',
|
||||
text_to_speech: 'Text to Speech',
|
||||
text_to_speech_desc: 'Text can be converted to speech',
|
||||
|
||||
apps: 'My Apps',
|
||||
sharing: 'Sharing',
|
||||
@@ -1779,6 +1780,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
fileUrl: 'File URL',
|
||||
addRemoteFile: 'Add Remote File',
|
||||
variableConfig: 'Variable Configuration',
|
||||
memoryCancelTipTitle: 'Are you sure you want to disable conversation memory? Conversations will no longer be saved to the memory store.',
|
||||
memoryTipTitle: 'Are you sure you want to enable conversation memory? Conversations will be saved to the memory store.',
|
||||
},
|
||||
login: {
|
||||
title: 'Red Bear Memory Science',
|
||||
|
||||
@@ -756,9 +756,9 @@ export const zh = {
|
||||
dify: 'Dify',
|
||||
pleaseUploadFile: '请上传文件',
|
||||
setting: '设置',
|
||||
funConfig: '功能',
|
||||
fileUpload: '文件上传',
|
||||
fileUploadDesc: '聊天输入框支持上传文件。类型包括图片、文档以及其它类型',
|
||||
features: '对话功能',
|
||||
file_upload: '文件上传',
|
||||
file_upload_desc: '聊天输入框支持上传文件。类型包括图片、文档以及其它类型',
|
||||
settings: '文件上传设置',
|
||||
uploadType: '上传类型',
|
||||
local: '本地上传',
|
||||
@@ -775,8 +775,8 @@ export const zh = {
|
||||
maxCount: '最大文件数',
|
||||
singleMaxSize: '单文件最大大小',
|
||||
unix: '个',
|
||||
textTranfer: '文字转语音',
|
||||
textTranferDesc: '文本可以转换成语言',
|
||||
text_to_speech: '文字转语音',
|
||||
text_to_speech_desc: '文本可以转换成语言',
|
||||
|
||||
apps: '我的应用',
|
||||
sharing: '共享',
|
||||
@@ -1082,6 +1082,7 @@ export const zh = {
|
||||
|
||||
fileSizeTip: '文件大小不能超过 {{size}}MB',
|
||||
fileAcceptTip: '不支持的文件类型:',
|
||||
fileNumTip: '文件数量不能超过{{num}}个',
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
exportSuccess: '导出成功',
|
||||
@@ -1775,6 +1776,8 @@ export const zh = {
|
||||
fileUrl: '文件链接',
|
||||
addRemoteFile: '添加远程文件',
|
||||
variableConfig: '变量配置',
|
||||
memoryCancelTipTitle: '确定关闭对话记忆功能吗?关闭后对话将不会保存到记忆库中',
|
||||
memoryTipTitle: '确定打开对话记忆功能吗?打开后对话将会保存到记忆库中',
|
||||
},
|
||||
login: {
|
||||
title: '红熊记忆科学',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 16:58:15
|
||||
* @Last Modified time: 2026-03-17 14:24:29
|
||||
*/
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx'
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
AiPromptModalRef,
|
||||
Source,
|
||||
ChatVariableConfigModalRef,
|
||||
FunConfigForm
|
||||
FeaturesConfigForm
|
||||
} from './types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
@@ -42,7 +42,7 @@ import ToolList from './components/ToolList/ToolList'
|
||||
import SkillList from './components/Skill'
|
||||
import ChatVariableConfigModal from './components/ChatVariableConfigModal';
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
import FunConfig from './components/FunConfig'
|
||||
import FeaturesConfig from './components/FeaturesConfig'
|
||||
|
||||
/**
|
||||
* Description wrapper component
|
||||
@@ -356,7 +356,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSave,
|
||||
funConfig: values?.funConfig
|
||||
features: values?.features
|
||||
}))
|
||||
|
||||
const aiPromptModalRef = useRef<AiPromptModalRef>(null)
|
||||
@@ -411,8 +411,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
setChatVariables(values?.variables || [])
|
||||
}, [values?.variables])
|
||||
|
||||
const handleSaveFunConfig = (value: FunConfigForm) => {
|
||||
form.setFieldValue('funConfig', value)
|
||||
const handleSaveFeaturesConfig = (value: FeaturesConfigForm) => {
|
||||
form.setFieldValue('features', value)
|
||||
}
|
||||
console.log('agent', values)
|
||||
return (
|
||||
@@ -426,7 +426,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
{defaultModel?.name ? <div className="rb:w-4 rb:h-4 rb:bg-[url('@/assets/images/application/model.svg')] rb:group-hover:bg-[url('@/assets/images/application/model_hover.svg')]"></div> : null}
|
||||
{defaultModel?.name || t('application.chooseModel')}
|
||||
</Button>
|
||||
{/* <FunConfig value={values?.funConfig as FunConfigForm} refresh={handleSaveFunConfig} /> */}
|
||||
<FeaturesConfig value={values?.features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} />
|
||||
<Button type="primary" onClick={() => handleSave()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
@@ -435,7 +435,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
<Form form={form}>
|
||||
<Form.Item name="default_model_config_id" hidden noStyle></Form.Item>
|
||||
<Form.Item name="model_parameters" hidden noStyle></Form.Item>
|
||||
<Form.Item name="funConfig" hidden noStyle></Form.Item>
|
||||
<Form.Item name="features" hidden noStyle></Form.Item>
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
<Card title={t('application.promptConfiguration')}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
|
||||
@@ -512,7 +512,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
</div>
|
||||
<RbCard height="calc(100vh - 160px)" bodyClassName="rb:p-[0]! rb:h-full rb:overflow-hidden">
|
||||
<Chat
|
||||
data={data as Config}
|
||||
data={values as Config}
|
||||
chatList={chatList}
|
||||
updateChatList={setChatList}
|
||||
handleSave={handleSave}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-05 13:47:23
|
||||
* @Last Modified time: 2026-03-17 14:48:57
|
||||
*/
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -19,7 +19,8 @@ import type {
|
||||
ChatData,
|
||||
SubAgentItem,
|
||||
ClusterRef,
|
||||
ModelConfigModalRef
|
||||
ModelConfigModalRef,
|
||||
FeaturesConfigForm
|
||||
} from './types'
|
||||
import Chat from './components/Chat'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -29,7 +30,7 @@ import RadioGroupCard from '@/components/RadioGroupCard'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import ModelConfigModal from './components/ModelConfigModal'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
|
||||
import FeaturesConfig from './components/FeaturesConfig'
|
||||
|
||||
const tagColors = ['processing', 'warning', 'default']
|
||||
const MAX_LENGTH = 5;
|
||||
@@ -166,7 +167,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSave,
|
||||
funConfig: data?.funConfig
|
||||
features: data?.features
|
||||
}))
|
||||
|
||||
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
|
||||
@@ -185,16 +186,21 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
model_parameters: values
|
||||
})
|
||||
}
|
||||
const handleSaveFeaturesConfig = (value: FeaturesConfigForm) => {
|
||||
form.setFieldValue('features', value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="rb:h-[calc(100vh-64px)]">
|
||||
<Col span={12} className="rb:h-full rb:overflow-x-auto rb:border-r rb:border-[#DFE4ED] rb:p-[20px_16px_24px_16px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-end rb:mb-5">
|
||||
<Flex gap={10} justify="end" align="center" className="rb:mb-5!">
|
||||
<FeaturesConfig value={values?.features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} />
|
||||
<Button type="primary" onClick={() => handleSave()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="features" hidden noStyle></Form.Item>
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card title={t('application.collaboration')}>
|
||||
<Form.Item
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
import { type FC, useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { type FC, useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { App, Flex, Dropdown, type MenuProps, Divider, Form, Space } from 'antd'
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
import { App } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
|
||||
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal'
|
||||
import { draftRun } from '@/api/application';
|
||||
import { draftRun } from '@/api/application'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import Chat from '@/components/Chat'
|
||||
import AudioRecorder from '@/components/AudioRecorder'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import UploadFiles from '@/views/Conversation/components/FileUpload'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import Runtime from '@/views/Workflow/components/Chat/Runtime';
|
||||
import ChatToolbar, { type ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
||||
import Runtime from '@/views/Workflow/components/Chat/Runtime'
|
||||
import { nodeLibrary } from '@/views/Workflow/constant'
|
||||
// import ButtonCheckbox from '@/components/ButtonCheckbox';
|
||||
|
||||
// import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
// import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
// import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
// import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import type { VariableConfigModalRef, WorkflowConfig } from '@/views/Workflow/types'
|
||||
import type { WorkflowConfig } from '@/views/Workflow/types'
|
||||
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
|
||||
import type { TestChatProps } from './type';
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import type { TestChatProps } from './type'
|
||||
import type { SSEMessage } from '@/utils/stream'
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
||||
|
||||
const formatParams = (message: string, conversation_id: string | null, files: any[] = [], variables: Record<string, any>) => {
|
||||
return {
|
||||
@@ -65,29 +54,25 @@ interface NodeData {
|
||||
elapsed_time?: string;
|
||||
error?: any;
|
||||
state: Record<string, any>;
|
||||
status?: 'completed' | 'failed'
|
||||
status?: 'completed' | 'failed';
|
||||
audio_url?: string;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
files: any[];
|
||||
variables: Variable[]
|
||||
}
|
||||
const TestChat: FC<TestChatProps> = ({
|
||||
application,
|
||||
config
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||
|
||||
const [loading, setLoading] = useState(false) // Send button loading state
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([]) // Chat message history
|
||||
const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state
|
||||
const [conversationId, setConversationId] = useState<string | null>(null) // Current conversation ID
|
||||
const [message, setMessage] = useState<string | undefined>(undefined) // Current input message
|
||||
const [form] = Form.useForm<FormData>()
|
||||
const queryValues = Form.useWatch([], form)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||
const [fileList, setFileList] = useState<any[]>([])
|
||||
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
||||
|
||||
useEffect(() => {
|
||||
getVariables()
|
||||
@@ -96,6 +81,8 @@ const TestChat: FC<TestChatProps> = ({
|
||||
const getVariables = () => {
|
||||
if (!application || !config) return
|
||||
|
||||
setFeatures(config?.features || {} as FeaturesConfigForm)
|
||||
|
||||
let initVariables: Variable[] = []
|
||||
|
||||
switch (application.type) {
|
||||
@@ -104,85 +91,35 @@ const TestChat: FC<TestChatProps> = ({
|
||||
const startNodes = nodes.filter(vo => vo.type === 'start')
|
||||
if (startNodes.length) {
|
||||
const curVariables = startNodes[0].config.variables as Variable[]
|
||||
|
||||
curVariables.forEach((vo) => {
|
||||
if (typeof vo.default !== 'undefined') {
|
||||
vo.value = vo.default
|
||||
}
|
||||
const lastVo = curVariables.find(item => item.name === vo.name)
|
||||
if (lastVo?.value) {
|
||||
vo.value = lastVo.value
|
||||
}
|
||||
})
|
||||
initVariables = curVariables
|
||||
}
|
||||
curVariables.forEach((vo) => {
|
||||
if (typeof vo.default !== 'undefined') {
|
||||
vo.value = vo.default
|
||||
}
|
||||
const lastVo = curVariables.find(item => item.name === vo.name)
|
||||
if (lastVo?.value) {
|
||||
vo.value = lastVo.value
|
||||
}
|
||||
})
|
||||
initVariables = curVariables
|
||||
}
|
||||
break
|
||||
case 'agent':
|
||||
initVariables = config.variables as Variable[]
|
||||
break
|
||||
}
|
||||
|
||||
form.setFieldValue('variables', [...initVariables])
|
||||
toolbarRef.current?.setVariables([...initVariables])
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the variable configuration modal
|
||||
*/
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen(queryValues.variables)
|
||||
}
|
||||
/**
|
||||
* Saves updated variable values from the modal
|
||||
*/
|
||||
const handleSave = (values: Variable[]) => {
|
||||
form.setFieldValue('variables', [...values])
|
||||
}
|
||||
/**
|
||||
* Handles file upload from local device
|
||||
*/
|
||||
const fileChange = (file?: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), file])
|
||||
}
|
||||
const handleRecordingComplete = async (file: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), file])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles dropdown menu actions for file upload
|
||||
*/
|
||||
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
|
||||
switch(key) {
|
||||
case 'define':
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
break
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds files from remote URL modal
|
||||
*/
|
||||
const addFileList = (list?: any[]) => {
|
||||
if (!list || list.length <= 0) return
|
||||
form.setFieldValue('files', [...(queryValues.files || []), ...(list || [])])
|
||||
}
|
||||
/**
|
||||
* Updates the entire file list (used when removing files)
|
||||
*/
|
||||
const updateFileList = (list?: any[]) => {
|
||||
form.setFieldValue('files', [...list || []])
|
||||
}
|
||||
const isNeedVariableConfig = useMemo(() => {
|
||||
return queryValues?.variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
|
||||
}, [queryValues?.variables])
|
||||
|
||||
const addUserMessage = (message: string, files: any[]) => {
|
||||
const newUserMessage: ChatItem = {
|
||||
setChatList(prev => [...prev, {
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now(),
|
||||
files
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
}])
|
||||
}
|
||||
|
||||
const addAssistantMessage = () => {
|
||||
const { type } = application || {}
|
||||
setChatList(prev => [...prev, {
|
||||
@@ -193,20 +130,22 @@ const TestChat: FC<TestChatProps> = ({
|
||||
}])
|
||||
}
|
||||
|
||||
const updateAssistantMessage = (content: string) => {
|
||||
const updateAssistantMessage = (content: string, audio_url?: string) => {
|
||||
setChatList(prev => {
|
||||
let newList = [...prev]
|
||||
const newList = [...prev]
|
||||
const lastMsg = newList[newList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content += content
|
||||
lastMsg.content += content;
|
||||
lastMsg.audioUrl = audio_url
|
||||
}
|
||||
return newList
|
||||
})
|
||||
}
|
||||
|
||||
const updateErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
setChatList(prev => {
|
||||
let newList = [...prev]
|
||||
const newList = [...prev]
|
||||
const lastMsg = newList[newList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content = null
|
||||
@@ -214,34 +153,37 @@ const TestChat: FC<TestChatProps> = ({
|
||||
return newList
|
||||
})
|
||||
}
|
||||
const handleSend = () => {
|
||||
if (loading || !application || !message || !message?.trim()) return
|
||||
// Validate required variables before sending
|
||||
const { variables, files } = queryValues;
|
||||
|
||||
const buildVariableParams = (variables: Variable[]) => {
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (variables && variables.length > 0) {
|
||||
if (variables?.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
variables.forEach(vo => {
|
||||
params[vo.name] = vo.value
|
||||
|
||||
params[vo.name] = vo.value ?? vo.defaultValue
|
||||
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
|
||||
isCanSend = false
|
||||
needRequired.push(vo.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (needRequired.length) {
|
||||
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
|
||||
}
|
||||
}
|
||||
if (!isCanSend) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
return { isCanSend, params }
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (loading || !application || !message || !message?.trim()) return
|
||||
const files = toolbarRef.current?.getFiles() || []
|
||||
const variables = toolbarRef.current?.getVariables() || []
|
||||
const { isCanSend, params } = buildVariableParams(variables)
|
||||
if (!isCanSend) return
|
||||
|
||||
addUserMessage(message, files)
|
||||
setMessage(undefined)
|
||||
form.setFieldValue('files', [])
|
||||
toolbarRef.current?.setFiles([])
|
||||
setFileList([])
|
||||
addAssistantMessage()
|
||||
setStreamLoading(true)
|
||||
setLoading(true)
|
||||
@@ -251,113 +193,82 @@ const TestChat: FC<TestChatProps> = ({
|
||||
formatParams(message, conversationId, files, params),
|
||||
handleStreamMessage
|
||||
)
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => setLoading(false))
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.map(item => {
|
||||
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
|
||||
|
||||
const { conversation_id, content, message_length, audio_url } = item.data as { conversation_id: string, content: string, message_length: number; audio_url?: string; };
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
if (conversation_id && conversationId !== conversation_id) setConversationId(conversation_id)
|
||||
break
|
||||
case 'message':
|
||||
updateAssistantMessage(content)
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break;
|
||||
if (conversation_id && conversationId !== conversation_id) setConversationId(conversation_id)
|
||||
break
|
||||
case 'end':
|
||||
if (audio_url) {
|
||||
updateAssistantMessage(content, audio_url)
|
||||
}
|
||||
updateErrorAssistantMessage(message_length)
|
||||
setStreamLoading(false)
|
||||
break;
|
||||
break
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const handleWorkflowSend = () => {
|
||||
if (loading || !application || !message || !message?.trim()) return
|
||||
|
||||
// Validate required variables before sending
|
||||
const { variables, files } = queryValues;
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (variables.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
variables.forEach(vo => {
|
||||
params[vo.name] = vo.value ?? vo.defaultValue
|
||||
|
||||
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
|
||||
isCanSend = false
|
||||
needRequired.push(vo.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (needRequired.length) {
|
||||
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
|
||||
}
|
||||
}
|
||||
if (!isCanSend) {
|
||||
return
|
||||
}
|
||||
const files = toolbarRef.current?.getFiles() || []
|
||||
const variables = toolbarRef.current?.getVariables() || []
|
||||
const { isCanSend, params } = buildVariableParams(variables)
|
||||
if (!isCanSend) return
|
||||
|
||||
setLoading(true)
|
||||
addUserMessage(message, files)
|
||||
addAssistantMessage()
|
||||
form.setFieldsValue({
|
||||
files: [],
|
||||
})
|
||||
|
||||
toolbarRef.current?.setFiles([])
|
||||
setFileList([])
|
||||
setMessage(undefined)
|
||||
setStreamLoading(true)
|
||||
|
||||
draftRun(
|
||||
application.id,
|
||||
formatParams(message, conversationId, files, params),
|
||||
handleWorkflowStreamMessage
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log('draftRun error', error)
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
status: 'failed',
|
||||
content: null,
|
||||
subContent: error.error
|
||||
}
|
||||
newList[lastIndex] = { ...newList[lastIndex], status: 'failed', content: null, subContent: error.error }
|
||||
}
|
||||
return newList
|
||||
})
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleWorkflowStreamMessage = (data: SSEMessage[]) => {
|
||||
data.forEach(item => {
|
||||
const { content, conversation_id } = item.data as NodeData;
|
||||
|
||||
switch (item.event) {
|
||||
// Append streaming text chunks to assistant message
|
||||
// Append streaming text chunks to assistant message
|
||||
case 'message':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
content: newList[lastIndex].content + content
|
||||
}
|
||||
newList[lastIndex] = { ...newList[lastIndex], content: newList[lastIndex].content + content }
|
||||
}
|
||||
return newList
|
||||
})
|
||||
@@ -388,10 +299,10 @@ const TestChat: FC<TestChatProps> = ({
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addWorkflowNodeStartMessage = (data: NodeData) => {
|
||||
const { node_id } = data;
|
||||
const { nodes } = config as WorkflowConfig
|
||||
|
||||
const node = nodes.find(n => n.id === node_id);
|
||||
const { name, type } = node || {}
|
||||
const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon
|
||||
@@ -428,6 +339,7 @@ const TestChat: FC<TestChatProps> = ({
|
||||
return newList
|
||||
})
|
||||
}
|
||||
|
||||
const updateWorkflowNodeEndMessage = (data: NodeData) => {
|
||||
const { node_id, input, output, error, elapsed_time, status } = data;
|
||||
setChatList(prev => {
|
||||
@@ -456,10 +368,10 @@ const TestChat: FC<TestChatProps> = ({
|
||||
return newList
|
||||
})
|
||||
}
|
||||
|
||||
const updateWorkflowCycleMessage = (data: NodeData) => {
|
||||
const { node_id, cycle_id, cycle_idx, input, output, error, elapsed_time, status } = data;
|
||||
const { nodes } = config as WorkflowConfig
|
||||
|
||||
const node = nodes.find(n => n.id === node_id);
|
||||
const { name, type } = node || {}
|
||||
const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon
|
||||
@@ -500,22 +412,9 @@ const TestChat: FC<TestChatProps> = ({
|
||||
return newList
|
||||
})
|
||||
}
|
||||
|
||||
const updateWorkflowEndMessage = (data: NodeData) => {
|
||||
const { error, status } = data as {
|
||||
content: string;
|
||||
conversation_id: string | null;
|
||||
cycle_id: string;
|
||||
cycle_idx: number;
|
||||
node_id: string;
|
||||
node_name?: string;
|
||||
node_type?: string;
|
||||
input?: any;
|
||||
output?: any;
|
||||
elapsed_time?: string;
|
||||
error?: any;
|
||||
state: Record<string, any>;
|
||||
status?: 'completed' | 'failed'
|
||||
};
|
||||
const { error, status, audio_url } = data;
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
@@ -525,13 +424,13 @@ const TestChat: FC<TestChatProps> = ({
|
||||
status,
|
||||
error,
|
||||
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content,
|
||||
audioUrl: audio_url
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
}
|
||||
|
||||
console.log('queryValues', queryValues)
|
||||
return (
|
||||
<div className="rb:w-250 rb:p-3 rb:mx-auto">
|
||||
<RbCard
|
||||
@@ -543,97 +442,29 @@ const TestChat: FC<TestChatProps> = ({
|
||||
<Chat
|
||||
empty={<Empty url={ChatIcon} title={t('application.testChatEmpty')} isNeedSubTitle={false} size={[240, 200]} />}
|
||||
contentClassName={clsx(`rb:mx-[16px] rb:pt-[24px]`, {
|
||||
'rb:h-[calc(100%-140px)]': !queryValues?.files?.length,
|
||||
'rb:h-[calc(100%-208px)]': !!queryValues?.files?.length,
|
||||
'rb:h-[calc(100%-140px)]': !fileList.length,
|
||||
'rb:h-[calc(100%-208px)]': !!fileList.length,
|
||||
})}
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setMessage}
|
||||
onSend={application?.type === 'workflow' ? handleWorkflowSend : handleSend}
|
||||
fileList={queryValues?.files || []}
|
||||
fileChange={updateFileList}
|
||||
fileList={fileList}
|
||||
fileChange={(list) => {
|
||||
setFileList(list || [])
|
||||
toolbarRef.current?.setFiles(list || [])
|
||||
}}
|
||||
labelFormat={(item) => item.role === 'user' ? t('application.you') : dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
renderRuntime={application?.type === 'workflow' ? (item, index) => {
|
||||
return <Runtime item={item} index={index} />
|
||||
} : undefined}
|
||||
renderRuntime={application?.type === 'workflow' ? (item, index) => <Runtime item={item} index={index} /> : undefined}
|
||||
>
|
||||
<Form form={form}>
|
||||
<Flex justify="space-between" className="rb:flex-1">
|
||||
<Space size={8} align="center">
|
||||
<Form.Item name="files" noStyle>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
|
||||
{
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
onChange={fileChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
],
|
||||
onClick: handleShowUpload
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="center" className="rb:size-7 rb:cursor-pointer rb:rounded-[14px] rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6]">
|
||||
<div
|
||||
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')]"
|
||||
></div>
|
||||
</Flex>
|
||||
</Dropdown>
|
||||
</Form.Item>
|
||||
{/* <Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Tooltip title={t(`memoryConversation.memory`)}></Tooltip>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
cicle={true}
|
||||
>
|
||||
</ButtonCheckbox>
|
||||
</Form.Item> */}
|
||||
<Form.Item name="variables" className="rb:mb-0!" hidden={!queryValues?.variables?.length}>
|
||||
<div
|
||||
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
|
||||
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
|
||||
})}
|
||||
onClick={handleEditVariables}
|
||||
>
|
||||
<SettingOutlined className="rb:mr-1" />
|
||||
{t(`memoryConversation.variableConfig`)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space size={8} align="center">
|
||||
<AudioRecorder
|
||||
onRecordingComplete={handleRecordingComplete}
|
||||
/>
|
||||
<Divider type="vertical" className="rb:ml-0! rb:mr-2!" />
|
||||
</Space>
|
||||
</Flex>
|
||||
</Form>
|
||||
<ChatToolbar
|
||||
ref={toolbarRef}
|
||||
features={features}
|
||||
onFilesChange={setFileList}
|
||||
/>
|
||||
</Chat>
|
||||
|
||||
<VariableConfigModal
|
||||
ref={variableConfigModalRef}
|
||||
refresh={handleSave}
|
||||
/>
|
||||
|
||||
<UploadFileListModal
|
||||
ref={uploadFileListModalRef}
|
||||
refresh={addFileList}
|
||||
/>
|
||||
</RbCard>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:39
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 15:20:32
|
||||
* @Last Modified time: 2026-03-17 15:27:57
|
||||
*/
|
||||
/**
|
||||
* Chat debugging component for application testing
|
||||
@@ -12,25 +12,25 @@
|
||||
|
||||
import { type FC, useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom'
|
||||
import clsx from 'clsx'
|
||||
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd';
|
||||
import { App } from 'antd';
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
|
||||
import type { ChatData, Config } from '../types'
|
||||
import type { ChatData, Config, FeaturesConfigForm } from '../types'
|
||||
import { runCompare, draftRun } from '@/api/application'
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import ChatInput from '@/components/Chat/ChatInput'
|
||||
import UploadFiles from '@/views/Conversation/components/FileUpload'
|
||||
import AudioRecorder from '@/components/AudioRecorder'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import ChatToolbar from '@/components/Chat/ChatToolbar'
|
||||
import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
||||
import type { Variable } from './VariableList/types'
|
||||
|
||||
|
||||
/**
|
||||
* Component props
|
||||
*/
|
||||
@@ -45,10 +45,12 @@ interface ChatProps {
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
/** Source type: multi-agent cluster or single agent */
|
||||
source?: 'multi_agent' | 'agent';
|
||||
chatVariables?: Variable[]; // Add chatVariables prop
|
||||
/** chatVariables prop */
|
||||
chatVariables?: Variable[];
|
||||
handleEditVariables?: () => void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Chat debugging component
|
||||
* Allows testing application with different model configurations side-by-side
|
||||
@@ -58,18 +60,29 @@ const Chat: FC<ChatProps> = ({
|
||||
handleEditVariables
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [compareLoading, setCompareLoading] = useState(false)
|
||||
const [fileList, setFileList] = useState<any[]>([])
|
||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
||||
|
||||
useEffect(() => {
|
||||
setCompareLoading(false)
|
||||
setLoading(false)
|
||||
}, [chatList.map(item => item.label).join(',')])
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.features) setFeatures(data.features)
|
||||
}, [data?.features])
|
||||
|
||||
useEffect(() => {
|
||||
setIsCluster(source === 'multi_agent')
|
||||
setFileList([])
|
||||
toolbarRef.current?.setFiles([])
|
||||
setMessage(undefined)
|
||||
}, [source])
|
||||
|
||||
@@ -111,8 +124,8 @@ const Chat: FC<ChatProps> = ({
|
||||
}
|
||||
}
|
||||
/** Update assistant message with streaming content */
|
||||
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
|
||||
if (!content || !model_config_id) return
|
||||
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string, audio_url?: string) => {
|
||||
if ((!content && !audio_url) || !model_config_id) return
|
||||
updateChatList(prev => {
|
||||
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
@@ -123,12 +136,13 @@ const Chat: FC<ChatProps> = ({
|
||||
if (lastMsg && lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
conversation_id: conversation_id,
|
||||
conversation_id,
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
content: lastMsg.content + (content || ''),
|
||||
audioUrl: audio_url
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -146,8 +160,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
|
||||
if (targetIndex > -1) {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[targetIndex]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const curChatMsgList = modelChatList[targetIndex].list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
@@ -169,13 +182,14 @@ const Chat: FC<ChatProps> = ({
|
||||
}
|
||||
/** Send message for agent comparison mode */
|
||||
const handleSend = (msg?: string) => {
|
||||
if (loading) return
|
||||
if (loading || !id) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = msg
|
||||
if (!message?.trim()) return
|
||||
const files = toolbarRef.current?.getFiles() || []
|
||||
// Validate required variables before sending
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
@@ -200,8 +214,9 @@ const Chat: FC<ChatProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
addUserMessage(message, fileList)
|
||||
addUserMessage(message, files)
|
||||
setMessage(message)
|
||||
toolbarRef.current?.setFiles([])
|
||||
setFileList([])
|
||||
addAssistantMessage()
|
||||
|
||||
@@ -209,13 +224,16 @@ const Chat: FC<ChatProps> = ({
|
||||
setCompareLoading(false)
|
||||
|
||||
data.map(item => {
|
||||
const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number };
|
||||
|
||||
const { model_config_id, conversation_id, content, message_length, audio_url } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number; audio_url: string };
|
||||
|
||||
switch (item.event) {
|
||||
case 'model_message':
|
||||
updateAssistantMessage(content, model_config_id, conversation_id)
|
||||
updateAssistantMessage(content, model_config_id, conversation_id, audio_url)
|
||||
break;
|
||||
case 'model_end':
|
||||
if (audio_url) {
|
||||
updateAssistantMessage(content, model_config_id, conversation_id, audio_url)
|
||||
}
|
||||
updateErrorAssistantMessage(message_length, model_config_id)
|
||||
break;
|
||||
case 'compare_end':
|
||||
@@ -226,9 +244,9 @@ const Chat: FC<ChatProps> = ({
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
runCompare(data.app_id, {
|
||||
runCompare(id, {
|
||||
message,
|
||||
files: fileList.map(file => {
|
||||
files: files.map(file => {
|
||||
if (file.url) {
|
||||
return file
|
||||
} else {
|
||||
@@ -246,9 +264,9 @@ const Chat: FC<ChatProps> = ({
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: params,
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
parallel: true,
|
||||
stream: true,
|
||||
timeout: 60,
|
||||
}, handleStreamMessage)
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
@@ -272,7 +290,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
created_at: Date.now()
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
@@ -284,8 +302,7 @@ const Chat: FC<ChatProps> = ({
|
||||
if (!content) return
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const curChatMsgList = modelChatList[0].list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
@@ -305,11 +322,9 @@ const Chat: FC<ChatProps> = ({
|
||||
/** Update cluster message when error occurs */
|
||||
const updateClusterErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const curChatMsgList = modelChatList[0].list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
@@ -326,17 +341,19 @@ const Chat: FC<ChatProps> = ({
|
||||
return [...modelChatList]
|
||||
})
|
||||
}
|
||||
/** Send message for cluster mode */
|
||||
|
||||
const handleClusterSend = (msg?: string) => {
|
||||
if (loading) return
|
||||
if (loading || !id) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = msg
|
||||
if (!message || message.trim() === '') return
|
||||
addUserMessage(message, fileList)
|
||||
const files = toolbarRef.current?.getFiles() || []
|
||||
addUserMessage(message, files)
|
||||
setMessage(undefined)
|
||||
toolbarRef.current?.setFiles([])
|
||||
setFileList([])
|
||||
addClusterAssistantMessage()
|
||||
|
||||
@@ -345,7 +362,7 @@ const Chat: FC<ChatProps> = ({
|
||||
|
||||
data.map(item => {
|
||||
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
|
||||
|
||||
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
@@ -369,13 +386,12 @@ const Chat: FC<ChatProps> = ({
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
draftRun(
|
||||
data.app_id,
|
||||
draftRun(id,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
stream: true,
|
||||
files: fileList.map(file => {
|
||||
files: files.map(file => {
|
||||
if (file.url) {
|
||||
return file
|
||||
} else {
|
||||
@@ -410,36 +426,6 @@ const Chat: FC<ChatProps> = ({
|
||||
const handleDelete = (index: number) => {
|
||||
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
|
||||
}
|
||||
const handleMessageChange = (message: string) => {
|
||||
setMessage(message)
|
||||
}
|
||||
const fileChange = (file?: any) => {
|
||||
setFileList([...fileList, file])
|
||||
}
|
||||
const handleRecordingComplete = async (file: any) => {
|
||||
setFileList([...fileList, {
|
||||
uid: file.file_id,
|
||||
response: { data: file },
|
||||
thumbUrl: file.url,
|
||||
type: file.type
|
||||
}])
|
||||
}
|
||||
|
||||
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'define':
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
break
|
||||
}
|
||||
}
|
||||
const addFileList = (list?: any[]) => {
|
||||
if (!list || list.length <= 0) return
|
||||
setFileList([...fileList, ...(list || [])])
|
||||
}
|
||||
const updateFileList = (list?: any[]) => {
|
||||
setFileList([...list || []])
|
||||
}
|
||||
const isNeedVariableConfig = chatVariables?.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
|
||||
|
||||
return (
|
||||
<div className="rb:relative rb:h-full rb:flex rb:flex-col">
|
||||
@@ -458,13 +444,10 @@ const Chat: FC<ChatProps> = ({
|
||||
"rb:border-r rb:border-[#DFE4ED]": index !== chatList.length - 1 && chatList.length > 1,
|
||||
})}>
|
||||
{chat.label &&
|
||||
<div className={clsx(
|
||||
"rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]",
|
||||
{
|
||||
'rb:rounded-tr-xl': index === chatList.length - 1,
|
||||
'rb:rounded-tl-xl': index === 0,
|
||||
}
|
||||
)}>
|
||||
<div className={clsx("rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]", {
|
||||
'rb:rounded-tr-xl': index === chatList.length - 1,
|
||||
'rb:rounded-tl-xl': index === 0,
|
||||
})}>
|
||||
<div className='rb:relative rb:p-[10px_12px] rb:overflow-hidden'>
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
|
||||
<div
|
||||
@@ -501,59 +484,37 @@ const Chat: FC<ChatProps> = ({
|
||||
message={message}
|
||||
className="rb:relative!"
|
||||
loading={loading}
|
||||
fileChange={updateFileList}
|
||||
fileChange={(list) => {
|
||||
setFileList(list || [])
|
||||
toolbarRef.current?.setFiles(list || [])
|
||||
}}
|
||||
fileList={fileList}
|
||||
onSend={isCluster ? handleClusterSend : handleSend}
|
||||
onChange={handleMessageChange}
|
||||
onChange={setMessage}
|
||||
>
|
||||
<Flex justify="space-between" className="rb:flex-1">
|
||||
<Flex gap={8} align="center">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
|
||||
{
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
onChange={fileChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
],
|
||||
onClick: handleShowUpload
|
||||
}}
|
||||
>
|
||||
<ChatToolbar
|
||||
ref={toolbarRef}
|
||||
features={features}
|
||||
onFilesChange={setFileList}
|
||||
extra={
|
||||
chatVariables && chatVariables.length > 0 ? (
|
||||
<div
|
||||
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]"
|
||||
></div>
|
||||
</Dropdown>
|
||||
{chatVariables && chatVariables.length > 0 && (
|
||||
<div
|
||||
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
|
||||
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
|
||||
className={clsx('rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]', {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': chatVariables.some(vo => vo.required && !vo.value),
|
||||
'rb:border-[#DFE4ED]': !chatVariables.some(vo => vo.required && !vo.value),
|
||||
})}
|
||||
onClick={handleEditVariables}
|
||||
>
|
||||
<SettingOutlined className="rb:mr-1" />
|
||||
{t(`memoryConversation.variableConfig`)}
|
||||
{t('memoryConversation.variableConfig')}
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</ChatInput>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
<UploadFileListModal
|
||||
ref={uploadFileListModalRef}
|
||||
refresh={addFileList}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 15:58:10
|
||||
* @Last Modified time: 2026-03-16 17:04:56
|
||||
*/
|
||||
import { type FC, useRef, useMemo, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -18,10 +18,10 @@ import exportIcon from '@/assets/images/export_hover.svg'
|
||||
import deleteIcon from '@/assets/images/delete_hover.svg'
|
||||
import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types';
|
||||
import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal'
|
||||
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FunConfigForm } from '../types'
|
||||
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FeaturesConfigForm } from '../types'
|
||||
import { deleteApplication, appExport } from '@/api/application'
|
||||
import CopyModal from './CopyModal'
|
||||
import FunConfig from './FunConfig'
|
||||
import FeaturesConfig from './FeaturesConfig'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
@@ -173,11 +173,11 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
return items
|
||||
}, [t, handleClick, application])
|
||||
|
||||
const funConfig = useMemo(() => {
|
||||
return (appRef?.current?.funConfig || { file_type: [] }) as FunConfigForm
|
||||
const features = useMemo(() => {
|
||||
return (appRef?.current?.features || { file_type: [] }) as FeaturesConfigForm
|
||||
}, [appRef])
|
||||
const handleSaveFunConfig = useCallback((value: FunConfigForm) => {
|
||||
appRef?.current?.handleSaveFunConfig?.(value)
|
||||
const handleSaveFeaturesConfig = useCallback((value: FeaturesConfigForm) => {
|
||||
appRef?.current?.handleSaveFeaturesConfig?.(value)
|
||||
}, [appRef])
|
||||
|
||||
console.log('formatMenuItems', formatMenuItems)
|
||||
@@ -211,7 +211,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
</div>
|
||||
{application?.type === 'workflow'
|
||||
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
||||
{/* <FunConfig value={funConfig} refresh={handleSaveFunConfig} /> */}
|
||||
<FeaturesConfig value={features} refresh={handleSaveFeaturesConfig} />
|
||||
<Button onClick={clear}>{t('workflow.clear')}</Button>
|
||||
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
|
||||
<Button onClick={run}>{t('workflow.run')}</Button>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 18:31:58
|
||||
*/
|
||||
/**
|
||||
* Copy Application Modal
|
||||
* Allows users to duplicate an existing application with a new name
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
|
||||
import { Form, Button, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
import FileUploadSettingModal from './FileUploadSettingModal'
|
||||
|
||||
interface FeaturesConfigModalProps {
|
||||
refresh: (value: FeaturesConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for copying applications
|
||||
*/
|
||||
const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<FeaturesConfigForm>();
|
||||
const values = Form.useWatch([], form)
|
||||
const fileUploadSettingModalRef = useRef<any>(null)
|
||||
|
||||
/** Close modal and reset form */
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
/** Open modal */
|
||||
const handleOpen = (initValue: FeaturesConfigForm) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue(initValue)
|
||||
};
|
||||
/** Copy application with new name */
|
||||
const handleSave = () => {
|
||||
setVisible(false);
|
||||
refresh(form.getFieldsValue())
|
||||
}
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
fileUploadSettingModalRef.current?.handleOpen(values?.file_upload)
|
||||
}
|
||||
|
||||
const handleSaveSettings = (settings: FeaturesConfigForm['file_upload']) => {
|
||||
form.setFieldValue('file_upload', { ...settings, enabled: values?.file_upload?.enabled ?? false })
|
||||
}
|
||||
|
||||
/** Expose methods to parent component */
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
console.log('settings values', values)
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.features')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.confirm')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Flex vertical gap={12}>
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t(`memoryConversation.web_search`)}
|
||||
name={['web_search', "enabled"]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t('application.text_to_speech')}
|
||||
name={['text_to_speech', "enabled"]}
|
||||
desc={t('application.text_to_speech_desc')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t('application.file_upload')}
|
||||
name={['file_upload', "enabled"]}
|
||||
desc={values?.file_upload?.enabled ? undefined : t('application.file_upload_desc')}
|
||||
/>
|
||||
{values?.file_upload?.enabled && (() => {
|
||||
const fu = values.file_upload
|
||||
const types = [
|
||||
{ type: 'image', enabled: fu.image_enabled, maxSize: fu.image_max_size_mb },
|
||||
{ type: 'audio', enabled: fu.audio_enabled, maxSize: fu.audio_max_size_mb },
|
||||
{ type: 'document', enabled: fu.document_enabled, maxSize: fu.document_max_size_mb },
|
||||
{ type: 'video', enabled: fu.video_enabled, maxSize: fu.video_max_size_mb },
|
||||
].filter(item => item.enabled)
|
||||
return types.length > 0 ? <>
|
||||
<Flex gap={12} className="rb:py-2!">
|
||||
<div className="rb:flex-1 rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:bg-white rb:text-[12px]">
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-2 rb:text-[12px] rb:text-[#5B6167] rb:border-b rb:border-b-[#DFE4ED]">
|
||||
<div className="rb:px-3 rb:py-1">{t(`application.supportedTypes`)}</div>
|
||||
<div className="rb:px-3 rb:py-1">{t('application.singleMaxSize')}</div>
|
||||
</div>
|
||||
{types.map((item, index) => (
|
||||
<div key={item.type} className={clsx('rb:grid rb:grid-cols-2 rb:gap-2', {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': index !== types.length - 1
|
||||
})}>
|
||||
<div className="rb:px-3 rb:py-1">{t(`application.${item.type}`)}</div>
|
||||
<div className="rb:px-3 rb:py-1">{item.maxSize} MB</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:py-1">{t('application.maxCount')}</div>
|
||||
{fu.max_file_count} {t('application.unix')}
|
||||
</div>
|
||||
</Flex>
|
||||
<Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
|
||||
</> : <Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
|
||||
})()}
|
||||
<Form.Item name="file_upload" hidden />
|
||||
</div>
|
||||
</Flex>
|
||||
</Form>
|
||||
</RbModal>
|
||||
|
||||
<FileUploadSettingModal
|
||||
ref={fileUploadSettingModalRef}
|
||||
onSave={handleSaveSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default FeaturesConfigModal;
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 18:36:09
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import RbModal from '@/components/RbModal';
|
||||
import type { FeaturesConfigForm } from '../../types'
|
||||
|
||||
type FileUpload = Omit<FeaturesConfigForm['file_upload'], 'settings'>
|
||||
|
||||
interface FileUploadSettingModalRef {
|
||||
handleOpen: (values?: FileUpload) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
interface FileUploadSettingModalProps {
|
||||
onSave: (values: FileUpload) => void;
|
||||
}
|
||||
|
||||
const fileTypeOptions = [
|
||||
{
|
||||
type: 'document',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
|
||||
formats: 'TXT, MD, MDX, MARKDOWN, PDF, DOC, DOCX',
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
|
||||
formats: 'JPG, JPEG, PNG, GIF, WEBP, SVG',
|
||||
},
|
||||
{
|
||||
type: 'audio',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
|
||||
formats: 'MP3, M4A, WAV, AMR, MPGA',
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
|
||||
formats: 'MP4, MOV, MPEG, WEBM',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultValues: FileUpload = {
|
||||
enabled: false,
|
||||
image_enabled: false,
|
||||
image_max_size_mb: 20,
|
||||
image_allowed_extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
|
||||
audio_enabled: false,
|
||||
audio_max_size_mb: 50,
|
||||
audio_allowed_extensions: ['mp3', 'wav', 'm4a', 'ogg', 'flac'],
|
||||
document_enabled: false,
|
||||
document_max_size_mb: 100,
|
||||
document_allowed_extensions: ['pdf', 'docx', 'xlsx', 'txt', 'csv', 'json'],
|
||||
video_enabled: false,
|
||||
video_max_size_mb: 500,
|
||||
video_allowed_extensions: ['mp4', 'mov', 'avi', 'webm'],
|
||||
max_file_count: 5,
|
||||
allowed_transfer_methods: 'both'
|
||||
}
|
||||
|
||||
const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadSettingModalProps>(({
|
||||
onSave,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<FileUpload>();
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleOpen = (values?: FileUpload) => {
|
||||
setVisible(true);
|
||||
if (values) {
|
||||
const methods = values.allowed_transfer_methods
|
||||
const transferMethod = Array.isArray(methods)
|
||||
? methods.length === 2 ? 'both' : methods[0]
|
||||
: methods
|
||||
form.setFieldsValue({ ...values, allowed_transfer_methods: transferMethod as any })
|
||||
} else {
|
||||
form.setFieldsValue(defaultValues)
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const vals = await form.validateFields();
|
||||
const methodMap: Record<string, string[]> = {
|
||||
local_file: ['local_file'],
|
||||
remote_url: ['remote_url'],
|
||||
both: ['local_file', 'remote_url'],
|
||||
}
|
||||
onSave({ ...vals, allowed_transfer_methods: methodMap[vals.allowed_transfer_methods as unknown as string] ?? [] });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.settings')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSave}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={defaultValues}>
|
||||
<Form.Item
|
||||
label={t('application.uploadType')}
|
||||
name="allowed_transfer_methods"
|
||||
>
|
||||
<Radio.Group block buttonStyle="solid">
|
||||
<Radio.Button value="local_file">{t('application.local')}</Radio.Button>
|
||||
<Radio.Button value="remote_url">URL</Radio.Button>
|
||||
<Radio.Button value="both">{t('application.both')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
|
||||
<Form.Item label={t('application.maxCount')} name="max_file_count">
|
||||
<InputNumber min={1} max={100} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('application.supportedTypes')}>
|
||||
<Flex vertical gap={12}>
|
||||
{fileTypeOptions.map((option) => {
|
||||
const enabledKey = `${option.type}_enabled` as keyof FileUpload
|
||||
const sizeKey = `${option.type}_max_size_mb` as keyof FileUpload
|
||||
const isEnabled = values?.[enabledKey]
|
||||
return (
|
||||
<div
|
||||
key={option.type}
|
||||
className={clsx('rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3', {
|
||||
'rb:bg-[#f5f7fc]': isEnabled
|
||||
})}
|
||||
>
|
||||
<Row gutter={12}>
|
||||
<Col flex="36px" className="rb:self-center">{option.icon}</Col>
|
||||
<Col flex="1">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Flex vertical>
|
||||
<div className="rb:font-medium">{t(`application.${option.type}`)}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167]">{option.formats}</div>
|
||||
</Flex>
|
||||
<Form.Item name={enabledKey} valuePropName="checked" noStyle>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
{isEnabled && (
|
||||
<Flex align="center" gap={12} className="rb:mt-3! rb:pt-3! rb:border-t rb:border-[#DFE4ED]">
|
||||
<div>{t('application.singleMaxSize')}: </div>
|
||||
<Form.Item name={sizeKey} noStyle>
|
||||
<InputNumber min={1} max={500} suffix="MB" className="rb:flex-1" />
|
||||
</Form.Item>
|
||||
<Form.Item name={`${option.type}_allowed_extensions`} hidden />
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default FileUploadSettingModal;
|
||||
@@ -1,45 +1,44 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-13 17:20:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:20:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 18:31:43
|
||||
*/
|
||||
import { type FC, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
|
||||
import FunConfigModal from './FunConfigModal'
|
||||
import type { FunConfigModalRef, FunConfigForm } from '../../types'
|
||||
import FeaturesConfigModal from './FeaturesConfigModal'
|
||||
import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types'
|
||||
|
||||
/** Props for the FunConfig component */
|
||||
interface FunConfigProps {
|
||||
/** Props for the FeaturesConfig component */
|
||||
interface FeaturesConfigProps {
|
||||
/** Current feature configuration values */
|
||||
value: FunConfigForm;
|
||||
value: FeaturesConfigForm;
|
||||
/** Callback to propagate updated config back to the parent */
|
||||
refresh: (value: FunConfigForm) => void;
|
||||
refresh: (value: FeaturesConfigForm) => void;
|
||||
}
|
||||
|
||||
const FunConfig: FC<FunConfigProps> = ({
|
||||
const FeaturesConfig: FC<FeaturesConfigProps> = ({
|
||||
value,
|
||||
refresh
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// Ref used to imperatively open the config modal
|
||||
const funConfigModalRef = useRef<FunConfigModalRef>(null)
|
||||
const funConfigModalRef = useRef<FeaturesConfigModalRef>(null)
|
||||
|
||||
/** Open the feature config modal pre-populated with the current values */
|
||||
const handleFunConfig = () => {
|
||||
console.log('funConfig', value)
|
||||
const handleFeaturesConfig = () => {
|
||||
funConfigModalRef.current?.handleOpen(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Button that triggers the feature configuration modal */}
|
||||
<Button onClick={handleFunConfig}>{t('application.funConfig')}</Button>
|
||||
<Button onClick={handleFeaturesConfig}>{t('application.features')}</Button>
|
||||
|
||||
{/* Modal for editing feature settings; calls refresh on save */}
|
||||
<FunConfigModal
|
||||
<FeaturesConfigModal
|
||||
ref={funConfigModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
@@ -47,4 +46,4 @@ const FunConfig: FC<FunConfigProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default FunConfig
|
||||
export default FeaturesConfig
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-11 15:42:13
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Radio, InputNumber, Flex, Switch, Row, Col } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import RbModal from '@/components/RbModal';
|
||||
import type { FunConfigForm } from '../../types'
|
||||
|
||||
interface FileUploadSettingModalRef {
|
||||
handleOpen: (values?: FileUploadSettings) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
interface FileUploadSettings extends Omit<FunConfigForm, 'enabled'> {}
|
||||
|
||||
interface FileUploadSettingModalProps {
|
||||
onSave: (values: FileUploadSettings) => void;
|
||||
}
|
||||
|
||||
const fileTypeOptions = [
|
||||
{
|
||||
type: 'document',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
|
||||
formats: 'TXT, MD, MDX, MARKDOWN, PDF, DOC, DOCX',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
|
||||
formats: 'JPG, JPEG, PNG, GIF, WEBP, SVG',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
{
|
||||
type: 'audio',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
|
||||
formats: 'MP3, M4A, WAV, AMR, MPGA',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
|
||||
formats: 'MP4, MOV, MPEG, WEBM',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
];
|
||||
|
||||
const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadSettingModalProps>(({
|
||||
onSave,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleOpen = (values?: FileUploadSettings) => {
|
||||
setVisible(true);
|
||||
// if (values) {
|
||||
// form.setFieldsValue(values);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
onSave(values);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.settings')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSave}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
uploadType: 'both',
|
||||
fileTypes: fileTypeOptions.map(opt => ({
|
||||
type: opt.type,
|
||||
enabled: false,
|
||||
maxCount: opt.defaultMaxCount,
|
||||
maxSize: opt.defaultMaxSize
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('application.uploadType')}
|
||||
name="uploadType"
|
||||
>
|
||||
<Radio.Group block buttonStyle="solid">
|
||||
<Radio.Button value="local">{t('application.local')}</Radio.Button>
|
||||
<Radio.Button value="url">URL</Radio.Button>
|
||||
<Radio.Button value="both">{t('application.both')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
|
||||
<Form.Item
|
||||
name="maxCount"
|
||||
label={t('application.maxCount')}
|
||||
>
|
||||
<InputNumber min={1} max={100} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('application.supportedTypes')}>
|
||||
<Form.List name="fileTypes">
|
||||
{(fields) => (
|
||||
<Flex vertical gap={12}>
|
||||
{fields.map((field, index) => {
|
||||
const option = fileTypeOptions[index];
|
||||
const isEnabled = values?.fileTypes?.[index]?.enabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.key}
|
||||
className={clsx("rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3", {
|
||||
'rb:bg-[#f5f7fc]': isEnabled
|
||||
})}
|
||||
>
|
||||
<Row gutter={12}>
|
||||
<Col flex="36px" className="rb:self-center">
|
||||
{option.icon}
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Flex vertical>
|
||||
<div className="rb:font-medium">{t(`application.${option.type}`)}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167]">{option.formats}</div>
|
||||
</Flex>
|
||||
<Form.Item name={[field.name, 'enabled']} valuePropName="checked" noStyle>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
{isEnabled && (
|
||||
<Flex align="center" gap={12} className="rb:mt-3! rb:pt-3! rb:border-t rb:border-[#DFE4ED]">
|
||||
<div>{t('application.singleMaxSize')}: </div>
|
||||
<Form.Item name={[field.name, 'maxSize']} noStyle>
|
||||
<InputNumber min={1} max={500} suffix="MB" className="rb:flex-1" />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
)}
|
||||
<Form.Item name={[field.name, 'type']} hidden>
|
||||
<input type="hidden" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default FileUploadSettingModal;
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:20:30
|
||||
*/
|
||||
/**
|
||||
* Copy Application Modal
|
||||
* Allows users to duplicate an existing application with a new name
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
|
||||
import { Form, Button, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { FunConfigModalRef } from '../../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import type { FunConfigForm } from '../../types'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
import FileUploadSettingModal from './FileUploadSettingModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface FunConfigModalProps {
|
||||
refresh: (value: FunConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for copying applications
|
||||
*/
|
||||
const FunConfigModal = forwardRef<FunConfigModalRef, FunConfigModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<FunConfigForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const values = Form.useWatch([], form)
|
||||
const fileUploadSettingModalRef = useRef<any>(null)
|
||||
|
||||
/** Close modal and reset form */
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
/** Open modal */
|
||||
const handleOpen = (initValue: FunConfigForm) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue(initValue)
|
||||
};
|
||||
/** Copy application with new name */
|
||||
const handleSave = () => {
|
||||
setVisible(false);
|
||||
setLoading(true)
|
||||
const values = form.getFieldsValue()
|
||||
refresh(values)
|
||||
}
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
fileUploadSettingModalRef.current?.handleOpen(values)
|
||||
}
|
||||
|
||||
const handleSaveSettings = (settings: any) => {
|
||||
form.setFieldsValue(settings)
|
||||
}
|
||||
|
||||
/** Expose methods to parent component */
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.funConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.copy')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Flex vertical gap={12}>
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t(`memoryConversation.web_search`)}
|
||||
name={['web_search', "enabled"]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t('application.textTranfer')}
|
||||
name={['textTranfer', "enabled"]}
|
||||
desc={t('application.textTranferDesc')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t('application.fileUpload')}
|
||||
name={['fileUpload', "enabled"]}
|
||||
desc={values?.fileUpload?.enabled ? undefined : t('application.fileUploadDesc')}
|
||||
/>
|
||||
{values?.fileUpload?.enabled && values?.fileTypes?.length > 0 ? <>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-2 rb:text-[12px] rb:text-[#5B6167]">
|
||||
<div>{t(`application.supportedTypes`)}</div>
|
||||
<div>{t('application.maxCount')}</div>
|
||||
<div>{t('application.singleMaxSize')}</div>
|
||||
</div>
|
||||
{values?.fileTypes?.filter(item => item.enabled).map(item => (
|
||||
<div key={item.type} className="rb:grid rb:grid-cols-3 rb:gap-2">
|
||||
<div>{t(`application.${item.type}`)}</div>
|
||||
<div>{item.maxCount} {t('application.unix')}</div>
|
||||
<div>{item.maxSize} MB</div>
|
||||
</div>
|
||||
))}
|
||||
<Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
|
||||
</> : null}
|
||||
<FormItem name="fileTypes" noStyle hidden></FormItem>
|
||||
<FormItem name="uploadType" noStyle hidden></FormItem>
|
||||
</div>
|
||||
</Flex>
|
||||
</Form>
|
||||
</RbModal>
|
||||
|
||||
<FileUploadSettingModal
|
||||
ref={fileUploadSettingModalRef}
|
||||
onSave={handleSaveSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default FunConfigModal;
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:26:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:26:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-17 15:53:06
|
||||
*/
|
||||
/**
|
||||
* Tool List Component
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
import Empty from '@/components/Empty'
|
||||
import ToolModal from './ToolModal'
|
||||
import { getToolMethods, getToolDetail } from '@/api/tools'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
/**
|
||||
* Tool list management component
|
||||
@@ -42,23 +43,25 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
|
||||
getToolMethods(item.tool_id)
|
||||
])
|
||||
|
||||
console.log('toolDetail', toolDetail)
|
||||
switch ((toolDetail as any).tool_type) {
|
||||
case 'mcp':
|
||||
const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
|
||||
return {
|
||||
...item,
|
||||
is_active: (toolDetail as any).is_active,
|
||||
label: mcpFilterItem?.description,
|
||||
method_id: mcpFilterItem?.method_id,
|
||||
value: mcpFilterItem?.name,
|
||||
description: mcpFilterItem?.description,
|
||||
parameters: mcpFilterItem?.parameters
|
||||
}
|
||||
break
|
||||
case 'builtin':
|
||||
if ((methods as any[]).length > 1) {
|
||||
const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
|
||||
return {
|
||||
...item,
|
||||
is_active: (toolDetail as any).is_active,
|
||||
label: builtinFilterItem?.description,
|
||||
method_id: builtinFilterItem?.method_id,
|
||||
value: builtinFilterItem?.name,
|
||||
@@ -68,17 +71,18 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
is_active: (toolDetail as any).is_active,
|
||||
label: (methods as any[])[0]?.description,
|
||||
method_id: (methods as any[])[0]?.method_id,
|
||||
value: (methods as any[])[0]?.name,
|
||||
description: (methods as any[])[0]?.description,
|
||||
parameters: (methods as any[])[0]?.parameters
|
||||
}
|
||||
break
|
||||
default:
|
||||
const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation)
|
||||
return {
|
||||
...item,
|
||||
is_active: (toolDetail as any).is_active,
|
||||
label: customFilterItem?.name,
|
||||
method_id: customFilterItem?.method_id,
|
||||
value: customFilterItem?.name,
|
||||
@@ -127,6 +131,7 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
|
||||
setToolList([...list])
|
||||
onChange && onChange(list)
|
||||
}
|
||||
console.log('toolList', toolList)
|
||||
return (
|
||||
<Card
|
||||
title={t('application.toolConfiguration')}
|
||||
@@ -143,8 +148,13 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.label}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.label}
|
||||
</div>
|
||||
<Tag color={item.is_active ? 'success' : 'error'} className="rb:mt-1">
|
||||
{item.is_active ? t('common.enable') : t('common.deleted')}
|
||||
</Tag>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:26:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:26:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-17 15:50:48
|
||||
*/
|
||||
/**
|
||||
* Type definitions for tool configuration in application settings
|
||||
@@ -32,6 +32,7 @@ export interface ToolOption {
|
||||
tool_id?: string;
|
||||
/** Whether tool is enabled */
|
||||
enabled?: boolean;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:01:04
|
||||
* @Last Modified time: 2026-03-16 17:42:12
|
||||
*/
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
@@ -78,7 +78,7 @@ export interface Config extends MultiAgentConfig {
|
||||
updated_at: number;
|
||||
skills?: SkillConfigForm | null;
|
||||
|
||||
funConfig?: FunConfigForm;
|
||||
features?: FeaturesConfigForm;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,8 +129,8 @@ export interface AgentRef {
|
||||
* @param flag - Whether to show success message
|
||||
*/
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
funConfig: Config['funConfig'];
|
||||
handleSaveFunConfig?: (value: FunConfigForm) => void;
|
||||
features: Config['features'];
|
||||
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,8 +142,8 @@ export interface ClusterRef {
|
||||
* @param flag - Whether to show success message
|
||||
*/
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
funConfig: Config['funConfig'];
|
||||
handleSaveFunConfig?: (value: FunConfigForm) => void;
|
||||
features: Config['features'];
|
||||
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,8 +162,8 @@ export interface WorkflowRef {
|
||||
/** Add variable */
|
||||
addVariable: () => void;
|
||||
config: WorkflowConfig | null;
|
||||
funConfig: WorkflowConfig['funConfig'];
|
||||
handleSaveFunConfig?: (value: FunConfigForm) => void;
|
||||
features: WorkflowConfig['features'];
|
||||
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -416,17 +416,55 @@ export interface FileTypeConfig {
|
||||
maxCount: number;
|
||||
maxSize: number;
|
||||
}
|
||||
export interface FunConfigForm {
|
||||
enabled: boolean;
|
||||
fileTypes: FileTypeConfig[]
|
||||
uploadType: 'local' | 'url' | 'both';
|
||||
interface FileSetttings {
|
||||
image_enabled: boolean;
|
||||
image_max_size_mb: number;
|
||||
image_allowed_extensions: string[];
|
||||
audio_enabled: boolean;
|
||||
audio_max_size_mb: number;
|
||||
audio_allowed_extensions: string[];
|
||||
document_enabled: boolean;
|
||||
document_max_size_mb: number;
|
||||
document_allowed_extensions: string[];
|
||||
video_enabled: boolean;
|
||||
video_max_size_mb: number;
|
||||
video_allowed_extensions: string[];
|
||||
max_file_count: number;
|
||||
allowed_transfer_methods: string[] | string;
|
||||
}
|
||||
export type FeaturesConfigForm = {
|
||||
file_upload: FileSetttings & {
|
||||
enabled: boolean;
|
||||
settings?: FileSetttings
|
||||
};
|
||||
opening_statement: {
|
||||
enabled: boolean;
|
||||
statement: string | null;
|
||||
suggested_questions: string[];
|
||||
};
|
||||
suggested_questions_after_answer: {
|
||||
enabled: boolean;
|
||||
};
|
||||
text_to_speech: {
|
||||
enabled: boolean;
|
||||
voice: string | null;
|
||||
language: string | null;
|
||||
autoplay: boolean;
|
||||
};
|
||||
citation: {
|
||||
enabled: boolean;
|
||||
};
|
||||
web_search: {
|
||||
enabled: boolean;
|
||||
search_engine: string | null;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Function config modal ref methods
|
||||
*/
|
||||
export interface FunConfigModalRef {
|
||||
export interface FeaturesConfigModalRef {
|
||||
/** Open function config modal */
|
||||
handleOpen: (value: FunConfigForm) => void;
|
||||
handleOpen: (value: FeaturesConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:09:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-06 12:20:43
|
||||
* @Last Modified time: 2026-03-17 14:42:31
|
||||
*/
|
||||
/**
|
||||
* File Upload Component
|
||||
@@ -20,7 +20,7 @@
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import { Upload, Progress, App } from 'antd';
|
||||
import type { UploadProps, UploadFile } from 'antd';
|
||||
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
|
||||
@@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { request } from '@/utils/request'
|
||||
import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||
|
||||
interface UploadFilesProps extends Omit<UploadProps, 'onChange'> {
|
||||
/** Upload API endpoint */
|
||||
@@ -48,14 +49,14 @@ interface UploadFilesProps extends Omit<UploadProps, 'onChange'> {
|
||||
disabled?: boolean;
|
||||
/** File size limit in MB */
|
||||
fileSize?: number;
|
||||
/** Allowed file types ['doc', 'xls', 'ppt', 'pdf'] */
|
||||
fileType?: string[];
|
||||
/** Auto-upload on file selection, default is true */
|
||||
isAutoUpload?: boolean;
|
||||
/** Maximum number of files allowed */
|
||||
maxCount?: number;
|
||||
/** Custom file removal callback */
|
||||
onRemove?: (file: UploadFile) => boolean | void | Promise<boolean | void>;
|
||||
|
||||
featureConfig: FeaturesConfigForm['file_upload']
|
||||
}
|
||||
|
||||
const transform_file_type = {
|
||||
@@ -130,11 +131,11 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
onChange,
|
||||
disabled = false,
|
||||
fileSize = 5,
|
||||
fileType = Object.entries(ALL_FILE_TYPE).map(([key]) => key),
|
||||
isAutoUpload = true,
|
||||
maxCount = 1,
|
||||
onRemove: customOnRemove,
|
||||
requestConfig,
|
||||
featureConfig,
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -142,18 +143,37 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
const [fileList, setFileList] = useState<UploadFile[]>(propFileList);
|
||||
const [accept, setAccept] = useState<string | undefined>();
|
||||
|
||||
const fileType = useMemo(() => {
|
||||
let types: string[] = [];
|
||||
['image', 'document', 'video', 'audio'].forEach(type => {
|
||||
if (featureConfig[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']]) {
|
||||
types = types.concat(featureConfig[`${type}_allowed_extensions` as keyof FeaturesConfigForm['file_upload']] as string[])
|
||||
}
|
||||
})
|
||||
|
||||
return types
|
||||
}, [featureConfig])
|
||||
|
||||
/**
|
||||
* Validates file type and size before upload
|
||||
* @returns Upload.LIST_IGNORE to prevent upload, or true to proceed
|
||||
*/
|
||||
const beforeUpload: RcUploadProps['beforeUpload'] = (file) => {
|
||||
// Validate file size
|
||||
if (fileSize) {
|
||||
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
|
||||
if (!isLtMaxSize) {
|
||||
message.error(t('common.fileSizeTip', { size: fileSize }));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
// Determine file category and get max size from featureConfig
|
||||
const mimePrefix = file.type?.split('/')[0]
|
||||
const categoryMap: Record<string, keyof FeaturesConfigForm['file_upload']> = {
|
||||
image: 'image_max_size_mb',
|
||||
video: 'video_max_size_mb',
|
||||
audio: 'audio_max_size_mb',
|
||||
}
|
||||
const maxSizeKey = categoryMap[mimePrefix] ?? 'document_max_size_mb'
|
||||
const maxSize = (featureConfig[maxSizeKey] as number) ?? fileSize
|
||||
|
||||
const fileSizeMB = file.size / 1024 / 1024
|
||||
const isLtMaxSize = fileSizeMB < maxSize;
|
||||
if (!isLtMaxSize) {
|
||||
message.error(t('common.fileSizeTip', { size: maxSize }));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
// Validate file type
|
||||
if (fileType && fileType.length > 0) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:09:47
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 17:47:09
|
||||
* @Last Modified time: 2026-03-17 10:28:04
|
||||
*/
|
||||
/**
|
||||
* Upload File List Modal Component
|
||||
@@ -18,25 +18,28 @@
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
|
||||
import { Form, Input, Select, Button, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { UploadFileListModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface UploadFileListModalProps {
|
||||
/** Callback to refresh parent component with new file list */
|
||||
refresh: (fileList?: any[]) => void;
|
||||
featureConfig: FeaturesConfigForm['file_upload']
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for adding remote files via URL
|
||||
*/
|
||||
const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListModalProps>(({
|
||||
refresh
|
||||
refresh,
|
||||
featureConfig
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -79,6 +82,20 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
|
||||
handleOpen
|
||||
}));
|
||||
|
||||
const fileTypeOptions = useMemo(() => {
|
||||
const options = [];
|
||||
if (featureConfig?.image_enabled) {
|
||||
options.push({ label: t('memoryConversation.image'), value: 'image' });
|
||||
}
|
||||
if (featureConfig?.audio_enabled) {
|
||||
options.push({ label: t('memoryConversation.audio'), value: 'audio' });
|
||||
}
|
||||
if (featureConfig?.video_enabled) {
|
||||
options.push({ label: t('memoryConversation.video'), value: 'video' });
|
||||
}
|
||||
return options;
|
||||
}, [featureConfig, t])
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('memoryConversation.addRemoteFile')}
|
||||
@@ -103,11 +120,7 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
|
||||
>
|
||||
<Select
|
||||
placeholder={t('memoryConversation.fileType')}
|
||||
options={[
|
||||
{ label: t('memoryConversation.image'), value: 'image' },
|
||||
{ label: t('memoryConversation.audio'), value: 'audio' },
|
||||
{ label: t('memoryConversation.video'), value: 'video' },
|
||||
]}
|
||||
options={fileTypeOptions}
|
||||
className="rb:w-30"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:58:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 12:10:44
|
||||
* @Last Modified time: 2026-03-17 15:39:17
|
||||
*/
|
||||
/**
|
||||
* Conversation Page
|
||||
@@ -14,13 +14,12 @@ import { type FC, useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { Flex, Skeleton, Form, Dropdown, type MenuProps, App, Divider } from 'antd'
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
import { Flex, Skeleton, App } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken, getExperienceConfig } from '@/api/application'
|
||||
import type { HistoryItem, QueryParams, UploadFileListModalRef } from './types'
|
||||
import type { HistoryItem } from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import { randomString } from '@/utils/common'
|
||||
@@ -34,20 +33,14 @@ import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import UploadFiles from './components/FileUpload'
|
||||
import AudioRecorder from '@/components/AudioRecorder'
|
||||
import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||
import UploadFileListModal from './components/UploadFileListModal'
|
||||
import type { VariableConfigModalRef } from '@/views/Workflow/types'
|
||||
import ChatToolbar, { type ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
||||
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
|
||||
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal';
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||
|
||||
/**
|
||||
* Conversation component for shared applications
|
||||
*/
|
||||
const Conversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const { message: messageApi, modal } = App.useApp()
|
||||
const { token } = useParams()
|
||||
const location = useLocation()
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
@@ -63,35 +56,20 @@ const Conversation: FC = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
||||
const [fileList, setFileList] = useState<any[]>([])
|
||||
const [webSearch, setWebSearch] = useState(false)
|
||||
const [memory, setMemory] = useState(true)
|
||||
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
||||
|
||||
const [form] = Form.useForm<QueryParams>()
|
||||
const queryValues = Form.useWatch<QueryParams>([], form)
|
||||
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
|
||||
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
|
||||
const [variables, setVariables] = useState<Variable[]>([]) // Workflow input variables
|
||||
|
||||
/**
|
||||
* Opens the variable configuration modal
|
||||
*/
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen(variables)
|
||||
}
|
||||
/**
|
||||
* Saves updated variable values from the modal
|
||||
*/
|
||||
const handleSave = (values: Variable[]) => {
|
||||
setVariables([...values])
|
||||
}
|
||||
useEffect(() => {
|
||||
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
||||
setShareToken(shareToken)
|
||||
if (shareToken && shareToken !== '') return
|
||||
getShareToken(token as string, userId || randomString(12, false))
|
||||
.then(res => {
|
||||
const response = res as { access_token: string } || {}
|
||||
const response = res as { access_token: string } || {}
|
||||
localStorage.setItem(`shareToken_${token}`, response.access_token ?? '')
|
||||
setShareToken(response.access_token ?? '')
|
||||
})
|
||||
@@ -102,12 +80,14 @@ const Conversation: FC = () => {
|
||||
getHistory()
|
||||
}
|
||||
}, [token, shareToken, page, hasMore, historyList])
|
||||
|
||||
useEffect(() => {
|
||||
if (shareToken && token) {
|
||||
getExperienceConfig(token)
|
||||
.then(res => {
|
||||
const response = res as { variables: Variable[] }
|
||||
setVariables(response.variables || [])
|
||||
const response = res as { variables: Variable[]; features: FeaturesConfigForm }
|
||||
toolbarRef.current?.setVariables(response.variables || [])
|
||||
setFeatures(response.features)
|
||||
})
|
||||
} else {
|
||||
setChatList([])
|
||||
@@ -118,7 +98,7 @@ const Conversation: FC = () => {
|
||||
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => {
|
||||
return items.reduce((groups: Record<string, HistoryItem[]>, item) => {
|
||||
const date = formatDateTime(item.created_at, 'YYYY-MM-DD')
|
||||
|
||||
|
||||
if (!groups[date]) {
|
||||
groups[date] = [];
|
||||
}
|
||||
@@ -129,9 +109,7 @@ const Conversation: FC = () => {
|
||||
|
||||
/** Fetch conversation history with pagination */
|
||||
const getHistory = (flag: boolean = false) => {
|
||||
if (!token || (pageLoading || !hasMore) && !flag) {
|
||||
return
|
||||
}
|
||||
if (!token || (pageLoading || !hasMore) && !flag) return
|
||||
setPageLoading(true);
|
||||
getConversationHistory(token, { page: flag ? 1 : page, pagesize: 20 })
|
||||
.then(res => {
|
||||
@@ -154,19 +132,14 @@ const Conversation: FC = () => {
|
||||
setHasMore(response.page.hasnext);
|
||||
setLoading(false);
|
||||
})
|
||||
.finally(() => {
|
||||
setPageLoading(false);
|
||||
})
|
||||
.finally(() => setPageLoading(false))
|
||||
}
|
||||
/** Switch to different conversation or start new one */
|
||||
const handleChangeHistory = (id: string | null) => {
|
||||
if (id !== conversation_id) {
|
||||
setConversationId(id)
|
||||
}
|
||||
if (!id) {
|
||||
setMessage('')
|
||||
}
|
||||
if (id !== conversation_id) setConversationId(id)
|
||||
if (!id) setMessage('')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation_id) {
|
||||
getConversationDetail(token as string, conversation_id)
|
||||
@@ -179,43 +152,38 @@ const Conversation: FC = () => {
|
||||
}
|
||||
}, [conversation_id])
|
||||
|
||||
/** Add user message to chat */
|
||||
const addUserMessage = (message: string = '', files?: any[]) => {
|
||||
const newUserMessage: ChatItem = {
|
||||
setChatList(prev => [...prev, {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now(),
|
||||
files
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
}])
|
||||
}
|
||||
/** Add empty assistant message placeholder */
|
||||
|
||||
const addAssistantMessage = () => {
|
||||
const newAssistantMessage: ChatItem = {
|
||||
setChatList(prev => [...prev, {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}
|
||||
setChatList(prev => [...prev, newAssistantMessage])
|
||||
content: ''
|
||||
}])
|
||||
}
|
||||
/** Update assistant message with streaming content */
|
||||
const updateAssistantMessage = (content: string = '') => {
|
||||
if (!content) return
|
||||
if (streamLoading) {
|
||||
setStreamLoading(false)
|
||||
}
|
||||
|
||||
const updateAssistantMessage = (content: string = '', audio_url?: string) => {
|
||||
if (!content && !audio_url) return
|
||||
if (streamLoading) setStreamLoading(false)
|
||||
setChatList(prev => {
|
||||
const lastList = [...prev]
|
||||
const lastIndex = lastList.length - 1
|
||||
const lastMsg = lastList[lastIndex]
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
return [
|
||||
...lastList.slice(0, lastList.length - 1),
|
||||
...lastList.slice(0, lastIndex),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
content: lastMsg.content + content,
|
||||
audioUrl: audio_url
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -223,22 +191,17 @@ const Conversation: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const isNeedVariableConfig = variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
|
||||
|
||||
/** Send message and handle streaming response */
|
||||
const handleSend = () => {
|
||||
if (!token || !shareToken) {
|
||||
return
|
||||
}
|
||||
const { files = [], ...rest } = queryValues || {}
|
||||
// Validate required variables before sending
|
||||
if (!token || !shareToken) return
|
||||
const files = toolbarRef.current?.getFiles() || []
|
||||
const variables = toolbarRef.current?.getVariables() || []
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (variables.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
variables.forEach(vo => {
|
||||
params[vo.name] = vo.value ?? vo.defaultValue
|
||||
|
||||
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
|
||||
isCanSend = false
|
||||
needRequired.push(vo.name)
|
||||
@@ -249,33 +212,34 @@ const Conversation: FC = () => {
|
||||
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
|
||||
}
|
||||
}
|
||||
if (!isCanSend) {
|
||||
return
|
||||
}
|
||||
if (!isCanSend) return
|
||||
|
||||
setLoading(true)
|
||||
setStreamLoading(true)
|
||||
addUserMessage(message, files)
|
||||
addAssistantMessage()
|
||||
toolbarRef.current?.setFiles([])
|
||||
setFileList([])
|
||||
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.forEach((item) => {
|
||||
switch(item.event) {
|
||||
const { content, conversation_id: curId, audio_url } = item.data as { content: string; conversation_id: string; audio_url?: string; }
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
case 'node_start':
|
||||
const { conversation_id: newId } = item.data as { conversation_id: string }
|
||||
const { conversation_id: newId } = item.data as { conversation_id: string }
|
||||
currentConversationId = newId
|
||||
break
|
||||
case 'message':
|
||||
const { content, conversation_id: curId } = item.data as { content: string; conversation_id: string; }
|
||||
updateAssistantMessage(content)
|
||||
|
||||
if (curId) {
|
||||
currentConversationId = curId;
|
||||
}
|
||||
updateAssistantMessage(content, audio_url)
|
||||
if (curId) currentConversationId = curId;
|
||||
break
|
||||
case 'end':
|
||||
case 'workflow_end':
|
||||
if (audio_url) {
|
||||
updateAssistantMessage(content, audio_url)
|
||||
}
|
||||
setLoading(false)
|
||||
if (currentConversationId && currentConversationId !== conversation_id) {
|
||||
setConversationId(currentConversationId)
|
||||
@@ -286,9 +250,9 @@ const Conversation: FC = () => {
|
||||
})
|
||||
};
|
||||
|
||||
form.setFieldValue('files', [])
|
||||
sendConversation({
|
||||
...rest,
|
||||
web_search: webSearch,
|
||||
memory,
|
||||
message: message || '',
|
||||
stream: true,
|
||||
conversation_id: conversation_id || null,
|
||||
@@ -315,32 +279,18 @@ const Conversation: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const fileChange = (file?: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), file])
|
||||
}
|
||||
const handleRecordingComplete = async (file: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), {
|
||||
uid: file.file_id,
|
||||
response: { data: file },
|
||||
thumbUrl: file.url,
|
||||
type: file.type
|
||||
}])
|
||||
}
|
||||
|
||||
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
|
||||
switch(key) {
|
||||
case 'define':
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
break
|
||||
}
|
||||
}
|
||||
const addFileList = (fileList?: any[]) => {
|
||||
if (!fileList || fileList.length <= 0) return
|
||||
form.setFieldValue('files', [...(queryValues.files || []), ...fileList])
|
||||
}
|
||||
const updateFileList = (fileList?: any[]) => {
|
||||
console.log('fileList', fileList)
|
||||
form.setFieldValue('files', [...(fileList || [])])
|
||||
const handleChangeMemory = (value: boolean) => {
|
||||
modal.confirm({
|
||||
title: value ? t('memoryConversation.memoryTipTitle') : t('memoryConversation.memoryCancelTipTitle'),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: () => {
|
||||
setMemory(value)
|
||||
},
|
||||
onCancel: () => {
|
||||
setMemory(!value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -349,8 +299,8 @@ const Conversation: FC = () => {
|
||||
<div className="rb:group rb:flex rb:items-center rb:justify-center rb:font-regular rb:cursor-pointer rb:mb-5 rb:border rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:text-[#155EEF] rb:rounded-lg rb:py-2.5"
|
||||
onClick={() => handleChangeHistory(null)}
|
||||
>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:mr-2 rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:mr-2 rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
||||
></div>
|
||||
{t('memoryConversation.startANewConversation')}
|
||||
</div>
|
||||
@@ -365,7 +315,6 @@ const Conversation: FC = () => {
|
||||
next={getHistory}
|
||||
hasMore={hasMore}
|
||||
loader={<Skeleton active />}
|
||||
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
|
||||
scrollableTarget="scrollableDiv"
|
||||
>
|
||||
{Object.entries(groupHistoryList).map(([date, items]) => (
|
||||
@@ -374,8 +323,8 @@ const Conversation: FC = () => {
|
||||
{items.map(item => (
|
||||
<div key={item.updated_at} className="rb:mb-3">
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === conversation_id,
|
||||
})}
|
||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === conversation_id,
|
||||
})}
|
||||
onClick={() => handleChangeHistory(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
@@ -391,109 +340,60 @@ const Conversation: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
|
||||
<div className='rb:w-190 rb:h-screen rb:mx-auto rb:pt-10'>
|
||||
<Chat
|
||||
empty={<Empty url={ChatEmpty} className="rb:h-full" size={[320,180]} title={t('memoryConversation.chatEmpty')} subTitle={t('memoryConversation.emptyDesc')} />}
|
||||
contentClassName={!queryValues?.files?.length ? "rb:h-[calc(100%-144px)]" : "rb:h-[calc(100%-208px)]"}
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setMessage}
|
||||
onSend={handleSend}
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
fileList={queryValues?.files || []}
|
||||
fileChange={updateFileList}
|
||||
>
|
||||
<Form form={form} initialValues={{ memory: false, web_search: false}}>
|
||||
<Flex justify="space-between" className="rb:flex-1">
|
||||
<Flex gap={8} align="center">
|
||||
<Form.Item name="files" noStyle>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
|
||||
{
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
action={shareFileUploadUrlWithoutApiPrefix}
|
||||
onChange={fileChange}
|
||||
requestConfig={{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${shareToken || ''}`,
|
||||
}}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
],
|
||||
onClick: handleShowUpload
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]"
|
||||
></div>
|
||||
</Dropdown>
|
||||
</Form.Item>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
|
||||
<div className='rb:w-190 rb:h-screen rb:mx-auto rb:pt-10'>
|
||||
<Chat
|
||||
empty={<Empty url={ChatEmpty} className="rb:h-full" size={[320, 180]} title={t('memoryConversation.chatEmpty')} subTitle={t('memoryConversation.emptyDesc')} />}
|
||||
contentClassName={!fileList.length ? "rb:h-[calc(100%-144px)]" : "rb:h-[calc(100%-208px)]"}
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setMessage}
|
||||
onSend={handleSend}
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
fileList={fileList}
|
||||
fileChange={(list) => {
|
||||
setFileList(list || [])
|
||||
toolbarRef.current?.setFiles(list || [])
|
||||
}}
|
||||
>
|
||||
<ChatToolbar
|
||||
ref={toolbarRef}
|
||||
features={features}
|
||||
onFilesChange={setFileList}
|
||||
uploadAction={shareFileUploadUrlWithoutApiPrefix}
|
||||
uploadRequestConfig={{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${shareToken || ''}`,
|
||||
}
|
||||
}}
|
||||
extra={
|
||||
<>
|
||||
{features.web_search?.enabled &&
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
checked={webSearch}
|
||||
onChange={setWebSearch}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
{t('memoryConversation.web_search')}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.memory`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
{variables.length > 0 && (
|
||||
<Form.Item name="variables" className="rb:mb-0!">
|
||||
<div
|
||||
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
|
||||
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
|
||||
})}
|
||||
onClick={handleEditVariables}
|
||||
>
|
||||
<SettingOutlined className="rb:mr-1" />
|
||||
{t(`memoryConversation.variableConfig`)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<AudioRecorder
|
||||
action={shareFileUploadUrlWithoutApiPrefix}
|
||||
requestConfig={{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${shareToken || ''}`,
|
||||
}
|
||||
}}
|
||||
onRecordingComplete={handleRecordingComplete}
|
||||
/>
|
||||
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Chat>
|
||||
}
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
checked={memory}
|
||||
onChange={handleChangeMemory}
|
||||
>
|
||||
{t('memoryConversation.memory')}
|
||||
</ButtonCheckbox>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Chat>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UploadFileListModal
|
||||
ref={uploadFileListModalRef}
|
||||
refresh={addFileList}
|
||||
/>
|
||||
<VariableConfigModal
|
||||
ref={variableConfigModalRef}
|
||||
refresh={handleSave}
|
||||
variables={variables}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default Conversation
|
||||
export default Conversation
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:10:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 18:51:48
|
||||
* @Last Modified time: 2026-03-17 15:05:21
|
||||
*/
|
||||
/**
|
||||
* Workflow Chat Component
|
||||
@@ -21,50 +21,56 @@
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { App, Space, Button, Flex, Dropdown, type MenuProps, Divider } from 'antd'
|
||||
import { App } from 'antd'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import VariableConfigModal from './VariableConfigModal'
|
||||
import { draftRun } from '@/api/application';
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import dayjs from 'dayjs'
|
||||
import type { ChatRef, VariableConfigModalRef, GraphRef } from '../../types'
|
||||
import type { ChatRef, GraphRef, WorkflowConfig } from '../../types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import type { Variable } from '../Properties/VariableList/types'
|
||||
import ChatInput from '@/components/Chat/ChatInput'
|
||||
import UploadFiles from '@/views/Conversation/components/FileUpload'
|
||||
import AudioRecorder from '@/components/AudioRecorder'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import ChatToolbar from '@/components/Chat/ChatToolbar'
|
||||
import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
||||
import Runtime from './Runtime';
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||
|
||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId, graphRef }, ref) => {
|
||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null }>(({ appId, graphRef, data }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
|
||||
// State management
|
||||
const [open, setOpen] = useState(false) // Drawer visibility
|
||||
const [loading, setLoading] = useState(false) // Send button loading state
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([]) // Chat message history
|
||||
const [variables, setVariables] = useState<Variable[]>([]) // Workflow input variables
|
||||
const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state
|
||||
const [conversationId, setConversationId] = useState<string | null>(null) // Current conversation ID
|
||||
const [fileList, setFileList] = useState<any[]>([]) // Uploaded files
|
||||
const [message, setMessage] = useState<string | undefined>(undefined) // Current input message
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||
const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => {
|
||||
(toolbarRef as React.MutableRefObject<ChatToolbarRef | null>).current = node
|
||||
}, [])
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [variables, setVariables] = useState<Variable[]>([])
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [fileList, setFileList] = useState<any[]>([])
|
||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
||||
|
||||
/**
|
||||
* Opens the chat drawer and loads workflow variables from the start node
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
setOpen(true)
|
||||
getVariables()
|
||||
if (data?.features) setFeatures(data.features)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open && graphRef.current && toolbarRef.current) {
|
||||
getVariables()
|
||||
}
|
||||
}, [open])
|
||||
/**
|
||||
* Extracts variables from the workflow's start node and merges with previous values
|
||||
*/
|
||||
@@ -84,7 +90,9 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
vo.value = lastVo.value
|
||||
}
|
||||
})
|
||||
setVariables(curVariables)
|
||||
console.log('curVariables', curVariables)
|
||||
setVariables([...curVariables])
|
||||
toolbarRef.current?.setVariables([...curVariables])
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -96,22 +104,12 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setVariables([])
|
||||
setConversationId(null)
|
||||
setMessage(undefined)
|
||||
toolbarRef.current?.setFiles([])
|
||||
toolbarRef.current?.setVariables([])
|
||||
setFileList([])
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
}
|
||||
/**
|
||||
* Opens the variable configuration modal
|
||||
*/
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen(variables)
|
||||
}
|
||||
/**
|
||||
* Saves updated variable values from the modal
|
||||
*/
|
||||
const handleSave = (values: Variable[]) => {
|
||||
setVariables([...values])
|
||||
}
|
||||
/**
|
||||
* Sends a message to execute the workflow
|
||||
*
|
||||
@@ -337,14 +335,16 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
})
|
||||
}
|
||||
|
||||
const files = toolbarRef.current?.getFiles() || []
|
||||
setMessage(undefined)
|
||||
toolbarRef.current?.setFiles([])
|
||||
setFileList([])
|
||||
const data = {
|
||||
message: message,
|
||||
variables: params,
|
||||
stream: true,
|
||||
conversation_id: conversationId,
|
||||
files: fileList.map(file => {
|
||||
files: files.map(file => {
|
||||
if (file.url) {
|
||||
return file
|
||||
} else {
|
||||
@@ -379,65 +379,20 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current input message
|
||||
*/
|
||||
const handleMessageChange = (message: string) => {
|
||||
setMessage(message)
|
||||
}
|
||||
/**
|
||||
* Handles file upload from local device
|
||||
*/
|
||||
const fileChange = (file?: any) => {
|
||||
setFileList([...fileList, file])
|
||||
}
|
||||
const handleRecordingComplete = async (file: any) => {
|
||||
setFileList([...fileList, {
|
||||
response: { data: file },
|
||||
thumbUrl: file.url,
|
||||
type: file.type
|
||||
}])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles dropdown menu actions for file upload
|
||||
*/
|
||||
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
|
||||
switch(key) {
|
||||
case 'define':
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
break
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds files from remote URL modal
|
||||
*/
|
||||
const addFileList = (list?: any[]) => {
|
||||
if (!list || list.length <= 0) return
|
||||
setFileList([...fileList, ...(list || [])])
|
||||
}
|
||||
/**
|
||||
* Updates the entire file list (used when removing files)
|
||||
*/
|
||||
const updateFileList = (list?: any[]) => {
|
||||
setFileList([...list || []])
|
||||
toolbarRef.current?.setFiles([...list || []])
|
||||
}
|
||||
|
||||
// Expose methods to parent component via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
console.log('fileList', fileList)
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<div className="rb:flex rb:items-center rb:gap-2.5">
|
||||
{t('workflow.run')}
|
||||
{variables.length > 0 && <Space>
|
||||
<Button size="small" onClick={handleEditVariables}>{t('application.variable')}</Button>
|
||||
</Space>}
|
||||
</div>}
|
||||
classNames={{
|
||||
body: 'rb:p-0!'
|
||||
@@ -466,48 +421,16 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
fileChange={updateFileList}
|
||||
fileList={fileList}
|
||||
onSend={handleSend}
|
||||
onChange={handleMessageChange}
|
||||
onChange={(msg) => setMessage(msg)}
|
||||
>
|
||||
<Flex justify="space-between" className="rb:flex-1">
|
||||
<Flex gap={8} align="center">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
|
||||
{
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
onChange={fileChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
],
|
||||
onClick: handleShowUpload
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]"
|
||||
></div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<ChatToolbar
|
||||
ref={toolbarCallbackRef}
|
||||
features={features}
|
||||
onFilesChange={setFileList}
|
||||
onVariablesChange={setVariables}
|
||||
/>
|
||||
</ChatInput>
|
||||
</div>
|
||||
|
||||
<VariableConfigModal
|
||||
ref={variableConfigModalRef}
|
||||
refresh={handleSave}
|
||||
variables={variables}
|
||||
/>
|
||||
|
||||
<UploadFileListModal
|
||||
ref={uploadFileListModalRef}
|
||||
refresh={addFileList}
|
||||
/>
|
||||
</RbDrawer>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
graphRef,
|
||||
addVariable,
|
||||
config,
|
||||
funConfig: config?.funConfig
|
||||
features: config?.features
|
||||
}))
|
||||
return (
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
||||
@@ -115,6 +115,7 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
/>
|
||||
<Chat
|
||||
ref={chatRef}
|
||||
data={config}
|
||||
graphRef={graphRef}
|
||||
appId={config?.app_id as string}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Graph } from '@antv/x6';
|
||||
import type { KnowledgeConfig } from './components/Properties/Knowledge/types'
|
||||
import type { Variable } from './components/Properties/VariableList/types'
|
||||
import type { FunConfigForm } from '@/views/ApplicationConfig/types'
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
||||
export interface NodeConfig {
|
||||
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string;
|
||||
placeholder?: string;
|
||||
@@ -91,7 +91,7 @@ export interface WorkflowConfig {
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
|
||||
funConfig?: FunConfigForm;
|
||||
features?: FeaturesConfigForm;
|
||||
}
|
||||
|
||||
export interface ChatRef {
|
||||
|
||||
Reference in New Issue
Block a user