feat(web): chat add variables
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 13:59:45
|
* @Date: 2026-02-03 13:59:45
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-28 16:34:15
|
* @Last Modified time: 2026-03-03 12:08:42
|
||||||
*/
|
*/
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
||||||
@@ -120,15 +120,19 @@ export const copyApplication = (app_id: string, new_name: string) => {
|
|||||||
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
||||||
return request.get(`/apps/${app_id}/statistics`, data)
|
return request.get(`/apps/${app_id}/statistics`, data)
|
||||||
}
|
}
|
||||||
// 导出工作流
|
// Upload workflow and analyze compatibility
|
||||||
export const exportWorkflow = (app_id: string, fileName: string) => {
|
|
||||||
return request.downloadFile(`/apps/${app_id}/workflow/export`, fileName, undefined, undefined, 'GET')
|
|
||||||
}
|
|
||||||
// 工作流上传+兼容性分析
|
|
||||||
export const importWorkflow = (formData: FormData) => {
|
export const importWorkflow = (formData: FormData) => {
|
||||||
return request.uploadFile(`/apps/workflow/import`, formData)
|
return request.uploadFile(`/apps/workflow/import`, formData)
|
||||||
}
|
}
|
||||||
// 完成工作流导入
|
// Complete workflow import
|
||||||
export const completeImportWorkflow = (data: { temp_id: string; name?: string; description?: string }) => {
|
export const completeImportWorkflow = (data: { temp_id: string; name?: string; description?: string }) => {
|
||||||
return request.post(`/apps/workflow/import/save`, data)
|
return request.post(`/apps/workflow/import/save`, data)
|
||||||
}
|
}
|
||||||
|
// Get experience config
|
||||||
|
export const getExperienceConfig = (share_token: string) => {
|
||||||
|
return request.get(`/public/share/config`, {}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1684,7 +1684,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
fileType: 'File Type',
|
fileType: 'File Type',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
fileUrl: 'File URL',
|
fileUrl: 'File URL',
|
||||||
addRemoteFile: 'Add Remote File'
|
addRemoteFile: 'Add Remote File',
|
||||||
|
variableConfig: 'Variable Configuration',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: 'Red Bear Memory Science',
|
title: 'Red Bear Memory Science',
|
||||||
@@ -2192,7 +2193,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
save: 'Save',
|
save: 'Save',
|
||||||
export: 'Export',
|
export: 'Export',
|
||||||
variableConfig: 'Variable Configuration',
|
variableConfig: 'Variable Configuration',
|
||||||
variableRequired: 'Required',
|
variableRequired: 'Required, please configure variable values',
|
||||||
addMessage: 'Add Message',
|
addMessage: 'Add Message',
|
||||||
answerDesc: 'Reply',
|
answerDesc: 'Reply',
|
||||||
addNode: 'Add Node',
|
addNode: 'Add Node',
|
||||||
|
|||||||
@@ -1681,7 +1681,8 @@ export const zh = {
|
|||||||
fileType: '文件类型',
|
fileType: '文件类型',
|
||||||
image: '图片',
|
image: '图片',
|
||||||
fileUrl: '文件链接',
|
fileUrl: '文件链接',
|
||||||
addRemoteFile: '添加远程文件'
|
addRemoteFile: '添加远程文件',
|
||||||
|
variableConfig: '变量配置',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: '红熊记忆科学',
|
title: '红熊记忆科学',
|
||||||
@@ -2192,7 +2193,7 @@ export const zh = {
|
|||||||
save: '保存',
|
save: '保存',
|
||||||
export: '导出',
|
export: '导出',
|
||||||
variableConfig: '变量配置',
|
variableConfig: '变量配置',
|
||||||
variableRequired: '必填',
|
variableRequired: '必填,请配置变量值',
|
||||||
addMessage: '添加消息',
|
addMessage: '添加消息',
|
||||||
answerDesc: '回复',
|
answerDesc: '回复',
|
||||||
addNode: '添加节点',
|
addNode: '添加节点',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:58:03
|
* @Date: 2026-02-03 16:58:03
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-10 17:41:05
|
* @Last Modified time: 2026-03-03 13:46:22
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Conversation Page
|
* Conversation Page
|
||||||
@@ -14,11 +14,12 @@ import { type FC, useState, useEffect, useRef } from 'react'
|
|||||||
import { useParams, useLocation } from 'react-router-dom'
|
import { useParams, useLocation } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||||
import { Flex, Skeleton, Form, Dropdown, type MenuProps } from 'antd'
|
import { Flex, Skeleton, Form, Dropdown, type MenuProps, App } from 'antd'
|
||||||
|
import { SettingOutlined } from '@ant-design/icons'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
|
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken, getExperienceConfig } from '@/api/application'
|
||||||
import type { HistoryItem, QueryParams, UploadFileListModalRef } from './types'
|
import type { HistoryItem, QueryParams, UploadFileListModalRef } from './types'
|
||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import { formatDateTime } from '@/utils/format';
|
import { formatDateTime } from '@/utils/format';
|
||||||
@@ -37,12 +38,16 @@ import UploadFiles from './components/FileUpload'
|
|||||||
// import AudioRecorder from '@/components/AudioRecorder'
|
// import AudioRecorder from '@/components/AudioRecorder'
|
||||||
import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||||
import UploadFileListModal from './components/UploadFileListModal'
|
import UploadFileListModal from './components/UploadFileListModal'
|
||||||
|
import type { VariableConfigModalRef } from '@/views/Workflow/types'
|
||||||
|
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
|
||||||
|
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conversation component for shared applications
|
* Conversation component for shared applications
|
||||||
*/
|
*/
|
||||||
const Conversation: FC = () => {
|
const Conversation: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { message: messageApi } = App.useApp()
|
||||||
const { token } = useParams()
|
const { token } = useParams()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const searchParams = new URLSearchParams(location.search)
|
const searchParams = new URLSearchParams(location.search)
|
||||||
@@ -64,6 +69,22 @@ const Conversation: FC = () => {
|
|||||||
const queryValues = Form.useWatch<QueryParams>([], form)
|
const queryValues = Form.useWatch<QueryParams>([], form)
|
||||||
|
|
||||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
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(() => {
|
useEffect(() => {
|
||||||
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
||||||
setShareToken(shareToken)
|
setShareToken(shareToken)
|
||||||
@@ -81,6 +102,17 @@ const Conversation: FC = () => {
|
|||||||
getHistory()
|
getHistory()
|
||||||
}
|
}
|
||||||
}, [token, shareToken, page, hasMore, historyList])
|
}, [token, shareToken, page, hasMore, historyList])
|
||||||
|
useEffect(() => {
|
||||||
|
if (shareToken && token) {
|
||||||
|
getExperienceConfig(token)
|
||||||
|
.then(res => {
|
||||||
|
const response = res as { variables: Variable[] }
|
||||||
|
setVariables(response.variables || [])
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setChatList([])
|
||||||
|
}
|
||||||
|
}, [shareToken, token])
|
||||||
|
|
||||||
/** Group conversation history by date */
|
/** Group conversation history by date */
|
||||||
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => {
|
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => {
|
||||||
@@ -191,12 +223,35 @@ const Conversation: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNeedVariableConfig = variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
|
||||||
|
|
||||||
/** Send message and handle streaming response */
|
/** Send message and handle streaming response */
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (!token || !shareToken) {
|
if (!token || !shareToken) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { files = [], ...rest } = queryValues || {}
|
const { files = [], ...rest } = queryValues || {}
|
||||||
|
// Validate required variables before sending
|
||||||
|
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
|
||||||
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setStreamLoading(true)
|
setStreamLoading(true)
|
||||||
addUserMessage(message, files)
|
addUserMessage(message, files)
|
||||||
@@ -247,7 +302,8 @@ const Conversation: FC = () => {
|
|||||||
upload_file_id: file.response.data.file_id
|
upload_file_id: file.response.data.file_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
variables: params
|
||||||
}, handleStreamMessage, shareToken)
|
}, handleStreamMessage, shareToken)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -384,6 +440,20 @@ const Conversation: FC = () => {
|
|||||||
{t(`memoryConversation.memory`)}
|
{t(`memoryConversation.memory`)}
|
||||||
</ButtonCheckbox>
|
</ButtonCheckbox>
|
||||||
</Form.Item>
|
</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>
|
||||||
{/* <Flex align="center">
|
{/* <Flex align="center">
|
||||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||||
@@ -399,6 +469,11 @@ const Conversation: FC = () => {
|
|||||||
ref={uploadFileListModalRef}
|
ref={uploadFileListModalRef}
|
||||||
refresh={addFileList}
|
refresh={addFileList}
|
||||||
/>
|
/>
|
||||||
|
<VariableConfigModal
|
||||||
|
ref={variableConfigModalRef}
|
||||||
|
refresh={handleSave}
|
||||||
|
variables={variables}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:57:46
|
* @Date: 2026-02-03 16:57:46
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-06 21:11:19
|
* @Last Modified time: 2026-03-03 13:46:55
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Type definitions for Conversation
|
* Type definitions for Conversation
|
||||||
@@ -51,6 +51,7 @@ export interface QueryParams {
|
|||||||
/** Current conversation ID */
|
/** Current conversation ID */
|
||||||
conversation_id?: string | null;
|
conversation_id?: string | null;
|
||||||
files?: any[];
|
files?: any[];
|
||||||
|
variables?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadFileListModalRef {
|
export interface UploadFileListModalRef {
|
||||||
|
|||||||
@@ -71,20 +71,20 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
key={name}
|
key={name}
|
||||||
name={[name, 'value']}
|
name={[name, 'value']}
|
||||||
label={field.type === 'boolean' ? undefined : `${field.name}·${field.description}`}
|
label={field.type === 'boolean' ? undefined : `${field.name}·${field.display_name || field.description}`}
|
||||||
valuePropName={field.type === 'boolean' ? 'checked' : 'value'}
|
valuePropName={field.type === 'boolean' ? 'checked' : 'value'}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: field.required, message: field.type === 'boolean' ? t('common.pleaseSelect') : t('common.pleaseEnter') },
|
{ required: field.required, message: field.type === 'boolean' ? t('common.pleaseSelect') : t('common.pleaseEnter') },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
field.type === 'string' && <Input placeholder={t('common.pleaseEnter')} />
|
(field.type === 'string' || field.type === 'text') && <Input placeholder={t('common.pleaseEnter')} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
field.type === 'number' && <InputNumber placeholder={t('common.pleaseEnter')} style={{ width: '100%' }} onChange={(value) => form.setFieldValue(['variables', name, 'value'], value)} />
|
field.type === 'number' && <InputNumber placeholder={t('common.pleaseEnter')} style={{ width: '100%' }} onChange={(value) => form.setFieldValue(['variables', name, 'value'], value)} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
field.type === 'boolean' && <Checkbox>{`${field.name}·${field.description}`}</Checkbox>
|
field.type === 'boolean' && <Checkbox>{`${field.name}·${field.display_name || field.description}`}</Checkbox>
|
||||||
}
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface Variable {
|
export interface Variable {
|
||||||
name: string;
|
name: string;
|
||||||
|
display_name?: string;
|
||||||
type: string;
|
type: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user