Merge branch 'develop' into feature/tool_yjp
This commit is contained in:
@@ -163,9 +163,14 @@ export const getImplicitInterestAreas = (end_user_id: string) => {
|
||||
export const getImplicitHabits = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/habits/${end_user_id}`)
|
||||
}
|
||||
// Implicit Memory - Generate user portrait
|
||||
export const generateProfile = (end_user_id: string) => {
|
||||
return request.post(`/memory/implicit-memory/generate_profile`, { end_user_id })
|
||||
}
|
||||
// Implicit Memory - Check if data exists
|
||||
export const implicitCheckData = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/check-data/${end_user_id}`)
|
||||
}
|
||||
// Short-term memory
|
||||
export const getShortTerm = (end_user_id: string) => {
|
||||
return request.get(`/memory/short/short_term`, { end_user_id })
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import RecordRTC from 'recordrtc'
|
||||
|
||||
import { fileUpload } from '@/api/fileStorage'
|
||||
import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
interface AudioRecorderProps {
|
||||
onRecordingComplete?: (file: { file_id: string; file_key: string; }, blob: Blob) => void
|
||||
className?: string
|
||||
onRecordingComplete?: (file: { file_id: string; file_key: string; url: string; type?: string; }, blob?: Blob) => void
|
||||
className?: string;
|
||||
action?: string;
|
||||
requestConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
onRecordingComplete,
|
||||
className = '',
|
||||
action = fileUploadUrlWithoutApiPrefix,
|
||||
requestConfig = {}
|
||||
}) => {
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const recorderRef = useRef<RecordRTC | null>(null)
|
||||
@@ -33,11 +38,17 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
|
||||
if (recorderRef.current) {
|
||||
recorderRef.current.stopRecording(() => {
|
||||
const blob = recorderRef.current!.getBlob()
|
||||
const url = recorderRef.current!.toURL()
|
||||
const formData = new FormData()
|
||||
formData.append('file', blob, `recording_${Date.now()}.webm`)
|
||||
fileUpload(formData)
|
||||
request
|
||||
.uploadFile(action, formData, requestConfig)
|
||||
.then(res => {
|
||||
onRecordingComplete?.(res as { file_id: string; file_key: string; }, blob)
|
||||
onRecordingComplete?.({
|
||||
...(res as { file_id: string; file_key: string }),
|
||||
type: blob.type,
|
||||
url
|
||||
}, blob)
|
||||
recorderRef.current?.destroy()
|
||||
recorderRef.current = null
|
||||
})
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-10 12:13:52
|
||||
* @Last Modified time: 2026-03-04 18:42:49
|
||||
*/
|
||||
import { type FC, useEffect, useMemo } from 'react'
|
||||
import { Flex, Input, Form } from 'antd'
|
||||
|
||||
import SendIcon from '@/assets/images/conversation/send.svg'
|
||||
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
|
||||
import LoadingIcon from '@/assets/images/conversation/loading.svg'
|
||||
@@ -80,9 +81,31 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (file.type.includes('video')) {
|
||||
return (
|
||||
<div key={file.uid} className="rb:w-45 rb:h-16 rb:inline-block rb:group rb:relative rb:rounded-lg">
|
||||
<video src={file.url} controls className="rb:w-45 rb:h-16 rb:rounded-lg rb:object-cover rb:cursor-pointer" />
|
||||
<div
|
||||
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(file)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (file.type.includes('audio')) {
|
||||
return (
|
||||
<div key={file.uid} className="rb:w-45 rb:h-16 rb:inline-flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5 rb:gap-2">
|
||||
<audio src={file.url} controls className="rb:w-45 rb:h-16" />
|
||||
<div
|
||||
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(file)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key={file.uid} className="rb:w-45 rb:text-[12px] rb:gap-2.5 rb:flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5">
|
||||
{(file.type.includes('word') || file.type.includes('wordprocessingml.document')) && <div
|
||||
{(file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document')) && <div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/word_disabled.svg')] rb:hover:bg-[url('@/assets/images/conversation/word.svg')]"
|
||||
></div>}
|
||||
{(file.type.includes('pdf')) && <div
|
||||
|
||||
@@ -453,9 +453,11 @@ export const en = {
|
||||
prevStep: 'Previous Step',
|
||||
exportSuccess: 'Export successful',
|
||||
recommend: 'Recommend',
|
||||
default: 'Default',
|
||||
logoTip: `Supported image formats: JPG, PNG \n Suggested size: square ratio \n Maximum size: ≤ 2MB`,
|
||||
imageSquareRequired: 'Please upload a square image',
|
||||
nameInvalid: 'Name cannot start or end with a space',
|
||||
notAllSpaces: 'Cannot be all spaces',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -603,7 +605,13 @@ export const en = {
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
bedrock: "Bedrock",
|
||||
|
||||
is_vision: 'Vision Support',
|
||||
is_omni: 'Omni Support',
|
||||
vision: 'Vision',
|
||||
audio: 'Audio',
|
||||
video: 'Video',
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: 'Home',
|
||||
@@ -1684,6 +1692,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
uploadFile: 'Upload File',
|
||||
fileType: 'File Type',
|
||||
image: 'Image',
|
||||
video: 'Video',
|
||||
audio: 'Audio',
|
||||
fileUrl: 'File URL',
|
||||
addRemoteFile: 'Add Remote File',
|
||||
variableConfig: 'Variable Configuration',
|
||||
@@ -1962,6 +1972,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
marketConnected: '● Connected',
|
||||
marketDisconnected: '○ Disconnected',
|
||||
marketConnecting: 'Connecting to {{name}}...',
|
||||
serverUrlInvalid: 'Must start with http:// or https://, and cannot have leading or trailing spaces',
|
||||
requestHeaderKeyInvalid: 'Only English letters, numbers, hyphens (-), and underscores (_) are allowed, and cannot start or end with a hyphen or underscore',
|
||||
},
|
||||
workflow: {
|
||||
coreNode: 'Core Nodes',
|
||||
@@ -2311,6 +2323,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
suggestions: 'Personalized Suggestions',
|
||||
suggestionLoading: 'Your personalized suggestions are being generated',
|
||||
item: 'item',
|
||||
noData: 'Emotion suggestion data does not exist, please click the refresh button to initialize',
|
||||
},
|
||||
reflectionEngine: {
|
||||
reflectionEngineConfig: 'Reflection Engine Configuration',
|
||||
@@ -2557,7 +2570,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
context_details: 'Preference Details',
|
||||
supporting_evidence: 'Preference Source',
|
||||
specific_examples: 'Source',
|
||||
wordEmpty: 'Click on a node in the left chart to view preference details'
|
||||
wordEmpty: 'Click on a node in the left chart to view preference details',
|
||||
noData: 'Portrait data does not exist, please click the refresh button to initialize',
|
||||
},
|
||||
shortTermDetail: {
|
||||
title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.',
|
||||
@@ -2615,6 +2629,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
updated_at: 'Updated At',
|
||||
entityTypes: 'Entity Types',
|
||||
|
||||
classSearchPlaceholder: 'Search types',
|
||||
addClass: 'Add Type',
|
||||
class_name: 'Type Name',
|
||||
class_description: 'Type Definition',
|
||||
|
||||
@@ -1033,9 +1033,11 @@ export const zh = {
|
||||
prevStep: '上一步',
|
||||
exportSuccess: '导出成功',
|
||||
recommend: '推荐',
|
||||
default: '默认',
|
||||
logoTip: `支持图片格式(JPG、PNG)\n 尺寸:正方形比例 \n 文件大小限制:≤ 2MB`,
|
||||
imageSquareRequired: '请上传正方形比例图片',
|
||||
nameInvalid: '不能是空格开头或结尾',
|
||||
notAllSpaces: '不能是纯空格',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: '搜索模型…',
|
||||
@@ -1184,6 +1186,12 @@ export const zh = {
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock",
|
||||
|
||||
is_vision: '支持视觉',
|
||||
is_omni: '支持全模态',
|
||||
vision: '视觉',
|
||||
audio: '音频',
|
||||
video: '视频',
|
||||
},
|
||||
timezones: {
|
||||
'Asia/Shanghai': '中国标准时间 (UTC+8)',
|
||||
@@ -1681,6 +1689,8 @@ export const zh = {
|
||||
uploadFile: '上传文件',
|
||||
fileType: '文件类型',
|
||||
image: '图片',
|
||||
video: '视频',
|
||||
audio: '音频',
|
||||
fileUrl: '文件链接',
|
||||
addRemoteFile: '添加远程文件',
|
||||
variableConfig: '变量配置',
|
||||
@@ -1959,6 +1969,8 @@ export const zh = {
|
||||
marketConnected: '● 已连接',
|
||||
marketDisconnected: '○ 未连接',
|
||||
marketConnecting: '正在连接 {{name}}...',
|
||||
serverUrlInvalid: '必须以 http:// 或 https:// 开头,且不能有前后空格',
|
||||
requestHeaderKeyInvalid: '只支持英文、数字、连字符(-)、下划线(_),不能以连字符或下划线开头结尾',
|
||||
},
|
||||
workflow: {
|
||||
coreNode: '核心节点',
|
||||
@@ -2312,6 +2324,7 @@ export const zh = {
|
||||
suggestions: '个性化建议',
|
||||
suggestionLoading: '您的个性化建议正在生成中',
|
||||
item: '个',
|
||||
noData: '情绪建议数据不存在,请点击刷新按钮进行初始化',
|
||||
},
|
||||
reflectionEngine: {
|
||||
reflectionEngineConfig: '反思引擎配置',
|
||||
@@ -2558,7 +2571,8 @@ export const zh = {
|
||||
context_details: '偏好详情',
|
||||
supporting_evidence: '偏好来源',
|
||||
specific_examples: '来源',
|
||||
wordEmpty: '点击左侧图表中的节点查看偏好详情'
|
||||
wordEmpty: '点击左侧图表中的节点查看偏好详情',
|
||||
noData: '画像数据不存在,请点击刷新按钮进行初始化',
|
||||
},
|
||||
shortTermDetail: {
|
||||
title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。',
|
||||
@@ -2616,6 +2630,7 @@ export const zh = {
|
||||
updated_at: '更新时间',
|
||||
entityTypes: '实体类型',
|
||||
|
||||
classSearchPlaceholder: '搜索类型',
|
||||
addClass: '添加类型',
|
||||
class_name: '类型名称',
|
||||
class_description: '类型定义',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 16:35:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 16:35:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-06 10:39:00
|
||||
*/
|
||||
/**
|
||||
* HTTP Request Utility Module
|
||||
@@ -183,7 +183,9 @@ service.interceptors.response.use(
|
||||
msg = msg || i18n.t('common.serverError');
|
||||
break;
|
||||
default:
|
||||
if (!msg && Array.isArray(error.response?.data?.detail)) {
|
||||
if (['SYSTEM_DEFAULT_SCENE_CANNOT_DELETE', 'SYSTEM_DEFAULT_CLASS_CANNOT_DELETE', 'SYSTEM_DEFAULT_SCENE_CANNOT_UPDATE'].includes(msg)) {
|
||||
msg = i18n.t(`common.${msg}`)
|
||||
} else if (!msg && Array.isArray(error.response?.data?.detail)) {
|
||||
msg = error.response?.data?.detail?.map((item: { msg: string }) => item.msg).join(';')
|
||||
} else {
|
||||
msg = msg || i18n.t('common.unknownError');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 16:35:43
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 16:35:43
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 18:19:24
|
||||
*/
|
||||
/**
|
||||
* Server-Sent Events (SSE) Stream Utility Module
|
||||
@@ -176,17 +176,17 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
||||
case 500:
|
||||
case 502:
|
||||
const errorData = await response.json();
|
||||
errorData.error || i18n.t('common.serviceUpgrading');
|
||||
message.warning(errorData.error || i18n.t('common.serviceUpgrading'));
|
||||
return;
|
||||
let errorInfo = errorData.error || i18n.t('common.serviceUpgrading')
|
||||
message.warning(errorInfo);
|
||||
throw errorInfo;
|
||||
case 400:
|
||||
const error = await response.json();
|
||||
message.warning(error.error);
|
||||
throw error || 'Bad Request';
|
||||
throw error.error || 'Bad Request';
|
||||
case 504:
|
||||
const errorJson = await response.json();
|
||||
message.warning(errorJson.error || i18n.t('common.serverError'));
|
||||
return;
|
||||
throw errorData.error;
|
||||
case 401:
|
||||
if (url?.includes('/public')) {
|
||||
return message.warning(i18n.t('common.publicApiCannotRefreshToken'));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:39
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-03 14:21:54
|
||||
* @Last Modified time: 2026-03-05 17:03:46
|
||||
*/
|
||||
/**
|
||||
* Chat debugging component for application testing
|
||||
@@ -13,7 +13,7 @@
|
||||
import { type FC, useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import { Flex, Dropdown, type MenuProps, App } from 'antd'
|
||||
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
|
||||
@@ -25,7 +25,7 @@ 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 AudioRecorder from '@/components/AudioRecorder'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import type { Variable } from './VariableList/types'
|
||||
@@ -88,7 +88,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
|
||||
if (isCluster) {
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
@@ -134,7 +134,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
})
|
||||
}
|
||||
/** Update assistant message when error occurs */
|
||||
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
|
||||
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
|
||||
if (message_length > 0 || !model_config_id) return
|
||||
|
||||
updateChatList(prev => {
|
||||
@@ -171,6 +171,29 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
.then(() => {
|
||||
const message = msg
|
||||
if (!message?.trim()) return
|
||||
// Validate required variables before sending
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (chatVariables && chatVariables.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
chatVariables.forEach(vo => {
|
||||
params[vo.name] = vo.value
|
||||
|
||||
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)
|
||||
setCompareLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
addUserMessage(message, fileList)
|
||||
setMessage(message)
|
||||
@@ -198,27 +221,6 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
// Validate required variables before sending
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (chatVariables && chatVariables.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
chatVariables.forEach(vo => {
|
||||
params[vo.name] = vo.value
|
||||
|
||||
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
|
||||
}
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
files: fileList.map(file => {
|
||||
@@ -243,7 +245,15 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
updateClusterErrorAssistantMessage(0)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -288,7 +298,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
})
|
||||
}
|
||||
/** Update cluster message when error occurs */
|
||||
const updateClusterErrorAssistantMessage = (message_length: number) => {
|
||||
const updateClusterErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
|
||||
updateChatList(prev => {
|
||||
@@ -331,7 +341,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
data.map(item => {
|
||||
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
|
||||
|
||||
switch(item.event) {
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
@@ -354,27 +364,35 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
draftRun(
|
||||
data.app_id,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
stream: true,
|
||||
files: fileList.map(file => {
|
||||
if (file.url) {
|
||||
return file
|
||||
} else {
|
||||
return {
|
||||
type: file.type,
|
||||
transfer_method: 'local_file',
|
||||
upload_file_id: file.response.data.file_id
|
||||
}
|
||||
draftRun(
|
||||
data.app_id,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
stream: true,
|
||||
files: fileList.map(file => {
|
||||
if (file.url) {
|
||||
return file
|
||||
} else {
|
||||
return {
|
||||
type: file.type,
|
||||
transfer_method: 'local_file',
|
||||
upload_file_id: file.response.data.file_id
|
||||
}
|
||||
}),
|
||||
},
|
||||
handleStreamMessage
|
||||
)
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
}),
|
||||
},
|
||||
handleStreamMessage
|
||||
)
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
updateClusterErrorAssistantMessage(0)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -393,12 +411,17 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
const fileChange = (file?: any) => {
|
||||
setFileList([...fileList, file])
|
||||
}
|
||||
// const handleRecordingComplete = async (file: any) => {
|
||||
// console.log('file', 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) {
|
||||
switch (key) {
|
||||
case 'define':
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
break
|
||||
@@ -415,99 +438,98 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
return (
|
||||
<div className="rb:relative rb:h-full rb:flex rb:flex-col">
|
||||
{chatList.length === 0
|
||||
? <Empty
|
||||
url={DebuggingEmpty}
|
||||
? <Empty
|
||||
url={DebuggingEmpty}
|
||||
size={[300, 200]}
|
||||
title={t('application.debuggingEmpty')}
|
||||
subTitle={t('application.debuggingEmptyDesc')}
|
||||
title={t('application.debuggingEmpty')}
|
||||
subTitle={t('application.debuggingEmptyDesc')}
|
||||
className="rb:h-full"
|
||||
/>
|
||||
: <>
|
||||
<div className={clsx(`rb:relative rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full rb:flex-1 rb:min-h-0`)}>
|
||||
{chatList.map((chat, index) => (
|
||||
<div key={index} className={clsx('rb:flex rb:flex-col', {
|
||||
"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='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
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
: <>
|
||||
<div className={clsx(`rb:relative rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full rb:flex-1 rb:min-h-0`)}>
|
||||
{chatList.map((chat, index) => (
|
||||
<div key={index} className={clsx('rb:flex rb:flex-col', {
|
||||
"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='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
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:mt-6': true,
|
||||
'rb:h-[calc(100vh-282px)]': isCluster,
|
||||
'rb:h-[calc(100vh-380px)]': !isCluster,
|
||||
}}
|
||||
contentClassNames={{
|
||||
'rb:max-w-[400px]!': chatList.length === 1,
|
||||
'rb:max-w-[260px]!': chatList.length === 2,
|
||||
'rb:max-w-[150px]!': chatList.length === 3,
|
||||
'rb:max-w-[108px]!': chatList.length === 4,
|
||||
}}
|
||||
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chat.list || []}
|
||||
streamLoading={compareLoading}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rb:relative rb:flex rb:items-center rb:gap-2.5 rb:m-4 rb:mb-1">
|
||||
<ChatInput
|
||||
message={message}
|
||||
className="rb:relative!"
|
||||
loading={loading}
|
||||
fileChange={updateFileList}
|
||||
fileList={fileList}
|
||||
onSend={isCluster ? handleClusterSend : handleSend}
|
||||
onChange={handleMessageChange}
|
||||
>
|
||||
<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
|
||||
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']}
|
||||
onChange={fileChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
],
|
||||
onClick: handleShowUpload
|
||||
}
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:mt-6': true,
|
||||
'rb:h-[calc(100vh-282px)]': isCluster,
|
||||
'rb:h-[calc(100vh-380px)]': !isCluster,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
contentClassNames={{
|
||||
'rb:max-w-[400px]!': chatList.length === 1,
|
||||
'rb:max-w-[260px]!': chatList.length === 2,
|
||||
'rb:max-w-[150px]!': chatList.length === 3,
|
||||
'rb:max-w-[108px]!': chatList.length === 4,
|
||||
}}
|
||||
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chat.list || []}
|
||||
streamLoading={compareLoading}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rb:relative rb:flex rb:items-center rb:gap-2.5 rb:m-4 rb:mb-1">
|
||||
<ChatInput
|
||||
message={message}
|
||||
className="rb:relative!"
|
||||
loading={loading}
|
||||
fileChange={updateFileList}
|
||||
fileList={fileList}
|
||||
onSend={isCluster ? handleClusterSend : handleSend}
|
||||
onChange={handleMessageChange}
|
||||
>
|
||||
<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>
|
||||
{/* <Flex align="center">
|
||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
|
||||
</Flex> */}
|
||||
</Flex>
|
||||
</ChatInput>
|
||||
</div>
|
||||
</>
|
||||
</ChatInput>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
<UploadFileListModal
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:09:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-11 11:32:48
|
||||
* @Last Modified time: 2026-03-05 15:09:22
|
||||
*/
|
||||
/**
|
||||
* File Upload Component
|
||||
@@ -25,6 +25,7 @@ import { Upload, Progress, App } from 'antd';
|
||||
import type { UploadProps, UploadFile } from 'antd';
|
||||
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { request } from '@/utils/request'
|
||||
import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||
|
||||
@@ -56,27 +57,36 @@ interface UploadFilesProps extends Omit<UploadProps, 'onChange'> {
|
||||
/** Custom file removal callback */
|
||||
onRemove?: (file: UploadFile) => boolean | void | Promise<boolean | void>;
|
||||
}
|
||||
|
||||
const transform_file_type = {
|
||||
'text/plain': 'document/text',
|
||||
'text/markdown': 'document/markdown',
|
||||
'text/x-markdown': 'document/x-markdown',
|
||||
|
||||
'application/pdf': 'document/pdf',
|
||||
|
||||
'application/msword': 'document/doc',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document/docx',
|
||||
|
||||
'application/vnd.ms-powerpoint': 'document/ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'document/pptx',
|
||||
}
|
||||
// Mapping of file extensions to MIME types
|
||||
const ALL_FILE_TYPE: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
// txt: 'text/plain',
|
||||
txt: 'text/plain',
|
||||
md: 'text/markdown',
|
||||
xmd: 'text/x-markdown',
|
||||
|
||||
pdf: 'application/pdf',
|
||||
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
csv: 'text/csv',
|
||||
|
||||
ppt: 'application/vnd.ms-powerpoint',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
// md: 'text/markdown',
|
||||
// htm: 'text/html',
|
||||
// html: 'text/html',
|
||||
// json: 'application/json',
|
||||
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
@@ -84,6 +94,23 @@ const ALL_FILE_TYPE: {
|
||||
bmp: 'image/bmp',
|
||||
webp: 'image/webp',
|
||||
svg: 'image/svg+xml',
|
||||
|
||||
mp4: 'video/mp4',
|
||||
mov: 'video/quicktime',
|
||||
avi: 'video/x-msvideo',
|
||||
mkv: 'video/x-matroska',
|
||||
webm: 'video/webm',
|
||||
flv: 'video/x-flv',
|
||||
wmv: 'video/x-ms-wmv',
|
||||
|
||||
mp3: 'audio/mpeg',
|
||||
wav: 'audio/wav',
|
||||
ogg: 'audio/ogg',
|
||||
aac: 'audio/aac',
|
||||
flac: 'audio/flac',
|
||||
m4a: 'audio/mp4',
|
||||
wma: 'audio/x-ms-wma',
|
||||
xm4a: 'audio/x-m4a',
|
||||
}
|
||||
export interface UploadFilesRef {
|
||||
/** Current file list */
|
||||
@@ -178,6 +205,10 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
* Handles upload state changes
|
||||
*/
|
||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
|
||||
newFileList.map(file => {
|
||||
const type = (file.type && transform_file_type[file.type as keyof typeof transform_file_type]) || file.type || 'document'
|
||||
file.type = type
|
||||
})
|
||||
setFileList(newFileList);
|
||||
if (onChange) {
|
||||
onChange(maxCount === 1 ? newFileList[newFileList.length - 1] : newFileList);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:09:47
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 10:17:54
|
||||
* @Last Modified time: 2026-03-04 17:47:09
|
||||
*/
|
||||
/**
|
||||
* Upload File List Modal Component
|
||||
@@ -104,7 +104,9 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
|
||||
<Select
|
||||
placeholder={t('memoryConversation.fileType')}
|
||||
options={[
|
||||
{ label: t('memoryConversation.image'), value: 'image' }
|
||||
{ label: t('memoryConversation.image'), value: 'image' },
|
||||
{ label: t('memoryConversation.audio'), value: 'audio' },
|
||||
{ label: t('memoryConversation.video'), value: 'video' },
|
||||
]}
|
||||
className="rb:w-30"
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,7 @@ 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 } from 'antd'
|
||||
import { Flex, Skeleton, Form, Dropdown, type MenuProps, App, Divider } from 'antd'
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -35,7 +35,7 @@ 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 AudioRecorder from '@/components/AudioRecorder'
|
||||
import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
|
||||
import UploadFileListModal from './components/UploadFileListModal'
|
||||
import type { VariableConfigModalRef } from '@/views/Workflow/types'
|
||||
@@ -305,17 +305,27 @@ const Conversation: FC = () => {
|
||||
}),
|
||||
variables: params
|
||||
}, handleStreamMessage, shareToken)
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const fileChange = (file?: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), file])
|
||||
}
|
||||
// const handleRecordingComplete = async (file: any) => {
|
||||
// console.log('file', 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) {
|
||||
@@ -329,6 +339,7 @@ const Conversation: FC = () => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), ...fileList])
|
||||
}
|
||||
const updateFileList = (fileList?: any[]) => {
|
||||
console.log('fileList', fileList)
|
||||
form.setFieldValue('files', [...(fileList || [])])
|
||||
}
|
||||
|
||||
@@ -383,7 +394,7 @@ const Conversation: FC = () => {
|
||||
<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="rb:h-[calc(100%-180px)]"
|
||||
contentClassName={!queryValues?.files?.length ? "rb:h-[calc(100%-144px)]" : "rb:h-[calc(100%-208px)]"}
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
@@ -405,13 +416,12 @@ const Conversation: FC = () => {
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
action={shareFileUploadUrlWithoutApiPrefix}
|
||||
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']}
|
||||
onChange={fileChange}
|
||||
requestConfig={{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${shareToken || ''}`,
|
||||
} }}
|
||||
}}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -455,10 +465,19 @@ const Conversation: FC = () => {
|
||||
</Form.Item>
|
||||
)}
|
||||
</Flex>
|
||||
{/* <Flex align="center">
|
||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||
<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>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Chat>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@/api/knowledgeBase'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import SliderInput from '@/components/SliderInput'
|
||||
import { stringRegExp } from '@/utils/validator'
|
||||
const { TextArea } = Input;
|
||||
const { confirm } = Modal
|
||||
|
||||
@@ -519,12 +520,16 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('knowledgeBase.createForm.name')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.nameRequired') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('knowledgeBase.createForm.nameRequired') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.createForm.name')} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="description" label={t('knowledgeBase.createForm.description')}>
|
||||
<Form.Item name="description" label={t('knowledgeBase.createForm.description')} rules={[{ max: 500 }]}>
|
||||
<TextArea rows={2} placeholder={t('knowledgeBase.createForm.description')} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:33:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:33:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-05 16:28:58
|
||||
*/
|
||||
/**
|
||||
* Memory Management Page
|
||||
@@ -110,9 +110,15 @@ const MemoryManagement: React.FC = () => {
|
||||
<List.Item key={item.config_id}>
|
||||
<RbCard
|
||||
title={item.config_name}
|
||||
className="rb:relative"
|
||||
>
|
||||
{item.is_system_default &&
|
||||
<div className="rb:absolute rb:-right-px rb:-top-px rb:bg-[#FF5D34] rb:rounded-[0px_7px_0px_8px] rb:text-[12px] rb:text-white rb:font-regular rb:leading-4 rb:py-0.5 rb:px-1">
|
||||
{t('common.default')}
|
||||
</div>
|
||||
}
|
||||
<Tooltip title={item.config_desc}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1 rb:h-[17px]">{item.config_desc}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1 rb:h-4.25">{item.config_desc}</div>
|
||||
</Tooltip>
|
||||
<RbAlert className="rb:mt-3 ">
|
||||
<div className={clsx("rb:flex rb:gap-5 rb:font-regular rb:text-[14px]")}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:33:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 17:33:24
|
||||
* @Last Modified time: 2026-03-05 16:33:53
|
||||
*/
|
||||
/**
|
||||
* Memory management form data type
|
||||
@@ -42,6 +42,7 @@ export interface Memory {
|
||||
workspace_id: string;
|
||||
scene_id: string;
|
||||
scene_name: string;
|
||||
is_system_default: boolean;
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 17:24:05
|
||||
* @Last Modified time: 2026-03-04 11:31:43
|
||||
*/
|
||||
/**
|
||||
* Custom Model Modal
|
||||
@@ -10,8 +10,8 @@
|
||||
* Supports logo upload, type/provider selection, and tagging
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Checkbox } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CustomModelForm, ModelListItem, CustomModelModalRef, CustomModelModalProps } from '../types';
|
||||
@@ -35,6 +35,14 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CustomModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const modelType = Form.useWatch(['type'], form);
|
||||
const isOmni = Form.useWatch(['is_omni'], form);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOmni) {
|
||||
form.setFieldsValue({ is_vision: true })
|
||||
}
|
||||
}, [isOmni])
|
||||
|
||||
/** Close modal and reset state */
|
||||
const handleClose = () => {
|
||||
@@ -49,9 +57,12 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
const { capability, is_omni, ...rest} = model
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
logo: model.logo && model.logo.startsWith('http') ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
...rest,
|
||||
logo: model.logo && model.logo.startsWith('http') ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined,
|
||||
is_omni,
|
||||
is_vision: capability?.includes('vision') || false,
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
@@ -79,9 +90,14 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { logo, ...rest } = values;
|
||||
const { logo, type, is_vision, is_omni, ...rest } = values;
|
||||
const formData: CustomModelForm = {
|
||||
...rest
|
||||
...rest,
|
||||
type,
|
||||
}
|
||||
if (!['embedding', 'rerank'].includes(type as string)) {
|
||||
formData.capability = is_omni ? ["vision", "audio"] : is_vision ? ['vision'] : []
|
||||
formData.is_omni = is_omni
|
||||
}
|
||||
|
||||
if (typeof logo === 'object' && logo?.response?.data.file_id) {
|
||||
@@ -108,7 +124,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
console.log('modelType', modelType)
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createCustomModel')}
|
||||
@@ -180,7 +196,6 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
name={["api_keys", 0, "api_key"]}
|
||||
label={t('modelNew.api_key')}
|
||||
@@ -196,6 +211,17 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
|
||||
{!['embedding', 'rerank'].includes(modelType as string) &&
|
||||
<>
|
||||
<Form.Item name="is_omni" valuePropName="checked" className="rb:mb-2!">
|
||||
<Checkbox>{t('modelNew.is_omni')}</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="is_vision" valuePropName="checked" className="rb:mb-0!">
|
||||
<Checkbox disabled={isOmni}>{t('modelNew.is_vision')}</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:20
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:54:54
|
||||
* @Last Modified time: 2026-03-04 11:51:01
|
||||
*/
|
||||
/**
|
||||
* Sub-Model Modal
|
||||
@@ -10,8 +10,8 @@
|
||||
* Uses cascader for hierarchical selection
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Cascader, App, type CascaderProps } from 'antd';
|
||||
import { type ReactNode, forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Cascader, App, type CascaderProps, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps } from './types';
|
||||
@@ -19,6 +19,7 @@ import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelProviderUrl, getModelNewList } from '@/api/models'
|
||||
import type { ProviderModelItem } from '../../types'
|
||||
import Tag from '@/components/Tag';
|
||||
|
||||
const { SHOW_CHILD } = Cascader;
|
||||
|
||||
@@ -27,7 +28,7 @@ const { SHOW_CHILD } = Cascader;
|
||||
*/
|
||||
interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
label: string | ReactNode;
|
||||
children?: Option[];
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -116,7 +117,11 @@ const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
|
||||
}))
|
||||
return {
|
||||
...vo,
|
||||
label: vo.name,
|
||||
label: <Space>
|
||||
{vo.name}
|
||||
<Tag>{t(`modelNew.${vo.type}`)}</Tag>
|
||||
{vo.capability?.filter(item => item !== 'video').map(vo => <Tag key={vo}>{t(`modelNew.${vo}`)}</Tag>)}
|
||||
</Space>,
|
||||
value: vo.id,
|
||||
children: children
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:45
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:49:45
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 11:50:47
|
||||
*/
|
||||
/**
|
||||
* Model List Detail Drawer
|
||||
@@ -133,9 +133,10 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space className="rb:mt-1!">
|
||||
subTitle={<Space size={8} className="rb:mt-1!">
|
||||
<Tag>{t(`modelNew.${item.type}`)}</Tag>
|
||||
<Tag color="warning">{item.api_keys.length}{t('modelNew.apiKeyNum')}</Tag>
|
||||
{item.capability?.filter(item => item !=='video').map(vo => <Tag key={vo}>{t(`modelNew.${vo}`)}</Tag>)}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:54:26
|
||||
* @Last Modified time: 2026-03-04 11:50:31
|
||||
*/
|
||||
/**
|
||||
* Model Square Detail Drawer
|
||||
@@ -89,9 +89,10 @@ const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProp
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
subTitle={<Space size={8} className="rb:mt-1!">
|
||||
<Tag>{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success">{t(`modelNew.official`)}</Tag>}
|
||||
{item.capability?.filter(item => item !== 'video').map(vo => <Tag key={vo}>{t(`modelNew.${vo}`)}</Tag>)}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:50:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:50:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 11:39:20
|
||||
*/
|
||||
/**
|
||||
* Type definitions for Model Management
|
||||
@@ -148,7 +148,9 @@ export interface ModelListItem {
|
||||
/** Update timestamp */
|
||||
updated_at: number;
|
||||
/** Associated API keys */
|
||||
api_keys: ModelApiKey[]
|
||||
api_keys: ModelApiKey[];
|
||||
capability?: string[];
|
||||
is_omni?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,6 +263,8 @@ export interface ModelPlazaItem {
|
||||
add_count: number;
|
||||
/** Whether user has added this model */
|
||||
is_added: boolean;
|
||||
capability?: string[];
|
||||
is_omni?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,6 +295,9 @@ export interface CustomModelForm {
|
||||
/** API base URL */
|
||||
api_base: string;
|
||||
}>
|
||||
is_vision?: boolean;
|
||||
is_omni?: boolean;
|
||||
capability?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -185,6 +185,7 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 2000 },
|
||||
{ pattern: /^(?!\s*$).+$/, message: t('common.notAllSpaces') },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('ontology.scenarioPlaceholder')} />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:24
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 18:02:13
|
||||
* @Last Modified time: 2026-03-06 11:25:59
|
||||
*/
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -17,7 +17,7 @@ const { Header } = Layout;
|
||||
*/
|
||||
interface ConfigHeaderProps {
|
||||
/** Page title/name */
|
||||
name?: string;
|
||||
name?: string | ReactNode;
|
||||
/** Subtitle content displayed below the title */
|
||||
subTitle?: ReactNode | string;
|
||||
/** Extra content displayed on the right side */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-06 10:56:44
|
||||
*/
|
||||
import { type FC, useState, useRef, type MouseEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -144,8 +144,13 @@ const Ontology: FC = () => {
|
||||
title={item.scene_name}
|
||||
extra={<Tag>{item.type_num} {t('ontology.typeCount')}</Tag>}
|
||||
onClick={() => handleJump(item)}
|
||||
className="rb:cursor-pointer"
|
||||
className="rb:cursor-pointer rb:relative"
|
||||
>
|
||||
{item.is_system_default &&
|
||||
<div className="rb:absolute rb:-right-px rb:-top-px rb:bg-[#FF5D34] rb:rounded-[0px_7px_0px_8px] rb:text-[12px] rb:text-white rb:font-regular rb:leading-4 rb:py-0.5 rb:px-1">
|
||||
{t('common.default')}
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
@@ -164,18 +169,20 @@ const Ontology: FC = () => {
|
||||
</div>
|
||||
))}
|
||||
<Divider size="middle" />
|
||||
<Flex gap={8} wrap>
|
||||
<Flex gap={8} wrap align="center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-4.5">{t('ontology.entityTypes')}: </div>
|
||||
{item.entity_type?.map((type, i) => (
|
||||
<Tag key={i} color={i % 2 ? 'processing' : 'success'}>{type}</Tag>
|
||||
))}
|
||||
<div className="rb:flex-1 rb:overflow-hidden rb:wrap-break-word! rb:line-clamp-1!">
|
||||
{item.entity_type?.map((type, i) => (
|
||||
<Tag key={i} color={i % 2 ? 'processing' : 'success'} className="rb:ml-2">{type}</Tag>
|
||||
))}
|
||||
</div>
|
||||
{item.type_num > 3 && (
|
||||
<Tag color="default">+{item.type_num - 3}</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
<Space size={16}>
|
||||
<div className="rb:mt-4 rb:h-5 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
{!item.is_system_default && <Space size={16}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={(e) => handleEdit(item, e)}
|
||||
@@ -184,7 +191,7 @@ const Ontology: FC = () => {
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={(e) => handleDelete(item, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</Space>}
|
||||
</div>
|
||||
</RbCard>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:20
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 17:56:35
|
||||
* @Last Modified time: 2026-03-06 11:26:49
|
||||
*/
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -17,6 +17,7 @@ import OntologyClassModal from '../components/OntologyClassModal'
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import OntologyClassExtractModal from '../components/OntologyClassExtractModal'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
/**
|
||||
* Ontology detail page component
|
||||
@@ -99,19 +100,22 @@ const Detail: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
name={data.scene_name}
|
||||
name={<Space>
|
||||
{data.scene_name}
|
||||
<Tag color="warning">{t('common.default')}</Tag>
|
||||
</Space>}
|
||||
subTitle={<Tooltip title={data.scene_description}><div className="rb:h-4 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{data.scene_description}</div></Tooltip>}
|
||||
extra={<Space>
|
||||
extra={data.is_system_default ? undefined : (<Space>
|
||||
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={handleAdd}>+ {t('ontology.addClass')}</Button>
|
||||
<Button className="rb:h-6! rb:px-2! rb:leading-5.5!" type="primary" onClick={handleExtract}>+ {t('ontology.extract')}</Button>
|
||||
</Space>}
|
||||
</Space>)}
|
||||
/>
|
||||
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
<Col span={6} offset={18}>
|
||||
<SearchInput
|
||||
placeholder={t('ontology.searchPlaceholder')}
|
||||
placeholder={t('ontology.classSearchPlaceholder')}
|
||||
onSearch={(value) => setQuery({ class_name: value })}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
@@ -123,10 +127,10 @@ const Detail: FC = () => {
|
||||
<Col key={item.class_id} span={6}>
|
||||
<RbCard
|
||||
title={item.class_name}
|
||||
extra={<div
|
||||
extra={data.is_system_default ? undefined : (<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>}
|
||||
></div>)}
|
||||
className="rb:bg-transparent!"
|
||||
>
|
||||
<Tooltip title={item.class_description}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-06 10:55:23
|
||||
*/
|
||||
/**
|
||||
* Query parameters for ontology list pagination and filtering
|
||||
@@ -38,6 +38,8 @@ export interface OntologyItem {
|
||||
updated_at: number;
|
||||
/** Total count of classes in the scene */
|
||||
classes_count: number;
|
||||
/** Whether this is the system default configuration */
|
||||
is_system_default: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +94,7 @@ export interface OntologyClassData {
|
||||
scene_description: string;
|
||||
/** Array of class items */
|
||||
items: OntologyClassItem[];
|
||||
is_system_default: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CustomToolItem, CustomToolModalRef, ToolItem } from '../types'
|
||||
import RbModal from '@/components/RbModal';
|
||||
import { parseSchema, addTool, updateTool } from '@/api/tools';
|
||||
import Table from '@/components/Table';
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface CustomToolModalProps {
|
||||
@@ -134,7 +135,11 @@ const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('tool.name')}
|
||||
rules={[{ required: true, message: t('common.enterNamePlaceholder') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('tool.enterNamePlaceholder') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('tool.enterNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -9,6 +9,7 @@ import RequestHeaderModal from './RequestHeaderModal';
|
||||
import Table from '@/components/Table';
|
||||
import { addTool, updateTool, testConnection } from '@/api/tools'
|
||||
import type { McpServiceModalRef } from '../types'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -174,14 +175,22 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
|
||||
name={['config', "server_url"]}
|
||||
label={t('tool.serviceEndpoint')}
|
||||
extra={t('tool.serviceEndpointExtra')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 500 },
|
||||
{ pattern: /^https?:\/\/\S+$/, message: t('tool.serverUrlInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('tool.serviceEndpointPlaceholder')} />
|
||||
</FormItem>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('tool.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ max: 50 },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('tool.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
@@ -207,6 +216,7 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('tool.description')}
|
||||
rules={[{ max: 500 }]}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder={t('common.inputPlaceholder', { title: t('tool.description') })}/>
|
||||
</FormItem>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { RequestHeader, RequestHeaderModalRef } from './McpServiceModal'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { stringRegExp } from '@/utils/validator';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -82,7 +83,11 @@ const RequestHeaderModal = forwardRef<RequestHeaderModalRef, RequestHeaderModalP
|
||||
<FormItem
|
||||
name="key"
|
||||
label={t('tool.requestHeaderName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/, message: t('tool.requestHeaderKeyInvalid') },
|
||||
{ max: 100 }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
@@ -90,7 +95,11 @@ const RequestHeaderModal = forwardRef<RequestHeaderModalRef, RequestHeaderModalP
|
||||
<FormItem
|
||||
name="value"
|
||||
label={t('tool.requestHeaderValue')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: stringRegExp, message: t('common.nameInvalid') },
|
||||
{ max: 2000 }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter',)} />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:31:50
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 18:31:50
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:22:03
|
||||
*/
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { App } from 'antd'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -20,6 +21,7 @@ import RbAlert from '@/components/RbAlert'
|
||||
* @property {Array} suggestions - List of suggestions with actionable steps
|
||||
*/
|
||||
interface Suggestions {
|
||||
exists?: boolean;
|
||||
health_summary: string;
|
||||
suggestions: Array<{
|
||||
type: string;
|
||||
@@ -35,14 +37,17 @@ interface Suggestions {
|
||||
* Displays emotional health suggestions with actionable steps
|
||||
* Shows health summary and prioritized recommendations
|
||||
*/
|
||||
const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const Suggestions = forwardRef<{ handleRefresh: () => void; }, { refresh: () => void; }>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const { modal } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
|
||||
const modalInstanceRef = useRef<{ destroy: () => void } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getSuggestionData()
|
||||
return () => modalInstanceRef.current?.destroy()
|
||||
}, [id])
|
||||
|
||||
const getSuggestionData = () => {
|
||||
@@ -52,7 +57,18 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =>
|
||||
setLoading(true)
|
||||
getEmotionSuggestions(id)
|
||||
.then((res) => {
|
||||
setSuggestions(res as Suggestions)
|
||||
const response = res as Suggestions
|
||||
if (!response.exists && (!response.suggestions || !response.suggestions?.length)) {
|
||||
modalInstanceRef.current = modal.warning({
|
||||
title: t('statementDetail.noData'),
|
||||
okText: t('common.refresh'),
|
||||
onOk: () => {
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setSuggestions(res as Suggestions)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-08 19:46:02
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:26:55
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col } from 'antd'
|
||||
import { Row, Col, App } from 'antd'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import Preferences from '../components/Preferences'
|
||||
@@ -9,16 +15,44 @@ import InterestAreas from '../components/InterestAreas'
|
||||
import Habits from '../components/Habits'
|
||||
import {
|
||||
generateProfile,
|
||||
implicitCheckData,
|
||||
} from '@/api/memory'
|
||||
|
||||
const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
/**
|
||||
* ImplicitDetail Component - Displays user's implicit memory profile
|
||||
* Shows unconscious preferences, personality traits, interests and habits
|
||||
*/
|
||||
const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }, { refresh: () => void; }>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const { modal } = App.useApp()
|
||||
const preferencesRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const portraitRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const interestAreasRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const habitsRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
|
||||
// Check if implicit data exists, prompt user to initialize if not
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
let modalInstance: { destroy: () => void } | null = null
|
||||
implicitCheckData(id)
|
||||
.then(res => {
|
||||
if (!(res as { exists: boolean }).exists) {
|
||||
modalInstance = modal.warning({
|
||||
title: t('implicitDetail.noData'),
|
||||
okText: t('common.refresh'),
|
||||
onOk: () => {
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return () => modalInstance?.destroy()
|
||||
}, [id])
|
||||
|
||||
// Refresh all implicit memory components by regenerating profile
|
||||
const handleRefresh = () => {
|
||||
if (!id) {
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-19 16:54:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:28:00
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { Row, Col, Space } from 'antd';
|
||||
import { useParams } from 'react-router-dom'
|
||||
@@ -9,9 +15,17 @@ import Suggestions from '../components/Suggestions'
|
||||
import { generateSuggestions } from '@/api/memory'
|
||||
|
||||
|
||||
const StatementDetail = forwardRef((_props, ref) => {
|
||||
/**
|
||||
* StatementDetail - Displays emotional memory analysis for a user
|
||||
* Shows word cloud, emotion tags, health index, and personalized suggestions
|
||||
*/
|
||||
const StatementDetail = forwardRef<{ handleRefresh: () => void },{ refresh: () => void; }>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { id } = useParams()
|
||||
const suggestionsRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
|
||||
// Regenerate suggestions and refresh the Suggestions child component
|
||||
const handleRefresh = () => {
|
||||
if (!id) {
|
||||
return Promise.resolve()
|
||||
@@ -41,7 +55,7 @@ const StatementDetail = forwardRef((_props, ref) => {
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions ref={suggestionsRef} />
|
||||
<Suggestions ref={suggestionsRef} refresh={refresh} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-07 20:37:34
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 16:27:14
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dropdown, Button } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import StatementDetail from './StatementDetail'
|
||||
@@ -19,11 +24,16 @@ import {
|
||||
import refreshIcon from '@/assets/images/refresh_hover.svg'
|
||||
import GraphDetail from './GraphDetail'
|
||||
|
||||
/**
|
||||
* Detail page for user memory - renders different memory type views
|
||||
* based on the `type` route param
|
||||
*/
|
||||
const Detail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id, type } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState<string>('')
|
||||
// Refs for child components that support imperative refresh
|
||||
const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
const statementDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
const implicitDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
@@ -33,6 +43,7 @@ const Detail: FC = () => {
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// Fetch end user profile to display the user's name in the header
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
getEndUserProfile(id).then((res) => {
|
||||
@@ -40,15 +51,21 @@ const Detail: FC = () => {
|
||||
setName(response.other_name || response.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Build dropdown menu items for switching between memory types
|
||||
const items = useMemo(() => {
|
||||
return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGET_MEMORY']
|
||||
.map(key => ({ key, label: t(`userMemory.${key}`) }))
|
||||
}, [t])
|
||||
|
||||
// Navigate to the selected memory type detail page
|
||||
const onClick = ({ key }: { key: string }) => {
|
||||
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
|
||||
}
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Trigger refresh on the active memory type's child component
|
||||
const handleRefresh = () => {
|
||||
setLoading(true)
|
||||
let response: any = null
|
||||
@@ -64,6 +81,7 @@ const Detail: FC = () => {
|
||||
break
|
||||
}
|
||||
|
||||
// If the child returns a Promise, wait for it before clearing loading state
|
||||
if (response instanceof Promise) {
|
||||
response.finally(() => {
|
||||
setLoading(false)
|
||||
@@ -99,9 +117,9 @@ const Detail: FC = () => {
|
||||
</Button>}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail ref={statementDetailRef} />}
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail ref={statementDetailRef} refresh={handleRefresh} />}
|
||||
{type === 'FORGET_MEMORY' && <ForgetDetail ref={forgetDetailRef} />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail ref={implicitDetailRef} />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail ref={implicitDetailRef} refresh={handleRefresh} />}
|
||||
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
||||
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}
|
||||
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:10:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 12:10:17
|
||||
* @Last Modified time: 2026-03-04 18:51:48
|
||||
*/
|
||||
/**
|
||||
* Workflow Chat Component
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { App, Space, Button, Flex, Dropdown, type MenuProps } from 'antd'
|
||||
import { App, Space, Button, Flex, Dropdown, type MenuProps, Divider } from 'antd'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
@@ -38,7 +38,7 @@ 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 AudioRecorder from '@/components/AudioRecorder'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import Runtime from './Runtime';
|
||||
@@ -359,6 +359,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setStreamLoading(true)
|
||||
draftRun(appId, data, handleStreamMessage)
|
||||
.catch((error) => {
|
||||
console.log('draftRun error', error)
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
@@ -390,9 +391,13 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
const fileChange = (file?: any) => {
|
||||
setFileList([...fileList, file])
|
||||
}
|
||||
// const handleRecordingComplete = async (file: any) => {
|
||||
// console.log('file', file)
|
||||
// }
|
||||
const handleRecordingComplete = async (file: any) => {
|
||||
setFileList([...fileList, {
|
||||
response: { data: file },
|
||||
thumbUrl: file.url,
|
||||
type: file.type
|
||||
}])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles dropdown menu actions for file upload
|
||||
@@ -424,6 +429,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
handleClose
|
||||
}));
|
||||
|
||||
console.log('fileList', fileList)
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<div className="rb:flex rb:items-center rb:gap-2.5">
|
||||
@@ -470,7 +477,6 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
{
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']}
|
||||
onChange={fileChange}
|
||||
/>
|
||||
)
|
||||
@@ -484,10 +490,10 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
></div>
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
{/* <Flex align="center">
|
||||
<Flex align="center">
|
||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
|
||||
</Flex> */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ChatInput>
|
||||
</div>
|
||||
|
||||
@@ -80,6 +80,7 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
|
||||
{
|
||||
(field.type === 'string' || field.type === 'text') && <Input placeholder={t('common.pleaseEnter')} />
|
||||
}
|
||||
{ field.type === 'paragraph' && <Input.TextArea placeholder={t('common.pleaseEnter')} /> }
|
||||
{
|
||||
field.type === 'number' && <InputNumber placeholder={t('common.pleaseEnter')} style={{ width: '100%' }} onChange={(value) => form.setFieldValue(['variables', name, 'value'], value)} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user