Merge #5 into develop_web from feature/20251219_zy

optimize: check en.ts

* feature/20251219_zy: (6 commits)
  optimize: UI update
  components: Add Chat component
  optimize: 1. stream request optimize; 2. replace Chat component
  feature: memory extraction engine debug switch to streaming output
  feature: add api key
  optimize: check en.ts

Signed-off-by: zhaoying <zhaoying@redbearai.com>
Merged-by: zhaoying <zhaoying@redbearai.com>

CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/5
This commit is contained in:
赵莹
2025-12-18 10:24:15 +08:00
50 changed files with 4241 additions and 1327 deletions

33
web/src/api/apiKey.ts Normal file
View 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`)
}

View File

@@ -1,7 +1,8 @@
import { request } from '@/utils/request'
import type { Application } 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'
// 应用列表
export const getApplicationListUrl = '/apps'
@@ -37,10 +38,10 @@ export const saveMultiAgentConfig = (app_id: string, values: Config) => {
return request.put(`/apps/${app_id}/multi-agent`, 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 +77,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}`

View File

@@ -9,6 +9,7 @@ import type {
ConfigForm as ExtractionConfigForm
} from '@/views/MemoryExtractionEngine/types'
import type { TestParams } from '@/views/MemoryConversation'
import { handleSSE, type SSEMessage } from '@/utils/stream'
// 记忆对话
export const readService = (query: TestParams) => {
@@ -132,8 +133,8 @@ 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)
}
/*************** end 记忆管理 相关接口 ******************************/

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 29</title>
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-应用管理" transform="translate(-24, -249)" stroke="#212332">
<g id="记忆库" transform="translate(12, 241)">
<g id="编组-29" transform="translate(12, 8)">
<g id="编组-30" transform="translate(1.5, 2)">
<path d="M5.15208739,12 L3.96504286,11.9873871 C3.14287934,11.9786512 2.48098017,11.3096817 2.48098017,10.4874718 L2.48098017,8.17573134 L2.48098017,8.17573134 L0.701703053,7.96314675 C0.482349244,7.93693878 0.325773604,7.73787163 0.351981572,7.51851782 C0.360586526,7.44649662 0.388612869,7.37817062 0.433061291,7.3208519 L1.79815052,5.56049306 L1.79815052,5.56049306 L1.79815052,5.43150782 C1.79815052,2.43176888 4.30576994,0 7.39907526,0 C10.4923806,0 13,2.43176888 13,5.43150782" id="路径" stroke-linecap="round"></path>
<path d="M8,6 L11,6 C11.5522847,6 12,6.44771525 12,7 L12,11 C12,11.5522847 11.5522847,12 11,12 L8,12 C7.44771525,12 7,11.5522847 7,11 L7,7 C7,6.44771525 7.44771525,6 8,6 Z" id="矩形"></path>
<line x1="7" y1="8" x2="12" y2="8" id="路径-26"></line>
<line x1="7" y1="10" x2="12" y2="10" id="路径-26备份"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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>
);

View 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

View File

@@ -0,0 +1,80 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-10 16:49:13
*/
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-[120px]">
{/* 消息输入表单 */}
<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-[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>
</div>
)
}
export default ChatInput

View 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

