diff --git a/web/src/api/apiKey.ts b/web/src/api/apiKey.ts new file mode 100644 index 00000000..56ad79c4 --- /dev/null +++ b/web/src/api/apiKey.ts @@ -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) => { + 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`) +} \ No newline at end of file diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 583ff8b9..72521d92 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -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, onMessage?: (data: string) => void) => { +export const runCompare = (app_id: string, values: Record, onMessage?: (data: SSEMessage[]) => void) => { return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage) } -export const draftRun = (app_id: string, values: Record, onMessage?: (data: string) => void) => { +export const draftRun = (app_id: string, values: Record, 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}` diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 7c7ae4c1..b52d5431 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -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 记忆管理 相关接口 ******************************/ diff --git a/web/src/assets/images/menu/userMemory_active.svg b/web/src/assets/images/menu/userMemory_active.svg deleted file mode 100644 index 554dc0bc..00000000 --- a/web/src/assets/images/menu/userMemory_active.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - 编组 29 - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/components/ButtonCheckbox/index.tsx b/web/src/components/ButtonCheckbox/index.tsx index bff09c88..65813809 100644 --- a/web/src/components/ButtonCheckbox/index.tsx +++ b/web/src/components/ButtonCheckbox/index.tsx @@ -34,12 +34,12 @@ const ButtonCheckbox: FC = ({ } return ( -
- {icon && !checked && } - {checkedIcon && checked && } + {icon && !checked && } + {checkedIcon && checked && } {children}
); diff --git a/web/src/components/Chat/ChatContent.tsx b/web/src/components/Chat/ChatContent.tsx new file mode 100644 index 00000000..2067f57e --- /dev/null +++ b/web/src/components/Chat/ChatContent.tsx @@ -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 = ({ + 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 ( +
+ {data.length === 0 + ? empty // 显示空状态 + : data.map((item, index) => ( +
+ {/* 流式加载时且内容为空则不显示 */} + {streamLoading && item.content === '' + ? null + : <> + {/* 顶部标签(如时间戳、用户名等) */} + {labelPosition === 'top' && +
+ {labelFormat(item)} +
+ } + {/* 消息气泡框 */} +
+ {/* 使用Markdown组件渲染消息内容 */} + +
+ {/* 底部标签(如时间戳、用户名等) */} + {labelPosition === 'bottom' && +
+ {labelFormat(item)} +
+ } + + } +
+ )) + } +
+ ) +} + +export default ChatContent diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx new file mode 100644 index 00000000..8dd19410 --- /dev/null +++ b/web/src/components/Chat/ChatInput.tsx @@ -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 ( +
+ + {/* 消息输入表单 */} +
+ + 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(); + } + }} + /> + +
+ + {/* 底部操作区域 */} + + {/* 子组件内容(如按钮等) */} + {children} + {/* 发送按钮 - 根据状态显示不同图标 */} + {loading + ? + : !values || !values?.message || values?.message?.trim() === '' + ? + : + } + +
+
+ ) +} + +export default ChatInput diff --git a/web/src/components/Chat/index.tsx b/web/src/components/Chat/index.tsx new file mode 100644 index 00000000..7db29bfc --- /dev/null +++ b/web/src/components/Chat/index.tsx @@ -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 = ({ + empty, + data, + onChange, + onSend, + streamLoading = false, + loading, + contentClassName = '', + children, + labelFormat, + errorDesc +}) => { + return ( +
+ {/* 聊天内容显示区域 */} + + + {/* 聊天输入框区域 */} + + {children} + +
+ ) +} +export default Chat diff --git a/web/src/components/Chat/types.ts b/web/src/components/Chat/types.ts new file mode 100644 index 00000000..851a8ccc --- /dev/null +++ b/web/src/components/Chat/types.ts @@ -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; + contentClassNames?: string | Record; + /** 聊天数据列表 */ + data: ChatItem[]; + /** 流式加载状态 */ + streamLoading: boolean; + /** 空状态显示内容 */ + empty?: ReactNode; + /** 标签位置:顶部或底部 */ + labelPosition?: 'top' | 'bottom'; + /** 标签格式化函数 */ + labelFormat: (item: ChatItem) => any; + errorDesc?: string; +} \ No newline at end of file diff --git a/web/src/components/CustomSelect/index.tsx b/web/src/components/CustomSelect/index.tsx index 411e36e0..97ca4e4b 100644 --- a/web/src/components/CustomSelect/index.tsx +++ b/web/src/components/CustomSelect/index.tsx @@ -9,7 +9,7 @@ interface ApiResponse { items?: T[]; } -interface CustomSelectProps { +interface CustomSelectProps extends Omit { url: string; params?: Record; valueKey?: string; diff --git a/web/src/components/PageScrollList/index.tsx b/web/src/components/PageScrollList/index.tsx index 0fc9fff2..a56969e8 100644 --- a/web/src/components/PageScrollList/index.tsx +++ b/web/src/components/PageScrollList/index.tsx @@ -29,7 +29,7 @@ interface PageScrollListProps { const PageScrollList = forwardRef(({ renderItem, - query = {}, + query, url, column = 4, className = '', @@ -51,11 +51,11 @@ const PageScrollList = forwardRef(({ 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 { diff --git a/web/src/components/RbAlert/index.tsx b/web/src/components/RbAlert/index.tsx index a92c34b5..63933ee3 100644 --- a/web/src/components/RbAlert/index.tsx +++ b/web/src/components/RbAlert/index.tsx @@ -16,7 +16,7 @@ const colors = { const RbAlert: FC = ({ color = 'blue', icon, className, children }) => { return ( -
+
{icon && {icon}} {children}
diff --git a/web/src/components/RbCard/Card.tsx b/web/src/components/RbCard/Card.tsx index beb237cd..f86b1c60 100644 --- a/web/src/components/RbCard/Card.tsx +++ b/web/src/components/RbCard/Card.tsx @@ -52,7 +52,7 @@ const RbCard: FC = ({ title={typeof title === 'function' ? title() : title ?
{avatarUrl - ? + ? : avatar ? avatar : null }
= ({ color = 'processing', children, className }) => { return ( - + {children} ) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 88698e22..79c35c7d 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -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 Yang’s reforms were thor Student: Then switching to Tang history: after the An Lushan Rebellion, the central government began reforms, so why did regional warlordism (the fanzhen problem) actually get worse? Memory Bear: After the rebellion, regional warlordism intensified for several reasons: military governors (jiedushi) held the power to recruit troops, control local finances, and command military forces, effectively becoming regional warlords; the central government’s finances declined due to the breakdown of the equal-field system and the collapse of the tax-labor system, making it increasingly unable to support the army, which pushed military forces to rely on the jiedushi; the recruitment-based military system made soldiers loyal to individual commanders rather than the state; eunuchs controlled the imperial guards, the civil bureaucracy lost influence, and the central government’s ability to balance regional power weakened. -` +`, + + warning: 'When you modify the configuration items on the left, click [Debug], and the extraction conclusions will be updated in real time here', + processing: 'Configuration updated, re-extracting sample memory...', + success: 'Memory extraction completed!', + overallProgress: 'Overall Progress', + text_preprocessing: 'Text Preprocessing', + fragment: 'Fragment', + knowledge_extraction: 'Knowledge Extraction', + creating_nodes_edges: 'Creating Entity Relationships', + deduplication: 'Deduplication and Disambiguation', + status: { + pending: 'Pending', + processing: 'Processing', + completed: 'Completed', + failed: 'Failed' + }, + time: 'Time: ', + text_preprocessing_desc: 'Text split into {{count}} semantic fragments', + knowledge_extraction_desc: 'Knowledge extraction completed, identified {{entities}} entities, {{statements}} statements, {{temporal_ranges_count}} temporal extractions, {{triplets}} triplets', + creating_nodes_edges_desc: 'Entity relationship creation completed, {{num}} relationships in total', + deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total' }, memoryConversation: { searchPlaceholder: 'Input user ID...', @@ -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' + }, }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7b68d8df..5ddf7b80 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -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: '过期' + }, }, } \ No newline at end of file diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index 4d7faf7a..bdcf7d78 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -54,6 +54,7 @@ const componentMap: Record>> = 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')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index 6c485a12..0b87e192 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -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" } ] diff --git a/web/src/store/menu.json b/web/src/store/menu.json index 448400b0..cda4a6b0 100644 --- a/web/src/store/menu.json +++ b/web/src/store/menu.json @@ -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 } ] } \ No newline at end of file diff --git a/web/src/utils/apiKeyReplacer.ts b/web/src/utils/apiKeyReplacer.ts new file mode 100644 index 00000000..a2914e46 --- /dev/null +++ b/web/src/utils/apiKeyReplacer.ts @@ -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)) +} \ No newline at end of file diff --git a/web/src/utils/stream.ts b/web/src/utils/stream.ts index 2c827c2f..3ef1db39 100644 --- a/web/src/utils/stream.ts +++ b/web/src/utils/stream.ts @@ -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; diff --git a/web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx b/web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx new file mode 100644 index 00000000..2c154f51 --- /dev/null +++ b/web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx @@ -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 void }>(({ handleCopy }, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [data, setData] = useState({} 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 ( + +
{t('apiKey.baseInfo')}
+ {['id', 'name', 'is_expired', 'created_at'].map((key, index) => ( +
+ {t(`apiKey.${key}`)} + + { key === 'created_at' + ? formatDateTime(data[key], 'YYYY-MM-DD HH:mm:ss') + : key === 'is_expired' + ? {data[key] ? t('apiKey.inactive') : t('apiKey.active')} + : String(data[key as keyof ApiKey]) + } + +
+ ))} + +
+ {maskApiKeys(data.api_key)} + + +
+ +
{t('apiKey.permissionInfo')}
+ +
+ {t(`apiKey.memoryEngine`)} + + + +
+
+ {t(`apiKey.knowledgeBase`)} + + + +
+ + {/* 高级设置 */} + {data.expires_at && <> +
{t('apiKey.advancedSettings')}
+ +
+ {t(`apiKey.expires_at`)} + + {data.expires_at ? formatDateTime(data.expires_at as number, 'yyyy-MM-DD') : '-'} + +
+ } +
+ ); +}); + +export default ApiKeyDetailModal; \ No newline at end of file diff --git a/web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx b/web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx new file mode 100644 index 00000000..f0bf4e11 --- /dev/null +++ b/web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx @@ -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(({ + refresh, +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [editVo, setEditVo] = useState(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 ( + +
+
{t('apiKey.baseInfo')}
+ + + + + + + + +
{t('apiKey.permissionInfo')}
+ + + + + + + + + + {/* 高级设置 */} +
{t('apiKey.advancedSettings')}
+ + + current && current < dayjs().subtract(1, 'day').endOf('day')} + /> + +
+
+ ); +}); + +export default ApiKeyModal; \ No newline at end of file diff --git a/web/src/views/ApiKeyManagement/index.tsx b/web/src/views/ApiKeyManagement/index.tsx new file mode 100644 index 00000000..007526de --- /dev/null +++ b/web/src/views/ApiKeyManagement/index.tsx @@ -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(null); + const apiKeyDetailModalRef = useRef(null) + const scrollListRef = useRef(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 ( + <> +
+ +
+ + ) => { + let apiKeyItem = item as unknown as ApiKey + return ( + + {['id', 'is_expired', 'created_at'].map((key, index) => ( +
+ {t(`apiKey.${key}`)} + + { key === 'created_at' + ? formatDateTime(apiKeyItem[key], 'YYYY-MM-DD HH:mm:ss') + : key === 'is_expired' + ? {apiKeyItem[key] ? t('apiKey.inactive') : t('apiKey.active')} + : String(apiKeyItem[key as keyof ApiKey]) + } + +
+ ))} + +
+ {maskApiKeys(apiKeyItem.api_key)} + + +
+ + + {apiKeyItem.scopes?.includes('memory') && {t('apiKey.memoryEngine')}} + {apiKeyItem.scopes?.includes('rag') && {t('apiKey.knowledgeBase')}} + + +
+ + + +
+
+ ); + }} + /> + + + + + ); +}; + +export default ApiKeyManagement; \ No newline at end of file diff --git a/web/src/views/ApiKeyManagement/types.ts b/web/src/views/ApiKeyManagement/types.ts new file mode 100644 index 00000000..2df67193 --- /dev/null +++ b/web/src/views/ApiKeyManagement/types.ts @@ -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) +} \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index dc600f1f..2e032d84 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -239,6 +239,7 @@ const Agent = forwardRef((_props, ref) => { return [ ...(prev || []).map(item => ({ ...item, + conversation_id: undefined, list: [] })), newChatItem diff --git a/web/src/views/ApplicationConfig/Api.tsx b/web/src/views/ApplicationConfig/Api.tsx index 90612687..1281f573 100644 --- a/web/src/views/ApplicationConfig/Api.tsx +++ b/web/src/views/ApplicationConfig/Api.tsx @@ -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(null); + const apiKeyConfigModalRef = useRef(null); + const [apiKeyList, setApiKeyList] = useState([]) const handleCopy = (content: string) => { copy(content) message.success(t('common.copySuccess')) } - return ( -
- {/*
*/} - - -
- - {['GET', 'POST', 'PUT', 'DELETE'].map((method) => ( - - ))} - -
- {copyContent} - - + ))} + + +
+ {copyContent} + + +
+
+ + + {t('application.addApiKey')} + } + > +
{t('application.apiKeySubTitle')}
+ {/* 总览数据 */} + + + + + + + + + {/* API Key 列表 */} + {apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => ( +
+
+
+
{item.name}
+ ID: {item.id} +
+
handleEdit(item)} + >
+
handleDelete(item)} + >
+
+
+
+ {maskApiKeys(item.api_key)} + +
+ + + + + + + + + + + +
-
- + {t('application.addApiKey')} - // } - > -
- {t('application.apiKeyTitle')} -

{t('application.apiKeyDesc')}

-
- {apiKeyList.map((item, index) => ( -
- {item} + ))} + + - - - {/*
handleDelete(index)} - >
*/} -
-
- ))} -
- {/* -
- {t('application.requestExample')} - -
-
- curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d -
- -
- {t('application.responseExample')} -
-
- curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d -
-
- -
- {limitList.map(item => ( -
-
-
- {t(`application.${item.key}`)} -
{item.value}{item.unit}
-
- -
- -
- ))} -
-
- -
- {sdkList.map(item => ( -
- {t(`application.${item}`)} -
- ))} -
-
- - {t('application.WebhookReturnsTimeout')} ({t('application.WebhookReturnsTimeoutDesc')})} - > - - - {t('application.whitelistIP')} ({t('application.whitelistIPDesc')})} - > - - - - {t('application.publicAPIDocumentation')} - - */} - - {/* */} + +
); } diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 66e8f5a9..7541e938 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -199,7 +199,7 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { chatList={chatList} updateChatList={setChatList} handleSave={handleSave} - source="cluster" + source="multi_agent" /> diff --git a/web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx b/web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx new file mode 100644 index 00000000..f66c02c5 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx @@ -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(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const values = Form.useWatch([], form) + const [editVo, setEditVo] = useState({} 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 ( + +
+ {/* QPS 限制(每秒请求数) */} + <> +
+ {t(`application.qpsLimit`)}({t('application.qpsLimitTip')}) +
+
+ {t('application.qpsLimitDesc')} +
+
+ + + +
+ 1 + {t('application.currentValue')}: {values?.rate_limit}{t('application.qpsLimitUnit')} +
+
+ + {/* 日调用量限制 */} + <> +
+ {t(`application.dailyUsageLimit`)} +
+
+ {t('application.dailyUsageLimitDesc')} +
+
+ + + +
+ 100 + {t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')} +
+
+ +
+
+ ); +}); + +export default ApiKeyConfigModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ApiKeyModal.tsx b/web/src/views/ApplicationConfig/components/ApiKeyModal.tsx new file mode 100644 index 00000000..2b18f07a --- /dev/null +++ b/web/src/views/ApplicationConfig/components/ApiKeyModal.tsx @@ -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(({ + 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 ( + +
+ {/* Key 名称 */} + + + + {/* 描述 */} + + + +
+
+ ); +}); + +export default ApiKeyModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index 9a70b5f2..ce4f94c9 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -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>; handleSave: (flag?: boolean) => Promise; - source?: 'cluster' | 'agent'; + source?: 'multi_agent' | 'agent'; } const Chat: FC = ({ 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(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 = {} + 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 = ({ 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 = {}; - 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 = ({ 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 = ({ chatList, data, updateChatList, handleSave, sourc
{chat.label}
handleDelete(index)} >
} - {!chat.list || chat.list.length === 0 - ? - : ( -
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 ( -
-
{vo.role === 'question' ? 'You' : chat.label}
-
- -
-
- ) - })} -
- ) - } + } + data={chat.list || []} + streamLoading={compareLoading} + labelPosition="top" + labelFormat={(item) => item.role === 'user' ? 'You' : chat.label} + errorDesc={t('application.ReplyException')} + /> +
))}
-
+
- + - + })} onClick={isCluster ? handleClusterSend : handleSend} />
} diff --git a/web/src/views/ApplicationConfig/components/ModelByProvider.tsx b/web/src/views/ApplicationConfig/components/ModelByProvider.tsx deleted file mode 100644 index f108ccd4..00000000 --- a/web/src/views/ApplicationConfig/components/ModelByProvider.tsx +++ /dev/null @@ -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 = () => { - const { t } = useTranslation(); - const [options, setOptions] = useState([]); - 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 ( - - ); -} -export default CustomSelect; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/index.tsx b/web/src/views/ApplicationConfig/index.tsx index 8ad25989..4fbba8b4 100644 --- a/web/src/views/ApplicationConfig/index.tsx +++ b/web/src/views/ApplicationConfig/index.tsx @@ -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(null) @@ -52,7 +50,7 @@ const ApplicationConfig: React.FC = () => { /> {activeTab === 'arrangement' && application?.type === 'agent' && } {activeTab === 'arrangement' && application?.type === 'multi_agent' && } - {activeTab === 'api' && } + {activeTab === 'api' && } {activeTab === 'release' && } ); diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index 607110a6..c5cda44e 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -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; } \ No newline at end of file diff --git a/web/src/views/ApplicationManagement/types.ts b/web/src/views/ApplicationManagement/types.ts index 4c5f928a..03d8b1b0 100644 --- a/web/src/views/ApplicationManagement/types.ts +++ b/web/src/views/ApplicationManagement/types.ts @@ -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[]; diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index 149b2261..e19f4f06 100644 --- a/web/src/views/Conversation/index.tsx +++ b/web/src/views/Conversation/index.tsx @@ -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('') const [conversation_id, setConversationId] = useState(null) const [historyList, setHistoryList] = useState([]) const [groupHistoryList, setGroupHistoryList] = useState>({}) @@ -36,14 +39,18 @@ const Conversation: FC = () => { const [hasMore, setHasMore] = useState(true); const scrollRef = useRef(null); const [shareToken, setShareToken] = useState(localStorage.getItem(`shareToken_${token}`)) + + const [form] = Form.useForm() + const queryValues = Form.useWatch([], 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 ( -
-
+
handleChangeHistory(null)} >
{t('memoryConversation.startANewConversation')}
@@ -216,11 +232,11 @@ const Conversation: FC = () => { scrollableTarget="scrollableDiv" > {Object.entries(groupHistoryList).map(([date, items]) => ( -
-
{date.replace(/\u200e|\u200f/g, '')}
+
+
{date.replace(/\u200e|\u200f/g, '')}
{items.map(item => ( -
-
+
handleChangeHistory(item.id)} @@ -237,18 +253,38 @@ const Conversation: FC = () => {
-
+
- } - query={query} + empty={} + 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')} + > +
+ + + + {t(`memoryConversation.web_search`)} + + + + + {t(`memoryConversation.memory`)} + + + +
+
) diff --git a/web/src/views/Conversation/types.ts b/web/src/views/Conversation/types.ts index f1fb199e..b1f28879 100644 --- a/web/src/views/Conversation/types.ts +++ b/web/src/views/Conversation/types.ts @@ -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; } \ No newline at end of file diff --git a/web/src/views/MemberManagement/components/MemberModal.tsx b/web/src/views/MemberManagement/components/MemberModal.tsx index cd1e9613..052c5e29 100644 --- a/web/src/views/MemberManagement/components/MemberModal.tsx +++ b/web/src/views/MemberManagement/components/MemberModal.tsx @@ -132,7 +132,7 @@ const MemberModal = forwardRef(({ label={t('member.email')} rules={[{ required: true, message: t('common.pleaseEnter') }]} > - + 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[]; -} - -const Chat: FC = ({ empty, data, query, onChange, onSend, loading, source = 'memory' }) => { - const scrollContainerRefs = useRef<(HTMLDivElement | null)>(null) - const [isMemory, setIsMemory] = useState(source === 'memory') - - useEffect(() => { - setIsMemory(source === 'memory') - }, [source]) - useEffect(() => { - setTimeout(() => { - if (scrollContainerRefs.current) { - scrollContainerRefs.current.scrollTop = scrollContainerRefs.current.scrollHeight; - } - }, 0); - }, [data]) - - return ( -
- {data.length === 0 ? ( - - {/* Empty */} -
- {empty} -
- - -
- ) - : ( -
- {data.map((item, index) => ( -
-
- -
-
{dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
-
- ))} -
- )} - - -
- ) -} -export default Chat diff --git a/web/src/views/MemoryConversation/components/ChatInput.tsx b/web/src/views/MemoryConversation/components/ChatInput.tsx deleted file mode 100644 index 2b98f515..00000000 --- a/web/src/views/MemoryConversation/components/ChatInput.tsx +++ /dev/null @@ -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 = ({ 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 ( -
- - - onChange({ ...query, message: e.target.value })} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey && e.target.value?.trim() !== '' && !loading) { - e.preventDefault(); - onSend(); - } - }} - /> - - - - {source === 'memory' && - - {searchSwitchList.map(item => ( - handleChange(item.value)} - > - {t(`memoryConversation.${item.label}`)} - - ))} - - } - {source === 'conversation' && - - - - {t(`memoryConversation.web_search`)} - - - - - {t(`memoryConversation.memory`)} - - - - } - {loading ? : - !values || !values?.message || values?.message?.trim() === '' ? - - : - } - - -
- ) -} - -export default ChatInput diff --git a/web/src/views/MemoryConversation/index.tsx b/web/src/views/MemoryConversation/index.tsx index c92044cc..bb8df5e4 100644 --- a/web/src/views/MemoryConversation/index.tsx +++ b/web/src/views/MemoryConversation/index.tsx @@ -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; - 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 }) => ( -
+
{children}
) @@ -49,17 +81,13 @@ const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => ( const MemoryConversation: FC = () => { const { t } = useTranslation() const { message } = App.useApp(); - const [query, setQuery] = useState({ - group_id: '', - message: '', - search_switch: '0', - history: [], - }) const [userId, setUserId] = useState() const [loading, setLoading] = useState(false) - const [chatData, setChatData] = useState<{ content: string; created_at: string | number; role: string }[]>([]) + const [chatData, setChatData] = useState([]) const [logs, setLogs] = useState([]) const [userList, setUserList] = useState([]) + const [search_switch, setSearchSwitch] = useState('0') + const [msg, setMsg] = useState('') 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 ( <> @@ -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 = () => { > + } + 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')} + > + + {searchSwitchList.map(item => ( + handleChange(item.value)} + > + {t(`memoryConversation.${item.label}`)} + + ))} + + @@ -147,8 +195,8 @@ const MemoryConversation: FC = () => { {logs.map((log, logIndex) => (
{ } )} > -
{log.title}
+
{log.title}
{log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0 ? {log.data.map(vo => ( <>
{vo.id}. {vo.question}
-
{vo.reason}
+
{vo.reason}
))} @@ -175,7 +223,7 @@ const MemoryConversation: FC = () => { <>
{key}
{(log.data as Record)[key].map((item, index) => ( -
{item}
+
{item}
))} @@ -183,15 +231,15 @@ const MemoryConversation: FC = () => {
: log.type === 'search_result' && log.raw_results ? -
{log.query}
-
+
{log.query}
+
{typeof log.raw_results === 'string' ? : <> - {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) => (
{item.statement}
))} - {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) => (
{item.content}
))} @@ -203,26 +251,26 @@ const MemoryConversation: FC = () => { : log.type === 'verification' ?
{log.query}
-
{log.reason}
-
{log.result}
+
{log.reason}
+
{log.result}
: log.type === 'output_type' ? -
{log.query}
+
{log.query}
{log.summary}
: log.type === 'input_summary' && log.raw_results ? -
{log.query}
-
{log.summary}
-
+
{log.query}
+
{log.summary}
+
{typeof log.raw_results === 'string' ? : <> - {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) => (
{item.statement}
))} - {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) => (
{item.content}
))} diff --git a/web/src/views/MemoryExtractionEngine/components/Card.tsx b/web/src/views/MemoryExtractionEngine/components/Card.tsx index e98dd5d1..7734ec08 100644 --- a/web/src/views/MemoryExtractionEngine/components/Card.tsx +++ b/web/src/views/MemoryExtractionEngine/components/Card.tsx @@ -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 = ({ expanded, handleExpand, className, + headerClassName, bodyClassName, }) => { const { t } = useTranslation() @@ -37,12 +39,13 @@ const Card: FC = ({ onClick={() => handleExpand(type)} > {expanded ? t('common.foldUp') : t('common.expanded')} -
)} className={className} + headerClassName={headerClassName} bodyClassName={bodyClassName} > {(expanded || !(type && handleExpand)) && children} diff --git a/web/src/views/MemoryExtractionEngine/components/Result.tsx b/web/src/views/MemoryExtractionEngine/components/Result.tsx new file mode 100644 index 00000000..d0850606 --- /dev/null +++ b/web/src/views/MemoryExtractionEngine/components/Result.tsx @@ -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 = ({ loading, handleSave }) => { + const { t } = useTranslation(); + const { id } = useParams() + const [runLoading, setRunLoading] = useState(false) + const [testResult, setTestResult] = useState({} as TestResult) + + const [textPreprocessing, setTextPreprocessing] = useState(initObj as ModuleItem) + const [knowledgeExtraction, setKnowledgeExtraction] = useState(initObj as ModuleItem) + const [creatingNodesEdges, setCreatingNodesEdges] = useState(initObj as ModuleItem) + const [deduplication, setDeduplication] = useState(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 ( + + {status === 'pending' && } + {status === 'processing' && } + {t(`memoryExtractionEngine.status.${status}`)} + + ) + } + const formatTime = (data: ModuleItem, color?: string) => { + if (typeof data.end_at === 'number' && typeof data.start_at === 'number') { + return
{t('memoryExtractionEngine.time')}{data.end_at - data.start_at}ms
+ } + return null + } + const lowercaseFirst = (str: string) => str.charAt(0).toLowerCase() + str.slice(1) + return ( + +
+ {runLoading + ? <> + } className="rb:mb-3.5"> + {t('memoryExtractionEngine.processing')} + + {/* 整体进度 */} +
+
+ {t('memoryExtractionEngine.overallProgress')} + {`${completedNum}/4`} +
+ +
+ + : !testResult || Object.keys(testResult).length === 0 + ? } className="rb:mb-3.5"> + {t('memoryExtractionEngine.warning')} + + : } className="rb:mb-3.5"> + {t('memoryExtractionEngine.success')} + + } + + {/* 文本预处理 */} + + {textPreprocessing.data.map((vo, index) => ( +
+ +
+ ))} + {formatTime(textPreprocessing)} + {textPreprocessing.result && + } className="rb:mt-3"> + {t('memoryExtractionEngine.text_preprocessing_desc', { count: textPreprocessing.result.total_chunks })}, + {t('memoryExtractionEngine.chunkerStrategy')}: {t(`memoryExtractionEngine.${lowercaseFirst(textPreprocessing.result.chunker_strategy)}`)} + + } +
+ {/* 知识抽取 */} + + {knowledgeExtraction.data.map(vo => +
{vo.statement}
+ )} + {formatTime(knowledgeExtraction)} + {knowledgeExtraction.result && } 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 + })} + } +
+ {/* 创建实体关系 */} + + {creatingNodesEdges.data?.map((vo, index) => ( +
+ {vo?.result_type === 'entity_nodes_creation' + ? <>{vo.type_display_name}: {vo.entity_names.join(', ')} + : <>{vo?.relationship_text} + } +
+ ))} + {formatTime(creatingNodesEdges, '#9C6FFF')} + {creatingNodesEdges.result && } className="rb:mt-3"> + {t('memoryExtractionEngine.creating_nodes_edges_desc', {num: creatingNodesEdges.result.entity_entity_edges_count})} + } +
+ {/* 去重消歧 */} + + {Object.keys(deduplicationData).length > 0 && Object.keys(deduplicationData).map(key => { + return deduplicationData[key].map((vo, index) => ( +
+ {vo.message} +
+ )) + })} + {formatTime(deduplication, '#9C6FFF')} + {deduplication.result && } className="rb:mt-3"> + {t('memoryExtractionEngine.deduplication_desc', { count: deduplication.result.summary.total_merges })}
+
} +
+ + {testResult && Object.keys(testResult).length > 0 && resultObj && Object.keys(resultObj).length > 0 && + +
+ {Object.keys(resultObj).map((key, index) => { + const keys = (resultObj as Record)[key].split('.') + return ( +
+
{(testResult?.[keys[0] as keyof TestResult] as any)?.[keys[1]]}
+
{t(`memoryExtractionEngine.${key}`)}
+
+ {} + {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`) + } +
+
+ )})} +
+
+ } + + {testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 && + +
{t('memoryExtractionEngine.identifyDuplicates')}
+ {testResult.dedup.impact.map((item, index) => ( +
+ -{t('memoryExtractionEngine.identifyDuplicatesDesc', { ...item })} +
+ ))} + + } className="rb:mt-3"> + {t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })} + +
+ } + + {testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 && + + {testResult.disambiguation.effects.map((item, index) => ( +
0, + })}> +
Disagreement Case {index +1}:
+ -{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → {item.result} +
+ ))} + + } className="rb:mt-3"> + {t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })} + +
+ } + + {testResult?.core_entities && testResult?.core_entities.length > 0 && + +
+ {testResult.core_entities.map((item, idx) => ( +
+
{item.type}({item.count})
+ +
+ {item.entities.map((entity, index) => ( +
+ -{entity} +
+ ))} +
+
+ ))} +
+
+ } + + {testResult?.triplet_samples && testResult?.triplet_samples.length > 0 && + + + {testResult.triplet_samples.map((item, index) => ( +
+ -({item.subject}, {item.predicate}, {item.object}) +
+ ))} +
+ } className="rb:mt-3"> + {t('memoryExtractionEngine.extractRelationalTriplesDesc', { count: testResult.triplet_samples.length })} + +
+ } +
+
+ +
+ + +
+
+ ) +} +export default Result \ No newline at end of file diff --git a/web/src/views/MemoryExtractionEngine/constant.ts b/web/src/views/MemoryExtractionEngine/constant.ts new file mode 100644 index 00000000..9337b372 --- /dev/null +++ b/web/src/views/MemoryExtractionEngine/constant.ts @@ -0,0 +1,1698 @@ +import type { ConfigVo } from './types' + +export 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' }, + // ], + // }, + // ] + // }, + ] + } +] + +export const mockModuleData = [ + { + "data": [ + { + "chunk_index": 1, + "content": "用户: 学生:宋朝军事弱势的根源是什么?\n记忆熊:宋朝军事弱势主要源于制度性因素:禁军由文官控制导致作战效率低下,厢军装备不足;军费长期占财政支出一半以上,持续战争加剧财政压力,反向制约军力发展;对外战略被动,缺乏战略纵深且机动能力弱;文官集团压制武将,军权分散,军事专业化难以提升。\n\n学生:我对财政压力与军力不足的关系表示困惑。\n记忆熊:简单说,宋朝军费占财政一半以上,钱越花越多,仗却越打越输。...", + "full_length": 416, + "dialog_id": "7d11cacd843d45e5942a3ebdc7eb5c8a", + "chunker_strategy": "RecursiveChunker" + }, + { + "chunk_index": 2, + "content": "\n学生:我对\"六部缺乏协调机制\"的具体影响表示理解不足。\n记忆熊:原来丞相相当于\"总理\",六部像今天的各部委,大事小情由他统筹。废丞相后,六部直接对皇帝,皇帝一人批不完,部就互相踢皮球。比如打仗,兵部要银子,户部说没有,工部说武器没材料,没人拍板,战事就拖延。\n\n学生:我在复习春秋战国史,对秦国为何能统一六国表示疑问,认为其他国家也较强。\n记忆熊:秦国统一的原因包括:商鞅变法彻底,建立法律、户籍和...", + "full_length": 428, + "dialog_id": "7d11cacd843d45e5942a3ebdc7eb5c8a", + "chunker_strategy": "RecursiveChunker" + } + ], + "status": "completed", + "result": { + "total_chunks": 2, + "total_dialogs": 1, + "chunker_strategy": "RecursiveChunker" + } + }, + { + "data": [ + { + "extraction_type": "statement", + "statement_index": 1, + "statement": "记忆熊认为宋朝军事弱势主要源于制度性因素。", + "statement_id": "dc0e8b331e584525bda5b63beece6449" + }, + { + "extraction_type": "statement", + "statement_index": 2, + "statement": "记忆熊指出禁军由文官控制导致作战效率低下。", + "statement_id": "b60c2c4cbbc3469a8eccf63eaff8af7c" + }, + { + "extraction_type": "statement", + "statement_index": 3, + "statement": "记忆熊指出厢军装备不足。", + "statement_id": "56b912b3424c41c582849ea47f3c9a67" + }, + { + "extraction_type": "statement", + "statement_index": 4, + "statement": "记忆熊指出宋朝军费长期占财政支出一半以上。", + "statement_id": "6e9f5a974b864731b4f45b156ee2b2b9" + }, + { + "extraction_type": "statement", + "statement_index": 5, + "statement": "记忆熊指出持续战争加剧财政压力,反向制约军力发展。", + "statement_id": "4fbcf48493fa40cd97d2e758046a8114" + }, + { + "extraction_type": "statement", + "statement_index": 6, + "statement": "记忆熊指出宋朝对外战略被动,缺乏战略纵深且机动能力弱。", + "statement_id": "672bb8a4aac548a481ab3c6866ff1537" + }, + { + "extraction_type": "statement", + "statement_index": 7, + "statement": "记忆熊指出文官集团压制武将,军权分散,军事专业化难以提升。", + "statement_id": "94f51d5939d440a89600cc1fede8203e" + }, + { + "extraction_type": "statement", + "statement_index": 8, + "statement": "学生对财政压力与军力不足的关系表示困惑。", + "statement_id": "74304297767144fb98e1f28de4397eba" + }, + { + "extraction_type": "statement", + "statement_index": 9, + "statement": "记忆熊解释宋朝军费占财政一半以上,钱越花越多,仗却越打越输。", + "statement_id": "52169673071844d58cc475f350e0e878" + }, + { + "extraction_type": "statement", + "statement_index": 10, + "statement": "记忆熊指出财政被军费拖垮后,朝廷只能削减装备、裁撤兵员。", + "statement_id": "92c5b675666a444d8bba605682376018" + }, + { + "extraction_type": "triplet", + "triplet_index": 1, + "subject": "记忆熊", + "predicate": "MENTIONS", + "object": "宋朝军事弱势" + }, + { + "extraction_type": "triplet", + "triplet_index": 2, + "subject": "宋朝军事弱势", + "predicate": "RESULTED_IN", + "object": "制度性因素" + }, + { + "extraction_type": "triplet", + "triplet_index": 3, + "subject": "记忆熊", + "predicate": "MENTIONS", + "object": "禁军由文官控制导致作战效率低下" + }, + { + "extraction_type": "triplet", + "triplet_index": 4, + "subject": "禁军由文官控制", + "predicate": "RESULTED_IN", + "object": "作战效率低下" + }, + { + "extraction_type": "triplet", + "triplet_index": 5, + "subject": "记忆熊", + "predicate": "MENTIONS", + "object": "厢军装备不足" + }, + { + "extraction_type": "triplet", + "triplet_index": 6, + "subject": "记忆熊", + "predicate": "MENTIONS", + "object": "宋朝" + }, + { + "extraction_type": "triplet", + "triplet_index": 7, + "subject": "记忆熊", + "predicate": "MENTIONS", + "object": "军费" + }, + { + "extraction_type": "triplet", + "triplet_index": 8, + "subject": "军费", + "predicate": "HAS_A", + "object": "财政支出" + }, + { + "extraction_type": "triplet", + "triplet_index": 9, + "subject": "宋朝", + "predicate": "HAS_REVENUE", + "object": "财政支出" + }, + { + "extraction_type": "triplet", + "triplet_index": 10, + "subject": "持续战争", + "predicate": "RESULTED_IN", + "object": "财政压力" + }, + { + "extraction_type": "temporal", + "temporal_index": 1, + "statement": "记忆熊认为宋朝军事弱势主要源于制度性因素。", + "valid_at": null, + "invalid_at": null + }, + { + "extraction_type": "temporal", + "temporal_index": 2, + "statement": "记忆熊指出禁军由文官控制导致作战效率低下。", + "valid_at": null, + "invalid_at": null + }, + { + "extraction_type": "temporal", + "temporal_index": 3, + "statement": "记忆熊指出厢军装备不足。", + "valid_at": null, + "invalid_at": null + }, + { + "extraction_type": "temporal", + "temporal_index": 4, + "statement": "记忆熊指出宋朝军费长期占财政支出一半以上。", + "valid_at": null, + "invalid_at": null + }, + { + "extraction_type": "temporal", + "temporal_index": 5, + "statement": "记忆熊指出持续战争加剧财政压力,反向制约军力发展。", + "valid_at": null, + "invalid_at": null + } + ], + "status": "completed", + "result": { + "statements": { + "count": 38 + }, + "entities": { + "count": 148 + }, + "triplets": { + "count": 88 + }, + "temporal_ranges_count": 38 + } + }, + { + "data": [ + { + "result_type": "entity_nodes_creation", + "entity_type": "Person", + "type_display_name": "人物实体节点", + "entity_names": [ + "记忆熊", + "记忆熊", + "记忆熊", + "记忆熊", + "记忆熊", + "学生", + "记忆熊", + "学生", + "记忆熊", + "丞相", + "..." + ], + "total_count": 21 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Phenomenon", + "type_display_name": "Phenomenon实体节点", + "entity_names": [ + "宋朝军事弱势" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Factor", + "type_display_name": "Factor实体节点", + "entity_names": [ + "制度性因素" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Policy", + "type_display_name": "政策实体节点", + "entity_names": [ + "禁军由文官控制", + "商鞅变法" + ], + "total_count": 2 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Condition", + "type_display_name": "条件实体节点", + "entity_names": [ + "作战效率低下", + "厢军装备不足", + "军力发展受制约", + "军权分散", + "军事专业化难以提升", + "官僚体系僵化", + "缺乏协作机制", + "缺乏协调机制", + "难以支撑军队" + ], + "total_count": 9 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Statement", + "type_display_name": "Statement实体节点", + "entity_names": [ + "禁军由文官控制导致作战效率低下", + "没有银子", + "武器没材料" + ], + "total_count": 3 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Organization", + "type_display_name": "组织实体节点", + "entity_names": [ + "宋朝", + "宋朝", + "文官集团", + "宋朝", + "朝廷", + "官僚体系", + "六部", + "厂卫机构", + "锦衣卫", + "东厂", + "..." + ], + "total_count": 25 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "EconomicMetric", + "type_display_name": "EconomicMetric实体节点", + "entity_names": [ + "军费", + "财政支出", + "财政", + "军费", + "支出" + ], + "total_count": 5 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Event", + "type_display_name": "事件实体节点", + "entity_names": [ + "持续战争", + "削减装备和裁撤兵员", + "战事失利", + "废除丞相制度", + "废除丞相制度", + "废除丞相制度", + "大事小情", + "废除丞相制度", + "无法批阅完所有政务", + "政令执行困难", + "..." + ], + "total_count": 15 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "EconomicFactor", + "type_display_name": "经济因素实体节点", + "entity_names": [ + "财政压力" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "EconomicIndicator", + "type_display_name": "EconomicIndicator实体节点", + "entity_names": [ + "财政支出" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "MilitaryStrategy", + "type_display_name": "MilitaryStrategy实体节点", + "entity_names": [ + "对外战略被动" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "MilitaryCondition", + "type_display_name": "MilitaryCondition实体节点", + "entity_names": [ + "缺乏战略纵深", + "军力不足" + ], + "total_count": 2 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "MilitaryCapability", + "type_display_name": "MilitaryCapability实体节点", + "entity_names": [ + "机动能力弱" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "PersonGroup", + "type_display_name": "PersonGroup实体节点", + "entity_names": [ + "武将" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Concept", + "type_display_name": "概念实体节点", + "entity_names": [ + "财政压力", + "军力不足", + "恶性循环", + "行政紧张", + "系统行政训练", + "专业分工", + "六部缺乏协调机制", + "六部缺乏协调机制", + "秦国统一六国的原因" + ], + "total_count": 9 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "EconomicCondition", + "type_display_name": "EconomicCondition实体节点", + "entity_names": [ + "财政压力" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Action", + "type_display_name": "Action实体节点", + "entity_names": [ + "削减装备", + "裁撤兵员", + "再花钱募兵", + "建立法律制度", + "建立户籍制度", + "建立军功爵制度" + ], + "total_count": 6 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "State", + "type_display_name": "State实体节点", + "entity_names": [ + "军队更弱", + "不足", + "理解不足" + ], + "total_count": 3 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Outcome", + "type_display_name": "Outcome实体节点", + "entity_names": [ + "打仗更吃亏", + "降低行政效率", + "政令推行困难", + "提升国家组织能力", + "士兵效忠个人而非国家" + ], + "total_count": 5 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "HistoricalPeriod", + "type_display_name": "历史时期实体节点", + "entity_names": [ + "宋朝", + "春秋战国史", + "唐朝史" + ], + "total_count": 3 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "InstitutionalPolicy", + "type_display_name": "InstitutionalPolicy实体节点", + "entity_names": [ + "废除丞相制度" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "StateOfAffairs", + "type_display_name": "StateOfAffairs实体节点", + "entity_names": [ + "中央决策高度集中于皇帝" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Role", + "type_display_name": "Role实体节点", + "entity_names": [ + "协调中枢", + "节度使" + ], + "total_count": 2 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Institution", + "type_display_name": "Institution实体节点", + "entity_names": [ + "科举" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Position", + "type_display_name": "职位实体节点", + "entity_names": [ + "丞相", + "总理", + "丞相", + "总理" + ], + "total_count": 4 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Function", + "type_display_name": "Function实体节点", + "entity_names": [ + "统筹大事小情" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "AdministrativeStructure", + "type_display_name": "AdministrativeStructure实体节点", + "entity_names": [ + "六部直接对皇帝负责", + "六部直接对皇帝负责" + ], + "total_count": 2 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "AdministrativeProblem", + "type_display_name": "AdministrativeProblem实体节点", + "entity_names": [ + "皇帝一人批不完政务" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Behavior", + "type_display_name": "Behavior实体节点", + "entity_names": [ + "互相推诿责任" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Resource", + "type_display_name": "Resource实体节点", + "entity_names": [ + "银子" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Situation", + "type_display_name": "Situation实体节点", + "entity_names": [ + "没人拍板" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "HistoricalState", + "type_display_name": "历史国家实体节点", + "entity_names": [ + "秦国" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "PoliticalCharacteristic", + "type_display_name": "PoliticalCharacteristic实体节点", + "entity_names": [ + "旧贵族势力弱", + "中央集权程度高" + ], + "total_count": 2 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Location", + "type_display_name": "地点实体节点", + "entity_names": [ + "关中" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Attribute", + "type_display_name": "Attribute实体节点", + "entity_names": [ + "资源丰富", + "易守难攻", + "政策连续性强" + ], + "total_count": 3 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "HistoricalEvent", + "type_display_name": "历史事件实体节点", + "entity_names": [ + "安史之乱" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "PoliticalAction", + "type_display_name": "PoliticalAction实体节点", + "entity_names": [ + "中央整顿" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "PoliticalPhenomenon", + "type_display_name": "PoliticalPhenomenon实体节点", + "entity_names": [ + "藩镇割据加剧" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "Right", + "type_display_name": "Right实体节点", + "entity_names": [ + "募兵权", + "财政调度权", + "军事指挥权" + ], + "total_count": 3 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "EconomicEntity", + "type_display_name": "EconomicEntity实体节点", + "entity_names": [ + "中央财政" + ], + "total_count": 1 + }, + { + "result_type": "entity_nodes_creation", + "entity_type": "System", + "type_display_name": "系统实体节点", + "entity_names": [ + "募兵制" + ], + "total_count": 1 + }, + { + "result_type": "relationship_creation", + "relationship_index": 1, + "source_entity": "记忆熊", + "relation_type": "MENTIONS", + "target_entity": "宋朝军事弱势", + "relationship_text": "记忆熊 -[MENTIONS]-> 宋朝军事弱势" + }, + { + "result_type": "relationship_creation", + "relationship_index": 2, + "source_entity": "宋朝军事弱势", + "relation_type": "RESULTED_IN", + "target_entity": "制度性因素", + "relationship_text": "宋朝军事弱势 -[RESULTED_IN]-> 制度性因素" + }, + { + "result_type": "relationship_creation", + "relationship_index": 3, + "source_entity": "记忆熊", + "relation_type": "MENTIONS", + "target_entity": "禁军由文官控制导致作战效率低下", + "relationship_text": "记忆熊 -[MENTIONS]-> 禁军由文官控制导致作战效率低下" + }, + { + "result_type": "relationship_creation", + "relationship_index": 4, + "source_entity": "禁军由文官控制", + "relation_type": "RESULTED_IN", + "target_entity": "作战效率低下", + "relationship_text": "禁军由文官控制 -[RESULTED_IN]-> 作战效率低下" + }, + { + "result_type": "relationship_creation", + "relationship_index": 5, + "source_entity": "记忆熊", + "relation_type": "MENTIONS", + "target_entity": "厢军装备不足", + "relationship_text": "记忆熊 -[MENTIONS]-> 厢军装备不足" + }, + { + "result_type": "relationship_creation", + "relationship_index": 6, + "source_entity": "记忆熊", + "relation_type": "MENTIONS", + "target_entity": "宋朝", + "relationship_text": "记忆熊 -[MENTIONS]-> 宋朝" + }, + { + "result_type": "relationship_creation", + "relationship_index": 7, + "source_entity": "记忆熊", + "relation_type": "MENTIONS", + "target_entity": "军费", + "relationship_text": "记忆熊 -[MENTIONS]-> 军费" + }, + { + "result_type": "relationship_creation", + "relationship_index": 8, + "source_entity": "军费", + "relation_type": "HAS_A", + "target_entity": "财政支出", + "relationship_text": "军费 -[HAS_A]-> 财政支出" + }, + { + "result_type": "relationship_creation", + "relationship_index": 9, + "source_entity": "宋朝", + "relation_type": "HAS_REVENUE", + "target_entity": "财政支出", + "relationship_text": "宋朝 -[HAS_REVENUE]-> 财政支出" + }, + { + "result_type": "relationship_creation", + "relationship_index": 10, + "source_entity": "持续战争", + "relation_type": "RESULTED_IN", + "target_entity": "财政压力", + "relationship_text": "持续战争 -[RESULTED_IN]-> 财政压力" + } + ], + "status": "completed", + "result": { + "dialogue_nodes_count": 1, + "chunk_nodes_count": 2, + "statement_nodes_count": 38, + "entity_nodes_count": 148, + "statement_chunk_edges_count": 38, + "statement_entity_edges_count": 148, + "entity_entity_edges_count": 88 + } + }, + { + "data": [ + { + "result_type": "entity_merge", + "merged_entity_name": "记忆熊", + "merged_count": 9, + "message": "记忆熊合并9个:相似实体已合并" + }, + { + "result_type": "entity_merge", + "merged_entity_name": "宋朝", + "merged_count": 4, + "message": "宋朝合并4个:相似实体已合并" + }, + { + "result_type": "entity_merge", + "merged_entity_name": "军费", + "merged_count": 2, + "message": "军费合并2个:相似实体已合并" + }, + { + "result_type": "entity_merge", + "merged_entity_name": "财政支出", + "merged_count": 2, + "message": "财政支出合并2个:相似实体已合并" + }, + { + "result_type": "entity_merge", + "merged_entity_name": "财政压力", + "merged_count": 3, + "message": "财政压力合并3个:相似实体已合并" + }, + { + "result_type": "entity_disambiguation", + "disambiguated_entity_name": "节度使", + "disambiguation_type": "消歧阻断:Role vs Person", + "confidence": "0.95", + "reason": "实体A类型为'Role',实体B类型为'Person',二者类型完全不同且无别名交集或名称-别名匹配。'节度使'是官职而非具体人物,语义上不应合并。尽管名称文本相似度高,但类型相似度为0.0,且上下文...", + "message": "节度使消歧完成:消歧阻断:Role vs Person" + } + ], + "status": "completed", + "result": { + "entities": { + "original_count": 148, + "final_count": 110, + "reduced_count": 38, + "reduction_rate": 25.7 + }, + "statement_entity_edges": { + "original_count": 148, + "final_count": 148, + "reduced_count": 0 + }, + "entity_entity_edges": { + "original_count": 88, + "final_count": 81, + "reduced_count": 7 + }, + "dedup_examples": [ + { + "type": "精确匹配", + "entity_name": "记忆熊", + "entity_type": "Person", + "merge_count": 8, + "description": "记忆熊实体去重合并8个" + }, + { + "type": "精确匹配", + "entity_name": "宋朝", + "entity_type": "Organization", + "merge_count": 2, + "description": "宋朝实体去重合并2个" + }, + { + "type": "精确匹配", + "entity_name": "军费", + "entity_type": "EconomicMetric", + "merge_count": 1, + "description": "军费实体去重合并1个" + }, + { + "type": "精确匹配", + "entity_name": "学生", + "entity_type": "Person", + "merge_count": 5, + "description": "学生实体去重合并5个" + }, + { + "type": "精确匹配", + "entity_name": "废除丞相制度", + "entity_type": "Event", + "merge_count": 3, + "description": "废除丞相制度实体去重合并3个" + } + ], + "disamb_examples": [ + { + "entity1_name": "节度使", + "entity1_type": "Role", + "entity2_name": "节度使", + "entity2_type": "Person", + "description": "节度使和节度使,消歧区分成功" + } + ], + "summary": { + "total_merges": 39, + "total_disambiguations": 1 + } + } + } +] +// 按type聚合数据的处理函数 +export const groupDataByType = (data: any[], groupKey: string) => { + const grouped: { [key: string]: any[] } = {} + + data.forEach(item => { + if (item[groupKey]) { + if (!grouped[item[groupKey]]) { + grouped[item[groupKey]] = [] + } + grouped[item[groupKey]].push(item) + } else { + if (!grouped.unknown) { + grouped.unknown = [] + } + grouped.unknown.push(item) + } + }) + + return grouped +} + +export const mockTestResult = { + "generated_at": "2025-12-12T09:48:43.389893", + "entities": { + "extracted_count": 148 + }, + "dedup": { + "total_merged_count": 39, + "breakdown": { + "exact": 30, + "fuzzy": 0, + "llm": 9 + }, + "impact": [ + { + "name": "记忆熊", + "type": "Person", + "appear_count": 9, + "merge_count": 8 + }, + { + "name": "宋朝", + "type": "Organization", + "appear_count": 5, + "merge_count": 2 + }, + { + "name": "军费", + "type": "EconomicMetric", + "appear_count": 2, + "merge_count": 1 + }, + { + "name": "学生", + "type": "Person", + "appear_count": 6, + "merge_count": 5 + }, + { + "name": "废除丞相制度", + "type": "Event", + "appear_count": 6, + "merge_count": 3 + }, + { + "name": "六部", + "type": "Organization", + "appear_count": 4, + "merge_count": 3 + }, + { + "name": "六部缺乏协调机制", + "type": "Concept", + "appear_count": 2, + "merge_count": 1 + }, + { + "name": "丞相", + "type": "Position", + "appear_count": 4, + "merge_count": 1 + }, + { + "name": "总理", + "type": "Position", + "appear_count": 2, + "merge_count": 1 + }, + { + "name": "各部委", + "type": "Organization", + "appear_count": 2, + "merge_count": 1 + }, + { + "name": "六部直接对皇帝负责", + "type": "AdministrativeStructure", + "appear_count": 2, + "merge_count": 1 + }, + { + "name": "秦国", + "type": "Organization", + "appear_count": 5, + "merge_count": 2 + }, + { + "name": "文官集团", + "type": "Organization", + "appear_count": 2, + "merge_count": 1 + } + ] + }, + "disambiguation": { + "block_count": 1, + "effects": [ + { + "left": { + "name": "节度使", + "type": "Role" + }, + "right": { + "name": "节度使", + "type": "Person" + }, + "result": "成功区分" + } + ] + }, + "memory": { + "chunks": 2 + }, + "triplets": { + "count": 88 + }, + "core_entities": [ + { + "type": "Organization", + "type_cn": "组织", + "count": 16, + "entities": [ + "厂卫机构", + "西厂", + "东厂", + "工部", + "地方军阀" + ] + }, + { + "type": "Event", + "type_cn": "事件", + "count": 12, + "entities": [ + "均田制瓦解", + "无法批阅完所有政务", + "废除丞相制度", + "持续战争", + "政令执行困难" + ] + }, + { + "type": "Condition", + "type_cn": "Condition", + "count": 9, + "entities": [ + "缺乏协作机制", + "作战效率低下", + "厢军装备不足", + "军权分散", + "军事专业化难以提升" + ] + }, + { + "type": "Person", + "type_cn": "人物", + "count": 8, + "entities": [ + "官员", + "宦官", + "节度使", + "皇帝", + "文士" + ] + }, + { + "type": "Concept", + "type_cn": "Concept", + "count": 8, + "entities": [ + "行政紧张", + "军力不足", + "秦国统一六国的原因", + "六部缺乏协调机制", + "专业分工" + ] + }, + { + "type": "Action", + "type_cn": "Action", + "count": 6, + "entities": [ + "再花钱募兵", + "建立军功爵制度", + "裁撤兵员", + "削减装备", + "建立法律制度" + ] + }, + { + "type": "Outcome", + "type_cn": "Outcome", + "count": 5, + "entities": [ + "打仗更吃亏", + "提升国家组织能力", + "降低行政效率", + "士兵效忠个人而非国家", + "政令推行困难" + ] + }, + { + "type": "EconomicMetric", + "type_cn": "EconomicMetric", + "count": 4, + "entities": [ + "财政", + "财政支出", + "支出", + "军费" + ] + }, + { + "type": "Statement", + "type_cn": "Statement", + "count": 3, + "entities": [ + "没有银子", + "禁军由文官控制导致作战效率低下", + "武器没材料" + ] + }, + { + "type": "State", + "type_cn": "State", + "count": 3, + "entities": [ + "军队更弱", + "理解不足", + "不足" + ] + }, + { + "type": "HistoricalPeriod", + "type_cn": "HistoricalPeriod", + "count": 3, + "entities": [ + "春秋战国史", + "唐朝史", + "宋朝" + ] + }, + { + "type": "Attribute", + "type_cn": "Attribute", + "count": 3, + "entities": [ + "资源丰富", + "易守难攻", + "政策连续性强" + ] + }, + { + "type": "Right", + "type_cn": "Right", + "count": 3, + "entities": [ + "军事指挥权", + "财政调度权", + "募兵权" + ] + }, + { + "type": "Policy", + "type_cn": "Policy", + "count": 2, + "entities": [ + "商鞅变法", + "禁军由文官控制" + ] + }, + { + "type": "MilitaryCondition", + "type_cn": "MilitaryCondition", + "count": 2, + "entities": [ + "军力不足", + "缺乏战略纵深" + ] + }, + { + "type": "Role", + "type_cn": "Role", + "count": 2, + "entities": [ + "节度使", + "协调中枢" + ] + }, + { + "type": "Position", + "type_cn": "Position", + "count": 2, + "entities": [ + "总理", + "丞相" + ] + }, + { + "type": "PoliticalCharacteristic", + "type_cn": "PoliticalCharacteristic", + "count": 2, + "entities": [ + "旧贵族势力弱", + "中央集权程度高" + ] + }, + { + "type": "Phenomenon", + "type_cn": "Phenomenon", + "count": 1, + "entities": [ + "宋朝军事弱势" + ] + }, + { + "type": "Factor", + "type_cn": "Factor", + "count": 1, + "entities": [ + "制度性因素" + ] + }, + { + "type": "EconomicFactor", + "type_cn": "EconomicFactor", + "count": 1, + "entities": [ + "财政压力" + ] + }, + { + "type": "EconomicIndicator", + "type_cn": "EconomicIndicator", + "count": 1, + "entities": [ + "财政支出" + ] + }, + { + "type": "MilitaryStrategy", + "type_cn": "MilitaryStrategy", + "count": 1, + "entities": [ + "对外战略被动" + ] + }, + { + "type": "MilitaryCapability", + "type_cn": "MilitaryCapability", + "count": 1, + "entities": [ + "机动能力弱" + ] + }, + { + "type": "PersonGroup", + "type_cn": "PersonGroup", + "count": 1, + "entities": [ + "武将" + ] + }, + { + "type": "EconomicCondition", + "type_cn": "EconomicCondition", + "count": 1, + "entities": [ + "财政压力" + ] + }, + { + "type": "InstitutionalPolicy", + "type_cn": "InstitutionalPolicy", + "count": 1, + "entities": [ + "废除丞相制度" + ] + }, + { + "type": "StateOfAffairs", + "type_cn": "StateOfAffairs", + "count": 1, + "entities": [ + "中央决策高度集中于皇帝" + ] + }, + { + "type": "Institution", + "type_cn": "Institution", + "count": 1, + "entities": [ + "科举" + ] + }, + { + "type": "Function", + "type_cn": "Function", + "count": 1, + "entities": [ + "统筹大事小情" + ] + }, + { + "type": "AdministrativeStructure", + "type_cn": "AdministrativeStructure", + "count": 1, + "entities": [ + "六部直接对皇帝负责" + ] + }, + { + "type": "AdministrativeProblem", + "type_cn": "AdministrativeProblem", + "count": 1, + "entities": [ + "皇帝一人批不完政务" + ] + }, + { + "type": "Behavior", + "type_cn": "Behavior", + "count": 1, + "entities": [ + "互相推诿责任" + ] + }, + { + "type": "Resource", + "type_cn": "Resource", + "count": 1, + "entities": [ + "银子" + ] + }, + { + "type": "Situation", + "type_cn": "Situation", + "count": 1, + "entities": [ + "没人拍板" + ] + }, + { + "type": "HistoricalState", + "type_cn": "HistoricalState", + "count": 1, + "entities": [ + "秦国" + ] + }, + { + "type": "Location", + "type_cn": "地点", + "count": 1, + "entities": [ + "关中" + ] + }, + { + "type": "HistoricalEvent", + "type_cn": "HistoricalEvent", + "count": 1, + "entities": [ + "安史之乱" + ] + }, + { + "type": "PoliticalAction", + "type_cn": "PoliticalAction", + "count": 1, + "entities": [ + "中央整顿" + ] + }, + { + "type": "PoliticalPhenomenon", + "type_cn": "PoliticalPhenomenon", + "count": 1, + "entities": [ + "藩镇割据加剧" + ] + }, + { + "type": "EconomicEntity", + "type_cn": "EconomicEntity", + "count": 1, + "entities": [ + "中央财政" + ] + }, + { + "type": "System", + "type_cn": "System", + "count": 1, + "entities": [ + "募兵制" + ] + }, + { + "type": "WorkRole", + "type_cn": "WorkRole", + "count": 1, + "entities": [ + "掌控禁军" + ] + } + ], + "triplet_samples": [ + { + "subject": "记忆熊", + "predicate": "MENTIONS", + "predicate_cn": "提到", + "object": "宋朝军事弱势" + }, + { + "subject": "宋朝军事弱势", + "predicate": "RESULTED_IN", + "predicate_cn": "resulted in", + "object": "制度性因素" + }, + { + "subject": "记忆熊", + "predicate": "MENTIONS", + "predicate_cn": "提到", + "object": "禁军由文官控制导致作战效率低下" + }, + { + "subject": "禁军由文官控制", + "predicate": "RESULTED_IN", + "predicate_cn": "resulted in", + "object": "作战效率低下" + }, + { + "subject": "记忆熊", + "predicate": "MENTIONS", + "predicate_cn": "提到", + "object": "厢军装备不足" + }, + { + "subject": "记忆熊", + "predicate": "MENTIONS", + "predicate_cn": "提到", + "object": "宋朝" + }, + { + "subject": "记忆熊", + "predicate": "MENTIONS", + "predicate_cn": "提到", + "object": "军费" + } + ], + "self_reflexion": [ + { + "conflict": { + "data": [ + { + "id": "76be6d82d8804beda6baa3d3447d6cbc", + "statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。", + "group_id": "group_123", + "chunk_id": "4a0804127d35456f86d4f06e1fa458f7", + "created_at": "2025-12-12 09:48:00.166068", + "expired_at": null, + "valid_at": null, + "invalid_at": null, + "entity_ids": [] + } + ], + "conflict": true, + "conflict_memory": { + "id": "e268a6fff35543fab471986c188e023e", + "statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。", + "group_id": "group_123", + "chunk_id": "e6cb5f56020e4a8d925d148e1d2fbda0", + "created_at": "2025-12-12 09:48:00.166068", + "expired_at": null, + "valid_at": null, + "invalid_at": null, + "entity_ids": [] + } + }, + "reflexion": { + "reason": "同一学生在不同时间点重复提出对'六部缺乏协调机制'具体影响的理解困难,表明原有解释未能有效解决其认知障碍,存在记忆冗余与教学反馈失效的冲突。", + "solution": "保留后出现的记忆记录(chunk_id为4a0804127d35456f86d4f06e1fa458f7)作为最新学习状态,将其设为有效;将前次相同内容的记忆(id为e268a6fff35543fab471986c188e023e)标记为失效,避免重复干预,并基于后续完整解释优化知识呈现逻辑。" + }, + "resolved": { + "original_memory_id": "e268a6fff35543fab471986c188e023e", + "resolved_memory": { + "id": "e268a6fff35543fab471986c188e023e", + "statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。", + "group_id": "group_123", + "chunk_id": "e6cb5f56020e4a8d925d148e1d2fbda0", + "created_at": "2025-12-12 09:48:00.166068", + "expired_at": null, + "valid_at": null, + "invalid_at": "2025-12-12 09:48:00.166068", + "entity_ids": [] + } + } + } + ] + } \ No newline at end of file diff --git a/web/src/views/MemoryExtractionEngine/index.tsx b/web/src/views/MemoryExtractionEngine/index.tsx index af35c5bc..ac7b3b70 100644 --- a/web/src/views/MemoryExtractionEngine/index.tsx +++ b/web/src/views/MemoryExtractionEngine/index.tsx @@ -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 (
- + {config.variableName && {t('memoryExtractionEngine.variableName')}: {config.variableName}} {config.control && {t('memoryExtractionEngine.control')}: {t(`memoryExtractionEngine.${config.control}`)}} {config.type && {t('memoryExtractionEngine.type')}: {config.type}} - {config.meaning &&
{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}
} + {config.meaning &&
{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}
}
) } @@ -253,12 +38,9 @@ const MemoryExtractionEngine: FC = () => { const [expandedKeys, setExpandedKeys] = useState(keys) const [form] = Form.useForm() const [modelForm] = Form.useForm() - // const [data, setData] = useState() const modelValues = Form.useWatch([], modelForm) const values = Form.useWatch([], form) - const [testResult, setTestResult] = useState(null) const [loading, setLoading] = useState(false) - const [runLoading, setRunLoading] = useState(false) const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false) const [modelList, setModelList] = useState([]) @@ -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 ( <> -
{t('memoryExtractionEngine.title')}
-
{t('memoryExtractionEngine.subTitle')}
+
{t('memoryExtractionEngine.title')}
+
{t('memoryExtractionEngine.subTitle')}
@@ -388,12 +144,12 @@ const MemoryExtractionEngine: FC = () => { handleExpand={handleExpand} > {expandedKeys.includes('example') && -
+
} - +
{
{ )} >
{t(`memoryExtractionEngine.${vo.title}`)}
-
{t(`memoryExtractionEngine.${vo.title}SubTitle`)}
+
{t(`memoryExtractionEngine.${vo.title}SubTitle`)}
{vo.list.map(config => (
{config.control === 'button' && -
+
- -{t(`memoryExtractionEngine.${config.label}`)} - + -{t(`memoryExtractionEngine.${config.label}`)} +
@@ -442,10 +198,10 @@ const MemoryExtractionEngine: FC = () => { } {config.control === 'select' && <> -
+
-{t(`memoryExtractionEngine.${config.label}`)}
-
+
@@ -454,17 +210,17 @@ const MemoryExtractionEngine: FC = () => { options={config.options ? config.options.map(item => ({ ...item, label: t(`memoryExtractionEngine.${item.label}`) })) : []} /> - +
} {config.control === 'slider' && <> -
+
-{t(`memoryExtractionEngine.${config.label}`)}
-
- +
+ @@ -475,7 +231,7 @@ const MemoryExtractionEngine: FC = () => { step={config.step || 0.01} /> -
+
{config.min || 0} {t('memoryExtractionEngine.CurrentValue')}: {values?.[config.variableName as keyof ConfigForm]}
@@ -484,16 +240,16 @@ const MemoryExtractionEngine: FC = () => { } {config.control === 'inputNumber' && <> -
+
-{t(`memoryExtractionEngine.${config.label}`)}
-
+
- +
} @@ -508,148 +264,10 @@ const MemoryExtractionEngine: FC = () => { - -
- {testResult && Object.keys(testResult).length > 0 - ? <> - } className="rb:mb-[14px]"> - {t('memoryExtractionEngine.warning')} - - - - {resultObj && Object.keys(resultObj).length > 0 && - -
- {Object.keys(resultObj).map(key => { - const keys = (resultObj as Record)[key].split('.') - return ( -
-
{testResult?.[keys[0] as keyof TestResult]?.[keys[1]]}
-
{t(`memoryExtractionEngine.${key}`)}
-
- {} - {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`) - } -
-
- )})} -
-
- } - - {testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 && - -
{t('memoryExtractionEngine.identifyDuplicates')}
- {testResult.dedup.impact.map((item, index) => ( -
- -{t('memoryExtractionEngine.identifyDuplicatesDesc', { ...item })} -
- ))} - - } className="rb:mt-[12px]"> - {t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })} - -
- } - - {testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 && - - {testResult.disambiguation.effects.map((item, index) => ( -
0, - })}> -
Disagreement Case {index +1}:
- -{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → {item.result} -
- ))} - - } className="rb:mt-[12px]"> - {t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })} - -
- } - - {testResult?.core_entities && testResult?.core_entities.length > 0 && - -
- {testResult.core_entities.map(item => ( -
-
{item.type}({item.count})
- -
- {item.entities.map((entity, index) => ( -
- -{entity} -
- ))} -
-
- ))} -
-
- } - - {testResult?.triplet_samples && testResult?.triplet_samples.length > 0 && - - - {testResult.triplet_samples.map((item, index) => ( -
- -({item.subject}, {item.predicate}, {item.object}) -
- ))} -
- } className="rb:mt-[12px]"> - {t('memoryExtractionEngine.extractRelationalTriplesDesc', { count: testResult.triplet_samples.length })} - -
- } -
- - : loading - ? - : - } -
- -
- - -
-
+ diff --git a/web/src/views/ModelManagement/components/ConfigModal.tsx b/web/src/views/ModelManagement/components/ConfigModal.tsx index af5be7a5..e4bdf84c 100644 --- a/web/src/views/ModelManagement/components/ConfigModal.tsx +++ b/web/src/views/ModelManagement/components/ConfigModal.tsx @@ -123,7 +123,7 @@ const ConfigModal = forwardRef(({ items.map((item) => ({ label: item, value: item }))} + format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))} /> @@ -138,10 +138,9 @@ const ConfigModal = forwardRef(({ items.map((item) => ({ label: item, value: item }))} + format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))} /> - {/* TODO:改成模型名称 */} { return ( { return ( { const [categories, setCategories] = useState<{ name: string }[]>([]) const [selectedNode, setSelectedNode] = useState(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() + + // 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 + ) } } }}