Merge origin/develop_web into feature/20251219_yjp
- Resolved conflict in web/src/components/RbModal/index.tsx - Combined className and maskClosable properties
6
web/.gitignore
vendored
@@ -24,9 +24,3 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
package-lock.json
|
||||
|
||||
# 文档和截图(不上传到仓库)
|
||||
操作说明.md
|
||||
记忆熊系统功能使用说明.md
|
||||
截图清单.md
|
||||
images/
|
||||
@@ -10,10 +10,14 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/layout": "^1.2.14-beta.8",
|
||||
"@antv/x6": "^3.0.1",
|
||||
"@antv/x6-react-shape": "^3.0.1",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@lexical/react": "^0.39.0",
|
||||
"antd": "^5.27.4",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -23,6 +27,8 @@
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"i18next": "^25.6.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lexical": "^0.39.0",
|
||||
"mermaid": "^11.12.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -31,7 +37,6 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
@@ -46,6 +51,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
|
||||
@@ -31,6 +31,8 @@ import { cookieUtils } from './utils/request';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
const { locale, language, timeZone } = useI18n()
|
||||
|
||||
33
web/src/api/apiKey.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
|
||||
// API Key列表
|
||||
export const getApiKeyListUrl = '/apikeys'
|
||||
export const getApiKeyList = (data: Record<string, unknown>) => {
|
||||
return request.get(getApiKeyListUrl, data)
|
||||
}
|
||||
|
||||
// API Key详情
|
||||
export const getApiKey = (id: string) => {
|
||||
return request.get(`/apikeys/${id}`)
|
||||
}
|
||||
|
||||
// 创建API Key
|
||||
export const createApiKey = (values: ApiKey) => {
|
||||
return request.post('/apikeys', values)
|
||||
}
|
||||
|
||||
// 更新API Key
|
||||
export const updateApiKey = (id: string, values: ApiKey) => {
|
||||
return request.put(`/apikeys/${id}`, values)
|
||||
}
|
||||
|
||||
// 删除 API Key
|
||||
export const deleteApiKey = (id: string) => {
|
||||
return request.delete(`/apikeys/${id}`)
|
||||
}
|
||||
|
||||
// 使用统计
|
||||
export const getApiKeyStats = (app_key_id: string) => {
|
||||
return request.get(`/apikeys/${app_key_id}/stats`)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
||||
import type { Config } from '@/views/ApplicationConfig/types'
|
||||
import { handleSSE } from '@/utils/stream'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
import type { QueryParams } from '@/views/Conversation/types'
|
||||
import type { WorkflowConfig } from '@/views/Workflow/types'
|
||||
|
||||
// 应用列表
|
||||
export const getApplicationListUrl = '/apps'
|
||||
@@ -12,20 +14,24 @@ export const getApplicationList = (data: Record<string, unknown>) => {
|
||||
export const getApplicationConfig = (id: string) => {
|
||||
return request.get(`/apps/${id}/config`)
|
||||
}
|
||||
// 获取集群应配置
|
||||
// 获取集群应用配置
|
||||
export const getMultiAgentConfig = (id: string) => {
|
||||
return request.get(`/apps/${id}/multi-agent`)
|
||||
}
|
||||
// 获取 workflow应用配置
|
||||
export const getWorkflowConfig = (id: string) => {
|
||||
return request.get(`/apps/${id}/workflow`)
|
||||
}
|
||||
// 应用详情
|
||||
export const getApplication = (id: string) => {
|
||||
return request.get(`/apps/${id}`)
|
||||
}
|
||||
// 更新应用
|
||||
export const updateApplication = (id: string, values: Application) => {
|
||||
export const updateApplication = (id: string, values: ApplicationModalData) => {
|
||||
return request.put(`/apps/${id}`, values)
|
||||
}
|
||||
// 创建应用
|
||||
export const addApplication = (values: Application) => {
|
||||
export const addApplication = (values: ApplicationModalData) => {
|
||||
return request.post('/apps', values)
|
||||
}
|
||||
// 保存Agent配置
|
||||
@@ -36,11 +42,15 @@ export const saveAgentConfig = (app_id: string, values: Config) => {
|
||||
export const saveMultiAgentConfig = (app_id: string, values: Config) => {
|
||||
return request.put(`/apps/${app_id}/multi-agent`, values)
|
||||
}
|
||||
// 保存workflow配置
|
||||
export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
|
||||
return request.put(`/apps/${app_id}/workflow`, values)
|
||||
}
|
||||
// 模型比对试运行
|
||||
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
|
||||
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
|
||||
}
|
||||
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
|
||||
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
|
||||
}
|
||||
// 删除应用
|
||||
@@ -76,18 +86,7 @@ export const getConversationHistory = (share_token: string, data: { page: number
|
||||
})
|
||||
}
|
||||
// 发送体验对话
|
||||
export const sendConversation = (share_token: string, values: {
|
||||
message: string;
|
||||
web_search: boolean;
|
||||
memory: boolean;
|
||||
stream: boolean;
|
||||
conversation_id: string | null;
|
||||
}, onMessage, shareToken: string) => {
|
||||
// return request.post(`/public/share/chat`, values, {
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}`
|
||||
// }
|
||||
// })
|
||||
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
|
||||
return handleSSE(`/public/share/chat`, values, onMessage, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${shareToken}`
|
||||
|
||||
@@ -8,7 +8,14 @@ import type {
|
||||
import type {
|
||||
ConfigForm as ExtractionConfigForm
|
||||
} from '@/views/MemoryExtractionEngine/types'
|
||||
import type {
|
||||
ConfigForm as EmotionConfig
|
||||
} from '@/views/EmotionEngine/types'
|
||||
import type {
|
||||
ConfigForm as SelfReflectionEngineConfig
|
||||
} from '@/views/SelfReflectionEngine/types'
|
||||
import type { TestParams } from '@/views/MemoryConversation'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
|
||||
// 记忆对话
|
||||
export const readService = (query: TestParams) => {
|
||||
@@ -95,6 +102,23 @@ export const getChunkInsight = (end_user_id: string) => {
|
||||
export const getRagContent = (end_user_id: string) => {
|
||||
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
|
||||
}
|
||||
// 情感分布分析
|
||||
export const getWordCloud = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 })
|
||||
}
|
||||
// 高频情绪关键词
|
||||
export const getEmotionTags = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/tags`, { group_id, limit: 20 })
|
||||
}
|
||||
// 情绪健康指数
|
||||
export const getEmotionHealth = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/health`, { group_id, limit: 20 })
|
||||
}
|
||||
// 个性化建议
|
||||
export const getEmotionSuggestions = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 })
|
||||
}
|
||||
|
||||
/*************** end 用户记忆 相关接口 ******************************/
|
||||
|
||||
/****************** 记忆管理 相关接口 *******************************/
|
||||
@@ -132,9 +156,30 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
|
||||
return request.post('/memory-storage/update_config_extracted', values)
|
||||
}
|
||||
// 记忆萃取引擎-试运行
|
||||
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string }) => {
|
||||
return request.post('/memory-storage/pilot_run', values)
|
||||
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE('/memory-storage/pilot_run', values, onMessage)
|
||||
}
|
||||
// 情绪引擎-获取配置
|
||||
export const getMemoryEmotionConfig = (config_id: number | string) => {
|
||||
return request.get('/memory/emotion/read_config', { config_id: config_id })
|
||||
}
|
||||
// 情绪引擎-更新配置
|
||||
export const updateMemoryEmotionConfig = (values: EmotionConfig) => {
|
||||
return request.post('/memory/emotion/updated_config', values)
|
||||
}
|
||||
// 反思引擎-获取配置
|
||||
export const getMemoryReflectionConfig = (config_id: number | string) => {
|
||||
return request.get('/memory/reflection/configs', { config_id: config_id })
|
||||
}
|
||||
// 反思引擎-更新配置
|
||||
export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => {
|
||||
return request.post('/memory/reflection/save', values)
|
||||
}
|
||||
// 反思引擎-试运行
|
||||
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; dialogue_text: string; }) => {
|
||||
return request.get('/memory/reflection/run', values)
|
||||
}
|
||||
|
||||
/*************** end 记忆管理 相关接口 ******************************/
|
||||
|
||||
|
||||
|
||||
BIN
web/src/assets/images/application/chat.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
web/src/assets/images/application/debuggingEmpty.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
web/src/assets/images/conversation/analysisEmpty.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
web/src/assets/images/empty/404.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
web/src/assets/images/empty/noPermission.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
web/src/assets/images/menu/apiKey.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
web/src/assets/images/menu/apiKey_active.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
web/src/assets/images/menu/tool.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
web/src/assets/images/menu/tool_active.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
web/src/assets/images/workflow/agent_arbitration.png
Normal file
|
After Width: | Height: | Size: 835 B |
BIN
web/src/assets/images/workflow/agent_collaboration.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
web/src/assets/images/workflow/agent_scheduling.png
Normal file
|
After Width: | Height: | Size: 785 B |
BIN
web/src/assets/images/workflow/aggregator.png
Normal file
|
After Width: | Height: | Size: 668 B |
BIN
web/src/assets/images/workflow/answer.png
Normal file
|
After Width: | Height: | Size: 753 B |
BIN
web/src/assets/images/workflow/arrow.png
Normal file
|
After Width: | Height: | Size: 775 B |
BIN
web/src/assets/images/workflow/classification.png
Normal file
|
After Width: | Height: | Size: 849 B |
BIN
web/src/assets/images/workflow/code_execution.png
Normal file
|
After Width: | Height: | Size: 684 B |
BIN
web/src/assets/images/workflow/condition.png
Normal file
|
After Width: | Height: | Size: 343 B |
BIN
web/src/assets/images/workflow/empty.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
web/src/assets/images/workflow/end.png
Normal file
|
After Width: | Height: | Size: 792 B |
BIN
web/src/assets/images/workflow/http_request.png
Normal file
|
After Width: | Height: | Size: 745 B |
BIN
web/src/assets/images/workflow/iteration.png
Normal file
|
After Width: | Height: | Size: 612 B |
BIN
web/src/assets/images/workflow/llm.png
Normal file
|
After Width: | Height: | Size: 591 B |
BIN
web/src/assets/images/workflow/loop.png
Normal file
|
After Width: | Height: | Size: 815 B |
BIN
web/src/assets/images/workflow/memory_enhancement.png
Normal file
|
After Width: | Height: | Size: 810 B |
BIN
web/src/assets/images/workflow/model_selection.png
Normal file
|
After Width: | Height: | Size: 908 B |
BIN
web/src/assets/images/workflow/model_voting.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
web/src/assets/images/workflow/output_audit.png
Normal file
|
After Width: | Height: | Size: 624 B |
BIN
web/src/assets/images/workflow/parallel.png
Normal file
|
After Width: | Height: | Size: 979 B |
BIN
web/src/assets/images/workflow/parameter_extraction.png
Normal file
|
After Width: | Height: | Size: 699 B |
BIN
web/src/assets/images/workflow/process_evolution.png
Normal file
|
After Width: | Height: | Size: 516 B |
BIN
web/src/assets/images/workflow/rag.png
Normal file
|
After Width: | Height: | Size: 741 B |
BIN
web/src/assets/images/workflow/reasoning_control.png
Normal file
|
After Width: | Height: | Size: 815 B |
BIN
web/src/assets/images/workflow/robot-2-line@2x.png
Normal file
|
After Width: | Height: | Size: 471 B |
BIN
web/src/assets/images/workflow/self_optimization.png
Normal file
|
After Width: | Height: | Size: 922 B |
BIN
web/src/assets/images/workflow/self_reflection.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
web/src/assets/images/workflow/sensitive_detection.png
Normal file
|
After Width: | Height: | Size: 803 B |
BIN
web/src/assets/images/workflow/start.png
Normal file
|
After Width: | Height: | Size: 567 B |
BIN
web/src/assets/images/workflow/task_planning.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
web/src/assets/images/workflow/template_rendering.png
Normal file
|
After Width: | Height: | Size: 408 B |
BIN
web/src/assets/images/workflow/tools.png
Normal file
|
After Width: | Height: | Size: 869 B |
@@ -34,12 +34,12 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("rb:flex rb:items-center rb:border rb:rounded-[8px] rb:px-[8px] rb:text-[12px] rb:h-[24px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
<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:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
|
||||
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
|
||||
})} onClick={handleChange}>
|
||||
{icon && !checked && <img src={icon} className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
|
||||
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
|
||||
{icon && !checked && <img src={icon} className="rb:w-4 rb:h-4 rb:mr-1" />}
|
||||
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-4 rb:h-4 rb:mr-1" />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
84
web/src/components/Chat/ChatContent.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:17
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2025-12-11 13:40:18
|
||||
*/
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import type { ChatContentProps } from './types'
|
||||
|
||||
/**
|
||||
* 聊天内容显示组件
|
||||
* 负责渲染聊天消息列表,支持不同角色的消息样式和自动滚动
|
||||
*/
|
||||
const ChatContent: FC<ChatContentProps> = ({
|
||||
classNames,
|
||||
contentClassNames,
|
||||
data = [],
|
||||
streamLoading = false,
|
||||
empty,
|
||||
labelPosition = 'bottom',
|
||||
labelFormat,
|
||||
errorDesc
|
||||
}) => {
|
||||
// 滚动容器引用,用于控制自动滚动到底部
|
||||
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
|
||||
|
||||
// 当数据变化时,自动滚动到底部显示最新消息
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}, [data])
|
||||
return (
|
||||
<div ref={scrollContainerRef} className={clsx("rb:relative rb:overflow-y-auto", classNames)}>
|
||||
{data.length === 0
|
||||
? empty // 显示空状态
|
||||
: data.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:relative", {
|
||||
'rb:mt-6': index !== 0, // 非第一条消息添加上边距
|
||||
'rb:right-0 rb:text-right': item.role === 'user', // 用户消息右对齐
|
||||
'rb:left-0 rb:text-left': item.role === 'assistant', // 助手消息左对齐
|
||||
})}>
|
||||
{/* 流式加载时且内容为空则不显示 */}
|
||||
{streamLoading && item.content === ''
|
||||
? null
|
||||
: <>
|
||||
{/* 顶部标签(如时间戳、用户名等) */}
|
||||
{labelPosition === 'top' &&
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
|
||||
{labelFormat(item)}
|
||||
</div>
|
||||
}
|
||||
{/* 消息气泡框 */}
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-100', contentClassNames, {
|
||||
// 错误消息样式(内容为null且非助手消息)
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': errorDesc && item.role === 'assistant' && item.content === null,
|
||||
// 助手消息样式
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
|
||||
// 用户消息样式
|
||||
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant' && (item.content || item.content === ''),
|
||||
})}>
|
||||
{/* 使用Markdown组件渲染消息内容 */}
|
||||
<Markdown content={item.content ?? errorDesc ?? ''} />
|
||||
</div>
|
||||
{/* 底部标签(如时间戳、用户名等) */}
|
||||
{labelPosition === 'bottom' &&
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
|
||||
{labelFormat(item)}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatContent
|
||||
80
web/src/components/Chat/ChatInput.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2025-12-20 15:38:40
|
||||
*/
|
||||
import { useEffect } 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'
|
||||
import type { ChatInputProps } from './types'
|
||||
|
||||
/**
|
||||
* 聊天输入框组件
|
||||
* 提供消息输入、发送功能,支持键盘快捷键和加载状态显示
|
||||
*/
|
||||
const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputProps) => {
|
||||
const [form] = Form.useForm()
|
||||
// 监听表单值变化,用于控制发送按钮状态
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 当外部message为空时,清空表单
|
||||
useEffect(() => {
|
||||
if (!message) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [form, message])
|
||||
|
||||
// 当加载状态时,清空输入框
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<div className="rb:absolute rb:bottom-3 rb:left-0 rb:right-0">
|
||||
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:min-h-30">
|
||||
{/* 消息输入表单 */}
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="message" noStyle>
|
||||
<Input.TextArea
|
||||
className="rb:m-[10px_12px_10px_12px]! rb:p-0! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto]"
|
||||
variant="borderless"
|
||||
autoSize={{ minRows: 2, maxRows: 2 }}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
// Enter键发送,Shift+Enter换行
|
||||
if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) {
|
||||
e.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 底部操作区域 */}
|
||||
<Flex align="center" justify="space-between" className="rb:m-[0_10px_10px_10px]!">
|
||||
{/* 子组件内容(如按钮等) */}
|
||||
{children}
|
||||
{/* 发送按钮 - 根据状态显示不同图标 */}
|
||||
{loading
|
||||
? <img src={LoadingIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" />
|
||||
: !values || !values?.message || values?.message?.trim() === ''
|
||||
? <img src={SendDisabledIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" />
|
||||
: <img src={SendIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" onClick={onSend} />
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
47
web/src/components/Chat/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:09
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2025-12-11 13:43:51
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import ChatInput from './ChatInput'
|
||||
import type { ChatProps } from './types'
|
||||
import ChatContent from './ChatContent'
|
||||
|
||||
/**
|
||||
* 聊天组件 - 主要组件,由内容区域和输入框组成
|
||||
* 提供完整的聊天界面功能,包括消息显示和输入交互
|
||||
*/
|
||||
const Chat: FC<ChatProps> = ({
|
||||
empty,
|
||||
data,
|
||||
onChange,
|
||||
onSend,
|
||||
streamLoading = false,
|
||||
loading,
|
||||
contentClassName = '',
|
||||
children,
|
||||
labelFormat,
|
||||
errorDesc
|
||||
}) => {
|
||||
return (
|
||||
<div className="rb:h-full rb:relative rb:pt-2">
|
||||
{/* 聊天内容显示区域 */}
|
||||
<ChatContent
|
||||
classNames={contentClassName}
|
||||
data={data}
|
||||
streamLoading={streamLoading}
|
||||
empty={empty}
|
||||
labelFormat={labelFormat}
|
||||
errorDesc={errorDesc}
|
||||
/>
|
||||
|
||||
{/* 聊天输入框区域 */}
|
||||
<ChatInput onChange={onChange} onSend={onSend} loading={loading}>
|
||||
{children}
|
||||
</ChatInput>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Chat
|
||||
84
web/src/components/Chat/types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:45:54
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2025-12-11 13:43:52
|
||||
*/
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* 聊天消息项接口
|
||||
*/
|
||||
export interface ChatItem {
|
||||
/** 消息唯一标识 */
|
||||
id?: string;
|
||||
/** 会话ID */
|
||||
conversation_id?: string | null;
|
||||
/** 消息角色:用户或助手 */
|
||||
role?: 'user' | 'assistant';
|
||||
/** 消息内容 */
|
||||
content?: string | null;
|
||||
/** 创建时间 */
|
||||
created_at?: number | string
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天组件主要属性接口
|
||||
*/
|
||||
export interface ChatProps {
|
||||
/** 空状态显示内容 */
|
||||
empty?: ReactNode;
|
||||
/** 聊天数据列表 */
|
||||
data: ChatItem[];
|
||||
/** 输入内容变化回调 */
|
||||
onChange: (message: string) => void;
|
||||
/** 发送消息回调 */
|
||||
onSend: () => void;
|
||||
/** 流式加载状态 */
|
||||
streamLoading?: boolean;
|
||||
/** 加载状态 */
|
||||
loading: boolean;
|
||||
/** 内容区域自定义样式类名 */
|
||||
contentClassName?: string;
|
||||
/** 子组件内容 */
|
||||
children?: ReactNode;
|
||||
/** 标签格式化函数 */
|
||||
labelFormat: (item: ChatItem) => any;
|
||||
errorDesc?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天输入框组件属性接口
|
||||
*/
|
||||
export interface ChatInputProps {
|
||||
/** 当前输入消息 */
|
||||
message?: string;
|
||||
/** 输入内容变化回调 */
|
||||
onChange: (message: string) => void;
|
||||
/** 发送消息回调 */
|
||||
onSend: () => void;
|
||||
/** 加载状态 */
|
||||
loading: boolean;
|
||||
/** 子组件内容 */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天内容区域组件属性接口
|
||||
*/
|
||||
export interface ChatContentProps {
|
||||
/** 自定义样式类名 */
|
||||
classNames?: string | Record<string, boolean>;
|
||||
contentClassNames?: string | Record<string, boolean>;
|
||||
/** 聊天数据列表 */
|
||||
data: ChatItem[];
|
||||
/** 流式加载状态 */
|
||||
streamLoading: boolean;
|
||||
/** 空状态显示内容 */
|
||||
empty?: ReactNode;
|
||||
/** 标签位置:顶部或底部 */
|
||||
labelPosition?: 'top' | 'bottom';
|
||||
/** 标签格式化函数 */
|
||||
labelFormat: (item: ChatItem) => any;
|
||||
errorDesc?: string;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ interface ApiResponse<T> {
|
||||
items?: T[];
|
||||
}
|
||||
|
||||
interface CustomSelectProps {
|
||||
interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
|
||||
url: string;
|
||||
params?: Record<string, unknown>;
|
||||
valueKey?: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ interface EmptyProps {
|
||||
url?: string;
|
||||
size?: number | number[];
|
||||
title?: string;
|
||||
isNeedSubTitle?: boolean;
|
||||
subTitle?: string;
|
||||
className?: string;
|
||||
}
|
||||
@@ -13,6 +14,7 @@ const Empty: FC<EmptyProps> = ({
|
||||
url,
|
||||
size = 200,
|
||||
title,
|
||||
isNeedSubTitle = true,
|
||||
subTitle,
|
||||
className = '',
|
||||
}) => {
|
||||
@@ -20,12 +22,12 @@ const Empty: FC<EmptyProps> = ({
|
||||
const width = Array.isArray(size) ? size[0] : size ? size : url ? 200 : 88;
|
||||
const height = Array.isArray(size) ? size[1] : size ? size : url ? 200 : 88;
|
||||
|
||||
subTitle = subTitle || t('empty.tableEmpty');
|
||||
const curSubTitle = isNeedSubTitle ? (subTitle || t('empty.tableEmpty')) : null;
|
||||
return (
|
||||
<div className={`rb:flex rb:items-center rb:justify-center rb:flex-col ${className}`}>
|
||||
<img src={url || emptyIcon} alt="404" style={{ width: `${width}px`, height: `${height}px` }} />
|
||||
{title && <div className="rb:mt-[8px] rb:leading-[20px]">{title}</div>}
|
||||
{subTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-[16px] rb:text-[#5B6167]`}>{subTitle}</div>}
|
||||
{title && <div className="rb:mt-2 rb:leading-5">{title}</div>}
|
||||
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{subTitle}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs';
|
||||
import AppHeader from '@/components/Header';
|
||||
import Sider from '@/components/SiderMenu'
|
||||
import { useUser } from '@/store/user';
|
||||
import { cookieUtils } from '@/utils/request';
|
||||
|
||||
|
||||
const { Content } = Layout;
|
||||
@@ -18,7 +19,12 @@ const AuthLayout: FC = () => {
|
||||
// 自动更新面包屑导航
|
||||
useNavigationBreadcrumbs('manage');
|
||||
useEffect(() => {
|
||||
getUserInfo()
|
||||
const authToken = cookieUtils.get('authToken')
|
||||
if (!authToken && !window.location.hash.includes('#/login')) {
|
||||
window.location.href = `/#/login`;
|
||||
} else {
|
||||
getUserInfo()
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs';
|
||||
import AppHeader from '@/components/Header';
|
||||
import Sider from '@/components/SiderMenu';
|
||||
import { useUser } from '@/store/user';
|
||||
import { cookieUtils } from '@/utils/request';
|
||||
|
||||
|
||||
const { Content } = Layout;
|
||||
@@ -18,8 +19,13 @@ const AuthSpaceLayout: FC = () => {
|
||||
// 自动更新面包屑导航
|
||||
useNavigationBreadcrumbs('space');
|
||||
useEffect(() => {
|
||||
getUserInfo()
|
||||
getStorageType()
|
||||
const authToken = cookieUtils.get('authToken')
|
||||
if (!authToken && !window.location.hash.includes('#/login')) {
|
||||
window.location.href = `/#/login`;
|
||||
} else {
|
||||
getUserInfo()
|
||||
getStorageType()
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,7 @@ interface PageScrollListProps {
|
||||
|
||||
const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
|
||||
renderItem,
|
||||
query = {},
|
||||
query,
|
||||
url,
|
||||
column = 4,
|
||||
className = '',
|
||||
@@ -51,11 +51,11 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
|
||||
request.get(url, {
|
||||
page: page,
|
||||
pagesize: PAGE_SIZE,
|
||||
...query,
|
||||
...(query||{}),
|
||||
})
|
||||
.then((res) => {
|
||||
const response = res as ApiResponse;
|
||||
const results = Array.isArray(response.items) ? response.items : Array.isArray(response.hosts) ? response.hosts : Array.isArray(response) ? response : [];
|
||||
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : [];
|
||||
if (flag) {
|
||||
setData(results);
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { type FC, type ReactNode } from 'react'
|
||||
interface RbAlertProps {
|
||||
color?: 'blue' | 'green' | 'orange' | 'purple',
|
||||
children: ReactNode | string;
|
||||
icon: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ const colors = {
|
||||
|
||||
const RbAlert: FC<RbAlertProps> = ({ color = 'blue', icon, className, children }) => {
|
||||
return (
|
||||
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-[16px] rb:border-[1px] rb:rounded-[6px]`}>
|
||||
{icon && <span className="rb:text-[16px] rb:mr-[9px]">{icon}</span>}
|
||||
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-4 rb:border rb:rounded-md`}>
|
||||
{icon && <span className="rb:text-[16px] rb:mr-2.25">{icon}</span>}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ const RbCard: FC<RbCardProps> = ({
|
||||
title={typeof title === 'function' ? title() : title ?
|
||||
<div className="rb:flex rb:items-center">
|
||||
{avatarUrl
|
||||
? <img src={avatarUrl} className="rb:mr-[13px] rb:w-[48px] rb:h-[48px] rb:rounded-[8px]" />
|
||||
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
|
||||
: avatar ? avatar : null
|
||||
}
|
||||
<div className={
|
||||
|
||||
@@ -25,9 +25,10 @@ const RbModal: FC<ModalProps> = ({
|
||||
onOk={onOk}
|
||||
destroyOnHidden={true}
|
||||
className={`rb-modal ${className || ''}`}
|
||||
maskClosable={false}
|
||||
{...props}
|
||||
>
|
||||
<div className='rb:max-h-[550px] rb:overflow-y-auto rb:overflow-x-hidden'>
|
||||
<div className='rb:max-h-137.5 rb:overflow-y-auto rb:overflow-x-hidden'>
|
||||
{children}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -21,11 +21,11 @@ import modelActiveIcon from '@/assets/images/menu/model_active.svg';
|
||||
import memoryIcon from '@/assets/images/menu/memory.svg';
|
||||
import memoryActiveIcon from '@/assets/images/menu/memory_active.svg';
|
||||
import spaceIcon from '@/assets/images/menu/space.svg';
|
||||
import spaceActiveIcon from '@/assets/images/menu/space_acitve.svg';
|
||||
import spaceActiveIcon from '@/assets/images/menu/space_active.svg';
|
||||
import userIcon from '@/assets/images/menu/user.svg';
|
||||
import userActiveIcon from '@/assets/images/menu/user_active.svg';
|
||||
import userMemoryIcon from '@/assets/images/menu/userMemory.svg';
|
||||
import userMemoryActiveIcon from '@/assets/images/menu/userMemory_acitve.svg';
|
||||
import userMemoryActiveIcon from '@/assets/images/menu/userMemory_active.svg';
|
||||
import applicationIcon from '@/assets/images/menu/application.svg';
|
||||
import applicationActiveIcon from '@/assets/images/menu/application_active.svg';
|
||||
import knowledgeIcon from '@/assets/images/menu/knowledge.svg';
|
||||
@@ -34,6 +34,10 @@ import memoryConversationIcon from '@/assets/images/menu/memoryConversation.svg'
|
||||
import memoryConversationActiveIcon from '@/assets/images/menu/memoryConversation_active.svg';
|
||||
import memberIcon from '@/assets/images/menu/member.svg';
|
||||
import memberActiveIcon from '@/assets/images/menu/member_active.svg';
|
||||
import toolIcon from '@/assets/images/menu/tool.png';
|
||||
import toolActiveIcon from '@/assets/images/menu/tool_active.png';
|
||||
import apiKeyIcon from '@/assets/images/menu/apiKey.png';
|
||||
import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png';
|
||||
|
||||
// 图标路径映射表
|
||||
const iconPathMap: Record<string, string> = {
|
||||
@@ -57,6 +61,10 @@ const iconPathMap: Record<string, string> = {
|
||||
'memoryConversationActive': memoryConversationActiveIcon,
|
||||
'member': memberIcon,
|
||||
'memberActive': memberActiveIcon,
|
||||
'tool': toolIcon,
|
||||
'toolActive': toolActiveIcon,
|
||||
'apiKey': apiKeyIcon,
|
||||
'apiKeyActive': apiKeyActiveIcon,
|
||||
};
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
|
||||
interface TagProps {
|
||||
export interface TagProps {
|
||||
color?: 'processing' | 'error' | 'success' | 'warning' | 'default',
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
@@ -16,7 +16,7 @@ const colors = {
|
||||
|
||||
const Tag: FC<TagProps> = ({ color = 'processing', children, className }) => {
|
||||
return (
|
||||
<span className={`rb:inline-block rb:px-[4px] rb:py-[2px] rb:rounded-[4px] rb:text-[12px] rb:font-regular! rb:leading-[16px] rb:border-[1px] ${colors[color]} ${className || ''}`}>
|
||||
<span className={`rb:inline-block rb:px-1 rb:py-0.5 rb:rounded-sm rb:text-[12px] rb:font-regular! rb:leading-4 rb:border ${colors[color]} ${className || ''}`}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ export const en = {
|
||||
userMemory: 'User Memory',
|
||||
memberManagement: 'Member Management',
|
||||
memorySummary: 'Memory Summary',
|
||||
memoryConversation: 'Memory Verification',
|
||||
memoryConversation: 'Memory Validation',
|
||||
memorySummaryHandlers: 'Memory Summary Handlers',
|
||||
createMemorySummary: 'Create Memory Summary',
|
||||
memoryManagement: 'Memory Management',
|
||||
@@ -33,6 +33,11 @@ export const en = {
|
||||
knowledgeCreateDataset: 'Create Dataset',
|
||||
knowledgeDocumentDetails: 'Document Details',
|
||||
userMemoryDetail: 'UserMemory Detail',
|
||||
apiKeyManagement: 'API KEY Management',
|
||||
toolManagement: 'Tool Management',
|
||||
emotionEngine: 'Emotion Engine',
|
||||
emotionDetail: 'Emotion Memory',
|
||||
selfReflectionEngine: 'Self Reflection Engine',
|
||||
},
|
||||
dashboard: {
|
||||
totalMemoryCapacity: 'Total Memory Capacity',
|
||||
@@ -57,13 +62,13 @@ export const en = {
|
||||
forgettingExecutionRate: 'Forgetting Execution Rate',
|
||||
|
||||
memoryClassificationDistribution: 'Memory classification distribution',
|
||||
knowledgeBaseTypeDistribution: 'Distribution of knowledge base types',
|
||||
memoryGrowthTrend: 'Memory growth trend',
|
||||
knowledgeBaseTypeDistribution: 'Distribution of Knowledge Base Types',
|
||||
memoryGrowthTrend: 'Memory Growth Trend',
|
||||
corporateMemory: 'Corporate Memory',
|
||||
recentMemoryActivities: 'Recent memory activities',
|
||||
recentMemoryActivities: 'Recent Memory Activities',
|
||||
apiCallTrend: 'API call trend',
|
||||
quickOperation: 'Quick Operation',
|
||||
popularMemoryTags: 'Popular memory tags',
|
||||
popularMemoryTags: 'Popular Memory Tags',
|
||||
|
||||
title: 'Real-time Monitoring of Your AI Memory Core and Agent Status',
|
||||
loading: 'Loading...',
|
||||
@@ -115,9 +120,9 @@ export const en = {
|
||||
statements_count_desc: 'Manage {{count}} knowledge statements',
|
||||
triplet_count: 'Entity Relation Extraction',
|
||||
triplet_count_desc: 'Build {{entities_count}} entity nodes and {{relations_count}} relation connections',
|
||||
temporal_count: 'Time extraction',
|
||||
temporal_count: 'Time Extraction',
|
||||
temporal_count_desc: 'Record {{count}} time series information',
|
||||
|
||||
|
||||
dialogue: 'Dialogue',
|
||||
chunk: 'Chunk',
|
||||
statement: 'Statement',
|
||||
@@ -262,6 +267,7 @@ export const en = {
|
||||
exportList: 'Export List',
|
||||
selectPlaceholder: 'Please select {{title}}',
|
||||
inputPlaceholder: 'Please enter {{title}}',
|
||||
enterPlaceholder: 'Enter {{title}}',
|
||||
saveSuccess: 'Save Success',
|
||||
saveFailure: 'Save Failure',
|
||||
pleaseSelect: 'Please select',
|
||||
@@ -288,8 +294,8 @@ export const en = {
|
||||
addOption: 'Add Option',
|
||||
viewDetail: 'View Detail',
|
||||
deleteSuccess: 'Delete successfully',
|
||||
foldUp: 'Fold Up',
|
||||
expanded: 'Expanded',
|
||||
foldUp: 'Collapse',
|
||||
expanded: 'Expand',
|
||||
clickUploadIcon: 'click on the upload icon',
|
||||
export: 'Export',
|
||||
active: 'Active',
|
||||
@@ -318,10 +324,7 @@ export const en = {
|
||||
loginApiCannotRefreshToken: 'Login API cannot refresh token',
|
||||
logoutApiCannotRefreshToken: 'Logout API cannot refresh token',
|
||||
publicApiCannotRefreshToken: 'Public API cannot refresh token',
|
||||
refreshTokenNotExist: 'Refresh token does not exist',
|
||||
reset: 'Reset',
|
||||
statusEnabled: 'Enabled',
|
||||
statusDisabled: 'Disabled',
|
||||
refreshTokenNotExist: 'Refresh token does not exist'
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -329,7 +332,7 @@ export const en = {
|
||||
provider: 'Provider',
|
||||
status: 'Status',
|
||||
created: 'Created',
|
||||
configureBtn: 'Click to Configure',
|
||||
configureBtn: 'Run Configuration',
|
||||
name: 'Name',
|
||||
displayName: 'Display Name',
|
||||
nameRequired: 'Please enter model name',
|
||||
@@ -401,11 +404,19 @@ export const en = {
|
||||
|
||||
saveConfig: 'Save Config',
|
||||
apiKeyName: 'API Key Name',
|
||||
|
||||
llm: 'LLM',
|
||||
chat: 'Chat',
|
||||
embedding: 'Embedding',
|
||||
rerank: 'Rerank',
|
||||
openai: "Openai",
|
||||
dashscope: "Dashscope",
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: 'Home',
|
||||
selectSpace: 'Please select a workspace.',
|
||||
preview:'Preview',
|
||||
pleaseUploadFileFirst: 'Please upload file first',
|
||||
shareSuccess: 'Share successfully',
|
||||
shareFailed: 'Share failed',
|
||||
@@ -439,7 +450,7 @@ export const en = {
|
||||
recallTestDescription:'Input test questions to evaluate the recall effectiveness and relevance of the knowledge base',
|
||||
similarityThreshold: 'Similarity Threshold',
|
||||
startTesting: 'Start Testing',
|
||||
semanticSimilarity: 'Semantic similarity',
|
||||
semanticSimilarity: 'Semantic Similarity',
|
||||
recallResult: 'Result',
|
||||
setting: 'Setting',
|
||||
similarity: 'Similarity',
|
||||
@@ -501,7 +512,7 @@ export const en = {
|
||||
delete: 'Delete',
|
||||
rechunking: 'Rechunking',
|
||||
download: 'Download',
|
||||
selectSource:'Please select the source',
|
||||
selectSource:'Please select a source',
|
||||
confirmDelete: 'Are you sure you want to delete this document?',
|
||||
knowledgeBaseSettings: 'Knowledge Base Settings',
|
||||
modelConfiguration: 'Model Configuration',
|
||||
@@ -547,7 +558,6 @@ export const en = {
|
||||
fileName: 'File Name',
|
||||
fileList: 'File List',
|
||||
blockPreview: 'Block Preview',
|
||||
processingDocuments: 'Processing documents, please wait...',
|
||||
chunkContent: 'Chunk Content',
|
||||
sampleChunk: 'Sample Chunk Content',
|
||||
noFilesSelected: 'No files',
|
||||
@@ -557,10 +567,6 @@ export const en = {
|
||||
waiting: 'Waiting',
|
||||
startUpload: 'Total {{count}} files | Start Upload',
|
||||
startUploading: 'Start Uploading',
|
||||
startUploadConfirmTitle: 'Start Processing Documents',
|
||||
startUploadConfirmContent: 'Document processing will run in the background. You can choose to return to the list page immediately or stay on this page to view the processing progress.',
|
||||
returnToList: 'Return to List',
|
||||
stayOnPage: 'Stay on Page',
|
||||
uploadSuccess: 'Upload Success',
|
||||
datasetName: 'Dataset Name',
|
||||
pleaseEnterDatasetName: 'Please enter dataset name',
|
||||
@@ -652,6 +658,8 @@ export const en = {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
configurationName: 'Configuration Name',
|
||||
emotionEngine: 'Emotion Engine',
|
||||
reflectionEngine: 'Self-Reflection Engine'
|
||||
},
|
||||
member: {
|
||||
username: 'Username',
|
||||
@@ -659,16 +667,14 @@ export const en = {
|
||||
role: 'Role',
|
||||
lastLoginTime: 'Last Login Time',
|
||||
editMember: 'Edit Member',
|
||||
createMember: 'Create Member',
|
||||
createMember: 'Add Member',
|
||||
email: 'Email',
|
||||
inviteToMember: 'Invite to Member',
|
||||
inviteToMember: 'Member Role',
|
||||
member: 'Member',
|
||||
memberDesc: 'Can only use the application, cannot create the application',
|
||||
admin: 'Admin',
|
||||
adminDesc: 'Can create applications and manage team settings',
|
||||
sendInvitation: 'Send Invitation',
|
||||
manager: 'Admin',
|
||||
managerDesc: 'Can create applications and manage team settings',
|
||||
managerDesc: 'Can access applications, but cannot create or manage them',
|
||||
inviteLinkDesc: 'Invite link 【{{inviteLink}}】, please copy and send to the member',
|
||||
inviteLinkTip: 'Please copy the invite link and send it to the user to complete the invitation',
|
||||
},
|
||||
@@ -745,10 +751,10 @@ export const en = {
|
||||
workflowDesc: 'To be opened, please stay tuned',
|
||||
editApplication: 'Edit Application Info',
|
||||
|
||||
|
||||
|
||||
currentModel: 'Current Model',
|
||||
modelConfig: 'Model Config',
|
||||
parameterConfig: 'Parameter Config',
|
||||
parameterConfig: 'Parameter Configuration',
|
||||
|
||||
apply: 'Apply',
|
||||
resetDefault: 'Reset Default',
|
||||
@@ -792,7 +798,7 @@ export const en = {
|
||||
promptConfiguration: 'Prompt Configuration',
|
||||
configurationDesc: 'Define the role, capabilities, and behavioral guidelines of the Agent',
|
||||
aiPrompt: 'AI Prompt',
|
||||
promptPlaceholder: 'You are a professional AI assistant, and your responsibilities are ..',
|
||||
promptPlaceholder: 'You are a professional AI assistant, and your responsibility is to help users solve problems.',
|
||||
knowledgeBaseAssociation: 'Knowledge base association',
|
||||
associatedKnowledgeBase: 'Associated Knowledge Base',
|
||||
addKnowledgeBase: 'Add Knowledge Base',
|
||||
@@ -905,7 +911,7 @@ export const en = {
|
||||
frequency_penalty_desc: 'Frequency penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
presence_penalty_desc: 'Presence Penalty',
|
||||
n: 'Number of replies generated (n)',
|
||||
n: 'Number of Replies Generated (n)',
|
||||
n_desc: 'Number of replies generated',
|
||||
|
||||
contains: 'Contains {{include_count}} documents',
|
||||
@@ -918,7 +924,7 @@ export const en = {
|
||||
versionNumber: 'Version Number',
|
||||
versionNumberTip: 'Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0)',
|
||||
versionDescription: 'Version Description',
|
||||
versionDescriptionTip: 'Suggest explaining the feature updates, bug fixes, and optimization items for this release',
|
||||
versionDescriptionTip: 'Please describe the feature updates, bug fixes, and optimizations included in this release.',
|
||||
releasePreview: 'Release Preview',
|
||||
globalConfig: 'Global Config',
|
||||
globalConfigDesc: 'The global configuration will be applied to all associated knowledge bases as the default configuration. The configuration of a single knowledge base will override the global configuration.',
|
||||
@@ -939,7 +945,7 @@ export const en = {
|
||||
similarity_threshold: 'Semantic similarity threshold',
|
||||
similarity_threshold_desc: 'Only return results with semantic similarity higher than this threshold',
|
||||
similarity_threshold_desc1: 'The minimum similarity threshold for semantic retrieval',
|
||||
|
||||
|
||||
vector_similarity_weight: 'Vector Similarity Weight',
|
||||
vector_similarity_weight_desc: 'Only return results with BM25 scores above this threshold',
|
||||
vector_similarity_weight_desc1: 'The minimum BM25 score threshold for word segmentation retrieval',
|
||||
@@ -957,7 +963,7 @@ export const en = {
|
||||
versionNameTip: 'Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0)',
|
||||
agentName: 'Agent Name',
|
||||
roleType: 'Role Type',
|
||||
|
||||
|
||||
coordinator: 'Coordinator',
|
||||
analyzer: 'Analyzer',
|
||||
executor: 'Executor',
|
||||
@@ -967,8 +973,28 @@ export const en = {
|
||||
capabilities: 'Capabilities',
|
||||
subAgent: 'Sub Agent',
|
||||
maxChatCount: 'Add up to 4 models',
|
||||
addApiKey: 'Add API Key',
|
||||
ReplyException: 'Reply exception'
|
||||
ReplyException: 'Reply exception',
|
||||
|
||||
endpointConfigurationSubTitle: 'Configure API access address and supported HTTP methods',
|
||||
apiKeys: 'API Keys Management',
|
||||
apiKeySubTitle: 'Manage API keys, view usage and traffic statistics for each key',
|
||||
addApiKey: 'Add New API Key',
|
||||
apiKeyName: 'Key Name',
|
||||
apiKeyNamePlaceholder: 'e.g.: Production, Testing, Development',
|
||||
apiKeyDescPlaceholder: 'Describe the purpose of this Key',
|
||||
apiKeyTotal: 'Total Keys',
|
||||
apiKeyRequestTotal: 'Total Requests',
|
||||
qps: 'Average QPS',
|
||||
qpsLimit: 'QPS Limit',
|
||||
qpsLimitTip: '(Requests per second)',
|
||||
apiLimitConfig: 'Rate Limiting Configuration',
|
||||
qpsLimitDesc: 'Limit the maximum number of requests this Key can make per second',
|
||||
dailyUsageLimit: 'Daily Usage Limit',
|
||||
dailyUsageLimitDesc: 'Limit the maximum total number of requests this Key can make per day',
|
||||
dailyUsageLimitUnit: 'times/day',
|
||||
apiKeyDeleteContent: 'Once deleted, it cannot be recovered, and applications using this Key will not be able to access the API',
|
||||
currentValue: 'Current Value',
|
||||
qpsLimitUnit: 'times/second',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -984,7 +1010,7 @@ export const en = {
|
||||
memoryInsight: 'Memory Insight',
|
||||
relationshipNetwork: 'Relationship Network',
|
||||
aboutMe: 'About Me',
|
||||
foldUp: 'Fold Up',
|
||||
foldUp: 'Collapse',
|
||||
interestDistribution: 'Interest Distribution',
|
||||
memoryDetails: 'Memory Details',
|
||||
importantMomentsInLife: 'Important Moments in Life',
|
||||
@@ -995,7 +1021,7 @@ export const en = {
|
||||
memoryDetailEmpty: 'Please select a memory node',
|
||||
memoryDetailEmptyDesc: 'Click on any node in the above view to view detailed information',
|
||||
|
||||
totalNumOfMemories: 'Total number of memories',
|
||||
totalNumOfMemories: 'Total Number of Memories',
|
||||
footprintCity: 'Footprint City',
|
||||
totalNumOfPhotos: 'Total number of photos',
|
||||
importantRelationships: 'Important Relationships',
|
||||
@@ -1005,7 +1031,7 @@ export const en = {
|
||||
emotions: 'Emotions',
|
||||
occupation: 'Occupation',
|
||||
memories: 'memories',
|
||||
expanded: 'Expanded',
|
||||
expanded: 'Expand',
|
||||
description: 'Description',
|
||||
entityType: 'Entity Type',
|
||||
conversationMemory: 'Conversation Storage Content',
|
||||
@@ -1021,32 +1047,32 @@ export const en = {
|
||||
associated: 'Associated',
|
||||
notAssociated: 'Not Associated',
|
||||
storageType: 'Storage Type',
|
||||
rag: 'RAG storage',
|
||||
rag: 'RAG Storage',
|
||||
ragDesc: 'Based on vector retrieval, suitable for document Q&A and semantic search',
|
||||
neo4j: 'Graph storage',
|
||||
neo4j: 'Graph Storage',
|
||||
neo4jDesc: 'Based on knowledge graph, suitable for relational reasoning and path query',
|
||||
llmModel: 'LLM Model',
|
||||
embeddingModel: 'Embedding Model',
|
||||
rerankModel: 'Rerank Model'
|
||||
},
|
||||
memoryExtractionEngine: {
|
||||
title: 'Memory Engine module configuration center',
|
||||
title: 'Memory Engine Module Configuration Center',
|
||||
subTitle: 'Configure the parameters of six core modules, and view in real-time the impact on the memory processing conclusions of the "sample memory text (insights from the technology conference)". Any parameter changes will be instantly reflected in the results area on the right.',
|
||||
example: 'Example memory text',
|
||||
storageLayerModule: 'Storage layer module',
|
||||
example: 'Example Memory Text',
|
||||
storageLayerModule: 'Storage Layer Module',
|
||||
|
||||
enableLlmDedupBlockwise: 'Entity de-duplication (LLM decision-making)',
|
||||
enableLlmDisambiguation: 'Memory disambiguation function (LLM decision)',
|
||||
tNameStrict: 'Name matching threshold',
|
||||
tTypeStrict: 'Type matching threshold',
|
||||
tOverall: 'Comprehensive matching threshold',
|
||||
enableLlmDedupBlockwise: 'Entity De-duplication (LLM decision-making)',
|
||||
enableLlmDisambiguation: 'Memory Disambiguation Function (LLM decision)',
|
||||
tNameStrict: 'Name Matching Threshold',
|
||||
tTypeStrict: 'Type Matching Threshold',
|
||||
tOverall: 'Comprehensive Matching Threshold',
|
||||
|
||||
arrangementLayerModule: 'Arrangement layer module',
|
||||
queryMode: 'Query mode',
|
||||
arrangementLayerModule: 'Arrangement Layer Module',
|
||||
queryMode: 'Query Mode',
|
||||
queryModeSubTitle: 'Control whether to activate deeper search functions',
|
||||
deepRetrieval: 'Deep Retrieval',
|
||||
deepRetrievalMeaning: 'Control whether to initiate deep memory retrieval (true/false).',
|
||||
dataPreprocessing: 'Data preprocessing',
|
||||
dataPreprocessing: 'Data Preprocessing',
|
||||
dataPreprocessingSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.',
|
||||
|
||||
entityDeduplicationModuleThreshold: 'Entity de-duplication - name matching threshold',
|
||||
@@ -1055,20 +1081,19 @@ export const en = {
|
||||
control: 'Control',
|
||||
button: 'button',
|
||||
inputNumber: 'progress value',
|
||||
slider: 'progress value',
|
||||
slider: 'Slider',
|
||||
select: 'select',
|
||||
location: 'Location',
|
||||
CurrentValue: 'Current Value',
|
||||
type: 'Type',
|
||||
Meaning: 'Meaning',
|
||||
|
||||
exampleMemoryExtractionResults: 'Example memory extraction results',
|
||||
exampleMemoryExtractionResults: 'Example Memory Extraction Results',
|
||||
exampleMemoryExtractionResultsSubTitle: '(from a technology conference)',
|
||||
warning: 'When you modify the configuration items on the left, the extraction conclusion will be updated in real-time here',
|
||||
|
||||
extractTheNumberOfEntities: 'Extract the number of entities',
|
||||
extractTheNumberOfEntitiesDesc: 'Merge after deduplication: {{num}} (exact: {{exact}}, fuzzy: {{fuzzy}}, LLM: {{llm}})',
|
||||
|
||||
|
||||
numberOfEntityDisambiguation: 'Number of entity disambiguation',
|
||||
numberOfEntityDisambiguationDesc: 'Total {{num}} times (blocking: {{block_count}})',
|
||||
|
||||
@@ -1093,26 +1118,26 @@ export const en = {
|
||||
lateChunker: 'Late Chunker',
|
||||
debug: 'Debug',
|
||||
model: 'Model',
|
||||
chunkerStrategy: 'Chunker strategy',
|
||||
chunkerStrategy: 'Chunker Strategy',
|
||||
chunkerStrategyDesc: 'Choose a partitioning strategy.',
|
||||
intelligentSemanticPruning: 'Intelligent semantic pruning',
|
||||
intelligentSemanticPruning: 'Intelligent Semantic Pruning',
|
||||
intelligentSemanticPruningSubTitle: 'Whether to activate the intelligent semantic pruning function, select pruning scenarios, and set thresholds.',
|
||||
intelligentSemanticPruningFunction: 'Intelligent semantic pruning function',
|
||||
intelligentSemanticPruningFunction: 'Intelligent Semantic Pruning Function',
|
||||
intelligentSemanticPruningFunctionDesc: 'Whether to activate intelligent semantic pruning (true/false).',
|
||||
intelligentSemanticPruningScene: 'Intelligent semantic pruning scene',
|
||||
intelligentSemanticPruningScene: 'Intelligent Semantic Pruning Scene',
|
||||
intelligentSemanticPruningSceneDesc: 'Select intelligent semantic pruning scene (education, online_service, outbound).',
|
||||
intelligentSemanticPruningThreshold: 'Intelligent semantic pruning threshold',
|
||||
intelligentSemanticPruningThreshold: 'Intelligent Semantic Pruning Threshold',
|
||||
intelligentSemanticPruningThresholdDesc: 'Set intelligent semantic pruning threshold (0-0.9).',
|
||||
selfReflexionEngine: 'Self-reflexion engine',
|
||||
reflectionEngine: 'Self-Reflexion Engine',
|
||||
selfReflexionEngineSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.',
|
||||
enableSelfReflexion: 'Enable self-reflexion',
|
||||
iterationPeriod: 'Iteration period',
|
||||
iterationPeriod: 'Iteration Period',
|
||||
iterationPeriodDesc: 'Set the iteration period for self-reflexion (hourly, 3_hours, 6_hours, 12_hours, daily).',
|
||||
reflexionRange: 'Reflexion range',
|
||||
reflexionRange: 'Reflexion Range',
|
||||
reflexionRangeDesc: "When selecting 'Database', the iteration cycle is non configurable and fixed at daily",
|
||||
retrieval: 'Retrieval',
|
||||
database: 'Database',
|
||||
reflectOnTheBaseline: 'Reflect on the baseline',
|
||||
reflectOnTheBaseline: 'Reflect on the Baseline',
|
||||
basedOnTime: 'Based on time',
|
||||
basedOnFacts: 'Based on facts',
|
||||
basedOnFactsAndTime: 'Based on facts and time',
|
||||
@@ -1124,15 +1149,15 @@ export const en = {
|
||||
education: 'Education',
|
||||
online_service: 'Online service',
|
||||
outbound: 'Outbound',
|
||||
entityDeduplicationDisambiguation: 'Entity de-duplication disambiguation',
|
||||
entityDeduplicationDisambiguation: 'Entity De-duplication Disambiguation',
|
||||
entityDeduplicationDisambiguationSubTitle: 'Control the LLM decision-making function for memory deduplication and disambiguation, set various matching thresholds, and affect the accuracy of memory deduplication.',
|
||||
semanticAnchorAnnotationModule: 'Semantic anchor annotation module',
|
||||
semanticAnchorAnnotationModule: 'Semantic Anchor Annotation Module',
|
||||
semanticAnchorAnnotationModuleSubTitle: 'Control the granularity of statement extraction and whether to include dialog context.',
|
||||
statementGranularity: 'Statement granularity',
|
||||
statementGranularity: 'Statement Granularity',
|
||||
statementGranularityDesc: 'Statement extraction granularity (1-3): 1 represents breaking down sentences into different statements, 2 represents sentence level, and 3 represents merging sentences into paragraphs.',
|
||||
includeDialogueContext: 'Include dialogue context',
|
||||
includeDialogueContext: 'Include Dialogue Context',
|
||||
includeDialogueContextDesc: 'Control whether the complete dialogue context is included in the extraction process (true/false).',
|
||||
maxDialogueContextChars: 'Max dialogue context chars',
|
||||
maxDialogueContextChars: 'Max Dialogue Context Chars',
|
||||
maxDialogueContextCharsDesc: 'The maximum number of characters included in the dialogue context (to avoid character limit issues) (greater than 100).',
|
||||
coreEntitiesAfterDedup: 'Core entities after deduplication',
|
||||
extractRelationalTriples: 'Extracted relational triples (partial)',
|
||||
@@ -1155,7 +1180,28 @@ Memory Bear: Qin succeeded for several reasons: Shang Yang’s reforms were thor
|
||||
|
||||
Student: Then switching to Tang history: after the An Lushan Rebellion, the central government began reforms, so why did regional warlordism (the fanzhen problem) actually get worse?
|
||||
Memory Bear: After the rebellion, regional warlordism intensified for several reasons: military governors (jiedushi) held the power to recruit troops, control local finances, and command military forces, effectively becoming regional warlords; the central government’s finances declined due to the breakdown of the equal-field system and the collapse of the tax-labor system, making it increasingly unable to support the army, which pushed military forces to rely on the jiedushi; the recruitment-based military system made soldiers loyal to individual commanders rather than the state; eunuchs controlled the imperial guards, the civil bureaucracy lost influence, and the central government’s ability to balance regional power weakened.
|
||||
`
|
||||
`,
|
||||
|
||||
warning: 'When you modify the configuration items on the left, click [Debug], and the extraction conclusions will be updated in real time here',
|
||||
processing: 'Configuration updated, re-extracting sample memory...',
|
||||
success: 'Memory extraction completed!',
|
||||
overallProgress: 'Overall Progress',
|
||||
text_preprocessing: 'Text Preprocessing',
|
||||
fragment: 'Fragment',
|
||||
knowledge_extraction: 'Knowledge Extraction',
|
||||
creating_nodes_edges: 'Creating Entity Relationships',
|
||||
deduplication: 'Deduplication and Disambiguation',
|
||||
status: {
|
||||
pending: 'Pending',
|
||||
processing: 'Processing',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed'
|
||||
},
|
||||
time: 'Time: ',
|
||||
text_preprocessing_desc: 'Text split into {{count}} semantic fragments',
|
||||
knowledge_extraction_desc: 'Knowledge extraction completed, identified {{entities}} entities, {{statements}} statements, {{temporal_ranges_count}} temporal extractions, {{triplets}} triplets',
|
||||
creating_nodes_edges_desc: 'Entity relationship creation completed, {{num}} relationships in total',
|
||||
deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total'
|
||||
},
|
||||
memoryConversation: {
|
||||
searchPlaceholder: 'Input user ID...',
|
||||
@@ -1237,6 +1283,417 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
tableEmpty: 'There are currently no data',
|
||||
loadingEmpty: 'The content is loading…',
|
||||
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
|
||||
},
|
||||
apiKey: {
|
||||
name: 'Project Name',
|
||||
createApiKey: 'Create API Key',
|
||||
updateApiKey: 'Edit API Key',
|
||||
id: 'ID',
|
||||
created_at: 'Created At',
|
||||
description: 'Description',
|
||||
memoryEngine: 'Memory Engine',
|
||||
knowledgeBase: 'Knowledge Base',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
expires_at: 'Expiration At',
|
||||
apiKey: 'API Key',
|
||||
status: 'Status',
|
||||
createdAt: 'Created At',
|
||||
expiresAt: 'Expires At',
|
||||
requestsPerMinute: 'Requests/Minute',
|
||||
viewDetail: 'View Details',
|
||||
disable: 'Disable',
|
||||
enable: 'Enable',
|
||||
baseInfo: 'Basic Information',
|
||||
permissionInfo: 'Permission Information',
|
||||
is_expired: 'Status',
|
||||
active: 'Active',
|
||||
inactive: 'Expired'
|
||||
},
|
||||
tool: {
|
||||
mcp: 'MCP Service',
|
||||
inner: 'Built-in Tools',
|
||||
custom: 'Custom Tools',
|
||||
mcpSearchPlaceholder: 'Search MCP services...',
|
||||
innerSearchPlaceholder: 'Search tools...',
|
||||
customSearchPlaceholder: 'Search custom tools...',
|
||||
addService: 'Add MCP Service',
|
||||
addServiceSuccess: 'Service added successfully',
|
||||
server_url: 'Service URL',
|
||||
lastConnection: 'Last Connection',
|
||||
responseTime: 'Response Time',
|
||||
status: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
},
|
||||
testConnectionSuccess: 'Connection test successful',
|
||||
serviceEndpoint: 'Service Endpoint URL',
|
||||
serviceEndpointPlaceholder: 'URL of the service endpoint',
|
||||
serviceEndpointExtra: 'Complete access address of the MCP service',
|
||||
nameAndIcon: 'Name and Icon',
|
||||
namePlaceholder: 'Name your MCP service',
|
||||
serverIdentifier: 'Server Identifier',
|
||||
serverIdentifierPlaceholder: 'Unique server identifier, e.g. my-mcp-server',
|
||||
serverIdentifierLength: 'Maximum 24 characters',
|
||||
serverIdentifierPattern: 'Supports lowercase letters, numbers, underscores and hyphens',
|
||||
description: 'Description',
|
||||
auth: 'Authentication',
|
||||
requestHeader: 'Request Headers',
|
||||
config: 'Configuration',
|
||||
authType: 'Authentication Type',
|
||||
noAuth: 'No Authentication',
|
||||
apiKey: 'API Key',
|
||||
basicAuth: 'Basic Auth',
|
||||
bearerToken: 'Bearer Token',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
requestHeaderDesc: 'Additional HTTP request headers sent to MCP server',
|
||||
addRequestHeader: 'Add Request Header',
|
||||
editRequestHeader: 'Edit Request Header',
|
||||
requestHeaderName: 'Request Header Name',
|
||||
requestHeaderValue: 'Request Header Value',
|
||||
timeout: 'Timeout (seconds)',
|
||||
sseReadTimeout: 'SSE Read Timeout (seconds)',
|
||||
saveAndTest: 'Save and Test',
|
||||
|
||||
timeFormat: 'Time Formatting',
|
||||
timeZoneConversion: 'Time Zone Conversion',
|
||||
timestampConversion: 'Timestamp Conversion',
|
||||
timeCalculation: 'Time Calculation',
|
||||
time_desc: 'Date and Time Processing',
|
||||
DateTimeTool_features: 'Provides time format conversion, time zone conversion, timestamp calculation and other functions',
|
||||
currentTime: 'Current Time',
|
||||
timestamp: 'Timestamp',
|
||||
localTime: 'Local Time',
|
||||
utcTime: 'UTC Time',
|
||||
secondsTimestamp: 'Timestamp (seconds)',
|
||||
millisecondsTimestamp: 'Timestamp (milliseconds)',
|
||||
enterTimestamp: 'Enter Timestamp',
|
||||
conversion: 'Conversion',
|
||||
conversionResult: 'Conversion Result',
|
||||
chooseFormatType: 'Choose Format',
|
||||
|
||||
JsonTool_desc: 'Data Format Conversion',
|
||||
JsonTool_features: 'JSON formatting, compression, validation and conversion functions',
|
||||
jsonFormat: 'JSON Formatting',
|
||||
jsonGzip: 'JSON Compression',
|
||||
jsonCheck: 'JSON Validation',
|
||||
jsonConversion: 'Format Conversion',
|
||||
jsonEg: 'Example JSON',
|
||||
enterJson: 'Enter JSON',
|
||||
jsonPlaceholder: 'Enter JSON data, e.g.: {"name": "test", "value": 123}',
|
||||
clear: 'Clear',
|
||||
parse: 'Paste',
|
||||
format: 'Format',
|
||||
minify: 'Minify',
|
||||
validate: 'Validate',
|
||||
convert: 'Escape',
|
||||
outputResult: 'Output Result',
|
||||
validJosn: 'JSON format is correct, validation passed!',
|
||||
|
||||
BaiduSearchTool_desc: 'Search Engine Service',
|
||||
BaiduSearchTool_features: 'Integrated Baidu Search API, providing web search, news search and other functions',
|
||||
webSearch: 'Web Search',
|
||||
newsSearch: 'News Search',
|
||||
imageSearch: 'Image Search',
|
||||
realTimeResults: 'Real-time Results',
|
||||
configStatus: 'Configuration Status',
|
||||
hasApiKey: 'API configured and enabled',
|
||||
needApiKey: 'Need to configure API Key',
|
||||
|
||||
MinerUTool_desc: 'PDF Parsing Tool',
|
||||
MinerUTool_features: 'High-precision PDF document parsing tool, supports text, table, and image extraction',
|
||||
pdfParser: 'PDF Parser',
|
||||
tableExtraction: 'Table Extraction',
|
||||
imageRecognition: 'Image Recognition',
|
||||
textExtraction: 'Text Extraction',
|
||||
|
||||
TextInTool_desc: 'OCR Text Recognition',
|
||||
TextInTool_features: 'Intelligent OCR text recognition service, supports multi-language and handwriting recognition',
|
||||
universalOCR: 'Universal OCR',
|
||||
handwritingRecognition: 'Handwriting Recognition',
|
||||
multilingualSupport: 'Multi-language Support',
|
||||
highPrecisionRecognition: 'High Precision Recognition',
|
||||
|
||||
configDesc: 'Configuration Description',
|
||||
BaiduSearchTool_config_desc: 'To use Baidu Search API, you need to apply for API Key and Secret Key on Baidu Open Platform first.',
|
||||
MinerUTool_config_desc: 'MinerU is a high-precision PDF document parsing tool that requires an API Key to use.',
|
||||
TextInTool_config_desc: 'TextIn provides intelligent OCR text recognition service with multi-language support.',
|
||||
link: 'Application URL',
|
||||
api_key: 'API Key',
|
||||
BaiduSearchTool_api_key_desc: 'API Key obtained from Baidu Open Platform',
|
||||
MinerUTool_api_key_desc: 'API Key obtained from MinerU platform',
|
||||
secret_key: 'Secret Key',
|
||||
BaiduSearchTool_secret_key_desc: 'Secret Key obtained from Baidu Open Platform',
|
||||
TextInTool_secret_key_desc: 'Secret Key obtained from TextIn platform',
|
||||
type: 'Search Type',
|
||||
pagesize: 'Results Per Page',
|
||||
pagesize_desc: 'Number of results returned per search ({{count1}}-{{count2}})',
|
||||
BaiduSearchTool_enable: 'Enable Baidu Search',
|
||||
BaiduSearchTool_safe_enable: 'Enable Safe Search',
|
||||
BaiduSearchTool_safe_enable_desc: 'Filter inappropriate content',
|
||||
api_address: 'API Address',
|
||||
MinerUTool_api_address_desc: 'Uses official API address by default, can be modified if privately deployed',
|
||||
TextInTool_api_address_desc: 'Uses official API address by default',
|
||||
parsing_mode: 'Parsing Mode',
|
||||
auto_recognition: 'Auto Recognition',
|
||||
pure_text_mode: 'Pure Text Mode',
|
||||
table_priority: 'Table Priority',
|
||||
image_priority: 'Image Priority',
|
||||
MinerUTool_timeout_desc: 'PDF parsing timeout (10-300 seconds)',
|
||||
MinerUTool_enable: 'Enable MinerU',
|
||||
MinerUTool_extract_images_enable: 'Extract Images',
|
||||
MinerUTool_extract_images_enable_desc: 'Whether to extract image content from PDF',
|
||||
app_id: 'APP ID',
|
||||
TextInTool_app_id_desc: 'App ID obtained from TextIn platform',
|
||||
language_identification: 'Recognition Language',
|
||||
automatic_detection: 'Automatic Detection',
|
||||
simplified_chinese: 'Simplified Chinese',
|
||||
traditional_chinese: 'Traditional Chinese',
|
||||
english: 'English',
|
||||
japanese: 'Japanese',
|
||||
korean_language: 'Korean',
|
||||
pattern_recognition: 'Recognition Mode',
|
||||
universal_identification: 'Universal Recognition',
|
||||
high_precision_identification: 'High Precision Recognition',
|
||||
handwriting_recognition: 'Handwriting Recognition',
|
||||
formula_recognition: 'Formula Recognition',
|
||||
TextInTool_enable: 'Enable TextIn',
|
||||
return_text_position_enable: 'Return Text Position Info',
|
||||
return_text_position_enable_desc: 'Whether to return coordinate positions of recognized text',
|
||||
|
||||
addCustom: 'Add Custom Tool',
|
||||
editCustom: 'Edit Custom Tool',
|
||||
schema: 'Schema',
|
||||
schemaPlaceholder: 'Enter your OpenAPI schema here',
|
||||
authentication: 'Authentication Method',
|
||||
tag: 'Tag',
|
||||
created_at: 'Created At',
|
||||
headerName: 'Header Name',
|
||||
null: 'None',
|
||||
tagDesc: 'Multiple tags separated by commas',
|
||||
availableTools: 'Available Tools',
|
||||
name: 'Name',
|
||||
desc: 'Description',
|
||||
method: 'Method',
|
||||
path: 'Path',
|
||||
viewDetail: 'View Details'
|
||||
},
|
||||
workflow: {
|
||||
coreNode: 'Core Nodes',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
answer: 'Answer',
|
||||
aiAndCognitiveProcessing: 'AI & Cognitive Processing',
|
||||
llm: 'Large Language Model (LLM)',
|
||||
model_selection: 'Model Selection',
|
||||
model_voting: 'Model Voting',
|
||||
rag: 'Knowledge Retrieval (RAG)',
|
||||
classification: 'Smart Classification',
|
||||
parameter_extraction: 'Parameter Extraction',
|
||||
flowControl: 'Flow Control',
|
||||
condition: 'Conditional Branch',
|
||||
iteration: 'Iteration',
|
||||
loop: 'Loop',
|
||||
parallel: 'Parallel Execution',
|
||||
aggregator: 'Aggregator',
|
||||
externalInteraction: 'External Interaction',
|
||||
http_request: 'HTTP Request',
|
||||
tools: 'Tools',
|
||||
code_execution: 'Code Execution',
|
||||
template_rendering: 'Template Rendering',
|
||||
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
||||
task_planning: 'Task Planning',
|
||||
reasoning_control: 'Reasoning Control',
|
||||
self_reflection: 'Self Reflection',
|
||||
memory_enhancement: 'Memory Enhancement',
|
||||
agentCollaborationNode: 'Agent Collaboration Nodes',
|
||||
agent_scheduling: 'Agent Scheduling',
|
||||
agent_collaboration: 'Agent Collaboration',
|
||||
agent_arbitration: 'Agent Arbitration',
|
||||
safetyAndCompliance: 'Safety & Compliance',
|
||||
sensitive_detection: 'Sensitive Detection',
|
||||
output_audit: 'Output Audit',
|
||||
evolutionAndGovernance: 'Evolution & Governance',
|
||||
self_optimization: 'Self Optimization',
|
||||
process_evolution: 'Process Evolution',
|
||||
|
||||
clickToConfigure: 'Click to configure node parameters',
|
||||
nodeProperties: 'Node Properties',
|
||||
empty: "Emmm... The box is empty, there's nothing here~",
|
||||
nodeName: 'Node Name',
|
||||
|
||||
|
||||
config: {
|
||||
llm: {
|
||||
model_id: 'Model',
|
||||
temperature: 'Temperature',
|
||||
max_tokens: 'Max Tokens',
|
||||
},
|
||||
start: {
|
||||
variables: 'Input Fields',
|
||||
|
||||
string: 'Text',
|
||||
number: 'Number',
|
||||
boolean: 'Checkbox',
|
||||
array: 'Dropdown Options',
|
||||
object: 'Object',
|
||||
|
||||
addVariable: 'Add Variable',
|
||||
editVariable: 'Edit Variable',
|
||||
variableType: 'Variable Type',
|
||||
variableName: 'Variable Name',
|
||||
description: 'Display Name',
|
||||
default: 'Default Value',
|
||||
required: 'Required',
|
||||
max_length: 'Max Length',
|
||||
defaultChecked: 'Checked',
|
||||
notDefaultChecked: 'Not Checked',
|
||||
options: 'Options',
|
||||
},
|
||||
end: {
|
||||
output: 'Reply'
|
||||
}
|
||||
},
|
||||
|
||||
clear: 'Clear',
|
||||
run: 'Run',
|
||||
save: 'Save',
|
||||
export: 'Export',
|
||||
variableConfig: 'Variable Configuration',
|
||||
variableRequired: 'required',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: 'Emotion Engine Configuration',
|
||||
|
||||
emotion_enabled: 'Enable Emotion Engine',
|
||||
emotion_enabled_desc: 'Automatically analyze emotional tendencies in conversations',
|
||||
|
||||
emotion_model_id: 'Emotion Analysis Model',
|
||||
emotion_model_id_desc: 'Different models vary in accuracy and speed',
|
||||
|
||||
emotion_extract_keywords: 'Emotion Keyword Extraction',
|
||||
emotion_extract_keywords_subTitle: 'Automatically extract emotion-related keywords from conversations',
|
||||
emotion_extract_keywords_desc: 'Extract emotional keywords like "happy", "disappointed", "excited" to better understand user emotions',
|
||||
emotion_min_intensity: 'Confidence Threshold',
|
||||
emotion_min_intensity_desc: 'Higher confidence leads to more accurate recognition, but may miss some information',
|
||||
|
||||
emotion_enable_subject: 'Emotion Subject Classification',
|
||||
emotion_enable_subject_subTitle: 'Identify emotion attribution (self/other/object)',
|
||||
emotion_enable_subject_desc: 'Distinguish emotion subjects: self (I feel happy), other (he is angry), object (this product is great)',
|
||||
|
||||
currentValue: 'Current Value',
|
||||
emotion_min_intensity_description: 'Confidence Threshold Description',
|
||||
question: 'What is Confidence Threshold?',
|
||||
answer: 'Confidence threshold is the "certainty level" standard for emotion engine to judge emotions. When the emotional confidence analyzed by AI is lower than the set threshold, the emotion will not be recorded.',
|
||||
differentTitle: 'Impact of Different Thresholds',
|
||||
advantage: 'Advantages',
|
||||
shortcoming: 'Disadvantages',
|
||||
scene: 'Applicable Scenarios',
|
||||
low_title: 'Low Threshold (0.0 - 0.4)',
|
||||
low_tag: 'Sensitive',
|
||||
low_advantage: 'Can capture more subtle emotional changes without missing potential emotional signals',
|
||||
low_shortcoming: 'May cause misjudgments, identifying neutral or unclear expressions as specific emotions',
|
||||
low_scene: 'Scenarios requiring comprehensive understanding of user emotional fluctuations with low accuracy requirements',
|
||||
middle_title: 'Medium Threshold (0.5 - 0.7)',
|
||||
middle_tag: 'Recommended',
|
||||
middle_advantage: 'Balances accuracy and coverage, can identify obvious emotions without being overly sensitive',
|
||||
middle_shortcoming: 'May miss some less obvious emotional expressions',
|
||||
middle_scene: 'Most daily conversation scenarios, suitable for general emotional analysis needs',
|
||||
high_title: 'High Threshold (0.8 - 1.0)',
|
||||
high_tag: 'Precise',
|
||||
high_advantage: 'Only records very clear emotional expressions, extremely high accuracy with low misjudgment rate',
|
||||
high_shortcoming: 'Will miss a large amount of less obvious emotional information, low data coverage',
|
||||
high_scene: 'Scenarios requiring extremely high accuracy, such as emotional crisis warnings and important decision references',
|
||||
|
||||
configSuggest: 'Configuration Suggestions',
|
||||
first: 'First Time Use',
|
||||
first_desc: 'Recommend starting with medium threshold (0.6-0.7), observe for a period and adjust based on actual results',
|
||||
customer_service: 'Customer Service Scenarios',
|
||||
customer_service_desc: 'Recommend using lower threshold (0.4-0.6) to timely capture user dissatisfaction',
|
||||
data_analysis: 'Data Analysis',
|
||||
data_analysis_desc: 'Recommend using medium threshold (0.6-0.7) to ensure data quality while having sufficient sample size',
|
||||
risk_warning: 'Risk Warning',
|
||||
risk_warning_desc: 'Recommend using higher threshold (0.7-0.8) to ensure warning accuracy',
|
||||
|
||||
actual_case: 'Actual Case',
|
||||
user_input: 'User Input',
|
||||
user_input_message: '"This feature is okay, but there are some minor issues"',
|
||||
neutral_emotion: 'Neutral Emotion',
|
||||
neutral_emotion_tag: 'All thresholds will record',
|
||||
minor_dissatisfaction: 'Minor Dissatisfaction',
|
||||
minor_dissatisfaction_tag: 'Only low/medium thresholds will record',
|
||||
expect_improvement: 'Expect Improvement',
|
||||
expect_improvement_tag: 'Only low threshold will record',
|
||||
confidence: 'Confidence'
|
||||
},
|
||||
emotionDetail: {
|
||||
wordCloud: 'Emotion Distribution Analysis',
|
||||
pieces: 'items',
|
||||
emotionTags: 'High-Frequency Emotion Keywords',
|
||||
joy: 'Joy',
|
||||
anger: 'Anger',
|
||||
sadness: 'Sadness',
|
||||
fear: 'Fear',
|
||||
neutral: 'Neutral',
|
||||
surprise: 'Surprise',
|
||||
|
||||
health: 'Emotional Health Index',
|
||||
positivity_rate: 'Positivity Rate',
|
||||
stability: 'Stability',
|
||||
resilience: 'Resilience',
|
||||
suggestions: 'Personalized Suggestions',
|
||||
},
|
||||
reflectionEngine: {
|
||||
reflectionEngineConfig: 'Reflection Engine Configuration',
|
||||
reflection_enabled: 'Enable Reflection Engine',
|
||||
reflection_enabled_desc: 'Transform episodic memory into semantic memory, forming long-term cognition',
|
||||
reflection_model_id: 'Reflection Model',
|
||||
reflection_model_id_desc: 'Different models vary in accuracy and speed',
|
||||
reflection_period_in_hours: 'Iteration Period',
|
||||
reflection_period_in_hours_desc: 'Determines how often the system performs memory reflection and refinement',
|
||||
reflexion_range: 'Reflection Range',
|
||||
partial: 'Partial Reflection (New memories only)',
|
||||
all: 'Full Reflection (All historical memories)',
|
||||
reflexion_range_desc: '',
|
||||
baseline: 'Reflection Baseline',
|
||||
baseline_desc: '',
|
||||
TIME: 'Time-based (Temporal relationships)',
|
||||
FACT: 'Fact-based (Knowledge points)',
|
||||
HYBRID: 'Fact + Time (Comprehensive dimension)',
|
||||
quality_assessment: 'Enable Quality Assessment',
|
||||
quality_assessment_desc: 'Automatically evaluate memory accuracy, completeness and timeliness',
|
||||
memory_verify: 'Enable Memory Verification',
|
||||
memory_verify_desc: 'Detect sensitive information and filter inappropriate content',
|
||||
oneHour: 'Every 1 hour',
|
||||
threeHours: 'Every 3 hours',
|
||||
sixHours: 'Every 6 hours',
|
||||
twelveHours: 'Every 12 hours',
|
||||
daily: 'Daily',
|
||||
run: 'Run Debug',
|
||||
example: 'Raw Data',
|
||||
exampleText: 'I went to Beijing for work in the spring of 2023, and have basically been working in Beijing ever since, without changing cities much. However, due to company restructuring, I was transferred to Shanghai for about half a year in the first half of 2024, during which time I checked in at the Shanghai office every day. At that time, my employment records still used my previous identity information, with ID number 11010119950308123X and bank card 6222023847595898, which have never changed. By the way, I have actually been living in Beijing since 2023 and have never left Beijing for long periods. The Shanghai period was more like remote collaboration.',
|
||||
runTitle: 'Reflection Test Run',
|
||||
status: 'Status',
|
||||
message: 'Message',
|
||||
|
||||
conflictDetection: 'Conflict Detection',
|
||||
reason: 'Conflict Reason',
|
||||
solution: 'Solution',
|
||||
|
||||
qualityAssessment: 'Quality Assessment',
|
||||
qualityAssessmentObj: {
|
||||
score: 'Quality Score',
|
||||
summary: 'Assessment Summary',
|
||||
},
|
||||
|
||||
privacyAudit: 'Privacy Audit',
|
||||
privacyAuditObj: {
|
||||
true: 'Yes',
|
||||
false: 'No',
|
||||
has_privacy: 'Contains Privacy Information',
|
||||
privacy_types: 'Privacy Types',
|
||||
summary: 'Audit Summary',
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,16 +28,21 @@ export const zh = {
|
||||
spaceManagement: '空间管理',
|
||||
memoryExtractionEngine: '记忆提取引擎',
|
||||
forgettingEngine: '遗忘引擎',
|
||||
apiKeyManagement: 'API KEY管理',
|
||||
knowledgePrivate: '详情',
|
||||
knowledgeShare: '详情',
|
||||
knowledgeCreateDataset: '新建数据集',
|
||||
knowledgeDocumentDetails: '详情',
|
||||
userMemoryDetail: '用户记忆详情',
|
||||
toolManagement: '工具管理',
|
||||
emotionEngine: '情感引擎',
|
||||
emotionDetail: '情绪记忆',
|
||||
selfReflectionEngine: '反思引擎',
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: '首页',
|
||||
selectSpace: '请选择空间',
|
||||
preview:'预览',
|
||||
preview: '预览',
|
||||
pleaseUploadFileFirst: '请先上传文件',
|
||||
shareSuccess: '分享成功',
|
||||
shareFailed: '分享失败',
|
||||
@@ -178,7 +183,6 @@ export const zh = {
|
||||
fileName: '文件名称',
|
||||
fileList: '文件列表',
|
||||
blockPreview: '分块预览',
|
||||
processingDocuments: '正在处理文档,请稍候...',
|
||||
chunkContent: '分块内容',
|
||||
sampleChunk: '示例分块内容',
|
||||
noFilesSelected: '暂无文件',
|
||||
@@ -188,10 +192,6 @@ export const zh = {
|
||||
waiting: '等待中',
|
||||
startUpload: '共{{count}}个文件 | 开始上传',
|
||||
startUploading: '开始上传',
|
||||
startUploadConfirmTitle: '开始处理文档',
|
||||
startUploadConfirmContent: '文档处理将在后台进行,您可以选择立即返回列表页或停留在此页面查看处理进度。',
|
||||
returnToList: '返回列表页',
|
||||
stayOnPage: '停留在此页',
|
||||
uploadSuccess: '上传成功',
|
||||
datasetName: '数据集名称',
|
||||
pleaseEnterDatasetName: '请输入数据集名称',
|
||||
@@ -297,7 +297,7 @@ export const zh = {
|
||||
number: '数字',
|
||||
checkbox: '复选框',
|
||||
apiVariable: 'API变量',
|
||||
|
||||
|
||||
displayName: '显示名称',
|
||||
maxLength: '最大长度',
|
||||
required: '必填',
|
||||
@@ -317,7 +317,7 @@ export const zh = {
|
||||
promptConfiguration: '提示词配置',
|
||||
configurationDesc: '定义Agent的角色、能力和行为准则',
|
||||
aiPrompt: 'AI提示词',
|
||||
promptPlaceholder: '你是一个专业的AI助手,你的职责是..',
|
||||
promptPlaceholder: '你是一个专业的AI助手,你的职责是帮助用户解决问题。',
|
||||
knowledgeBaseAssociation: '知识库关联',
|
||||
associatedKnowledgeBase: '关联知识库',
|
||||
addKnowledgeBase: '添加知识库',
|
||||
@@ -476,7 +476,7 @@ export const zh = {
|
||||
similarity_threshold: '语义相似度阈值',
|
||||
similarity_threshold_desc: '仅返回语义相似度高于此阈值的结果',
|
||||
similarity_threshold_desc1: '语义检索的最小相似度阈值',
|
||||
|
||||
|
||||
vector_similarity_weight: '向量相似度权重',
|
||||
vector_similarity_weight_desc: '仅返回BM25分数高于此阈值的结果',
|
||||
vector_similarity_weight_desc1: '分词检索的最小BM25分数阈值',
|
||||
@@ -490,6 +490,27 @@ export const zh = {
|
||||
chooseKnowledge: '选择知识库',
|
||||
active: '活跃',
|
||||
inactive: '不活跃',
|
||||
|
||||
endpointConfigurationSubTitle: '配置 API 访问地址和支持的 HTTP 方法',
|
||||
apiKeys: 'API Keys 管理',
|
||||
apiKeySubTitle: '管理 API 密钥,查看每个密钥的使用情况和流量统计',
|
||||
addApiKey: '添加新 API Key',
|
||||
apiKeyName: 'Key 名称',
|
||||
apiKeyNamePlaceholder: '例如:生产环境、测试环境、开发环境',
|
||||
apiKeyDescPlaceholder: '描述这个 Key 的用途',
|
||||
apiKeyTotal: '总 Keys',
|
||||
apiKeyRequestTotal: '总请求数',
|
||||
qps: '平均 QPS',
|
||||
qpsLimit: 'QPS 限制',
|
||||
qpsLimitTip: '(每秒请求数)',
|
||||
apiLimitConfig: '限流配置',
|
||||
qpsLimitDesc: '限制此 Key 每秒最多可以发起的请求数',
|
||||
dailyUsageLimit: '日调用量限制',
|
||||
dailyUsageLimitDesc: '限制此 Key 每天最多可以发起的请求总数',
|
||||
dailyUsageLimitUnit: '次/天',
|
||||
apiKeyDeleteContent: '删除后将无法恢复,使用此Key的应用将无法访问 API',
|
||||
currentValue: '当前值',
|
||||
qpsLimitUnit: '次/秒',
|
||||
},
|
||||
// 角色管理相关翻译
|
||||
role: {
|
||||
@@ -627,7 +648,7 @@ export const zh = {
|
||||
triplet_count_desc: '构建{{entities_count}}个实体节点和{{relations_count}}个关系连接',
|
||||
temporal_count: '时间提取',
|
||||
temporal_count_desc: '记录{{count}}条时间序列信息',
|
||||
|
||||
|
||||
dialogue: '对话',
|
||||
chunk: '分块',
|
||||
statement: '语句',
|
||||
@@ -739,8 +760,8 @@ export const zh = {
|
||||
copy: '复制',
|
||||
copySuccess: '复制成功',
|
||||
viewDetails: '查看详情',
|
||||
enabled: '启用',
|
||||
disabled: '停用',
|
||||
enabled: '已启用',
|
||||
disabled: '已停用',
|
||||
updateWarning: '更新警告',
|
||||
deleteWarning: '删除警告',
|
||||
deleteWarningContent: '确定要删除此{{content}}吗?',
|
||||
@@ -780,9 +801,7 @@ export const zh = {
|
||||
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
|
||||
publicApiCannotRefreshToken: '公共接口不能刷新token',
|
||||
refreshTokenNotExist: '刷新token不存在',
|
||||
reset: '重置',
|
||||
statusEnabled: '已启用',
|
||||
statusDisabled: '已禁用',
|
||||
reset: '重置'
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -880,6 +899,17 @@ export const zh = {
|
||||
|
||||
saveConfig: '保存配置',
|
||||
apiKeyName: 'API密钥名称',
|
||||
|
||||
llm: 'LLM',
|
||||
chat: 'Chat',
|
||||
embedding: 'Embedding',
|
||||
rerank: 'Rerank',
|
||||
openai: "Openai",
|
||||
dashscope: "Dashscope",
|
||||
ollama: "Ollama",
|
||||
xinference: "Xinference",
|
||||
gpustack: "Gpustack",
|
||||
bedrock: "Bedrock"
|
||||
},
|
||||
timezones: {
|
||||
'Asia/Shanghai': '中国标准时间 (UTC+8)',
|
||||
@@ -976,6 +1006,8 @@ export const zh = {
|
||||
active: '活跃',
|
||||
inactive: '不活跃',
|
||||
configurationName: '配置名称',
|
||||
emotionEngine: '情感引擎',
|
||||
reflectionEngine: '反思引擎'
|
||||
},
|
||||
member: {
|
||||
username: '用户名',
|
||||
@@ -988,8 +1020,6 @@ export const zh = {
|
||||
inviteToMember: '邀请成员',
|
||||
member: '成员',
|
||||
memberDesc: '只能使用应用,不能创建应用',
|
||||
admin: '管理员',
|
||||
adminDesc: '可以创建应用和管理团队设置',
|
||||
sendInvitation: '发送邀请',
|
||||
manager: '管理员',
|
||||
managerDesc: '可以创建应用和管理团队设置',
|
||||
@@ -1036,10 +1066,10 @@ export const zh = {
|
||||
|
||||
minimumRetention: '时间遗忘率 (λ_time)',
|
||||
minimumRetentionDesc: '控制记忆随时间的遗忘速度,值越高时间越短',
|
||||
forgettingRate: '记忆遗忘率 (λ_mem)',
|
||||
forgettingRate: '记忆遗忘率 (λ_mem)',
|
||||
forgettingRateDesc: '控制记忆遗忘的速度,值越高遗忘越快',
|
||||
offset: '最小保留度 (offset)',
|
||||
offsetDesc: '控制记忆保留的最小保留阈值 遗忘这地方改个文字描述',
|
||||
offset: '偏移量 (offset)',
|
||||
offsetDesc: '最小保留度的偏移量',
|
||||
CurrentValue: '当前值',
|
||||
range: '范围',
|
||||
forgettingEngineConfigParams: '遗忘引擎配置参数',
|
||||
@@ -1095,11 +1125,8 @@ export const zh = {
|
||||
storageType: '存储类型',
|
||||
rag: 'RAG存储',
|
||||
ragDesc: '基于向量检索,适合文档问答和语义搜索',
|
||||
neo4j: '图谱存储',
|
||||
neo4j: '图存储',
|
||||
neo4jDesc: '基于知识图谱,适合关系推理和路径查询',
|
||||
llmModel: 'LLM 模型',
|
||||
embeddingModel: 'Embedding 模型',
|
||||
rerankModel: 'Rerank 模型'
|
||||
},
|
||||
memoryExtractionEngine: {
|
||||
title: '记忆引擎模块配置中心',
|
||||
@@ -1136,11 +1163,10 @@ export const zh = {
|
||||
|
||||
exampleMemoryExtractionResults: '示例记忆提取结果',
|
||||
exampleMemoryExtractionResultsSubTitle: '(来自技术会议)',
|
||||
warning: '当您修改左侧的配置项时,提取结论将在此处实时更新',
|
||||
|
||||
extractTheNumberOfEntities: '提取实体数量',
|
||||
extractTheNumberOfEntitiesDesc: '去重后合并:{{num}}(精确:{{exact}},模糊:{{fuzzy}},LLM:{{llm}})',
|
||||
|
||||
|
||||
numberOfEntityDisambiguation: '实体消歧数量',
|
||||
numberOfEntityDisambiguationDesc: '总计{{num}}次(阻止:{{block_count}})',
|
||||
|
||||
@@ -1175,7 +1201,7 @@ export const zh = {
|
||||
intelligentSemanticPruningSceneDesc: '选择智能语义修剪场景(education、online_service、outbound)。',
|
||||
intelligentSemanticPruningThreshold: '智能语义修剪阈值',
|
||||
intelligentSemanticPruningThresholdDesc: '设置智能语义修剪阈值(0-0.9)。',
|
||||
selfReflexionEngine: '自我反思引擎',
|
||||
reflectionEngine: '自我反思引擎',
|
||||
selfReflexionEngineSubTitle: '通过反思和精炼,将情节记忆转化为更深层的语义记忆。',
|
||||
enableSelfReflexion: '启用自我反思',
|
||||
iterationPeriod: '迭代周期',
|
||||
@@ -1226,7 +1252,27 @@ export const zh = {
|
||||
记忆熊:秦国统一的原因包括:商鞅变法彻底,建立法律、户籍和军功爵制度,提升国家组织能力;旧贵族势力弱,中央集权程度高;关中地理优越,资源丰富且易守难攻;从孝公到秦始皇政策连续性强。
|
||||
|
||||
学生:那我换到唐朝史:安史之乱后,中央已开始整顿,为何藩镇割据反而加剧?
|
||||
记忆熊:安史之乱后藩镇割据加剧的原因包括:节度使掌握募兵权、财政调度权与军事指挥权,形成地方军阀;中央财政因均田制瓦解和租庸调失效而衰退,难以支撑军队,导致地方军事力量依附节度使;募兵制使士兵效忠个人而非国家;宦官掌控禁军,文官集团失势,中央制衡能力削弱。`
|
||||
记忆熊:安史之乱后藩镇割据加剧的原因包括:节度使掌握募兵权、财政调度权与军事指挥权,形成地方军阀;中央财政因均田制瓦解和租庸调失效而衰退,难以支撑军队,导致地方军事力量依附节度使;募兵制使士兵效忠个人而非国家;宦官掌控禁军,文官集团失势,中央制衡能力削弱。`,
|
||||
warning: '当您修改左侧的配置项后,点击【调试】,提取结论将在此处实时更新',
|
||||
processing: '配置已更新,正在重新萃取示例记忆...',
|
||||
success: '记忆萃取完成!',
|
||||
overallProgress: '整体进度',
|
||||
text_preprocessing: '文本预处理',
|
||||
fragment: '片段',
|
||||
knowledge_extraction: '知识抽取',
|
||||
creating_nodes_edges: '创建实体关系',
|
||||
deduplication: '去重消歧',
|
||||
status: {
|
||||
pending: '等待中',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败'
|
||||
},
|
||||
time: '耗时: ',
|
||||
text_preprocessing_desc: '文本切分为{{count}}个语义片段',
|
||||
knowledge_extraction_desc: '知识抽取完成,共识别{{entities}}个实体,{{statements}}个句子, {{temporal_ranges_count}}个时间提取, {{triplets}}个三元组',
|
||||
creating_nodes_edges_desc: '实体关系创建完成,共{{num}}条关系',
|
||||
deduplication_desc: '去重消歧完成,最终{{count}}个唯一实体'
|
||||
},
|
||||
memoryConversation: {
|
||||
searchPlaceholder: '输入用户ID...',
|
||||
@@ -1321,28 +1367,421 @@ export const zh = {
|
||||
websocketDemoCard: 'WebSocket 演示',
|
||||
sseDemoCard: 'SSE演示'
|
||||
},
|
||||
workflow: {
|
||||
title: '工作流编辑器',
|
||||
description: '拖拽节点创建连接,构建您的工作流程。点击节点可进行配置。',
|
||||
addNode: '添加节点',
|
||||
deleteNode: '删除选中',
|
||||
saveWorkflow: '保存工作流',
|
||||
startNode: '触发节点',
|
||||
conditionNode: '条件判断',
|
||||
actionNode: '执行动作',
|
||||
endNode: '结束节点',
|
||||
newNode: '新节点',
|
||||
node: '节点',
|
||||
nodesCreated: '已创建节点',
|
||||
loadingNodes: '正在加载节点 {{progress}}%',
|
||||
loadingFailed: '加载节点失败',
|
||||
create5kNodes: '创建5000节点',
|
||||
create10kNodes: '创建10000节点'
|
||||
},
|
||||
notFound: {
|
||||
title: '页面未找到',
|
||||
description: '请求的页面不存在。',
|
||||
backToHome: '返回首页'
|
||||
},
|
||||
apiKey: {
|
||||
name: '项目名称',
|
||||
createApiKey: '创建API Key',
|
||||
updateApiKey: '编辑API Key',
|
||||
id: 'ID',
|
||||
created_at: '创建时间',
|
||||
description: '描述',
|
||||
memoryEngine: '记忆引擎',
|
||||
knowledgeBase: '知识库',
|
||||
advancedSettings: '高级设置',
|
||||
expires_at: '过期时间',
|
||||
apiKey: 'API Key',
|
||||
status: '状态',
|
||||
createdAt: '创建时间',
|
||||
expiresAt: '过期时间',
|
||||
requestsPerMinute: '次/分钟',
|
||||
viewDetail: '查看详情',
|
||||
disable: '禁用',
|
||||
enable: '启用',
|
||||
baseInfo: '基础信息',
|
||||
permissionInfo: '授权信息',
|
||||
is_expired: '状态',
|
||||
active: '活跃',
|
||||
inactive: '过期'
|
||||
},
|
||||
tool: {
|
||||
mcp: 'MCP 服务',
|
||||
inner: '内置工具',
|
||||
custom: '自定义工具',
|
||||
mcpSearchPlaceholder: '搜索MCP服务...',
|
||||
innerSearchPlaceholder: '搜索工具...',
|
||||
customSearchPlaceholder: '搜索自定义工具...',
|
||||
addService: '添加MCP服务',
|
||||
addServiceSuccess: '服务添加成功',
|
||||
server_url: '服务地址',
|
||||
lastConnection: '最后连接',
|
||||
responseTime: '响应时间',
|
||||
status: {
|
||||
active: '活跃',
|
||||
inactive: '不活跃',
|
||||
},
|
||||
testConnectionSuccess: '测试连接成功',
|
||||
serviceEndpoint: '服务端点 URL',
|
||||
serviceEndpointPlaceholder: '服务端点的 URL',
|
||||
serviceEndpointExtra: 'MCP服务的完整访问地址',
|
||||
nameAndIcon: '名称和图标',
|
||||
namePlaceholder: '命名你的 MCP 服务',
|
||||
serverIdentifier: '服务器标识符',
|
||||
serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server',
|
||||
serverIdentifierLength: '最多 24 个字符',
|
||||
serverIdentifierPattern: '支持小写字母、数字、下划线和连字符',
|
||||
description: '描述信息',
|
||||
auth: '认证',
|
||||
requestHeader: '请求头',
|
||||
config: '配置',
|
||||
authType: '认证方式',
|
||||
noAuth: '无需认证',
|
||||
apiKey: 'API Key',
|
||||
basicAuth: 'Basic Auth',
|
||||
bearerToken: 'Bearer Token',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
requestHeaderDesc: '发送到 MCP 服务器的额外 HTTP 请求头',
|
||||
addRequestHeader: '添加请求头',
|
||||
editRequestHeader: '编辑请求头',
|
||||
requestHeaderName: '请求头名称',
|
||||
requestHeaderValue: '请求头值',
|
||||
timeout: '超时时间(秒)',
|
||||
sseReadTimeout: 'SSE 读取超时时间(秒)',
|
||||
saveAndTest: '保存并测试',
|
||||
|
||||
timeFormat: '时间格式化',
|
||||
timeZoneConversion: '时区转换',
|
||||
timestampConversion: '时间戳转换',
|
||||
timeCalculation: '时间计算',
|
||||
time_desc: '日期时间处理',
|
||||
DateTimeTool_features: '提供时间格式转换、时区转换、时间戳计算等功能',
|
||||
currentTime: '当前时间',
|
||||
timestamp: '时间戳',
|
||||
localTime: '本地时间',
|
||||
utcTime: 'UTC时间',
|
||||
secondsTimestamp: '时间戳(秒)',
|
||||
millisecondsTimestamp: '时间戳(毫秒)',
|
||||
enterTimestamp: '输入时间戳',
|
||||
conversion: '转换',
|
||||
conversionResult: '转换结果',
|
||||
chooseFormatType: '选择格式',
|
||||
|
||||
JsonTool_desc: '数据格式转换',
|
||||
JsonTool_features: 'JSON格式化、压缩、验证和转换功能',
|
||||
jsonFormat: 'JSON格式化',
|
||||
jsonGzip: 'JSON压缩',
|
||||
jsonCheck: 'JSON验证',
|
||||
jsonConversion: '格式转换',
|
||||
jsonEg: '示例JSON',
|
||||
enterJson: '输入JSON',
|
||||
jsonPlaceholder: '输入JSON数据,例如:{"name": "测试", "value": 123}',
|
||||
clear: '清空',
|
||||
parse: '粘贴',
|
||||
format: '格式化',
|
||||
minify: '压缩',
|
||||
validate: '验证',
|
||||
convert: '转义',
|
||||
outputResult: '输出结果',
|
||||
validJosn: 'JSON格式正确,验证通过!',
|
||||
|
||||
BaiduSearchTool_desc: '搜索引擎服务',
|
||||
BaiduSearchTool_features: '集成百度搜索API,提供网页搜索、新闻搜索等功能',
|
||||
webSearch: '网页搜索',
|
||||
newsSearch: '新闻搜索',
|
||||
imageSearch: '图片搜索',
|
||||
realTimeResults: '实时结果',
|
||||
configStatus: '配置状态',
|
||||
hasApiKey: 'API 已配置并启用',
|
||||
needApiKey: '需要配置API Key',
|
||||
|
||||
MinerUTool_desc: 'PDF解析工具',
|
||||
MinerUTool_features: '高精度PDF文档解析工具,支持文字、表格、图片提取',
|
||||
pdfParser: 'PDF解析',
|
||||
tableExtraction: '表格提取',
|
||||
imageRecognition: '图片识别',
|
||||
textExtraction: '文本提取',
|
||||
|
||||
TextInTool_desc: 'OCR文字识别',
|
||||
TextInTool_features: '智能OCR文字识别服务,支持多语言、手写体识别',
|
||||
universalOCR: '通用OCR',
|
||||
handwritingRecognition: '手写识别',
|
||||
multilingualSupport: '多语言支持',
|
||||
highPrecisionRecognition: '高精度识别',
|
||||
|
||||
configDesc: '配置说明',
|
||||
BaiduSearchTool_config_desc: '使用百度搜索API需要先在百度开放平台申请API Key和Secret Key。',
|
||||
MinerUTool_config_desc: 'MinerU是高精度PDF文档解析工具,需要API Key才能使用。',
|
||||
TextInTool_config_desc: 'TextIn提供智能OCR文字识别服务,支持多语言识别。',
|
||||
link: '申请地址',
|
||||
api_key: 'API Key',
|
||||
BaiduSearchTool_api_key_desc: '从百度开放平台获取的API Key',
|
||||
MinerUTool_api_key_desc: '从MinerU平台获取的API Key',
|
||||
secret_key: 'Secret Key',
|
||||
BaiduSearchTool_secret_key_desc: '从百度开放平台获取的Secret Key',
|
||||
TextInTool_secret_key_desc: '从TextIn平台获取的Secret Key',
|
||||
type: '搜索类型',
|
||||
pagesize: '每页结果数',
|
||||
pagesize_desc: '每次搜索返回的结果数量({{count1}}-{{count2}})',
|
||||
BaiduSearchTool_enable: '启用百度搜索',
|
||||
BaiduSearchTool_safe_enable: '启用安全搜索',
|
||||
BaiduSearchTool_safe_enable_desc: '过滤不适宜内容',
|
||||
api_address: 'API地址',
|
||||
MinerUTool_api_address_desc: '默认使用官方API地址,如有私有部署可修改',
|
||||
TextInTool_api_address_desc: '默认使用官方API地址',
|
||||
parsing_mode: '解析模式',
|
||||
auto_recognition: '自动识别',
|
||||
pure_text_mode: '纯文本模式',
|
||||
table_priority: '表格优先',
|
||||
image_priority: '图片优先',
|
||||
MinerUTool_timeout_desc: 'PDF解析超时时间(10-300秒)',
|
||||
MinerUTool_enable: '启用MinerU',
|
||||
MinerUTool_extract_images_enable: '提取图片',
|
||||
MinerUTool_extract_images_enable_desc: '是否提取PDF中的图片内容',
|
||||
app_id: 'APP ID',
|
||||
TextInTool_app_id_desc: '从TextIn平台获取的App ID',
|
||||
language_identification: '识别语言',
|
||||
automatic_detection: '自动检测',
|
||||
simplified_chinese: '简体中文',
|
||||
traditional_chinese: '繁体中文',
|
||||
english: '英文',
|
||||
japanese: '日文',
|
||||
korean_language: '韩文',
|
||||
pattern_recognition: '识别模式',
|
||||
universal_identification: '通用识别',
|
||||
high_precision_identification: '高精度识别',
|
||||
handwriting_recognition: '手写体识别',
|
||||
formula_recognition: '公式识别',
|
||||
TextInTool_enable: '启用TextIn',
|
||||
return_text_position_enable: '返回文本位置信息',
|
||||
return_text_position_enable_desc: '是否返回识别文字的坐标位置',
|
||||
|
||||
addCustom: '添加自定义工具',
|
||||
editCustom: '编辑自定义工具',
|
||||
schema: 'Schema',
|
||||
schemaPlaceholder: '在此处输入您的 OpenAPI schema',
|
||||
authentication: '鉴权方式',
|
||||
tag: '标签',
|
||||
created_at: '创建时间',
|
||||
headerName: 'Header 名称',
|
||||
null: '无',
|
||||
tagDesc: '多个标签用逗号分隔',
|
||||
availableTools: '可用工具',
|
||||
name: '名称',
|
||||
desc: '描述',
|
||||
method: '方法',
|
||||
path: '路径',
|
||||
viewDetail: '查看详情'
|
||||
},
|
||||
workflow: {
|
||||
coreNode: '核心节点',
|
||||
start: '开始(Start)',
|
||||
end: '结束(End)',
|
||||
answer: '回复(Answer)',
|
||||
aiAndCognitiveProcessing: 'AI与认知处理',
|
||||
llm: '大语言模型 (LLM)',
|
||||
model_selection: '多模型选择',
|
||||
model_voting: '多模型投票',
|
||||
rag: '知识检索 (RAG)',
|
||||
classification: '智能分类',
|
||||
parameter_extraction: '参数提取',
|
||||
flowControl: '流程控制',
|
||||
condition: '条件分支',
|
||||
iteration: '迭代 (Iteration)',
|
||||
loop: '循环 (Loop)',
|
||||
parallel: '并行执行',
|
||||
aggregator: '聚合器',
|
||||
externalInteraction: '外部交互',
|
||||
http_request: 'HTTP请求',
|
||||
tools: '工具 (Tools)',
|
||||
code_execution: '代码执行',
|
||||
template_rendering: '模板渲染',
|
||||
cognitiveUpgrading: '认知升级(创新)',
|
||||
task_planning: '任务规划',
|
||||
reasoning_control: '推理控制',
|
||||
self_reflection: '自我反思',
|
||||
memory_enhancement: '记忆增强',
|
||||
agentCollaborationNode: 'Agent 协作节点',
|
||||
agent_scheduling: 'Agent 调度',
|
||||
agent_collaboration: 'Agent 协同',
|
||||
agent_arbitration: 'Agent 仲裁',
|
||||
safetyAndCompliance: '安全与合规',
|
||||
sensitive_detection: '敏感识别',
|
||||
output_audit: '输出审计',
|
||||
evolutionAndGovernance: '演化与治理',
|
||||
self_optimization: '自我优化',
|
||||
process_evolution: '流程演化',
|
||||
|
||||
clickToConfigure: '点击配置节点参数',
|
||||
nodeProperties: '节点属性',
|
||||
empty: "Emmm…盒子是空的,这里什么都没有~",
|
||||
nodeName: '节点名称',
|
||||
|
||||
|
||||
config: {
|
||||
llm: {
|
||||
model_id: '模型',
|
||||
temperature: '温度',
|
||||
max_tokens: '最大令牌数',
|
||||
},
|
||||
start: {
|
||||
variables: '输入字段',
|
||||
|
||||
string: '文本',
|
||||
number: '数字',
|
||||
boolean: '复选框',
|
||||
array: '下拉选项',
|
||||
object: '对象',
|
||||
|
||||
addVariable: '添加变量',
|
||||
editVariable: '编辑变量',
|
||||
variableType: '变量类型',
|
||||
variableName: '变量名称',
|
||||
description: '显示名称',
|
||||
default: '默认值',
|
||||
required: '必填',
|
||||
max_length: '最大长度',
|
||||
defaultChecked: '选中',
|
||||
notDefaultChecked: '不选中',
|
||||
options: '选项',
|
||||
},
|
||||
end: {
|
||||
output: '回复'
|
||||
}
|
||||
},
|
||||
|
||||
clear: '清空',
|
||||
run: '运行',
|
||||
save: '保存',
|
||||
export: '导出',
|
||||
variableConfig: '变量配置',
|
||||
variableRequired: '必填',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
|
||||
emotion_enabled: '启用情感引擎',
|
||||
emotion_enabled_desc: '自动分析对话中的情感倾向',
|
||||
|
||||
emotion_model_id: '情感分析模型',
|
||||
emotion_model_id_desc: '不同模型在准确度和速度上有所差异',
|
||||
|
||||
emotion_extract_keywords: '情绪关键词提取',
|
||||
emotion_extract_keywords_subTitle: '自动提取对话中的情绪相关关键词',
|
||||
emotion_extract_keywords_desc: '提取如"开心"、"失望"、"期待"等情绪关键词,帮助更好地理解用户情绪',
|
||||
emotion_min_intensity: '置信度阈值',
|
||||
emotion_min_intensity_desc: '置信度越高,识别越准确,但可能遗漏部分信息',
|
||||
|
||||
emotion_enable_subject: '情绪主体分类 ',
|
||||
emotion_enable_subject_subTitle: '识别情绪归属(自己/他人/物体)',
|
||||
emotion_enable_subject_desc: '区分情绪主体: self (我感到开心)、other (他很生气)、object (这个产品很棒)',
|
||||
|
||||
currentValue: '当前值',
|
||||
emotion_min_intensity_description: '置信度阈值说明',
|
||||
question: '什么是置信度阈值?',
|
||||
answer: '置信度阈值是情感引擎判断情绪时的"确定程度"标准。当 AI 分析出的情感置信度低于设定阈值时,该情感将不会被记录。',
|
||||
differentTitle: '不同阈值的影响',
|
||||
advantage: '优点',
|
||||
shortcoming: '缺点',
|
||||
scene: '适用场景',
|
||||
low_title: '低阈值 (0.0 - 0.4)',
|
||||
low_tag: '灵敏',
|
||||
low_advantage: '能捕捉到更多细微的情感变化,不会遗漏潜在的情绪信号',
|
||||
low_shortcoming: '可能产生误判,将中性或不明确的表达识别为特定情感',
|
||||
low_scene: '需要全面了解用户情绪波动,对准确度要求不高的场景',
|
||||
middle_title: '中阈值 (0.5 - 0.7)',
|
||||
middle_tag: '推荐',
|
||||
middle_advantage: '平衡准确度和覆盖率,既能识别明显情感,也不会过度敏感',
|
||||
middle_shortcoming: '可能遗漏一些不太明显的情感表达',
|
||||
middle_scene: '大多数日常对话场景,适合一般性情感分析需求',
|
||||
high_title: '高阈值 (0.8 - 1.0)',
|
||||
high_tag: '精准',
|
||||
high_advantage: '只记录非常明确的情感表达,准确度极高,误判率低',
|
||||
high_shortcoming: '会遗漏大量不够明显的情感信息,数据覆盖率低',
|
||||
high_scene: '对准确度要求极高的场景,如情感危机预警、重要决策参考',
|
||||
|
||||
configSuggest: '配置建议',
|
||||
first: '初次使用',
|
||||
first_desc: '建议从中等阈值(0.6-0.7)开始,观察一段时间后根据实际效果调整',
|
||||
customer_service: '客服场景',
|
||||
customer_service_desc: '建议使用较低阈值(0.4-0.6),及时捕捉用户的不满情绪',
|
||||
data_analysis: '数据分析',
|
||||
data_analysis_desc: '建议使用中等阈值(0.6-0.7),保证数据质量的同时有足够样本量',
|
||||
risk_warning: '风险预警',
|
||||
risk_warning_desc: '建议使用较高阈值(0.7-0.8),确保预警的准确性',
|
||||
|
||||
actual_case: '实际案例',
|
||||
user_input: '用户输入',
|
||||
user_input_message: '"这个功能还行吧,不过有点小问题"',
|
||||
neutral_emotion: '中性情感',
|
||||
neutral_emotion_tag: '所有阈值都会记录',
|
||||
minor_dissatisfaction: '轻微不满',
|
||||
minor_dissatisfaction_tag: '仅低/中阈值会记录',
|
||||
expect_improvement: '期待改进',
|
||||
expect_improvement_tag: '仅低阈值会记录',
|
||||
confidence: '置信度'
|
||||
},
|
||||
emotionDetail: {
|
||||
wordCloud: '情感分布分析',
|
||||
pieces: '条',
|
||||
emotionTags: '高频情绪关键词',
|
||||
joy: '喜悦',
|
||||
anger: '愤怒',
|
||||
sadness: '悲伤',
|
||||
fear: '恐惧',
|
||||
neutral: '中性',
|
||||
surprise: '惊讶',
|
||||
|
||||
health: '情绪健康指数',
|
||||
positivity_rate: '积极率',
|
||||
stability: '稳定性',
|
||||
resilience: '恢复力',
|
||||
suggestions: '个性化建议',
|
||||
},
|
||||
reflectionEngine: {
|
||||
reflectionEngineConfig: '反思引擎配置',
|
||||
reflection_enabled: '启用反思引擎',
|
||||
reflection_enabled_desc: '将情节记忆转化为语义记忆,形成长期认知',
|
||||
reflection_model_id: '反思模型',
|
||||
reflection_model_id_desc: '不同模型在准确度和速度上有所差异',
|
||||
reflection_period_in_hours: '迭代周期',
|
||||
reflection_period_in_hours_desc: '决定系统多久进行一次记忆反思和提炼',
|
||||
reflexion_range: '反思范围',
|
||||
partial: '部分反思 (仅新增记忆)',
|
||||
all: '全部反思 (所有历史记忆)',
|
||||
reflexion_range_desc: '',
|
||||
baseline: '反思基线',
|
||||
baseline_desc: '',
|
||||
TIME: '基于时间(时序关系)',
|
||||
FACT: '基于事实(知识点)',
|
||||
HYBRID: '事实+时间(综合维度)',
|
||||
quality_assessment: '启用质量评估',
|
||||
quality_assessment_desc: '自动评估记忆的准确性、完整性和时效性',
|
||||
memory_verify: '启用记忆审核',
|
||||
memory_verify_desc: '检测敏感信息并过滤违规内容',
|
||||
oneHour: '每1个小时',
|
||||
threeHours: '每3个小时',
|
||||
sixHours: '每6个小时',
|
||||
twelveHours: '每12个小时',
|
||||
daily: '每天',
|
||||
run: '运行调试',
|
||||
example: '原始数据',
|
||||
exampleText: '我是 2023 年春天去北京工作的,后来基本一直都在北京上班,也没怎么换过城市。不过后来公司调整,2024 年上半年我被调到上海待了差不多半年,那段时间每天都是在上海办公室打卡。当时入职资料用的还是我之前的身份信息,身份证号是 11010119950308123X,银行卡是 6222023847595898,这些一直没变。对了,其实我 从 2023 年开始就一直在北京生活,从来没有长期离开过北京,上海那段更多算是远程配合',
|
||||
runTitle: '反思试运行',
|
||||
status: '状态',
|
||||
message: '消息',
|
||||
|
||||
conflictDetection: '冲突检测',
|
||||
reason: '冲突原因',
|
||||
solution: '解决方案',
|
||||
|
||||
qualityAssessment: '质量评估',
|
||||
qualityAssessmentObj: {
|
||||
score: '质量评分',
|
||||
summary: '评估摘要',
|
||||
},
|
||||
|
||||
privacyAudit: '隐私审核',
|
||||
privacyAuditObj: {
|
||||
true: '是',
|
||||
false: '否',
|
||||
has_privacy: '包含隐私信息',
|
||||
privacy_types: '隐私类型',
|
||||
summary: '审核摘要',
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -10,19 +10,19 @@ import routesConfig from './routes.json';
|
||||
// 递归收集所有路由中的element
|
||||
function collectElements(routes: RouteConfig[]): Set<string> {
|
||||
const elements = new Set<string>();
|
||||
|
||||
|
||||
function traverse(routeList: RouteConfig[]) {
|
||||
routeList.forEach(route => {
|
||||
// 添加当前路由的element
|
||||
elements.add(route.element);
|
||||
|
||||
|
||||
// 递归处理子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
traverse(route.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
traverse(routes);
|
||||
return elements;
|
||||
}
|
||||
@@ -54,10 +54,14 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
UserManagement: lazy(() => import('@/views/UserManagement')),
|
||||
ModelManagement: lazy(() => import('@/views/ModelManagement')),
|
||||
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
|
||||
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
|
||||
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
|
||||
EmotionDetail: lazy(() => import('@/views/UserMemoryDetail/pages/EmotionDetail')),
|
||||
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
|
||||
Login: lazy(() => import('@/views/Login')),
|
||||
InviteRegister: lazy(() => import('@/views/InviteRegister')),
|
||||
NoPermission: lazy(() => import('@/views/NoPermission')),
|
||||
NotFound: lazy(() => import('@/views/NotFound')),
|
||||
NotFound: lazy(() => import('@/views/NotFound'))
|
||||
};
|
||||
|
||||
// 检查并报告缺失的组件
|
||||
@@ -87,12 +91,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
|
||||
// 获取组件
|
||||
const componentKey = route.element as keyof typeof componentMap;
|
||||
const Component = componentMap[componentKey];
|
||||
|
||||
|
||||
if (!Component) {
|
||||
console.error(`Component ${route.element} not found in componentMap`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 如果有子路由
|
||||
if (route.children) {
|
||||
return (
|
||||
@@ -101,12 +105,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 如果有path属性,则为普通路由
|
||||
if (route.path) {
|
||||
return <Route key={index} path={route.path} element={<Component />} />;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
{ "path": "/knowledge-base/:knowledgeBaseId/share", "element": "Share" },
|
||||
{ "path": "/knowledge-base/:knowledgeBaseId/create-dataset", "element": "CreateDataset" },
|
||||
{ "path": "/knowledge-base/:knowledgeBaseId/DocumentDetails", "element": "DocumentDetails" },
|
||||
{ "path": "/api-key", "element": "ApiKeyManagement" },
|
||||
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
|
||||
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
|
||||
{ "path": "/user-memory/emotion/:id", "element": "EmotionDetail" },
|
||||
{ "path": "/no-permission", "element": "NoPermission" },
|
||||
{ "path": "/*", "element": "NotFound" }
|
||||
]
|
||||
|
||||
@@ -26,6 +26,19 @@
|
||||
"sort": 0,
|
||||
"subs": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"parent": 0,
|
||||
"code": "tool",
|
||||
"label": "工具管理",
|
||||
"i18nKey": "menu.toolManagement",
|
||||
"path": "/tool",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 1,
|
||||
"subs": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"parent": 0,
|
||||
@@ -183,6 +196,32 @@
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"parent": 7,
|
||||
"code": "emotionEngine",
|
||||
"label": "情感引擎",
|
||||
"i18nKey": "menu.emotionEngine",
|
||||
"path": "/emotion-engine/:id",
|
||||
"enable": true,
|
||||
"display": false,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"parent": 7,
|
||||
"code": "selfReflectionEngine",
|
||||
"label": "反思引擎",
|
||||
"i18nKey": "menu.selfReflectionEngine",
|
||||
"path": "/reflection-engine/:id",
|
||||
"enable": true,
|
||||
"display": false,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"subs": null
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -210,7 +249,21 @@
|
||||
"display": false,
|
||||
"level": 2,
|
||||
"sort": 0,
|
||||
"subs": null
|
||||
"subs": [
|
||||
{
|
||||
"id": 81,
|
||||
"parent": 8,
|
||||
"code": "emotionDetail",
|
||||
"label": "记忆详情",
|
||||
"i18nKey": "menu.emotionDetail",
|
||||
"path": "/user-memory/emotion/:id",
|
||||
"enable": true,
|
||||
"display": false,
|
||||
"level": 2,
|
||||
"sort": 0,
|
||||
"subs": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -243,6 +296,21 @@
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"parent": 0,
|
||||
"code": "apiKey",
|
||||
"label": "API KEY管理",
|
||||
"i18nKey": "menu.apiKeyManagement",
|
||||
"path": "/api-key",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -174,4 +174,10 @@ body {
|
||||
}
|
||||
.ant-breadcrumb a:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* X6 节点样式 */
|
||||
.x6-node foreignObject > body {
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
46
web/src/utils/apiKeyReplacer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* API密钥替换工具
|
||||
*/
|
||||
|
||||
const API_KEY_PATTERNS = {
|
||||
service: /sk-service-[A-Za-z0-9_-]+/g,
|
||||
agent: /sk-agent-[A-Za-z0-9_-]+/g,
|
||||
multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g,
|
||||
workflow: /sk-workflow-[A-Za-z0-9_-]+/g
|
||||
}
|
||||
const API_KEY_PREFIX = {
|
||||
service: 'sk-service-',
|
||||
agent: 'sk-agent-',
|
||||
multiAgent: 'sk-multi_agent-',
|
||||
workflow: 'sk-workflow-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换文本中的API密钥为*号
|
||||
* @param text 原始文本
|
||||
* @returns 替换后的文本
|
||||
*/
|
||||
export const maskApiKeys = (text: string): string => {
|
||||
if (!text) return text
|
||||
let result = text
|
||||
|
||||
Object.keys(API_KEY_PREFIX).map(type => {
|
||||
const key = type as keyof typeof API_KEY_PREFIX
|
||||
result = result.replace(API_KEY_PATTERNS[key as keyof typeof API_KEY_PREFIX], (match) => {
|
||||
const prefixLength = API_KEY_PREFIX[key].length
|
||||
const prefix = match.substring(0, prefixLength)
|
||||
return prefix + '*'.repeat(match.length - prefixLength)
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测文本中是否包含API密钥
|
||||
* @param text 待检测文本
|
||||
* @returns 是否包含API密钥
|
||||
*/
|
||||
export const hasApiKeys = (text: string): boolean => {
|
||||
return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text))
|
||||
}
|
||||
@@ -3,7 +3,47 @@ import i18n from '@/i18n'
|
||||
import { cookieUtils } from './request'
|
||||
const API_PREFIX = '/api'
|
||||
|
||||
export const handleSSE = async (url: string, data: any, onMessage?: (data: string) => void, config = {}) => {
|
||||
export interface SSEMessage {
|
||||
event?: string
|
||||
data?: string | object
|
||||
}
|
||||
export function parseSSEToJSON(sseString: string) {
|
||||
const events: SSEMessage[] = []
|
||||
const lines = sseString.trim().split('\n')
|
||||
|
||||
let currentEvent: SSEMessage = {}
|
||||
|
||||
try {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) {
|
||||
if (Object.keys(currentEvent).length > 0) {
|
||||
events.push(currentEvent)
|
||||
currentEvent = {}
|
||||
}
|
||||
currentEvent.event = line.substring(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
try {
|
||||
currentEvent.data = JSON.parse(dataStr.replace(/"/g, '"'))
|
||||
} catch {
|
||||
currentEvent.data = dataStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(currentEvent).length > 0) {
|
||||
events.push(currentEvent)
|
||||
}
|
||||
|
||||
return events
|
||||
} catch (error) {
|
||||
console.error('Parse stream error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => {
|
||||
try {
|
||||
const token = cookieUtils.get('authToken');
|
||||
const response = await fetch(`${API_PREFIX}${url}`, {
|
||||
@@ -37,7 +77,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: strin
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
if (onMessage) {
|
||||
onMessage(chunk);
|
||||
onMessage(parseSSEToJSON(chunk) ?? {});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
102
web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Switch, Button, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ApiKey, ApiKeyModalRef } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getApiKey } from '@/api/apiKey';
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer';
|
||||
|
||||
const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: string) => void }>(({ handleCopy }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState<ApiKey>({} as ApiKey)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (apiKey?: ApiKey) => {
|
||||
if (apiKey?.id) {
|
||||
getApiKey(apiKey.id)
|
||||
.then((res) => {
|
||||
setVisible(true);
|
||||
setData(res as ApiKey)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('apiKey.viewDetail')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.baseInfo')}</div>
|
||||
{['id', 'name', 'is_expired', 'created_at'].map((key, index) => (
|
||||
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
|
||||
'rb:mt-3': index !== 0
|
||||
})}>
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.${key}`)}</span>
|
||||
<span className="rb:text-right rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{ key === 'created_at'
|
||||
? formatDateTime(data[key], 'YYYY-MM-DD HH:mm:ss')
|
||||
: key === 'is_expired'
|
||||
? <Tag color={data[key] ? 'error' : 'processing'}>{data[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
|
||||
: <Tooltip title={String(data[key as keyof ApiKey])}>{String(data[key as keyof ApiKey])}</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{maskApiKeys(data.api_key)}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(data.api_key)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.permissionInfo')}</div>
|
||||
|
||||
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.memoryEngine`)}</span>
|
||||
<span>
|
||||
<Switch checked={data.scopes?.includes('memory')} disabled />
|
||||
</span>
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.knowledgeBase`)}</span>
|
||||
<span>
|
||||
<Switch checked={data.scopes?.includes('rag')} disabled />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 高级设置 */}
|
||||
{data.expires_at && <>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.advancedSettings')}</div>
|
||||
|
||||
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.expires_at`)}</span>
|
||||
<span>
|
||||
{data.expires_at ? formatDateTime(data.expires_at as number, 'YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</>}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyDetailModal;
|
||||
153
web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Switch, App, DatePicker } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ApiKey, ApiKeyModalRef } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import dayjs from 'dayjs'
|
||||
import { createApiKey, updateApiKey } from '@/api/apiKey';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface CreateModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ApiKey>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editVo, setEditVo] = useState<ApiKey | null>(null);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setEditVo(null);
|
||||
};
|
||||
|
||||
const handleOpen = (apiKey?: ApiKey) => {
|
||||
if (apiKey?.id) {
|
||||
const { scopes = [], expires_at } = apiKey
|
||||
// 编辑模式,填充表单
|
||||
form.setFieldsValue({
|
||||
name: apiKey.name,
|
||||
description: apiKey.description,
|
||||
memory: scopes.includes('memory'),
|
||||
rag: scopes.includes('rag'),
|
||||
expires_at: expires_at ? dayjs(expires_at) : undefined
|
||||
});
|
||||
setEditVo(apiKey);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = async () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const { memory, rag, expires_at, ...rest } = values
|
||||
let scopes = []
|
||||
|
||||
if (memory) {
|
||||
scopes.push('memory')
|
||||
}
|
||||
if (rag) {
|
||||
scopes.push('rag')
|
||||
}
|
||||
// 准备新的/更新的API Key数据
|
||||
const apiKeyData = {
|
||||
...rest,
|
||||
scopes,
|
||||
expires_at: expires_at ? dayjs(expires_at.valueOf()).endOf('day').valueOf() : null,
|
||||
type: 'service'
|
||||
};
|
||||
setLoading(true)
|
||||
const req = editVo?.id ? updateApiKey(editVo.id, apiKeyData as ApiKey) : createApiKey(apiKeyData as ApiKey)
|
||||
|
||||
req.then(() => {
|
||||
refresh();
|
||||
handleClose();
|
||||
message.success(t(editVo ? 'common.updateSuccess' : 'common.createSuccess'));
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo ? t('apiKey.updateApiKey') : t('apiKey.createApiKey')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.baseInfo')}</div>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('apiKey.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('apiKey.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} rows={3} />
|
||||
</FormItem>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.permissionInfo')}</div>
|
||||
|
||||
<FormItem
|
||||
name="memory"
|
||||
label={t('apiKey.memoryEngine')}
|
||||
layout="horizontal"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="rag"
|
||||
label={t('apiKey.knowledgeBase')}
|
||||
layout="horizontal"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
|
||||
{/* 高级设置 */}
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.advancedSettings')}</div>
|
||||
|
||||
<FormItem
|
||||
name="expires_at"
|
||||
label={t('apiKey.expires_at')}
|
||||
>
|
||||
<DatePicker
|
||||
className="rb:w-full"
|
||||
disabledDate={(current) => current && current < dayjs().subtract(1, 'day').endOf('day')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyModal;
|
||||
125
web/src/views/ApiKeyManagement/index.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, App, Space } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import type { ApiKey, ApiKeyModalRef } from './types';
|
||||
import ApiKeyModal from './components/ApiKeyModal';
|
||||
import ApiKeyDetailModal from './components/ApiKeyDetailModal';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getApiKeyListUrl, deleteApiKey } from '@/api/apiKey';
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Tag from '@/components/Tag'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer';
|
||||
|
||||
const ApiKeyManagement: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { modal, message } = App.useApp();
|
||||
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
|
||||
const apiKeyDetailModalRef = useRef<ApiKeyModalRef>(null)
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
|
||||
const refresh = () => {
|
||||
scrollListRef.current?.refresh();
|
||||
}
|
||||
|
||||
const handleEdit = (item?: ApiKey) => {
|
||||
apiKeyModalRef.current?.handleOpen(item);
|
||||
}
|
||||
const handleView = (item: ApiKey) => {
|
||||
apiKeyDetailModalRef.current?.handleOpen(item);
|
||||
}
|
||||
const handleDelete = (item: ApiKey) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.name }),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deleteApiKey(item.id)
|
||||
.then(() => {
|
||||
refresh();
|
||||
message.success(t('common.deleteSuccess'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleCopy = (content: string) => {
|
||||
copy(content)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-end rb:mb-3 rb:p-4">
|
||||
<Button type="primary" onClick={() => handleEdit()}>
|
||||
{t('apiKey.createApiKey')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getApiKeyListUrl}
|
||||
query={{ is_active: true, type: 'service' }}
|
||||
column={2}
|
||||
renderItem={(item: Record<string, unknown>) => {
|
||||
let apiKeyItem = item as unknown as ApiKey
|
||||
return (
|
||||
<RbCard
|
||||
title={apiKeyItem.name}
|
||||
>
|
||||
{['id', 'is_expired', 'created_at'].map((key, index) => (
|
||||
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
|
||||
'rb:mt-3': index !== 0
|
||||
})}>
|
||||
<span className="rb:text-[#5B6167] rb:w-20">{t(`apiKey.${key}`)}</span>
|
||||
<span className="rb:flex-1 rb:text-left rb:py-px rb:rounded rb:font-medium">
|
||||
{ key === 'created_at'
|
||||
? formatDateTime(apiKeyItem[key], 'YYYY-MM-DD HH:mm:ss')
|
||||
: key === 'is_expired'
|
||||
? <Tag color={apiKeyItem[key] ? 'error' : 'processing'}>{apiKeyItem[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
|
||||
: String(apiKeyItem[key as keyof ApiKey])
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{maskApiKeys(apiKeyItem.api_key)}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(apiKeyItem.api_key)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space className="rb:pt-2 rb:min-h-6.25">
|
||||
{apiKeyItem.scopes?.includes('memory') && <Tag>{t('apiKey.memoryEngine')}</Tag>}
|
||||
{apiKeyItem.scopes?.includes('rag') && <Tag color="success">{t('apiKey.knowledgeBase')}</Tag>}
|
||||
</Space>
|
||||
|
||||
<div className="rb:mt-5 rb:flex rb:justify-end rb:gap-2.5">
|
||||
<Button icon={<EyeOutlined />} onClick={() => handleView(apiKeyItem)}></Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => handleEdit(apiKeyItem)}></Button>
|
||||
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(apiKeyItem)}></Button>
|
||||
</div>
|
||||
</RbCard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ApiKeyModal
|
||||
ref={apiKeyModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
<ApiKeyDetailModal
|
||||
ref={apiKeyDetailModalRef}
|
||||
handleCopy={handleCopy}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyManagement;
|
||||
40
web/src/views/ApiKeyManagement/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer'
|
||||
|
||||
export interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: 'agent' | 'multi_agent' | 'workflow' | 'service';
|
||||
scopes?: string[]; // 'memory' | 'rag' | 'app'
|
||||
|
||||
api_key: string;
|
||||
is_active: boolean;
|
||||
is_expired: boolean;
|
||||
created_at: number;
|
||||
expires_at?: number | Dayjs;
|
||||
memory?: boolean;
|
||||
rag?: boolean;
|
||||
|
||||
|
||||
updated_at: string;
|
||||
qps_limit?: number;
|
||||
daily_request_limit?: number;
|
||||
|
||||
rate_limit?: number;
|
||||
total_requests: number;
|
||||
quota_used: number;
|
||||
quota_limit: number;
|
||||
}
|
||||
|
||||
export interface ApiKeyModalRef {
|
||||
handleOpen: (apiKey?: ApiKey) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取掩码后的API密钥
|
||||
*/
|
||||
export const getMaskedApiKey = (apiKey: string): string => {
|
||||
return maskApiKeys(apiKey)
|
||||
}
|
||||
@@ -239,6 +239,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
return [
|
||||
...(prev || []).map(item => ({
|
||||
...item,
|
||||
conversation_id: undefined,
|
||||
list: []
|
||||
})),
|
||||
newChatItem
|
||||
|
||||
@@ -1,153 +1,194 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { type FC, useState, useRef, useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Space, App
|
||||
// Slider, Input,
|
||||
// Form,
|
||||
// Checkbox
|
||||
} from 'antd';
|
||||
import { Button, Space, App, Statistic, Row, Col } from 'antd';
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import Card from './components/Card';
|
||||
// import qpsRestrictions from '@/assets/images/application/qpsRestrictions.svg'
|
||||
// import dailyAdjustmentDosage from '@/assets/images/application/dailyAdjustmentDosage.svg'
|
||||
// import tokenCap from '@/assets/images/application/tokenCap.svg'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import type { ApiKeyModalRef, ApiKeyConfigModalRef } from './types'
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
import ApiKeyModal from './components/ApiKeyModal';
|
||||
import ApiKeyConfigModal from './components/ApiKeyConfigModal';
|
||||
import Tag from '@/components/Tag'
|
||||
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer'
|
||||
|
||||
// const limitList = [
|
||||
// { key: 'qpsRestrictions', value: '10', icon: qpsRestrictions, unit: ' times/second' },
|
||||
// { key: 'dailyAdjustmentDosage', value: '1000', icon: dailyAdjustmentDosage, unit: ' times/day' },
|
||||
// { key: 'tokenCap', value: '10', icon: tokenCap, unit: 'M Tokens/day' },
|
||||
// ]
|
||||
// const sdkList = ['pythonSDK', 'nodejsSDK', 'goSDK', 'curlExample']
|
||||
|
||||
const Api: FC<{apiKeyList?: string[]}> = ({apiKeyList = []}) => {
|
||||
const Api: FC<{ application: Application | null }> = ({ application }) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeMethods, setActiveMethod] = useState(['GET']);
|
||||
const { message } = App.useApp()
|
||||
// const [form] = Form.useForm();
|
||||
const activeMethods = ['GET'];
|
||||
const { message, modal } = App.useApp()
|
||||
const copyContent = window.location.origin + '/v1/chat'
|
||||
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
|
||||
const apiKeyConfigModalRef = useRef<ApiKeyConfigModalRef>(null);
|
||||
const [apiKeyList, setApiKeyList] = useState<ApiKey[]>([])
|
||||
|
||||
const handleCopy = (content: string) => {
|
||||
copy(content)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
return (
|
||||
<div className="rb:w-[1000px] rb:mt-[20px] rb:pb-[20px] rb:mx-auto">
|
||||
{/* <Form form={form} layout="vertical"> */}
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card title={t('application.endpointConfiguration')}>
|
||||
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
|
||||
<Space size={8}>
|
||||
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
|
||||
<Button key={method} type={activeMethods.includes(method) ? 'primary' : 'default'} onClick={() => setActiveMethod(prev => activeMethods.includes(method) ? prev.filter(m => m !== method) : [...prev, method])}>
|
||||
{method}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-[20px] rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:leading-[20px]">
|
||||
{copyContent}
|
||||
|
||||
<Button className="rb:px-[8px]! rb:h-[28px]! rb:group" onClick={() => handleCopy(copyContent)}>
|
||||
useEffect(() => {
|
||||
getApiList()
|
||||
}, [])
|
||||
const getApiList = () => {
|
||||
if (!application) {
|
||||
return
|
||||
}
|
||||
setApiKeyList([])
|
||||
getApiKeyList({
|
||||
type: application.type,
|
||||
is_active: true,
|
||||
resource_id: application.id,
|
||||
page: 1,
|
||||
pagesize: 10,
|
||||
}).then(res => {
|
||||
const response = res as { items: ApiKey[] }
|
||||
const list = response.items ?? []
|
||||
getAllStats([...list])
|
||||
})
|
||||
}
|
||||
const getAllStats = (list: ApiKey[]) => {
|
||||
const allList: ApiKey[] = []
|
||||
list.forEach(async item => {
|
||||
await getApiKeyStats(item.id)
|
||||
.then(res => {
|
||||
const response = res as { requests_today: number; total_requests: number; quota_limit: number; quota_used: number; }
|
||||
allList.push({
|
||||
...item,
|
||||
...response,
|
||||
})
|
||||
setApiKeyList(prev => [...prev, {
|
||||
...item,
|
||||
...response,
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
const handleAdd = () => {
|
||||
apiKeyModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEdit = (vo: ApiKey) => {
|
||||
apiKeyConfigModalRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleDelete = (vo: ApiKey) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deleteApiKey(vo.id)
|
||||
.then(() => {
|
||||
getApiList();
|
||||
message.success(t('common.deleteSuccess'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算total_requests总数
|
||||
const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0);
|
||||
return (
|
||||
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card
|
||||
title={t('application.endpointConfiguration')}
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.endpointConfigurationSubTitle')}</div>
|
||||
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<Space size={8}>
|
||||
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
|
||||
<div key={method} className={clsx("rb:w-20 rb:h-7 rb:leading-7 rb:text-center rb:rounded-md rb:text-regular", {
|
||||
'rb:bg-[#155EEF] rb:text-white': activeMethods.includes(method),
|
||||
'rb:bg-white': !activeMethods.includes(method),
|
||||
})}>
|
||||
{method}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{copyContent}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(copyContent)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('application.apiKeys')}
|
||||
extra={
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
|
||||
}
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.apiKeySubTitle')}</div>
|
||||
{/* 总览数据 */}
|
||||
<Row>
|
||||
<Col span={6}>
|
||||
<Statistic title={t('application.apiKeyTotal')} value={apiKeyList.length} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic title={t('application.apiKeyRequestTotal')} value={totalRequests} />
|
||||
</Col>
|
||||
</Row>
|
||||
{/* API Key 列表 */}
|
||||
{apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => (
|
||||
<div key={item.id} className="rb:mt-4 rb:p-[10px_12px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:max-w-[calc(100%-92px)]">
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{item.name}</div>
|
||||
<Tag className="rb:ml-2">ID: {item.id}</Tag>
|
||||
</div>
|
||||
<Space>
|
||||
<div
|
||||
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="rb:mb-3 rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{maskApiKeys(item.api_key)}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(item.api_key)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={8}>
|
||||
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.apiKeyRequestTotal')} value={item.total_requests} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qpsLimit')} value={item.rate_limit} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('application.authenticationMethod')}
|
||||
// extra={
|
||||
// <Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
|
||||
// }
|
||||
>
|
||||
<div className="rb:p-[10px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:font-medium rb:text-center">
|
||||
{t('application.apiKeyTitle')}
|
||||
<p className="rb:mt-[6px] rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t('application.apiKeyDesc')}</p>
|
||||
</div>
|
||||
{apiKeyList.map((item, index) => (
|
||||
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-[20px] rb:p-[12px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:leading-[20px]">
|
||||
{item}
|
||||
))}
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button className="rb:px-[8px]! rb:h-[28px]! rb:group" onClick={() => handleCopy(item)}>
|
||||
<div
|
||||
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
{/* <div
|
||||
className="rb:w-[24px] rb:h-[24px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div> */}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
{/* <Card title={t('application.requestResponseExample')}>
|
||||
<div className="rb:mb-[12px] rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:font-regular">
|
||||
{t('application.requestExample')}
|
||||
<Button>{t('application.downloadPostmanCollection')}</Button>
|
||||
</div>
|
||||
<div className="rb:p-[16px_20px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:text-[#5B6167] rb:leading-[18px]">
|
||||
curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d
|
||||
</div>
|
||||
|
||||
<div className="rb:mb-[12px] rb:mt-[24px] rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:font-regular">
|
||||
{t('application.responseExample')}
|
||||
</div>
|
||||
<div className="rb:p-[16px_20px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:text-[#5B6167] rb:leading-[18px]">
|
||||
curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={t('application.rateLimitingStrategy')}>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-[18px]">
|
||||
{limitList.map(item => (
|
||||
<div key={item.key} className="rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-[8px] rb:p-[16px_20px]">
|
||||
<div className="rb:flex rb:justify-between">
|
||||
<div className="rb:leading-[20px]">
|
||||
{t(`application.${item.key}`)}
|
||||
<div className="rb:text-[14px] rb:font-medium rb:text-[#155EEF] rb:mt-[8px]">{item.value}{item.unit}</div>
|
||||
</div>
|
||||
<img src={item.icon} className="rb:w-[24px] rb:h-[24px]" />
|
||||
</div>
|
||||
<Slider style={{ margin: '24px 0 0 0' }} value={item.value} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={t('application.sdkDownload')}>
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
|
||||
{sdkList.map(item => (
|
||||
<div key={item} className="rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-[8px] rb:p-[24px_20px] rb:text-center">
|
||||
{t(`application.${item}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={t('application.advancedSettings')}>
|
||||
<Form.Item
|
||||
name="WebhookReturnsTimeout"
|
||||
label={<>{t('application.WebhookReturnsTimeout')}<span className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular"> ({t('application.WebhookReturnsTimeoutDesc')})</span></>}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="whitelistIP"
|
||||
label={<>{t('application.whitelistIP')}<span className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular"> ({t('application.whitelistIPDesc')})</span></>}
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="whitelistIP"
|
||||
className="rb:mb-[0]!"
|
||||
>
|
||||
<Checkbox>{t('application.publicAPIDocumentation')}</Checkbox>
|
||||
</Form.Item>
|
||||
</Card> */}
|
||||
</Space>
|
||||
{/* </Form> */}
|
||||
<ApiKeyModal
|
||||
ref={apiKeyModalRef}
|
||||
application={application}
|
||||
refresh={getApiList}
|
||||
/>
|
||||
<ApiKeyConfigModal
|
||||
ref={apiKeyConfigModalRef}
|
||||
refresh={getApiList}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState, useRef, type Key } from 'react'
|
||||
import { type FC, useEffect, useState, useRef, forwardRef, useImperativeHandle, type Key } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Card from './components/Card'
|
||||
@@ -11,17 +11,19 @@ import type {
|
||||
Config,
|
||||
SubAgentModalRef,
|
||||
ChatData,
|
||||
SubAgentItem
|
||||
SubAgentItem,
|
||||
ClusterRef
|
||||
} from './types'
|
||||
import Chat from './components/Chat'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import SubAgentModal from './components/SubAgentModal'
|
||||
import Empty from '@/components/Empty'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
|
||||
|
||||
const tagColors = ['processing', 'warning', 'default']
|
||||
const MAX_LENGTH = 5;
|
||||
const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
|
||||
const Cluster = forwardRef<ClusterRef, { application: Application }>(({application}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm()
|
||||
@@ -113,6 +115,9 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
|
||||
form.setFieldsValue({ master_agent_name: option.children })
|
||||
}
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSave
|
||||
}))
|
||||
|
||||
return (
|
||||
<Row className="rb:h-[calc(100vh-64px)]">
|
||||
@@ -199,7 +204,7 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
|
||||
chatList={chatList}
|
||||
updateChatList={setChatList}
|
||||
handleSave={handleSave}
|
||||
source="cluster"
|
||||
source="multi_agent"
|
||||
/>
|
||||
</RbCard>
|
||||
</Col>
|
||||
@@ -210,6 +215,6 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
|
||||
/>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Cluster
|
||||
131
web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Slider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ApiKeyConfigModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updateApiKey } from '@/api/apiKey';
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
|
||||
interface ApiKeyConfigModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ApiKey>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const values = Form.useWatch<ApiKey>([], form)
|
||||
const [editVo, setEditVo] = useState<ApiKey | null>(null)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditVo(null)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (apiKey: ApiKey) => {
|
||||
setVisible(true);
|
||||
setEditVo(apiKey)
|
||||
form.setFieldsValue({
|
||||
daily_request_limit: apiKey.daily_request_limit,
|
||||
rate_limit: apiKey.rate_limit
|
||||
});
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!editVo?.id) return
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
updateApiKey(editVo.id, {
|
||||
...editVo,
|
||||
...values
|
||||
})
|
||||
handleClose()
|
||||
setTimeout(() => {
|
||||
refresh()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.apiLimitConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="rb:px-2.5!"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* QPS 限制(每秒请求数) */}
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`application.qpsLimit`)}({t('application.qpsLimitTip')})
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-2">
|
||||
{t('application.qpsLimitDesc')}
|
||||
</div>
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name="rate_limit"
|
||||
>
|
||||
<Slider
|
||||
style={{ margin: '0' }}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
|
||||
1
|
||||
<span>{t('application.currentValue')}: {values?.rate_limit}{t('application.qpsLimitUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
{/* 日调用量限制 */}
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
{t(`application.dailyUsageLimit`)}
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-2">
|
||||
{t('application.dailyUsageLimitDesc')}
|
||||
</div>
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name="daily_request_limit"
|
||||
>
|
||||
<Slider
|
||||
style={{ margin: '0' }}
|
||||
min={100}
|
||||
max={100000}
|
||||
step={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
|
||||
100
|
||||
<span>{t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyConfigModal;
|
||||
104
web/src/views/ApplicationConfig/components/ApiKeyModal.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import type { ApiKeyModalRef } from '../types'
|
||||
import { createApiKey } from '@/api/apiKey';
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ApiKeyModalProps {
|
||||
refresh: () => void;
|
||||
application?: Application | null;
|
||||
}
|
||||
|
||||
const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
|
||||
refresh,
|
||||
application
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
form.resetFields();
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!application) return
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
createApiKey({
|
||||
...values,
|
||||
type: application.type,
|
||||
resource_id: application.id,
|
||||
scopes: ['app']
|
||||
})
|
||||
.then(() => {
|
||||
handleClose()
|
||||
refresh()
|
||||
message.success(t('common.createSuccess'))
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.addApiKey')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* Key 名称 */}
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('application.apiKeyName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('application.invalidVariableName') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('application.apiKeyNamePlaceholder')} />
|
||||
</FormItem>
|
||||
{/* 描述 */}
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('application.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('application.apiKeyDescPlaceholder')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyModal;
|
||||
@@ -1,46 +1,125 @@
|
||||
import { type FC, useRef, useEffect, useState } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import { Input, Form } from 'antd'
|
||||
import ChatIcon from '@/assets/images/application/chat.svg'
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.svg'
|
||||
import type { ChatItem, ChatData, Config } from '../types'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
|
||||
import type { ChatData, Config } from '../types'
|
||||
import { runCompare, draftRun } from '@/api/application'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
|
||||
interface ChatProps {
|
||||
chatList: ChatData[];
|
||||
data: Config;
|
||||
updateChatList: (list: ChatData[]) => void;
|
||||
updateChatList: React.Dispatch<React.SetStateAction<ChatData[]>>;
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
source?: 'cluster' | 'agent';
|
||||
source?: 'multi_agent' | 'agent';
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<{ message: string }>()
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isCluster, setIsCluster] = useState(source === 'cluster')
|
||||
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [compareLoading, setCompareLoading] = useState(false)
|
||||
|
||||
// 当聊天列表更新时,自动滚动到底部
|
||||
|
||||
useEffect(() => {
|
||||
// 延迟一下,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
scrollContainerRefs.current.forEach(container => {
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}, [chatList]);
|
||||
useEffect(() => {
|
||||
setIsCluster(source === 'cluster')
|
||||
setIsCluster(source === 'multi_agent')
|
||||
}, [source])
|
||||
|
||||
const addUserMessage = (message: string) => {
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), newUserMessage]
|
||||
})))
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
if (isCluster) {
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
const assistantMessages: Record<string, ChatItem> = {}
|
||||
chatList.forEach(item => {
|
||||
assistantMessages[item.model_config_id as string] = assistantMessage
|
||||
})
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id as string]]
|
||||
})))
|
||||
}
|
||||
}
|
||||
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
|
||||
if (!content || !model_config_id) return
|
||||
updateChatList(prev => {
|
||||
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 lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
conversation_id: conversation_id,
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
}
|
||||
return prev;
|
||||
})
|
||||
}
|
||||
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
|
||||
if (message_length > 0 || !model_config_id) return
|
||||
|
||||
updateChatList(prev => {
|
||||
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 lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}
|
||||
const handleSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
@@ -48,182 +127,47 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'question',
|
||||
content: message,
|
||||
time: Date.now(),
|
||||
};
|
||||
updateChatList((prev: ChatData[]) => {
|
||||
return prev.map(item => ({
|
||||
...item,
|
||||
list: [
|
||||
...(item.list || []),
|
||||
newUserMessage
|
||||
]
|
||||
}))
|
||||
})
|
||||
if (!message?.trim()) return
|
||||
|
||||
addUserMessage(message)
|
||||
form.setFieldsValue({ message: undefined })
|
||||
// 添加空的助手消息用于流式更新
|
||||
const assistantMessages: Record<string, ChatItem> = {};
|
||||
if (isCluster) {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages['cluster'] = assistantMessage;
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
chatList.forEach(item => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages[item.model_config_id] = assistantMessage;
|
||||
});
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id]]
|
||||
})))
|
||||
}
|
||||
addAssistantMessage()
|
||||
|
||||
const handleStreamMessage = (data: string) => {
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setCompareLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
conversation_id: parsed.conversation_id,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.content) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: (msg.content || '') + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.message_length === 0 && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.message_length === 0) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (currentEvent === 'compare_end') {
|
||||
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 };
|
||||
|
||||
switch(item.event) {
|
||||
case 'model_message':
|
||||
updateAssistantMessage(content, model_config_id, conversation_id)
|
||||
break;
|
||||
case 'model_end':
|
||||
updateErrorAssistantMessage(message_length, model_config_id)
|
||||
break;
|
||||
case 'compare_end':
|
||||
setLoading(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (isCluster) {
|
||||
draftRun(data.app_id, { message, conversation_id: conversationId, stream: true }, handleStreamMessage)
|
||||
.finally(() => setLoading(false))
|
||||
} else {
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -231,6 +175,136 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const addClusterAssistantMessage = () => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
}
|
||||
const updateClusterAssistantMessage = (content?: string) => {
|
||||
if (!content) return
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
...modelChatList[0],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
})
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'assistant') {
|
||||
return { ...msg, content: (msg.content || '') + content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
const updateClusterErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
...modelChatList[0],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
})
|
||||
}
|
||||
const handleClusterSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
addUserMessage(message)
|
||||
form.setFieldsValue({ message: undefined })
|
||||
addClusterAssistantMessage()
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setCompareLoading(false)
|
||||
|
||||
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) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break
|
||||
case 'message':
|
||||
updateClusterAssistantMessage(content)
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break;
|
||||
case 'model_end':
|
||||
updateClusterErrorAssistantMessage(message_length)
|
||||
break;
|
||||
case 'compare_end':
|
||||
setLoading(false);
|
||||
break;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
draftRun(
|
||||
data.app_id,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
stream: true
|
||||
},
|
||||
handleStreamMessage
|
||||
)
|
||||
.finally(() => setLoading(false))
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
|
||||
}
|
||||
@@ -240,6 +314,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
{chatList.length === 0
|
||||
? <Empty
|
||||
url={DebuggingEmpty}
|
||||
size={[300, 200]}
|
||||
title={t('application.debuggingEmpty')}
|
||||
subTitle={t('application.debuggingEmptyDesc')}
|
||||
className="rb:h-full"
|
||||
@@ -257,69 +332,55 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
<div className={clsx(
|
||||
"rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]",
|
||||
{
|
||||
'rb:rounded-tr-[12px]': index === chatList.length - 1,
|
||||
'rb:rounded-tl-[12px]': index === 0,
|
||||
'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-[16px] rb:h-[16px] rb:cursor-pointer rb:absolute rb:top-[12px] rb:right-[12px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
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>
|
||||
}
|
||||
{!chat.list || chat.list.length === 0
|
||||
? <Empty url={ChatIcon} title={t('application.chatEmpty')} className="rb:h-full" />
|
||||
: (
|
||||
<div ref={el => scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, {
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !isCluster,
|
||||
})}>
|
||||
{chat.list?.map((vo, voIndex) => {
|
||||
if (compareLoading && voIndex === chat.list?.length - 1) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div key={voIndex} className={clsx("rb:relative rb:mt-[24px]", {
|
||||
'rb:right-[16px] rb:text-right': vo.role === 'question',
|
||||
'rb:left-[16px] rb:text-left': vo.role !== 'question',
|
||||
})}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{vo.role === 'question' ? 'You' : chat.label}</div>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block', {
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': vo.role !== 'question' && vo.content === null,
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': vo.role === 'question' && vo.content,
|
||||
'rb:bg-[#ffffff] rb:border-[rgba(235,235,235,1)]': vo.role !== 'question' && (vo.content || vo.content === ''),
|
||||
'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,
|
||||
})}>
|
||||
<Markdown content={vo.content === null ? t('application.ReplyException') : vo.content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:pt-[24px]': true,
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !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' ? 'You' : chat.label}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
/>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-[10px] rb:p-[16px]">
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:p-4">
|
||||
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Form.Item name="message" className="rb:mb-0!">
|
||||
<Input
|
||||
className="rb:h-[44px] rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('application.chatPlaceholder')}
|
||||
onPressEnter={handleSend}
|
||||
onPressEnter={isCluster ? handleClusterSend : handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-[44px] rb:h-[44px] rb:cursor-pointer", {
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
|
||||
'rb:opacity-50': loading,
|
||||
})} onClick={handleSend} />
|
||||
})} onClick={isCluster ? handleClusterSend : handleSend} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useRef } from 'react';
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Layout, Tabs, Dropdown } from 'antd';
|
||||
import { Layout, Tabs, Dropdown, Button } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from '../index.module.css'
|
||||
@@ -11,7 +11,7 @@ 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 } from '../types'
|
||||
import type { CopyModalRef, WorkflowRef } from '../types'
|
||||
import { deleteApplication } from '@/api/application'
|
||||
import CopyModal from './CopyModal'
|
||||
|
||||
@@ -29,8 +29,12 @@ interface ConfigHeaderProps {
|
||||
activeTab: string;
|
||||
handleChangeTab: (key: string) => void;
|
||||
refresh: () => void;
|
||||
workflowRef: React.RefObject<WorkflowRef>
|
||||
}
|
||||
const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleChangeTab, refresh }) => {
|
||||
const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
application, activeTab, handleChangeTab, refresh,
|
||||
workflowRef
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
@@ -46,7 +50,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
|
||||
const formatMenuItems = () => {
|
||||
const items = ['edit', 'copy', 'delete'].map(key => ({
|
||||
key,
|
||||
icon: <img src={menuIcons[key]} className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]" />,
|
||||
icon: <img src={menuIcons[key]} className="rb:w-4 rb:h-4 rb:mr-2" />,
|
||||
label: t(`common.${key}`),
|
||||
}))
|
||||
return {
|
||||
@@ -85,12 +89,23 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
|
||||
const goToApplication = () => {
|
||||
navigate('/application', { replace: true })
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
workflowRef.current?.handleSave()
|
||||
}
|
||||
const run = () => {
|
||||
workflowRef.current?.handleSave(false)
|
||||
.then(() => {
|
||||
workflowRef.current?.handleRun()
|
||||
})
|
||||
}
|
||||
const clear = () => {
|
||||
workflowRef?.current?.graphRef?.current?.clearCells()
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Header className="rb:w-full rb:h-[64px] rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-[32px]">
|
||||
<div className="rb:h-[32px] rb:flex rb:items-center rb:font-medium">
|
||||
<div className="rb:w-[32px] rb:h-[32px] rb:rounded-[8px] rb:mr-[13px] rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[24px] rb:text-[#ffffff]">
|
||||
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
||||
<div className="rb:h-8 rb:flex rb:items-center rb:font-medium">
|
||||
<div className="rb:w-8 rb:h-8 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[24px] rb:text-[#ffffff]">
|
||||
{application?.name[0]}
|
||||
</div>
|
||||
|
||||
@@ -101,7 +116,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div
|
||||
className="rb:w-[20px] rb:h-[20px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
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')]"
|
||||
></div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@@ -114,10 +129,19 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
|
||||
className={styles.tabs}
|
||||
/>
|
||||
</div>
|
||||
<div className="rb:h-[32px] rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-[8px]" />
|
||||
{application?.type === 'workflow'
|
||||
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
||||
<Button onClick={clear}>{t('workflow.clear')}</Button>
|
||||
<Button onClick={run}>{t('workflow.run')}</Button>
|
||||
<Button type="primary" onClick={save}>{t('workflow.save')}</Button>
|
||||
{/* <Button type="primary">{t('workflow.export')}</Button> */}
|
||||
<img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} />
|
||||
</div>
|
||||
: <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('application.returnToApplicationList')}
|
||||
</div>
|
||||
}
|
||||
</Header>
|
||||
<ApplicationModal
|
||||
ref={applicationModalRef}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cascader } from 'antd'
|
||||
import type { CascaderProps } from 'antd';
|
||||
import { getModelProviderList } from '@/api/models'
|
||||
|
||||
interface Option {
|
||||
value?: string | number | null;
|
||||
label: React.ReactNode;
|
||||
children?: Option[];
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
const CustomSelect: FC<CascaderProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
useEffect(() => {
|
||||
getProviderList()
|
||||
}, []);
|
||||
|
||||
const getProviderList = () => {
|
||||
getModelProviderList().then(res => {
|
||||
const response = res as string[]
|
||||
setOptions(response.map((key: string) => ({
|
||||
value: key,
|
||||
label: t(`model.${key}`),
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
})))
|
||||
})
|
||||
}
|
||||
const loadData = (selectedOptions: Option[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
console.log(targetOption)
|
||||
}
|
||||
return (
|
||||
<Cascader
|
||||
options={options}
|
||||
loadData={loadData}
|
||||
changeOnSelect
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default CustomSelect;
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ConfigHeader from './components/ConfigHeader'
|
||||
import type { AgentRef } from './types'
|
||||
import type { AgentRef, ClusterRef, WorkflowRef } from './types'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import Agent from './Agent'
|
||||
import Api from './Api'
|
||||
import ReleasePage from './ReleasePage'
|
||||
import Cluster from './Cluster'
|
||||
import { getApplication } from '@/api/application'
|
||||
import { randomString } from '@/utils/common'
|
||||
import Workflow from '@/views/Workflow';
|
||||
|
||||
const apiKeyList = [`app-${randomString(24, false)}`]
|
||||
const ApplicationConfig: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const agentRef = useRef<AgentRef>(null)
|
||||
const clusterRef = useRef<ClusterRef>(null)
|
||||
const workflowRef = useRef<WorkflowRef>(null)
|
||||
const [application, setApplication] = useState<Application | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('arrangement');
|
||||
|
||||
@@ -23,6 +24,16 @@ const ApplicationConfig: React.FC = () => {
|
||||
.then(() => {
|
||||
setActiveTab(key)
|
||||
})
|
||||
} else if (activeTab === 'arrangement' && application?.type === 'multi_agent' && clusterRef.current) {
|
||||
clusterRef.current.handleSave(false)
|
||||
.then(() => {
|
||||
setActiveTab(key)
|
||||
})
|
||||
} else if (activeTab === 'arrangement' && application?.type === 'workflow' && workflowRef.current) {
|
||||
workflowRef.current.handleSave(false)
|
||||
.then(() => {
|
||||
setActiveTab(key)
|
||||
})
|
||||
} else {
|
||||
setActiveTab(key)
|
||||
}
|
||||
@@ -49,10 +60,12 @@ const ApplicationConfig: React.FC = () => {
|
||||
handleChangeTab={handleChangeTab}
|
||||
application={application as Application}
|
||||
refresh={getApplicationInfo}
|
||||
workflowRef={workflowRef}
|
||||
/>
|
||||
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} />}
|
||||
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster application={application as Application} />}
|
||||
{activeTab === 'api' && <Api apiKeyList={apiKeyList} />}
|
||||
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} application={application as Application} />}
|
||||
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
|
||||
{activeTab === 'api' && <Api application={application} />}
|
||||
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import type { GraphRef } from '@/views/Workflow/types';
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
|
||||
export interface ModelConfig {
|
||||
label?: string;
|
||||
@@ -115,6 +118,14 @@ export interface ApplicationModalData {
|
||||
export interface AgentRef {
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
}
|
||||
export interface ClusterRef {
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
}
|
||||
export interface WorkflowRef {
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
handleRun: () => void;
|
||||
graphRef: GraphRef
|
||||
}
|
||||
export interface ApplicationModalRef {
|
||||
handleOpen: (application?: Config) => void;
|
||||
}
|
||||
@@ -139,11 +150,6 @@ export interface ApiExtensionModalData {
|
||||
export interface ApiExtensionModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface ChatItem {
|
||||
role: 'answer' | 'question';
|
||||
content?: string;
|
||||
time: number;
|
||||
}
|
||||
export interface ChatData {
|
||||
label?: string;
|
||||
model_config_id?: string;
|
||||
@@ -191,4 +197,10 @@ export interface SubAgentItem {
|
||||
}
|
||||
export interface SubAgentModalRef {
|
||||
handleOpen: (agent?: SubAgentItem) => void;
|
||||
}
|
||||
export interface ApiKeyModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface ApiKeyConfigModalRef {
|
||||
handleOpen: (apiKey: ApiKey) => void;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
|
||||
const response = editVo?.id ? updateApplication(editVo.id, {
|
||||
...editVo,
|
||||
...values,
|
||||
} as Application) : addApplication(values as Application)
|
||||
}) : addApplication(values)
|
||||
response.then(() => {
|
||||
refresh()
|
||||
handleClose()
|
||||
@@ -127,7 +127,6 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
|
||||
label: t(`application.${type}`),
|
||||
labelDesc: t(`application.${type}Desc`),
|
||||
icon: typeIcons[type],
|
||||
disabled: editVo?.id || type === 'workflow'
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface Application {
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_type?: string;
|
||||
type: string;
|
||||
type: 'agent' | 'multi_agent' | 'workflow';
|
||||
visibility: string;
|
||||
status: string;
|
||||
tags: string[];
|
||||
|
||||
@@ -2,16 +2,24 @@ 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 } from 'antd'
|
||||
import { Flex, Skeleton, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Chat, { type ChatItem } from '@/views/MemoryConversation/components/Chat'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.svg'
|
||||
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
|
||||
import type { HistoryItem } from './types'
|
||||
import type { HistoryItem, QueryParams } from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import { randomString } from '@/utils/common'
|
||||
import BgImg from '@/assets/images/conversation/bg.png'
|
||||
import Chat from '@/components/Chat'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
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 dayjs from 'dayjs'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
|
||||
const Conversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -20,13 +28,8 @@ const Conversation: FC = () => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const userId = searchParams.get('user_id')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatLoading, setChatLoading] = useState(false)
|
||||
const [query, setQuery] = useState<{
|
||||
message?: string;
|
||||
web_search?: boolean;
|
||||
memory?: boolean;
|
||||
conversation_id?: string;
|
||||
}>({})
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [message, setMessage] = useState<string>('')
|
||||
const [conversation_id, setConversationId] = useState<string | null>(null)
|
||||
const [historyList, setHistoryList] = useState<HistoryItem[]>([])
|
||||
const [groupHistoryList, setGroupHistoryList] = useState<Record<string, HistoryItem[]>>({})
|
||||
@@ -36,14 +39,18 @@ const Conversation: FC = () => {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
||||
|
||||
const [form] = Form.useForm<QueryParams>()
|
||||
const queryValues = Form.useWatch<QueryParams>([], form)
|
||||
useEffect(() => {
|
||||
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
||||
setShareToken(shareToken)
|
||||
if (shareToken && shareToken !== '') return
|
||||
getShareToken(token as string, userId || randomString(12, false))
|
||||
.then(res => {
|
||||
localStorage.setItem(`shareToken_${token}`, res?.access_token || '')
|
||||
setShareToken(res?.access_token || '')
|
||||
const response = res as { access_token: string } || {}
|
||||
localStorage.setItem(`shareToken_${token}`, response.access_token ?? '')
|
||||
setShareToken(response.access_token ?? '')
|
||||
})
|
||||
}, [token])
|
||||
|
||||
@@ -73,7 +80,7 @@ const Conversation: FC = () => {
|
||||
setPageLoading(true);
|
||||
getConversationHistory(token, { page: flag ? 1 : page, pagesize: 20 })
|
||||
.then(res => {
|
||||
const response = res as { items: HistoryItem[], page: { hasnext: boolean } }
|
||||
const response = res as { items: HistoryItem[], page: { hasnext: boolean; page: number; pagesize: number; total: number } }
|
||||
const results = response?.items || []
|
||||
let list = []
|
||||
if (flag) {
|
||||
@@ -101,7 +108,7 @@ const Conversation: FC = () => {
|
||||
setConversationId(id)
|
||||
}
|
||||
if (!id) {
|
||||
setQuery({})
|
||||
setMessage('')
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
@@ -116,72 +123,81 @@ const Conversation: FC = () => {
|
||||
}
|
||||
}, [conversation_id])
|
||||
|
||||
const addUserMessage = (message: string = '') => {
|
||||
const newUserMessage: ChatItem = {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now()
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const newAssistantMessage: ChatItem = {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}
|
||||
setChatList(prev => [...prev, newAssistantMessage])
|
||||
}
|
||||
const updateAssistantMessage = (content: string = '') => {
|
||||
if (!content) 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),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!token || !shareToken) {
|
||||
return
|
||||
}
|
||||
// 添加必需的id和conversation_id属性
|
||||
const newUserMessage: ChatItem = {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: query?.message || '',
|
||||
created_at: Date.now()
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
|
||||
setLoading(true)
|
||||
setChatLoading(true)
|
||||
setChatList(prev => [...prev, {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}])
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: string) => {
|
||||
setChatLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
setStreamLoading(true)
|
||||
addUserMessage(message)
|
||||
addAssistantMessage()
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && currentEvent === 'message') {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content) {
|
||||
setChatList(prev => prev.map((msg, msgIndex) => {
|
||||
if (msgIndex === prev!.length - 1 && msg.role === 'assistant') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}))
|
||||
}
|
||||
} else if (line.startsWith('data:') && currentEvent === 'start') {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
currentConversationId = parsed.conversation_id
|
||||
} else if (currentEvent === 'end') {
|
||||
setLoading(false);
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.forEach((item) => {
|
||||
switch(item.event) {
|
||||
case 'start':
|
||||
const { conversation_id: newId } = item.data as { conversation_id: string }
|
||||
currentConversationId = newId
|
||||
break
|
||||
case 'message':
|
||||
const { content } = item.data as { content: string }
|
||||
updateAssistantMessage(content)
|
||||
break
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
if (currentConversationId && currentConversationId !== conversation_id) {
|
||||
setConversationId(currentConversationId)
|
||||
getHistory(true)
|
||||
}
|
||||
}
|
||||
getHistory(true)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
sendConversation(token as string, {
|
||||
message: query?.message || '',
|
||||
web_search: query?.web_search || false,
|
||||
memory: query?.memory || false,
|
||||
sendConversation({
|
||||
...queryValues,
|
||||
message: message || '',
|
||||
stream: true,
|
||||
conversation_id: conversation_id || null,
|
||||
}, handleStreamMessage, shareToken)
|
||||
@@ -192,12 +208,12 @@ const Conversation: FC = () => {
|
||||
|
||||
return (
|
||||
<Flex className="rb:w-full rb:p-[-16px]!">
|
||||
<div className="rb:w-[345px] rb:h-[100vh] rb:overflow-hidden rb:border-r rb:border-[#EAECEE] rb:p-[12px]">
|
||||
<div className="rb:group rb:flex rb:items-center rb:justify-center rb:font-regular rb:cursor-pointer rb:mb-[20px] rb:border rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:text-[#155EEF] rb:rounded-[8px] rb:py-[10px]"
|
||||
<div className="rb:w-[345px] rb:h-screen rb:overflow-hidden rb:border-r rb:border-[#EAECEE] rb:p-3">
|
||||
<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-[20px] rb:h-[20px] rb:cursor-pointer rb:mr-[8px] rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
||||
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>
|
||||
@@ -216,11 +232,11 @@ const Conversation: FC = () => {
|
||||
scrollableTarget="scrollableDiv"
|
||||
>
|
||||
{Object.entries(groupHistoryList).map(([date, items]) => (
|
||||
<div key={date} className="rb:mt-[24px] rb:first:mt-0">
|
||||
<div className="rb:leading-[20px] rb:text-[#5B6167] rb:mb-[8px] rb:pl-[4px] rb:font-regular">{date.replace(/\u200e|\u200f/g, '')}</div>
|
||||
<div key={date} className="rb:mt-6 rb:first:mt-0">
|
||||
<div className="rb:leading-5 rb:text-[#5B6167] rb:mb-2 rb:pl-1 rb:font-regular">{date.replace(/\u200e|\u200f/g, '')}</div>
|
||||
{items.map(item => (
|
||||
<div key={item.updated_at} className="rb:mb-[12px]">
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-[8px] rb:leading-[20px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
<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,
|
||||
})}
|
||||
onClick={() => handleChangeHistory(item.id)}
|
||||
@@ -237,18 +253,38 @@ const Conversation: FC = () => {
|
||||
<img src={BgImg} className="rb:absolute rb:bottom-0 rb:left-0 rb:w-[345px]" />
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:h-[100vh] rb:px-[16px] rb:flex-[1_1_auto]">
|
||||
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
|
||||
<Chat
|
||||
source="conversation"
|
||||
empty={
|
||||
<Empty url={AnalysisEmptyIcon} subTitle={t('memoryConversation.emptyDesc')} />
|
||||
}
|
||||
query={query}
|
||||
empty={<Empty url={AnalysisEmptyIcon} className="rb:h-full" subTitle={t('memoryConversation.emptyDesc')} />}
|
||||
contentClassName="rb:h-[calc(100%-152px)]"
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setQuery}
|
||||
onChange={setMessage}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
>
|
||||
<Form form={form}>
|
||||
<Flex gap={8}>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{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>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Chat>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@@ -10,4 +10,12 @@ export interface HistoryItem {
|
||||
is_active: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
message?: string;
|
||||
web_search?: boolean;
|
||||
memory?: boolean;
|
||||
stream: boolean;
|
||||
conversation_id?: string | null;
|
||||
}
|
||||
252
web/src/views/EmotionEngine/index.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, Slider, Button, Alert, message, Switch, Space } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
|
||||
import { getMemoryEmotionConfig, updateMemoryEmotionConfig } from '@/api/memory'
|
||||
import type { ConfigForm } from './types'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
const configList = [
|
||||
{
|
||||
key: 'emotion_enabled',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
key: 'emotion_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
},
|
||||
{
|
||||
key: 'emotion_min_intensity',
|
||||
type: 'slider',
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05
|
||||
},
|
||||
{
|
||||
key: 'emotion_extract_keywords',
|
||||
type: 'switch',
|
||||
hasSubTitle: true
|
||||
},
|
||||
{
|
||||
key: 'emotion_enable_subject',
|
||||
type: 'switch',
|
||||
hasSubTitle: true
|
||||
},
|
||||
]
|
||||
|
||||
const EmotionEngine: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const [configData, setConfigData] = useState<ConfigForm>({} as ConfigForm);
|
||||
const [form] = Form.useForm<ConfigForm>();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
useEffect(() => {
|
||||
getConfigData()
|
||||
}, [id])
|
||||
|
||||
const getConfigData = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
getMemoryEmotionConfig(id)
|
||||
.then((res) => {
|
||||
const response = res as ConfigForm
|
||||
const initialValues = {
|
||||
...response,
|
||||
}
|
||||
setConfigData(initialValues);
|
||||
form.setFieldsValue(initialValues);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Failed to load data');
|
||||
})
|
||||
}
|
||||
const handleReset = () => {
|
||||
form.setFieldsValue(configData);
|
||||
}
|
||||
const handleSave = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
updateMemoryEmotionConfig({
|
||||
...values,
|
||||
config_id: id
|
||||
})
|
||||
.then(() => {
|
||||
messageApi.success(t('common.saveSuccess'))
|
||||
setConfigData({...(values || {})})
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={
|
||||
<div className="rb:flex rb:items-center">
|
||||
<img src={strategyImpactSimulator} className="rb:w-5 rb:h-5 rb:mr-2" />
|
||||
{t('emotionEngine.emotionEngineConfig')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
offset: 0,
|
||||
lambda_time: 0.03,
|
||||
lambda_mem: 0.03,
|
||||
}}
|
||||
>
|
||||
{configList.map(config => {
|
||||
if (config.type === 'slider') {
|
||||
return (
|
||||
<div key={config.key} className=" rb:mb-6">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`emotionEngine.${config.key}`)}
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ">
|
||||
{t(`emotionEngine.${config.key}_desc`)}
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
>
|
||||
<Slider
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
tooltip={{ open: false }} max={config.max} min={config.min} step={config.step} style={{ margin: '0' }} />
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
|
||||
|
||||
<>{t('emotionEngine.currentValue')}: {values?.[config.key as keyof ConfigForm] || 0}</>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (config.type === 'customSelect') {
|
||||
return (
|
||||
<div key={config.key}>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`emotionEngine.${config.key}`)}
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
extra={t(`emotionEngine.${config.key}_desc`)}
|
||||
>
|
||||
<CustomSelect
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
valueKey='id'
|
||||
labelKey='name'
|
||||
hasAll={false}
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`emotionEngine.${config.key}`)}</span>
|
||||
{config.hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_desc`)}</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Row gutter={16} className="rb:mt-3">
|
||||
<Col span={12}>
|
||||
<Button block onClick={handleReset}>{t('common.reset')}</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button type="primary" loading={loading} block onClick={handleSave}>{t('common.save')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RbCard
|
||||
title={t('emotionEngine.emotion_min_intensity_description')}
|
||||
>
|
||||
<div className="rb:font-medium">{t('emotionEngine.question')}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mt-2">{t('emotionEngine.answer')}</div>
|
||||
<div className="rb:font-medium rb:mt-4 rb:mb-2">{t('emotionEngine.differentTitle')}</div>
|
||||
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{['low', 'middle', 'high'].map((key, index) => (
|
||||
<Alert
|
||||
key={key}
|
||||
type={(['warning', 'info', 'success'] as const)[index] as 'warning' | 'info' | 'success'}
|
||||
message={
|
||||
<div>
|
||||
<div className="rb:w-full rb:font-medium rb:flex rb:justify-between">
|
||||
{t(`emotionEngine.${key}_title`)}
|
||||
<Tag color={(['warning', 'processing', 'success'] as const)[index] as 'warning' | 'processing' | 'success'}>{t(`emotionEngine.${key}_tag`)}</Tag>
|
||||
</div>
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">
|
||||
<div><span className="rb:font-medium">{t('emotionEngine.advantage')}: </span>{t(`emotionEngine.${key}_advantage`)}</div>
|
||||
<div><span className="rb:font-medium">{t('emotionEngine.shortcoming')}: </span>{t(`emotionEngine.${key}_shortcoming`)}</div>
|
||||
<div><span className="rb:font-medium">{t('emotionEngine.scene')}: </span>{t(`emotionEngine.${key}_scene`)}</div>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:font-medium rb:mt-6 rb:mb-3">{t('emotionEngine.configSuggest')}</div>
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{['first', 'customer_service', 'data_analysis', 'risk_warning'].map(key => (
|
||||
<div className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">{t(`emotionEngine.${key}`)}: {t(`emotionEngine.${key}_desc`)}</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:font-medium rb:mt-6 rb:mb-3">{t('emotionEngine.actual_case')}</div>
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
<div className="rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
<span className="rb:font-medium">{t('emotionEngine.user_input')}: </span>
|
||||
{t('emotionEngine.user_input_message')}
|
||||
</div>
|
||||
{['neutral_emotion', 'minor_dissatisfaction', 'expect_improvement'].map((key, index) => (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
<div className="rb:w-[50%] rb:flex rb:items-center rb:justify-between rb:text-[12px]">
|
||||
{t(`emotionEngine.${key}`)}
|
||||
<span>{t('emotionEngine.confidence')}: {key === 'neutral_emotion' ? 0.85 : key === 'minor_dissatisfaction' ? 0.45 : 0.32}</span>
|
||||
</div>
|
||||
|
||||
<Tag color={(['success', 'warning', 'processing'] as const)[index] as 'warning' | 'processing' | 'success'}>{t(`emotionEngine.${key}_tag`)}</Tag>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</RbCard>
|
||||
</Col>
|
||||
{contextHolder}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmotionEngine;
|
||||
48
web/src/views/EmotionEngine/types.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 标签表单数据类型
|
||||
export interface TagFormData {
|
||||
tagName: string;
|
||||
type: string;
|
||||
color: string;
|
||||
description?: string;
|
||||
applicableScope?: string[];
|
||||
semanticExpansion?: string;
|
||||
isActive?: boolean;
|
||||
// 扩展字段用于区分编辑和新增操作
|
||||
isEditing?: boolean;
|
||||
tagId?: string;
|
||||
}
|
||||
|
||||
// 记忆总览数据类型
|
||||
export interface MemoryOverviewRecord {
|
||||
id: number;
|
||||
memoryID: string,
|
||||
contentSummary: string;
|
||||
type: string;
|
||||
createTime: string;
|
||||
lastCallTime: string;
|
||||
retentionDegree: string;
|
||||
status: string;
|
||||
}
|
||||
// 定义组件暴露的方法接口
|
||||
export interface MemoryOverviewFormRef {
|
||||
handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void;
|
||||
}
|
||||
|
||||
// 遗忘曲线数据类型
|
||||
export interface CurveRecord {
|
||||
memoryID: string;
|
||||
type: string;
|
||||
currentRetentionRate: string;
|
||||
finallyActivated: string;
|
||||
expectedForgettingTime: string;
|
||||
reinforcementCount: string;
|
||||
}
|
||||
|
||||
export interface ConfigForm {
|
||||
config_id: number | string;
|
||||
emotion_enabled: boolean;
|
||||
emotion_model_id: string;
|
||||
emotion_extract_keywords: boolean;
|
||||
emotion_min_intensity: number;
|
||||
emotion_enable_subject: boolean;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
|
||||
label={t('member.email')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('member.email') })} disabled={!!editingUser} />
|
||||
<Input placeholder={t('common.enterPlaceholder', { title: t('member.email') })} disabled={!!editingUser} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import ChatInput from './ChatInput'
|
||||
import type { TestParams } from '../index'
|
||||
import dayjs from 'dayjs'
|
||||
import Markdown from '@/components/Markdown'
|
||||
|
||||
interface ChatProps {
|
||||
empty?: ReactNode;
|
||||
data: ChatItem[];
|
||||
query?: TestParams;
|
||||
onChange: (query: TestParams) => void;
|
||||
onSend: () => void;
|
||||
loading: boolean;
|
||||
source?: 'conversation' | 'memory';
|
||||
}
|
||||
export interface ChatItem {
|
||||
id?: string;
|
||||
conversation_id?: string | null;
|
||||
role?: 'user' | 'assistant';
|
||||
content?: string;
|
||||
message?: string;
|
||||
created_at?: number | string;
|
||||
meta_data?: Record<string, string | number>[];
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({ empty, data, query, onChange, onSend, loading, source = 'memory' }) => {
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)>(null)
|
||||
const [isMemory, setIsMemory] = useState<boolean>(source === 'memory')
|
||||
|
||||
useEffect(() => {
|
||||
setIsMemory(source === 'memory')
|
||||
}, [source])
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (scrollContainerRefs.current) {
|
||||
scrollContainerRefs.current.scrollTop = scrollContainerRefs.current.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:relative rb:pt-[8px]">
|
||||
{data.length === 0 ? (
|
||||
<Flex vertical justify="space-between" className="rb:h-full rb:w-full rb:relative">
|
||||
{/* Empty */}
|
||||
<div className="rb:h-[calc(100%-144px)] rb:overflow-y-auto rb:overflow-x-hidden rb:flex rb:items-center rb:justify-center">
|
||||
{empty}
|
||||
</div>
|
||||
|
||||
<ChatInput source={source} query={query} onChange={onChange} onSend={onSend} loading={loading} />
|
||||
</Flex>
|
||||
)
|
||||
: (
|
||||
<div ref={scrollContainerRefs} className={clsx("rb:relative rb:overflow-y-auto", {
|
||||
'rb:h-[calc(100%-152px)]': !isMemory,
|
||||
'rb:h-[calc(100vh-362px)]': isMemory
|
||||
})}>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:relative", {
|
||||
'rb:mt-[24px]': index !== 0,
|
||||
'rb:right-[0] rb:text-right': item.role === 'user',
|
||||
'rb:left-[0] rb:text-left': item.role === 'assistant',
|
||||
})}>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-[400px]', {
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
|
||||
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant',
|
||||
})}>
|
||||
<Markdown content={item.content || ''} />
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChatInput source={source} query={query} onChange={onChange} onSend={onSend} loading={loading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Chat
|
||||
@@ -1,143 +0,0 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Flex, Input, Form } from 'antd'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg'
|
||||
import SendIcon from '@/assets/images/conversation/send.svg'
|
||||
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import LoadingIcon from '@/assets/images/conversation/loading.svg'
|
||||
import type { TestParams } from '../index'
|
||||
|
||||
interface ChatInputProps {
|
||||
query?: TestParams;
|
||||
onChange: (query: TestParams) => void;
|
||||
onSend: () => void;
|
||||
loading: boolean;
|
||||
source: 'conversation' | 'memory';
|
||||
}
|
||||
const searchSwitchList = [
|
||||
{
|
||||
icon: DeepThinkingIcon,
|
||||
checkedIcon: DeepThinkingCheckedIcon,
|
||||
value: '0',
|
||||
label: 'deepThinking' // 深度思考
|
||||
},
|
||||
{
|
||||
icon: MemoryFunctionIcon,
|
||||
checkedIcon: MemoryFunctionCheckedIcon,
|
||||
value: '1',
|
||||
label: 'normalReply' // 普通回复
|
||||
},
|
||||
{
|
||||
icon: OnlineIcon,
|
||||
checkedIcon: OnlineCheckedIcon,
|
||||
value: '2',
|
||||
label: 'quickReply' // 快速回复
|
||||
},
|
||||
]
|
||||
|
||||
const ChatInput: FC<ChatInputProps> = ({ source,query, onChange, onSend, loading }) => {
|
||||
const [form] = Form.useForm()
|
||||
const { t } = useTranslation();
|
||||
const values = Form.useWatch([], form);
|
||||
const [search_switch, setSearchSwitch] = useState('0')
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({...values, search_switch})
|
||||
}
|
||||
}, [values, search_switch, onChange])
|
||||
useEffect(() => {
|
||||
if (!query?.message) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [form, query?.message])
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
form.setFieldsValue({
|
||||
search_switch: value,
|
||||
})
|
||||
setSearchSwitch(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical" className="rb:absolute rb:bottom-[12px] rb:left-0 rb:right-0">
|
||||
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-[12px] rb:min-h-[120px]">
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Input.TextArea
|
||||
className="rb:m-[10px_12px_10px_12px]! rb:p-[0]! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto]"
|
||||
// rows={4}
|
||||
variant="borderless"
|
||||
autoSize={{ minRows: 2, maxRows: 2 }}
|
||||
onChange={(e) => onChange({ ...query, message: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && e.target.value?.trim() !== '' && !loading) {
|
||||
e.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Flex align="center" justify="space-between" className="rb:m-[0_10px_10px_10px]!">
|
||||
{source === 'memory' &&
|
||||
<Flex gap={8}>
|
||||
{searchSwitchList.map(item => (
|
||||
<ButtonCheckbox
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
checkedIcon={item.checkedIcon}
|
||||
checked={search_switch === item.value}
|
||||
onChange={() => handleChange(item.value)}
|
||||
>
|
||||
{t(`memoryConversation.${item.label}`)}
|
||||
</ButtonCheckbox>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
{source === 'conversation' &&
|
||||
<Flex gap={8}>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-[0]!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{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>
|
||||
</Flex>
|
||||
}
|
||||
{loading ? <img src={LoadingIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" />:
|
||||
!values || !values?.message || values?.message?.trim() === '' ?
|
||||
<img src={SendDisabledIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" />
|
||||
: <img src={SendIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" onClick={onSend} />
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
@@ -1,16 +1,48 @@
|
||||
import { type FC, type ReactNode, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Col, Row, App, Skeleton, Space, Select } from 'antd'
|
||||
import { Col, Row, App, Skeleton, Space, Select, Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.svg'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png'
|
||||
import Card from './components/Card'
|
||||
import Chat from './components/Chat'
|
||||
import { readService, getUserMemoryList } from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import type { Data } from '@/views/UserMemory/types'
|
||||
import Chat from '@/components/Chat'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.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 dayjs from 'dayjs'
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
|
||||
|
||||
const searchSwitchList = [
|
||||
{
|
||||
icon: DeepThinkingIcon,
|
||||
checkedIcon: DeepThinkingCheckedIcon,
|
||||
value: '0',
|
||||
label: 'deepThinking' // 深度思考
|
||||
},
|
||||
{
|
||||
icon: MemoryFunctionIcon,
|
||||
checkedIcon: MemoryFunctionCheckedIcon,
|
||||
value: '1',
|
||||
label: 'normalReply' // 普通回复
|
||||
},
|
||||
{
|
||||
icon: OnlineIcon,
|
||||
checkedIcon: OnlineCheckedIcon,
|
||||
value: '2',
|
||||
label: 'quickReply' // 快速回复
|
||||
},
|
||||
]
|
||||
|
||||
export interface TestParams {
|
||||
group_id: string;
|
||||
@@ -30,8 +62,8 @@ interface DataItem {
|
||||
export interface LogItem {
|
||||
type: string;
|
||||
title: string;
|
||||
data?: DataItem[] | Record<string, string>;
|
||||
raw_results?: string;
|
||||
data?: DataItem[] | AnyObject;
|
||||
raw_results?: string | AnyObject;
|
||||
summary?: string;
|
||||
query?: string;
|
||||
reason?: string;
|
||||
@@ -41,7 +73,7 @@ export interface LogItem {
|
||||
}
|
||||
|
||||
const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<div className="rb:p-[12px] rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
|
||||
<div className="rb:p-3 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -49,17 +81,13 @@ const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
const MemoryConversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp();
|
||||
const [query, setQuery] = useState<TestParams>({
|
||||
group_id: '',
|
||||
message: '',
|
||||
search_switch: '0',
|
||||
history: [],
|
||||
})
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [chatData, setChatData] = useState<{ content: string; created_at: string | number; role: string }[]>([])
|
||||
const [chatData, setChatData] = useState<ChatItem[]>([])
|
||||
const [logs, setLogs] = useState<LogItem[]>([])
|
||||
const [userList, setUserList] = useState<Data[]>([])
|
||||
const [search_switch, setSearchSwitch] = useState('0')
|
||||
const [msg, setMsg] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
getUserMemoryList().then(res => {
|
||||
@@ -75,11 +103,12 @@ const MemoryConversation: FC = () => {
|
||||
message.warning(t('common.inputPlaceholder', { title: t('memoryConversation.userID') }))
|
||||
return
|
||||
}
|
||||
setChatData(prev => [...prev, { content: query.message || '', created_at: new Date().getTime(), role: 'user' }])
|
||||
setChatData(prev => [...prev, { content: msg, created_at: new Date().getTime(), role: 'user' }])
|
||||
setLoading(true)
|
||||
readService({
|
||||
...query,
|
||||
message: msg,
|
||||
group_id: userId,
|
||||
search_switch: search_switch,
|
||||
history: [],
|
||||
})
|
||||
.then(res => {
|
||||
@@ -92,6 +121,10 @@ const MemoryConversation: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSearchSwitch(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
@@ -101,7 +134,7 @@ const MemoryConversation: FC = () => {
|
||||
value: item.end_user?.id,
|
||||
label: item?.name,
|
||||
}))}
|
||||
filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
filterOption={(inputValue, option) => option?.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
showSearch={true}
|
||||
// filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
placeholder={t('memoryConversation.searchPlaceholder')}
|
||||
@@ -118,14 +151,29 @@ const MemoryConversation: FC = () => {
|
||||
>
|
||||
<Chat
|
||||
empty={
|
||||
<Empty url={ConversationEmptyIcon} size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
<Empty url={ConversationEmptyIcon} className="rb:h-full" size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
}
|
||||
contentClassName='rb:h-[calc(100vh-362px)]'
|
||||
data={chatData}
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
onChange={setMsg}
|
||||
onSend={handleSend}
|
||||
loading={loading}
|
||||
/>
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
>
|
||||
<Flex gap={8}>
|
||||
{searchSwitchList.map(item => (
|
||||
<ButtonCheckbox
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
checkedIcon={item.checkedIcon}
|
||||
checked={search_switch === item.value}
|
||||
onChange={() => handleChange(item.value)}
|
||||
>
|
||||
{t(`memoryConversation.${item.label}`)}
|
||||
</ButtonCheckbox>
|
||||
))}
|
||||
</Flex>
|
||||
</Chat>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -139,13 +187,16 @@ const MemoryConversation: FC = () => {
|
||||
<Empty
|
||||
url={AnalysisEmptyIcon}
|
||||
className="rb:h-full"
|
||||
title={t('memoryConversation.memoryConversationAnalysisEmpty')}
|
||||
subTitle={t('memoryConversation.memoryConversationAnalysisEmptySubTitle')}
|
||||
size={[270, 170]}
|
||||
/>
|
||||
: <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{logs.map((log, logIndex) => (
|
||||
<div key={logIndex}
|
||||
className={clsx(
|
||||
`rb:p-[16px_24px] rb:rounded-[8px]`,
|
||||
'rb:border-[1px] rb:border-[#DFE4ED]',
|
||||
`rb:p-[16px_24px] rb:rounded-lg`,
|
||||
'rb:border rb:border-[#DFE4ED]',
|
||||
{
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#155EEF]': logIndex % 3 === 0,
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#369F21]': logIndex % 3 === 1,
|
||||
@@ -153,14 +204,14 @@ const MemoryConversation: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-[24px]">{log.title}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-6">{log.title}</div>
|
||||
{log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0
|
||||
? <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{log.data.map(vo => (
|
||||
<ContentWrapper key={vo.id}>
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{vo.id}. {vo.question}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{vo.reason}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{vo.reason}</div>
|
||||
</>
|
||||
</ContentWrapper>
|
||||
))}
|
||||
@@ -172,7 +223,7 @@ const MemoryConversation: FC = () => {
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{key}</div>
|
||||
{(log.data as Record<string, string[]>)[key].map((item, index) => (
|
||||
<div key={index} className="rb:mt-[8px] rb:text-[#5B6167] rb:text-[12px]">{item}</div>
|
||||
<div key={index} className="rb:mt-2 rb:text-[#5B6167] rb:text-[12px]">{item}</div>
|
||||
))}
|
||||
</>
|
||||
</ContentWrapper>
|
||||
@@ -180,15 +231,15 @@ const MemoryConversation: FC = () => {
|
||||
</Space>
|
||||
: log.type === 'search_result' && log.raw_results
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className='rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]'>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className='rb:mt-2 rb:text-[12px] rb:text-[#5B6167]'>
|
||||
{typeof log.raw_results === 'string'
|
||||
? <Markdown content={log.raw_results} />
|
||||
: <>
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string }, index: number) => (
|
||||
<div key={index}>{item.statement}</div>
|
||||
))}
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string }, index: number) => (
|
||||
<div key={index}>{item.content}</div>
|
||||
))}
|
||||
</>
|
||||
@@ -200,26 +251,26 @@ const MemoryConversation: FC = () => {
|
||||
: log.type === 'verification'
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{log.query}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{log.reason}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{log.result}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{log.reason}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{log.result}</div>
|
||||
</ContentWrapper>
|
||||
: log.type === 'output_type'
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167]">{log.summary}</div>
|
||||
</ContentWrapper>
|
||||
: log.type === 'input_summary' && log.raw_results
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:text-[#5B6167] rb:mb-[8px]">{log.summary}</div>
|
||||
<div className='rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]'>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:text-[#5B6167] rb:mb-2">{log.summary}</div>
|
||||
<div className='rb:mt-2 rb:text-[12px] rb:text-[#5B6167]'>
|
||||
{typeof log.raw_results === 'string'
|
||||
? <Markdown content={log.raw_results} />
|
||||
: <>
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string; } , index: number) => (
|
||||
<div key={index}>{item.statement}</div>
|
||||
))}
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string; }, index: number) => (
|
||||
<div key={index}>{item.content}</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -12,6 +12,7 @@ interface CardProps {
|
||||
expanded?: boolean;
|
||||
handleExpand?: (type: string) => void;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ const Card: FC<CardProps> = ({
|
||||
expanded,
|
||||
handleExpand,
|
||||
className,
|
||||
headerClassName,
|
||||
bodyClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -37,12 +39,13 @@ const Card: FC<CardProps> = ({
|
||||
onClick={() => handleExpand(type)}
|
||||
>
|
||||
{expanded ? t('common.foldUp') : t('common.expanded')}
|
||||
<img src={down} className={clsx("rb:w-[16px] rb:h-[16px] rb:ml-[4px]", {
|
||||
<img src={down} className={clsx("rb:w-4 rb:h-4 rb:ml-1", {
|
||||
'rb:rotate-180': !expanded,
|
||||
})} />
|
||||
</div>
|
||||
)}
|
||||
className={className}
|
||||
headerClassName={headerClassName}
|
||||
bodyClassName={bodyClassName}
|
||||
>
|
||||
{(expanded || !(type && handleExpand)) && children}
|
||||
|
||||
426
web/src/views/MemoryExtractionEngine/components/Result.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Progress } from 'antd'
|
||||
import { ExclamationCircleFilled, CheckCircleFilled, ClockCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import clsx from 'clsx'
|
||||
import Card from './Card'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import type { TestResult } from '../types'
|
||||
import { pilotRunMemoryExtractionConfig } from '@/api/memory'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import Tag, { type TagProps } from '@/components/Tag'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { groupDataByType } from '../constant'
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
|
||||
const resultObj = {
|
||||
extractTheNumberOfEntities: 'entities.extracted_count',
|
||||
numberOfEntityDisambiguation: 'disambiguation.block_count',
|
||||
memoryFragments: 'memory.chunks',
|
||||
numberOfRelationalTriples: 'triplets.count'
|
||||
}
|
||||
interface ResultProps {
|
||||
loading: boolean;
|
||||
handleSave: () => void;
|
||||
}
|
||||
interface ModuleItem {
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
data: any[],
|
||||
result: any,
|
||||
start_at?: number;
|
||||
end_at?: number;
|
||||
}
|
||||
const tagColors: {
|
||||
[key: string]: TagProps['color']
|
||||
} = {
|
||||
pending: 'default',
|
||||
processing: 'processing',
|
||||
completed: 'success',
|
||||
failed: 'error'
|
||||
}
|
||||
const initObj = {
|
||||
data: [],
|
||||
status: 'pending',
|
||||
result: null
|
||||
}
|
||||
|
||||
const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const [runLoading, setRunLoading] = useState(false)
|
||||
const [testResult, setTestResult] = useState<TestResult>({} as TestResult)
|
||||
|
||||
const [textPreprocessing, setTextPreprocessing] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
const [knowledgeExtraction, setKnowledgeExtraction] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
const [creatingNodesEdges, setCreatingNodesEdges] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
const [deduplication, setDeduplication] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
|
||||
const handleRun = () => {
|
||||
if(!id) return
|
||||
setTextPreprocessing({...initObj} as ModuleItem)
|
||||
setKnowledgeExtraction({...initObj} as ModuleItem)
|
||||
setCreatingNodesEdges({...initObj} as ModuleItem)
|
||||
setDeduplication({...initObj} as ModuleItem)
|
||||
setTestResult({} as TestResult)
|
||||
const handleStreamMessage = (list: SSEMessage[]) => {
|
||||
|
||||
list.forEach((data: AnyObject) => {
|
||||
switch(data.event) {
|
||||
case 'text_preprocessing': // 开始预处理文本
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'text_preprocessing_result': // 预处理文本分块中
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
}))
|
||||
break
|
||||
case 'text_preprocessing_complete': // 预处理文本完成
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'knowledge_extraction': // 开始知识抽取
|
||||
setKnowledgeExtraction(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'knowledge_extraction_result': // 知识抽取中
|
||||
setKnowledgeExtraction(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
}))
|
||||
break
|
||||
case 'knowledge_extraction_complete': // 知识抽取完成
|
||||
setKnowledgeExtraction(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'creating_nodes_edges': // 开始创建节点和边
|
||||
setCreatingNodesEdges(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'creating_nodes_edges_result': // 创建节点和边中
|
||||
setCreatingNodesEdges(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
}))
|
||||
break
|
||||
case 'creating_nodes_edges_complete': // 创建节点和边完成
|
||||
setCreatingNodesEdges(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'deduplication': // 开始去重消歧
|
||||
setDeduplication(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'dedup_disambiguation_result': // 去重消歧中
|
||||
setDeduplication(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data.data]
|
||||
}))
|
||||
break
|
||||
case 'dedup_disambiguation_complete': // 去重消歧完成
|
||||
setDeduplication(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'generating_results': // 开始生成结果
|
||||
break
|
||||
case 'result': // 结果
|
||||
setTestResult(data.data?.extracted_result)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
setRunLoading(true)
|
||||
pilotRunMemoryExtractionConfig({
|
||||
config_id: id,
|
||||
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
||||
}, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
}
|
||||
const completedNum = [textPreprocessing, knowledgeExtraction, creatingNodesEdges, deduplication].filter(item => item.status === 'completed').length
|
||||
const deduplicationData = groupDataByType(deduplication.data, 'result_type')
|
||||
|
||||
const formatTag = (status: string) => {
|
||||
return (
|
||||
<Tag color={tagColors[status]}>
|
||||
{status === 'pending' && <ClockCircleOutlined className="rb:mr-1" />}
|
||||
{status === 'processing' && <LoadingOutlined spin className="rb:mr-1" />}
|
||||
{t(`memoryExtractionEngine.status.${status}`)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
const formatTime = (data: ModuleItem, color?: string) => {
|
||||
if (typeof data.end_at === 'number' && typeof data.start_at === 'number') {
|
||||
return <div className={`rb:mt-3 rb:text-[${color ?? '#155EEF'}]`}>{t('memoryExtractionEngine.time')}{data.end_at - data.start_at}ms</div>
|
||||
}
|
||||
return null
|
||||
}
|
||||
const lowercaseFirst = (str: string) => str.charAt(0).toLowerCase() + str.slice(1)
|
||||
return (
|
||||
<Card
|
||||
title={t('memoryExtractionEngine.exampleMemoryExtractionResults')}
|
||||
subTitle={t('memoryExtractionEngine.exampleMemoryExtractionResultsSubTitle')}
|
||||
className="rb:min-h-[calc(100vh-330px)]!"
|
||||
headerClassName="rb:pb-0! rb:pt-4!"
|
||||
bodyClassName="rb:min-h-[calc(100vh-388px)] rb:p-[16px_20px]!"
|
||||
>
|
||||
<div className="rb:min-h-[calc(100vh-480px)] rb:overflow-y-auto">
|
||||
{runLoading
|
||||
? <>
|
||||
<RbAlert color="blue" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.processing')}
|
||||
</RbAlert>
|
||||
{/* 整体进度 */}
|
||||
<div className="rb:mb-2">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[12px] rb:leading-4 rb:font-regular">
|
||||
{t('memoryExtractionEngine.overallProgress')}
|
||||
<span className="rb:text-[#155eef]">{`${completedNum}/4`}</span>
|
||||
</div>
|
||||
<Progress percent={completedNum * 100/4} showInfo={false} />
|
||||
</div>
|
||||
</>
|
||||
: !testResult || Object.keys(testResult).length === 0
|
||||
? <RbAlert color="orange" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.warning')}
|
||||
</RbAlert>
|
||||
: <RbAlert color="green" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.success')}
|
||||
</RbAlert>
|
||||
}
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
{/* 文本预处理 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.text_preprocessing`)}
|
||||
extra={formatTag(textPreprocessing.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{textPreprocessing.data.map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
<Markdown content={'-' + t('memoryExtractionEngine.fragment') + vo.chunk_index + ': ' + (vo.content.startsWith('\n') ? vo.content : '\n' + vo.content)} />
|
||||
</div>
|
||||
))}
|
||||
{formatTime(textPreprocessing)}
|
||||
{textPreprocessing.result &&
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.text_preprocessing_desc', { count: textPreprocessing.result.total_chunks })},
|
||||
{t('memoryExtractionEngine.chunkerStrategy')}: {t(`memoryExtractionEngine.${lowercaseFirst(textPreprocessing.result.chunker_strategy)}`)}
|
||||
</RbAlert>
|
||||
}
|
||||
</RbCard>
|
||||
{/* 知识抽取 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.knowledge_extraction`)}
|
||||
extra={formatTag(knowledgeExtraction.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{knowledgeExtraction.data.map(vo =>
|
||||
<div key={vo.statement_index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{vo.statement}</div>
|
||||
)}
|
||||
{formatTime(knowledgeExtraction)}
|
||||
{knowledgeExtraction.result && <RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.knowledge_extraction_desc', {
|
||||
entities: knowledgeExtraction.result.entities_count,
|
||||
statements: knowledgeExtraction.result.statements_count,
|
||||
temporal_ranges_count: knowledgeExtraction.result.temporal_ranges_count,
|
||||
triplets: knowledgeExtraction.result.triplets_count
|
||||
})}
|
||||
</RbAlert>}
|
||||
</RbCard>
|
||||
{/* 创建实体关系 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.creating_nodes_edges`)}
|
||||
extra={formatTag(creatingNodesEdges.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
{creatingNodesEdges.data?.map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
{vo?.result_type === 'entity_nodes_creation'
|
||||
? <>{vo.type_display_name}: {vo.entity_names.join(', ')}</>
|
||||
: <>{vo?.relationship_text}</>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
{formatTime(creatingNodesEdges, '#9C6FFF')}
|
||||
{creatingNodesEdges.result && <RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.creating_nodes_edges_desc', {num: creatingNodesEdges.result.entity_entity_edges_count})}
|
||||
</RbAlert>}
|
||||
</RbCard>
|
||||
{/* 去重消歧 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.deduplication`)}
|
||||
extra={formatTag(deduplication.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
{Object.keys(deduplicationData).length > 0 && Object.keys(deduplicationData).map(key => {
|
||||
return deduplicationData[key].map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
{vo.message}
|
||||
</div>
|
||||
))
|
||||
})}
|
||||
{formatTime(deduplication, '#9C6FFF')}
|
||||
{deduplication.result && <RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.deduplication_desc', { count: deduplication.result.summary.total_merges })}<br />
|
||||
</RbAlert>}
|
||||
</RbCard>
|
||||
|
||||
{testResult && Object.keys(testResult).length > 0 && resultObj && Object.keys(resultObj).length > 0 &&
|
||||
<RbCard>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[40px_57px]">
|
||||
{Object.keys(resultObj).map((key, index) => {
|
||||
const keys = (resultObj as Record<string, string>)[key].split('.')
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{(testResult?.[keys[0] as keyof TestResult] as any)?.[keys[1]]}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{t(`memoryExtractionEngine.${key}`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#369F21] rb:leading-3.5 rb:font-regular">
|
||||
{}
|
||||
{key === 'extractTheNumberOfEntities' && testResult.dedup
|
||||
? t(`memoryExtractionEngine.${key}Desc`, {
|
||||
num: testResult.dedup.total_merged_count,
|
||||
exact: testResult.dedup.breakdown.exact,
|
||||
fuzzy: testResult.dedup.breakdown.fuzzy,
|
||||
llm: testResult.dedup.breakdown.llm,
|
||||
})
|
||||
: key === 'numberOfEntityDisambiguation' && testResult.disambiguation
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.disambiguation.effects?.length, block_count: testResult.disambiguation.block_count })
|
||||
: key === 'numberOfRelationalTriples' && testResult.triplets
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.triplets.count })
|
||||
:t(`memoryExtractionEngine.${key}Desc`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.entityDeduplicationImpact')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-medium rb:leading-4">{t('memoryExtractionEngine.identifyDuplicates')}</div>
|
||||
{testResult.dedup.impact.map((item, index) => (
|
||||
<div key={index} className="rb:pl-2 rb:mt-2 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">
|
||||
-{t('memoryExtractionEngine.identifyDuplicatesDesc', { ...item })}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.theEffectOfEntityDisambiguationLLMDriven')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{testResult.disambiguation.effects.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:text-[12px] rb:text-[#5B6167] rb:leading-4", {
|
||||
'rb:mt-4': index > 0,
|
||||
})}>
|
||||
<div className="rb:font-medium rb:mb-2">Disagreement Case {index +1}:</div>
|
||||
-{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → <span className="rb:text-[#369F21]">{item.result}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.core_entities && testResult?.core_entities.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.coreEntitiesAfterDedup')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#369F21]!"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-6">
|
||||
{testResult.core_entities.map((item, idx) => (
|
||||
<div key={idx} className="rb:text-[12px]">
|
||||
<div className="rb:text-[#369F21] rb:font-medium">{item.type}({item.count})</div>
|
||||
|
||||
<div>
|
||||
{item.entities.map((entity, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:font-regular rb:leading-4">
|
||||
-{entity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.triplet_samples && testResult?.triplet_samples.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.extractRelationalTriples')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{testResult.triplet_samples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[12px]">
|
||||
-({item.subject}, <span className="rb:text-[#9C6FFF] rb:font-medium">{item.predicate}</span>, {item.object})
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<RbAlert color="purple" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.extractRelationalTriplesDesc', { count: testResult.triplet_samples.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-5">
|
||||
<Button block loading={loading} onClick={handleSave}>{t('common.save')}</Button>
|
||||
<Button block type="primary" loading={runLoading} onClick={handleRun}>{t('memoryExtractionEngine.debug')}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default Result
|
||||
1698
web/src/views/MemoryExtractionEngine/constant.ts
Normal file
@@ -1,18 +1,16 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, Button, App, Skeleton, Form } from 'antd'
|
||||
import { ExclamationCircleFilled, CheckCircleFilled } from '@ant-design/icons'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Card from './components/Card'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import Empty from '@/components/Empty'
|
||||
import type { ConfigForm, ConfigVo, Variable, TestResult } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig, pilotRunMemoryExtractionConfig } from '@/api/memory'
|
||||
import type { ConfigForm, Variable } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { getModelList } from '@/api/models';
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
|
||||
const keys = [
|
||||
// 'example',
|
||||
@@ -20,229 +18,16 @@ const keys = [
|
||||
'arrangementLayerModule'
|
||||
]
|
||||
|
||||
const configList: ConfigVo[] = [
|
||||
{
|
||||
type: 'storageLayerModule',
|
||||
data: [
|
||||
{
|
||||
title: 'entityDeduplicationDisambiguation',
|
||||
list: [
|
||||
{
|
||||
label: 'enableLlmDedupBlockwise',
|
||||
variableName: 'enable_llm_dedup_blockwise',
|
||||
control: 'button', // switch
|
||||
type: 'tinyint',
|
||||
},
|
||||
{
|
||||
label: 'enableLlmDisambiguation',
|
||||
variableName: 'enable_llm_disambiguation',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
},
|
||||
{
|
||||
label: 'tNameStrict',
|
||||
control: 'slider',
|
||||
variableName: 't_name_strict',
|
||||
type: 'decimal',
|
||||
},
|
||||
{
|
||||
label: 'tTypeStrict',
|
||||
control: 'slider',
|
||||
variableName: 't_type_strict',
|
||||
type: 'decimal',
|
||||
},
|
||||
{
|
||||
label: 'tOverall',
|
||||
control: 'slider',
|
||||
variableName: 't_overall',
|
||||
type: 'decimal',
|
||||
},
|
||||
]
|
||||
},
|
||||
// 语义锚点标注
|
||||
{
|
||||
title: 'semanticAnchorAnnotationModule',
|
||||
list: [
|
||||
// 句子提取颗粒度
|
||||
{
|
||||
label: 'statementGranularity',
|
||||
variableName: 'statement_granularity',
|
||||
control: 'slider',
|
||||
type: 'decimal',
|
||||
max: 3,
|
||||
min: 1,
|
||||
step: 1,
|
||||
meaning: 'statementGranularityDesc',
|
||||
},
|
||||
// 是否包含对话上下文
|
||||
{
|
||||
label: 'includeDialogueContext',
|
||||
variableName: 'include_dialogue_context',
|
||||
control: 'button', // switch
|
||||
type: 'tinyint',
|
||||
meaning: 'includeDialogueContextDesc'
|
||||
},
|
||||
// 上下文文字上限
|
||||
{
|
||||
label: 'maxDialogueContextChars',
|
||||
variableName: 'max_context',
|
||||
control: 'inputNumber',
|
||||
min: 100,
|
||||
type: 'decimal',
|
||||
meaning: 'maxDialogueContextCharsDesc',
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'arrangementLayerModule',
|
||||
data: [
|
||||
{
|
||||
title: 'queryMode',
|
||||
list: [
|
||||
{
|
||||
label: 'deepRetrieval',
|
||||
variableName: 'deep_retrieval',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
meaning: 'deepRetrievalMeaning',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'dataPreprocessing',
|
||||
list: [
|
||||
{
|
||||
label: 'chunkerStrategy',
|
||||
variableName: 'chunker_strategy',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'recursiveChunker', value: 'RecursiveChunker' }, // 递归分块
|
||||
{ label: 'tokenChunker', value: 'TokenChunker' }, // token 分块
|
||||
{ label: 'semanticChunker', value: 'SemanticChunker' }, // 语义分块
|
||||
{ label: 'neuralChunker', value: 'NeuralChunker' }, // 神经网络分块
|
||||
{ label: 'hybridChunker', value: 'HybridChunker' }, // 混合分块
|
||||
{ label: 'llmChunker', value: 'LLMChunker' }, // LLM 分块
|
||||
{ label: 'sentenceChunker', value: 'SentenceChunker' }, // 句子分块
|
||||
{ label: 'lateChunker', value: 'LateChunker' }, // 延迟分块
|
||||
],
|
||||
meaning: 'chunkerStrategyDesc',
|
||||
},
|
||||
]
|
||||
},
|
||||
// 智能语义剪枝
|
||||
{
|
||||
title: 'intelligentSemanticPruning',
|
||||
list: [
|
||||
// 智能语义剪枝功能
|
||||
{
|
||||
label: 'intelligentSemanticPruningFunction',
|
||||
variableName: 'pruning_enabled',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
meaning: 'intelligentSemanticPruningFunctionDesc',
|
||||
},
|
||||
// 智能语义剪枝场景
|
||||
{
|
||||
label: 'intelligentSemanticPruningScene',
|
||||
variableName: 'pruning_scene',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'education', value: 'education' },
|
||||
{ label: 'online_service', value: 'online_service' },
|
||||
{ label: 'outbound', value: 'outbound' },
|
||||
],
|
||||
meaning: 'intelligentSemanticPruningSceneDesc',
|
||||
},
|
||||
// 智能语义剪枝阈值
|
||||
{
|
||||
label: 'intelligentSemanticPruningThreshold',
|
||||
control: 'slider',
|
||||
variableName: 'pruning_threshold',
|
||||
type: 'decimal',
|
||||
max: 0.9,
|
||||
min: 0,
|
||||
step: 0.1,
|
||||
meaning: 'intelligentSemanticPruningThresholdDesc',
|
||||
},
|
||||
]
|
||||
},
|
||||
// 自我反思引擎
|
||||
{
|
||||
title: 'selfReflexionEngine',
|
||||
list: [
|
||||
// 是否启用反思引擎
|
||||
{
|
||||
label: 'enableSelfReflexion',
|
||||
variableName: 'enable_self_reflexion',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
},
|
||||
// 迭代周期
|
||||
{
|
||||
label: 'iterationPeriod',
|
||||
variableName: 'iteration_period',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'oneHour', value: '1' },
|
||||
{ label: 'threeHours', value: '3' },
|
||||
{ label: 'sixHours', value: '6' },
|
||||
{ label: 'twelveHours', value: '12' },
|
||||
{ label: 'daily', value: '24' },
|
||||
],
|
||||
meaning: 'iterationPeriodDesc',
|
||||
},
|
||||
// 反思范围
|
||||
{
|
||||
label: 'reflexionRange',
|
||||
variableName: 'reflexion_range',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'retrieval', value: 'retrieval' },
|
||||
{ label: 'database', value: 'database' },
|
||||
],
|
||||
meaning: 'reflexionRangeDesc',
|
||||
},
|
||||
// 反思基线
|
||||
{
|
||||
label: 'reflectOnTheBaseline',
|
||||
variableName: 'baseline',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'basedOnTime', value: 'TIME' },
|
||||
{ label: 'basedOnFacts', value: 'FACT' },
|
||||
{ label: 'basedOnFactsAndTime', value: 'TIME-FACT' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const resultObj = {
|
||||
extractTheNumberOfEntities: 'entities.extracted_count',
|
||||
numberOfEntityDisambiguation: 'disambiguation.block_count',
|
||||
memoryFragments: 'memory.chunks',
|
||||
numberOfRelationalTriples: 'triplets.count'
|
||||
}
|
||||
|
||||
const ConfigDesc: FC<{ config: Variable, className?: string }> = ({config, className}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={className}>
|
||||
<Space size={8} className={clsx("rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] ")}>
|
||||
<Space size={8} className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
|
||||
{config.variableName && <span className="rb:font-regular">{t('memoryExtractionEngine.variableName')}: {config.variableName}</span>}
|
||||
{config.control && <span className="rb:font-regular">{t('memoryExtractionEngine.control')}: {t(`memoryExtractionEngine.${config.control}`)}</span>}
|
||||
{config.type && <span className="rb:font-regular">{t('memoryExtractionEngine.type')}: {config.type}</span>}
|
||||
</Space>
|
||||
{config.meaning && <div className={clsx("rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] ")}>{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}</div>}
|
||||
{config.meaning && <div className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -253,12 +38,9 @@ const MemoryExtractionEngine: FC = () => {
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>(keys)
|
||||
const [form] = Form.useForm<ConfigForm>()
|
||||
const [modelForm] = Form.useForm()
|
||||
// const [data, setData] = useState<ConfigForm>()
|
||||
const modelValues = Form.useWatch([], modelForm)
|
||||
const values = Form.useWatch<ConfigForm>([], form)
|
||||
const [testResult, setTestResult] = useState<TestResult | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [runLoading, setRunLoading] = useState(false)
|
||||
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
|
||||
@@ -305,8 +87,6 @@ const MemoryExtractionEngine: FC = () => {
|
||||
if (id) {
|
||||
getConfig()
|
||||
getModels()
|
||||
const lastResult = localStorage.getItem(`${id}_testResult`)
|
||||
setTestResult(lastResult ? JSON.parse(lastResult) : null)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
@@ -332,35 +112,11 @@ const MemoryExtractionEngine: FC = () => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleRun = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
setRunLoading(true)
|
||||
updateMemoryExtractionConfig({
|
||||
...values,
|
||||
...modelValues,
|
||||
config_id: id,
|
||||
}).then(() => {
|
||||
pilotRunMemoryExtractionConfig({
|
||||
config_id: id,
|
||||
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
||||
}).then((res) => {
|
||||
message.success(t('common.testSuccess'))
|
||||
const response = res as { extracted_result: TestResult }
|
||||
setTestResult(response.extracted_result || {})
|
||||
localStorage.setItem(`${id}_testResult`, JSON.stringify(response.extracted_result || {}))
|
||||
})
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:mb-[8px]">{t('memoryExtractionEngine.title')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-[20px] rb:mb-[24px]">{t('memoryExtractionEngine.subTitle')}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-8 rb:mb-2">{t('memoryExtractionEngine.title')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mb-6">{t('memoryExtractionEngine.subTitle')}</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
@@ -388,12 +144,12 @@ const MemoryExtractionEngine: FC = () => {
|
||||
handleExpand={handleExpand}
|
||||
>
|
||||
{expandedKeys.includes('example') &&
|
||||
<div className="rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:leading-[20px]">
|
||||
<div className="rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:leading-5">
|
||||
<Markdown content={t('memoryExtractionEngine.exampleText')} />
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
<Row gutter={[16, 16]} className="rb:mt-[16px]">
|
||||
<Row gutter={[16, 16]} className="rb:mt-4">
|
||||
<Col span={14}>
|
||||
<Form
|
||||
form={form}
|
||||
@@ -412,8 +168,8 @@ const MemoryExtractionEngine: FC = () => {
|
||||
<div
|
||||
key={vo.title}
|
||||
className={clsx(
|
||||
`rb:p-[16px_24px] rb:rounded-[8px]`,
|
||||
'rb:border-[1px] rb:border-[#DFE4ED]',
|
||||
`rb:p-[16px_24px] rb:rounded-lg`,
|
||||
'rb:border rb:border-[#DFE4ED]',
|
||||
{
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#155EEF]': index % 2 === 0,
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#369F21]': index % 2 !== 0,
|
||||
@@ -421,20 +177,20 @@ const MemoryExtractionEngine: FC = () => {
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px]">{t(`memoryExtractionEngine.${vo.title}`)}</div>
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">{t(`memoryExtractionEngine.${vo.title}SubTitle`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`memoryExtractionEngine.${vo.title}SubTitle`)}</div>
|
||||
|
||||
{vo.list.map(config => (
|
||||
<div key={config.label}>
|
||||
{config.control === 'button' &&
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-[24px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-[20px]">-{t(`memoryExtractionEngine.${config.label}`)}</span>
|
||||
<ConfigDesc config={config} className="rb:ml-[8px]" />
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">-{t(`memoryExtractionEngine.${config.label}`)}</span>
|
||||
<ConfigDesc config={config} className="rb:ml-2" />
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-[8px] rb:mb-[0px]!"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
@@ -442,10 +198,10 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}
|
||||
{config.control === 'select' &&
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px] rb:mt-[24px] rb:mb-[8px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
-{t(`memoryExtractionEngine.${config.label}`)}
|
||||
</div>
|
||||
<div className="rb:pl-[8px]">
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
>
|
||||
@@ -454,17 +210,17 @@ const MemoryExtractionEngine: FC = () => {
|
||||
options={config.options ? config.options.map(item => ({ ...item, label: t(`memoryExtractionEngine.${item.label}`) })) : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
<ConfigDesc config={config} className="rb:mt-[-16px]!" />
|
||||
<ConfigDesc config={config} className="rb:-mt-4!" />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{config.control === 'slider' &&
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px] rb:mt-[24px] rb:mb-[8px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
-{t(`memoryExtractionEngine.${config.label}`)}
|
||||
</div>
|
||||
<div className="rb:pl-[8px]">
|
||||
<ConfigDesc config={config} className="rb:mb-[10px]" />
|
||||
<div className="rb:pl-2">
|
||||
<ConfigDesc config={config} className="rb:mb-2.5" />
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
>
|
||||
@@ -475,7 +231,7 @@ const MemoryExtractionEngine: FC = () => {
|
||||
step={config.step || 0.01}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-[20px] rb:mt-[-26px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:mt-[-26px]">
|
||||
{config.min || 0}
|
||||
<span>{t('memoryExtractionEngine.CurrentValue')}: {values?.[config.variableName as keyof ConfigForm]}</span>
|
||||
</div>
|
||||
@@ -484,16 +240,16 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}
|
||||
{config.control === 'inputNumber' &&
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px] rb:mt-[24px] rb:mb-[8px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
-{t(`memoryExtractionEngine.${config.label}`)}
|
||||
</div>
|
||||
<div className="rb:pl-[8px]">
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
>
|
||||
<InputNumber min={config.min || 0} style={{ width: '100%' }} placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<ConfigDesc config={config} className="rb:mt-[-16px]!" />
|
||||
<ConfigDesc config={config} className="rb:-mt-4!" />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -508,148 +264,10 @@ const MemoryExtractionEngine: FC = () => {
|
||||
</Form>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Card
|
||||
title={t('memoryExtractionEngine.exampleMemoryExtractionResults')}
|
||||
subTitle={t('memoryExtractionEngine.exampleMemoryExtractionResultsSubTitle')}
|
||||
className="rb:min-h-[calc(100vh-330px)]!"
|
||||
bodyClassName="rb:min-h-[calc(100vh-388px)]"
|
||||
>
|
||||
<div
|
||||
className="rb:min-h-[calc(100vh-480px)] rb:overflow-y-auto"
|
||||
>
|
||||
{testResult && Object.keys(testResult).length > 0
|
||||
? <>
|
||||
<RbAlert color="orange" icon={<ExclamationCircleFilled />} className="rb:mb-[14px]">
|
||||
{t('memoryExtractionEngine.warning')}
|
||||
</RbAlert>
|
||||
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
{resultObj && Object.keys(resultObj).length > 0 &&
|
||||
<RbCard>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[40px_57px]">
|
||||
{Object.keys(resultObj).map(key => {
|
||||
const keys = (resultObj as Record<string, string>)[key].split('.')
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{testResult?.[keys[0] as keyof TestResult]?.[keys[1]]}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:font-regular">{t(`memoryExtractionEngine.${key}`)}</div>
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#369F21] rb:leading-[14px] rb:font-regular">
|
||||
{}
|
||||
{key === 'extractTheNumberOfEntities'
|
||||
? t(`memoryExtractionEngine.${key}Desc`, {
|
||||
num: testResult.dedup.total_merged_count,
|
||||
exact: testResult.dedup.breakdown.exact,
|
||||
fuzzy: testResult.dedup.breakdown.fuzzy,
|
||||
llm: testResult.dedup.breakdown.llm,
|
||||
})
|
||||
: key === 'numberOfEntityDisambiguation'
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.disambiguation.effects?.length, block_count: testResult.disambiguation.block_count })
|
||||
: key === 'numberOfRelationalTriples'
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.triplets.count })
|
||||
:t(`memoryExtractionEngine.${key}Desc`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.entityDeduplicationImpact')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-medium rb:leading-[16px]">{t('memoryExtractionEngine.identifyDuplicates')}</div>
|
||||
{testResult.dedup.impact.map((item, index) => (
|
||||
<div key={index} className="rb:pl-[8px] rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">
|
||||
-{t('memoryExtractionEngine.identifyDuplicatesDesc', { ...item })}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-[12px]">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.theEffectOfEntityDisambiguationLLMDriven')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{testResult.disambiguation.effects.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]", {
|
||||
'rb:mt-[16px]': index > 0,
|
||||
})}>
|
||||
<div className="rb:font-medium rb:mb-[8px]">Disagreement Case {index +1}:</div>
|
||||
-{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → <span className="rb:text-[#369F21]">{item.result}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-[12px]">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.core_entities && testResult?.core_entities.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.coreEntitiesAfterDedup')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#369F21]!"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[24px]">
|
||||
{testResult.core_entities.map(item => (
|
||||
<div key={item.type} className="rb:text-[12px]">
|
||||
<div className="rb:text-[#369F21] rb:font-medium">{item.type}({item.count})</div>
|
||||
|
||||
<div>
|
||||
{item.entities.map((entity, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">
|
||||
-{entity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.triplet_samples && testResult?.triplet_samples.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.extractRelationalTriples')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{testResult.triplet_samples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[12px]">
|
||||
-({item.subject}, <span className="rb:text-[#9C6FFF] rb:font-medium">{item.predicate}</span>, {item.object})
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<RbAlert color="purple" icon={<CheckCircleFilled />} className="rb:mt-[12px]">
|
||||
{t('memoryExtractionEngine.extractRelationalTriplesDesc', { count: testResult.triplet_samples.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
</Space>
|
||||
</>
|
||||
: loading
|
||||
? <Skeleton />
|
||||
: <Empty className="rb:h-full" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[16px] rb:mt-[20px]">
|
||||
<Button block loading={loading} onClick={handleSave}>{t('common.save')}</Button>
|
||||
<Button block type="primary" loading={runLoading} onClick={handleRun}>{t('memoryExtractionEngine.debug')}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Result
|
||||
loading={loading}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
|
||||