View 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;
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -16,7 +16,7 @@ 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]`}>
<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-[9px]">{icon}</span>}
{children}
</div>

View File

@@ -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={

View File

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

View File

@@ -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,7 @@ export const en = {
knowledgeCreateDataset: 'Create Dataset',
knowledgeDocumentDetails: 'Document Details',
userMemoryDetail: 'UserMemory Detail',
apiKeyManagement: 'API KEY Management',
},
dashboard: {
totalMemoryCapacity: 'Total Memory Capacity',
@@ -57,13 +58,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 +116,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 +263,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 +290,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',
@@ -329,7 +331,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,6 +403,17 @@ 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',
@@ -439,7 +452,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 +514,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',
@@ -656,16 +669,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',
},
@@ -742,10 +753,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',
@@ -789,7 +800,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',
@@ -902,7 +913,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',
@@ -915,7 +926,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.',
@@ -936,7 +947,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',
@@ -954,7 +965,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',
@@ -964,8 +975,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',
@@ -981,7 +1012,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',
@@ -992,7 +1023,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',
@@ -1002,7 +1033,7 @@ export const en = {
emotions: 'Emotions',
occupation: 'Occupation',
memories: 'memories',
expanded: 'Expanded',
expanded: 'Expand',
description: 'Description',
entityType: 'Entity Type',
conversationMemory: 'Conversation Storage Content',
@@ -1018,32 +1049,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',
@@ -1052,20 +1083,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}})',
@@ -1090,26 +1120,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',
selfReflexionEngine: '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',
@@ -1121,15 +1151,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)',
@@ -1152,7 +1182,28 @@ Memory Bear: Qin succeeded for several reasons: Shang Yangs 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 governments 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 governments 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...',
@@ -1236,6 +1287,31 @@ 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'
},
},
};

View File

@@ -28,16 +28,18 @@ export const zh = {
spaceManagement: '空间管理',
memoryExtractionEngine: '记忆提取引擎',
forgettingEngine: '遗忘引擎',
apiKeyManagement: 'API KEY管理',
knowledgePrivate: '详情',
knowledgeShare: '详情',
knowledgeCreateDataset: '新建数据集',
knowledgeDocumentDetails: '详情',
userMemoryDetail: '用户记忆详情',
toolManagement: '工具管理',
},
knowledgeBase: {
home: '首页',
selectSpace: '请选择空间',
preview:'预览',
preview: '预览',
pleaseUploadFileFirst: '请先上传文件',
shareSuccess: '分享成功',
shareFailed: '分享失败',
@@ -294,7 +296,7 @@ export const zh = {
number: '数字',
checkbox: '复选框',
apiVariable: 'API变量',
displayName: '显示名称',
maxLength: '最大长度',
required: '必填',
@@ -314,7 +316,7 @@ export const zh = {
promptConfiguration: '提示词配置',
configurationDesc: '定义Agent的角色、能力和行为准则',
aiPrompt: 'AI提示词',
promptPlaceholder: '你是一个专业的AI助手你的职责是..',
promptPlaceholder: '你是一个专业的AI助手你的职责是帮助用户解决问题。',
knowledgeBaseAssociation: '知识库关联',
associatedKnowledgeBase: '关联知识库',
addKnowledgeBase: '添加知识库',
@@ -473,7 +475,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分数阈值',
@@ -487,6 +489,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: {
@@ -624,7 +647,7 @@ export const zh = {
triplet_count_desc: '构建{{entities_count}}个实体节点和{{relations_count}}个关系连接',
temporal_count: '时间提取',
temporal_count_desc: '记录{{count}}条时间序列信息',
dialogue: '对话',
chunk: '分块',
statement: '语句',
@@ -877,6 +900,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)',
@@ -985,8 +1019,6 @@ export const zh = {
inviteToMember: '邀请成员',
member: '成员',
memberDesc: '只能使用应用,不能创建应用',
admin: '管理员',
adminDesc: '可以创建应用和管理团队设置',
sendInvitation: '发送邀请',
manager: '管理员',
managerDesc: '可以创建应用和管理团队设置',
@@ -1033,7 +1065,7 @@ export const zh = {
minimumRetention: '时间遗忘率 (λ_time)',
minimumRetentionDesc: '控制记忆随时间的遗忘速度,值越高时间越短',
forgettingRate: '记忆遗忘率 (λ_mem)',
forgettingRate: '记忆遗忘率 (λ_mem)',
forgettingRateDesc: '控制记忆遗忘的速度,值越高遗忘越快',
offset: '最小保留度 (offset)',
offsetDesc: '控制记忆保留的最小保留阈值 遗忘这地方改个文字描述',
@@ -1133,11 +1165,10 @@ export const zh = {
exampleMemoryExtractionResults: '示例记忆提取结果',
exampleMemoryExtractionResultsSubTitle: '(来自技术会议)',
warning: '当您修改左侧的配置项时,提取结论将在此处实时更新',
extractTheNumberOfEntities: '提取实体数量',
extractTheNumberOfEntitiesDesc: '去重后合并:{{num}}(精确:{{exact}},模糊:{{fuzzy}}LLM{{llm}}',
numberOfEntityDisambiguation: '实体消歧数量',
numberOfEntityDisambiguationDesc: '总计{{num}}次(阻止:{{block_count}}',
@@ -1223,7 +1254,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...',
@@ -1342,6 +1393,31 @@ export const zh = {
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: '过期'
},
},
}

View File

@@ -54,6 +54,7 @@ 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')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),

View File

@@ -25,6 +25,7 @@
{ "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": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
]

View File

@@ -243,6 +243,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
}
]
}

View 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))
}

View File

@@ -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;

View File

@@ -0,0 +1,102 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Switch, Button } 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>{data[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
: String(data[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(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') : '-'}
</span>
</div>
</>}
</RbModal>
);
});
export default ApiKeyDetailModal;

View 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;

View 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]">{t(`apiKey.${key}`)}</span>
<span>
{ key === 'created_at'
? formatDateTime(apiKeyItem[key], 'YYYY-MM-DD HH:mm:ss')
: key === 'is_expired'
? <Tag>{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;

View 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)
}

View File

@@ -239,6 +239,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
return [
...(prev || []).map(item => ({
...item,
conversation_id: undefined,
list: []
})),
newChatItem

View File

@@ -1,153 +1,189 @@
import { type FC, useState } from 'react';
import { type FC, useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Space, App
// Slider, Input,
// Form,
// Checkbox
} from 'antd';
import { Button, Space, App, Statistic, Row, Col, Divider } 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 } 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 { 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: () => {
}
})
}
// 计算total_requests总数
const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0);
return (
<div className="rb:w-[1000px] 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) => (
<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-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.qps')} value={item.quota_used} />
</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>
);
}

View File

@@ -199,7 +199,7 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
chatList={chatList}
updateChatList={setChatList}
handleSave={handleSave}
source="cluster"
source="multi_agent"
/>
</RbCard>
</Col>

View File

@@ -0,0 +1,127 @@
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>({} as ApiKey)
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(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 = () => {
form.validateFields()
.then((values) => {
updateApiKey(editVo.id, {
...editVo,
...values
})
handleClose()
refresh()
})
}
// 暴露给父组件的方法
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-[-26px]">
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-[-26px]">
100
<span>{t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')}</span>
</div>
</div>
</>
</Form>
</RbModal>
);
});
export default ApiKeyConfigModal;

View File

@@ -0,0 +1,103 @@
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,
})
.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;

View File

@@ -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.png'
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
import type { ChatItem, ChatData, Config } from '../types'
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))
}
@@ -258,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')} isNeedSubTitle={false} className="rb:h-full" size={[240, 200]} />
: (
<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>
</>
}

View File

@@ -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;

View File

@@ -8,9 +8,7 @@ import Api from './Api'
import ReleasePage from './ReleasePage'
import Cluster from './Cluster'
import { getApplication } from '@/api/application'
import { randomString } from '@/utils/common'
const apiKeyList = [`app-${randomString(24, false)}`]
const ApplicationConfig: React.FC = () => {
const { id } = useParams();
const agentRef = useRef<AgentRef>(null)
@@ -52,7 +50,7 @@ const ApplicationConfig: React.FC = () => {
/>
{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 === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
</>
);

View File

@@ -1,4 +1,5 @@
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
import type { ChatItem } from '@/components/Chat/types'
export interface ModelConfig {
label?: string;
@@ -139,11 +140,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 +187,10 @@ export interface SubAgentItem {
}
export interface SubAgentModalRef {
handleOpen: (agent?: SubAgentItem) => void;
}
export interface ApiKeyModalRef {
handleOpen: () => void;
}
export interface ApiKeyConfigModalRef {
handleOpen: (apiKey: ApiKey) => void;
}

View File

@@ -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[];

View File

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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.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}>
@@ -147,8 +195,8 @@ const MemoryConversation: FC = () => {
{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,
@@ -156,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>
))}
@@ -175,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>
@@ -183,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>
))}
</>
@@ -203,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>
))}
</>

View File

@@ -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}

View 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

File diff suppressed because it is too large Load Diff

View 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>
</>

View File

@@ -123,7 +123,7 @@ const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: item, value: item }))}
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
/>
</Form.Item>
</>
@@ -138,10 +138,9 @@ const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: item, value: item }))}
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
/>
</Form.Item>
{/* TODO:改成模型名称 */}
<Form.Item
name="model_name"
label={t('model.modelName')}

View File

@@ -8,6 +8,7 @@ const NoPermission = () => {
return (
<Empty
url={noPermission}
size={[240, 240]}
title={t('empty.noPermission')}
subTitle={t('empty.noPermissionDesc')}
className="rb:h-[calc(100vh-84px)]"

View File

@@ -8,6 +8,7 @@ const NotFound = () => {
return (
<Empty
url={notFoundImg}
size={[328, 146]}
title={t('empty.notFound')}
subTitle={t('empty.notFoundDesc')}
className="rb:h-[calc(100vh-84px)]"

View File

@@ -7,16 +7,17 @@ export interface Data {
other_address: string;
created_at: string;
updated_at: string;
},
memory_num: {
total: number;
counts: {
dialogue: number;
chunk: number;
statement: number;
entity: number;
}
},
memory_num: {
total: number;
counts: {
dialogue: number;
chunk: number;
statement: number;
entity: number;
}
},
name?: string;
}
export interface ConfigModalData {
llm: string;

View File

@@ -1,4 +1,4 @@
import React, { type FC, useEffect, useState, useRef } from 'react'
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Col } from 'antd'
@@ -29,13 +29,9 @@ const RelationshipNetwork:FC = () => {
const [categories, setCategories] = useState<{ name: string }[]>([])
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
useEffect(() => {
if (!id) return
getEdgeData()
}, [id])
// 关系网络
const getEdgeData = () => {
const getEdgeData = useCallback(() => {
if (!id) return
setSelectedNode(null)
getMemorySearchEdges(id).then((res) => {
@@ -45,20 +41,20 @@ const RelationshipNetwork:FC = () => {
const categories: { name: string }[] = []
list.forEach(item => {
if (item.edge) {
if (item.edge && item.edge.target_id && item.edge.source_id) {
links.push({
...item.edge,
target: item.edge?.target_id,
source: item.edge?.source_id,
target: item.edge.target_id,
source: item.edge.source_id,
})
}
if (item.sourceNode) {
nodes.push(item.sourceNode)
categories.push({name: item.sourceNode.entity_type})
categories.push({name: item.sourceNode.entity_type || 'Unknown'})
}
if (item.targetNode) {
nodes.push(item.targetNode)
categories.push({name: item.targetNode.entity_type})
categories.push({name: item.targetNode.entity_type || 'Unknown'})
}
})
@@ -76,14 +72,58 @@ const RelationshipNetwork:FC = () => {
setLinks(uniqueLinks)
setCategories(uniqueCategories)
// Calculate node frequency based on appearance in links
const nodeFrequency = new Map<string, number>()
// Count each node's appearance in links (both as source and target)
uniqueLinks.forEach(link => {
// Increment source node frequency (only if source exists and is a string)
if (typeof link.source === 'string') {
nodeFrequency.set(link.source, (nodeFrequency.get(link.source) || 0) + 1)
}
// Increment target node frequency (only if target exists and is a string)
if (typeof link.target === 'string') {
nodeFrequency.set(link.target, (nodeFrequency.get(link.target) || 0) + 1)
}
})
// Set minimum frequency to 1 for nodes not in any links
uniqueNodes.forEach(node => {
if (node.id && typeof node.id === 'string') {
if (!nodeFrequency.has(node.id)) {
nodeFrequency.set(node.id, 1)
}
}
})
uniqueNodes.map(item => {
const index = uniqueCategories.findIndex((n) => n.name === item.entity_type)
const index = uniqueCategories.findIndex((n) => n.name === (item.entity_type || 'Unknown'))
item.category = index
item.symbolSize = index < 10 ? 5 : index <100 ? 8 : 10
// Get frequency for the node, ensuring id is a string
const frequency = (item.id && typeof item.id === 'string') ? (nodeFrequency.get(item.id) || 1) : 1
// Set symbolSize based on frequency
// Adjust these thresholds based on expected frequency ranges
if (frequency <= 1) {
item.symbolSize = 5
} else if (frequency <= 10) {
item.symbolSize = 10
} else if (frequency <= 15) {
item.symbolSize = 15
} else if (frequency <= 20) {
item.symbolSize = 25
} else {
item.symbolSize = 35
}
})
setNodes(uniqueNodes)
})
}
}, [id])
useEffect(() => {
if (!id) return
getEdgeData()
}, [id])
useEffect(() => {
const handleResize = () => {
@@ -95,7 +135,7 @@ const RelationshipNetwork:FC = () => {
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
@@ -106,6 +146,8 @@ const RelationshipNetwork:FC = () => {
resizeObserver.disconnect()
}
}, [nodes])
console.log('nodes', nodes)
return (
<>
{/* 关系网络 */}
@@ -175,12 +217,10 @@ const RelationshipNetwork:FC = () => {
if (params.dataType === 'node') {
// 处理节点点击事件
console.log('Node clicked:', params.data);
setSelectedNode(params.data)
if (selectedNode?.id === params.data.id) {
setSelectedNode(null)
} else {
setSelectedNode(params.data)
}
// 使用函数式更新避免状态依赖问题
setSelectedNode(prevSelected =>
prevSelected?.id === params.data.id ? null : params.data
)
}
}
}}