Merge #20 into develop_web from feature/20251219_zy

feat(web): remove mock data

* feature/20251219_zy: (5 commits)
  feat(web): update api key
  feat(web): Add Emotion Memory
  feat(web): Add  Reflection Engine
  feat(web): Add  Reflection Engine API
  feat(web): remove mock data

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

CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/20
This commit is contained in:
赵莹
2025-12-19 18:55:33 +08:00
23 changed files with 2168 additions and 155 deletions

View File

@@ -8,6 +8,12 @@ import type {
import type {
ConfigForm as ExtractionConfigForm
} from '@/views/MemoryExtractionEngine/types'
import type {
ConfigForm as EmotionConfig
} from '@/views/EmotionEngine/types'
import type {
ConfigForm as SelfReflectionEngineConfig
} from '@/views/SelfReflectionEngine/types'
import type { TestParams } from '@/views/MemoryConversation'
import { handleSSE, type SSEMessage } from '@/utils/stream'
@@ -96,6 +102,23 @@ export const getChunkInsight = (end_user_id: string) => {
export const getRagContent = (end_user_id: string) => {
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
}
// 情感分布分析
export const getWordCloud = (group_id: string) => {
return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 })
}
// 高频情绪关键词
export const getEmotionTags = (group_id: string) => {
return request.post(`/memory/emotion/tags`, { group_id, limit: 20 })
}
// 情绪健康指数
export const getEmotionHealth = (group_id: string) => {
return request.post(`/memory/emotion/health`, { group_id, limit: 20 })
}
// 个性化建议
export const getEmotionSuggestions = (group_id: string) => {
return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 })
}
/*************** end 用户记忆 相关接口 ******************************/
/****************** 记忆管理 相关接口 *******************************/
@@ -136,6 +159,27 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE('/memory-storage/pilot_run', values, onMessage)
}
// 情绪引擎-获取配置
export const getMemoryEmotionConfig = (config_id: number | string) => {
return request.get('/memory/emotion/read_config', { config_id: config_id })
}
// 情绪引擎-更新配置
export const updateMemoryEmotionConfig = (values: EmotionConfig) => {
return request.post('/memory/emotion/updated_config', values)
}
// 反思引擎-获取配置
export const getMemoryReflectionConfig = (config_id: number | string) => {
return request.get('/memory/reflection/configs', { config_id: config_id })
}
// 反思引擎-更新配置
export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => {
return request.post('/memory/reflection/save', values)
}
// 反思引擎-试运行
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; dialogue_text: string; }) => {
return request.get('/memory/reflection/run', values)
}
/*************** end 记忆管理 相关接口 ******************************/

View File

@@ -3,7 +3,7 @@ import { type FC, type ReactNode } from 'react'
interface RbAlertProps {
color?: 'blue' | 'green' | 'orange' | 'purple',
children: ReactNode | string;
icon: ReactNode;
icon?: ReactNode;
className?: string;
}
@@ -17,7 +17,7 @@ const colors = {
const RbAlert: FC<RbAlertProps> = ({ color = 'blue', icon, className, children }) => {
return (
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-4 rb:border rb:rounded-md`}>
{icon && <span className="rb:text-[16px] rb:mr-[9px]">{icon}</span>}
{icon && <span className="rb:text-[16px] rb:mr-2.25">{icon}</span>}
{children}
</div>
)

View File

@@ -16,9 +16,10 @@ const RbModal: FC<ModalProps> = ({
cancelText={t('common.cancel')}
onOk={onOk}
destroyOnHidden={true}
maskClosable={false}
{...props}
>
<div className='rb:max-h-[550px] rb:overflow-y-auto rb:overflow-x-hidden'>
<div className='rb:max-h-137.5 rb:overflow-y-auto rb:overflow-x-hidden'>
{children}
</div>
</Modal>

View File

@@ -34,6 +34,10 @@ export const en = {
knowledgeDocumentDetails: 'Document Details',
userMemoryDetail: 'UserMemory Detail',
apiKeyManagement: 'API KEY Management',
toolManagement: 'Tool Management',
emotionEngine: 'Emotion Engine',
emotionDetail: 'Emotion Memory',
selfReflectionEngine: 'Self Reflection Engine',
},
dashboard: {
totalMemoryCapacity: 'Total Memory Capacity',
@@ -320,10 +324,7 @@ export const en = {
loginApiCannotRefreshToken: 'Login API cannot refresh token',
logoutApiCannotRefreshToken: 'Logout API cannot refresh token',
publicApiCannotRefreshToken: 'Public API cannot refresh token',
refreshTokenNotExist: 'Refresh token does not exist',
reset: 'Reset',
statusEnabled: 'Enabled',
statusDisabled: 'Disabled',
refreshTokenNotExist: 'Refresh token does not exist'
},
model: {
searchPlaceholder: 'search model…',
@@ -416,9 +417,6 @@ export const en = {
bedrock: "Bedrock"
},
knowledgeBase: {
home: 'Home',
selectSpace: 'Please select a workspace.',
preview:'Preview',
pleaseUploadFileFirst: 'Please upload file first',
shareSuccess: 'Share successfully',
shareFailed: 'Share failed',
@@ -560,7 +558,6 @@ export const en = {
fileName: 'File Name',
fileList: 'File List',
blockPreview: 'Block Preview',
processingDocuments: 'Processing documents, please wait...',
chunkContent: 'Chunk Content',
sampleChunk: 'Sample Chunk Content',
noFilesSelected: 'No files',
@@ -570,10 +567,6 @@ export const en = {
waiting: 'Waiting',
startUpload: 'Total {{count}} files | Start Upload',
startUploading: 'Start Uploading',
startUploadConfirmTitle: 'Start Processing Documents',
startUploadConfirmContent: 'Document processing will run in the background. You can choose to return to the list page immediately or stay on this page to view the processing progress.',
returnToList: 'Return to List',
stayOnPage: 'Stay on Page',
uploadSuccess: 'Upload Success',
datasetName: 'Dataset Name',
pleaseEnterDatasetName: 'Please enter dataset name',
@@ -662,6 +655,8 @@ export const en = {
active: 'Active',
inactive: 'Inactive',
configurationName: 'Configuration Name',
emotionEngine: 'Emotion Engine',
reflectionEngine: 'Self-Reflection Engine'
},
member: {
username: 'Username',
@@ -1130,7 +1125,7 @@ export const en = {
intelligentSemanticPruningSceneDesc: 'Select intelligent semantic pruning scene (education, online_service, outbound).',
intelligentSemanticPruningThreshold: 'Intelligent Semantic Pruning Threshold',
intelligentSemanticPruningThresholdDesc: 'Set intelligent semantic pruning threshold (0-0.9).',
selfReflexionEngine: 'Self-Reflexion Engine',
reflectionEngine: 'Self-Reflexion Engine',
selfReflexionEngineSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.',
enableSelfReflexion: 'Enable self-reflexion',
iterationPeriod: 'Iteration Period',
@@ -1230,8 +1225,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
quickReply: 'Quick Reply',
web_search: 'Online search',
memory: 'Memory',
memoryConversationAnalysisEmpty: 'There is currently no dialogue analysis content available',
memoryConversationAnalysisEmptySubTitle: 'After entering your user ID, click on "Send" to view the conversation memory'
},
login: {
title: 'Red Bear Memory Science',
@@ -1313,5 +1306,391 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
active: 'Active',
inactive: 'Expired'
},
tool: {
mcp: 'MCP Service',
inner: 'Built-in Tools',
custom: 'Custom Tools',
mcpSearchPlaceholder: 'Search MCP services...',
addService: 'Add MCP Service',
addServiceSuccess: 'Service added successfully',
serviceAddress: 'Service Address',
lastConnection: 'Last Connection',
responseTime: 'Response Time',
status: {
running: 'Running',
stopped: 'Stopped',
abnormal: 'Abnormal',
unknown: 'Unknown'
},
serviceEndpoint: 'Service Endpoint URL',
serviceEndpointPlaceholder: 'Service endpoint URL',
serviceEndpointExtra: 'Complete access address for MCP service',
nameAndIcon: 'Name and Icon',
namePlaceholder: 'Name your MCP service',
serverIdentifier: 'Server Identifier',
serverIdentifierPlaceholder: 'Unique server identifier, e.g. my-mcp-server',
serverIdentifierLength: 'Maximum 24 characters',
serverIdentifierPattern: 'Supports lowercase letters, numbers, underscores and hyphens',
description: 'Description',
auth: 'Authentication',
requestHeader: 'Request Headers',
config: 'Configuration',
authType: 'Authentication Type',
noAuth: 'No Authentication',
apiKey: 'API Key',
basicAuth: 'Basic Auth',
bearerToken: 'Bearer Token',
username: 'Username',
password: 'Password',
requestHeaderDesc: 'Additional HTTP request headers sent to MCP server',
addRequestHeader: 'Add Request Header',
editRequestHeader: 'Edit Request Header',
requestHeaderName: 'Request Header Name',
requestHeaderValue: 'Request Header Value',
timeout: 'Timeout (seconds)',
sseReadTimeout: 'SSE Read Timeout (seconds)',
saveAndTest: 'Save and Test',
innerSearchPlaceholder: 'Search built-in tools',
timeFormat: 'Time Formatting',
timeZoneConversion: 'Time Zone Conversion',
timestampConversion: 'Timestamp Conversion',
timeCalculation: 'Time Calculation',
time_desc: 'Date and time processing',
time_features: 'Provides time format conversion, time zone conversion, timestamp calculation and other functions',
currentTime: 'Current Time',
timestamp: 'Timestamp',
localTime: 'Local Time',
utcTime: 'UTC Time',
secondsTimestamp: 'Timestamp (seconds)',
millisecondsTimestamp: 'Timestamp (milliseconds)',
enterTimestamp: 'Enter timestamp',
conversion: 'Conversion',
conversionResult: 'Conversion Result',
chooseFormatType: 'Choose Format',
json_desc: 'Data format conversion',
json_features: 'JSON formatting, compression, validation and conversion functions',
jsonFormat: 'JSON Formatting',
jsonGzip: 'JSON Compression',
jsonCheck: 'JSON Validation',
jsonConversion: 'Format Conversion',
jsonEg: 'Example JSON',
enterJson: 'Enter JSON',
jsonPlaceholder: 'Enter JSON data, e.g.: {"name": "test", "value": 123}',
clear: 'Clear',
parse: 'Paste',
format: 'Format',
gzip: 'Compress',
check: 'Validate',
escape: 'Escape',
outputResult: 'Output Result',
validJosn: 'JSON format is correct, validation passed!',
baidu_desc: 'Search engine service',
baidu_features: 'Integrated Baidu Search API, providing web search, news search and other functions',
webSearch: 'Web Search',
newsSearch: 'News Search',
imageSearch: 'Image Search',
realTimeResults: 'Real-time Results',
configStatus: 'Configuration Status',
hasApiKey: 'API configured and enabled',
needApiKey: 'Need to configure API Key',
minerU_desc: 'PDF parsing tool',
minerU_features: 'High-precision PDF document parsing tool, supports text, table, and image extraction',
pdfParser: 'PDF Parser',
tableExtraction: 'Table Extraction',
imageRecognition: 'Image Recognition',
textExtraction: 'Text Extraction',
textIn_desc: 'OCR text recognition',
textIn_features: 'Intelligent OCR text recognition service, supports multilingual and handwriting recognition',
universalOCR: 'Universal OCR',
handwritingRecognition: 'Handwriting Recognition',
multilingualSupport: 'Multilingual Support',
highPrecisionRecognition: 'High Precision Recognition',
configDesc: 'Configuration Description',
baidu_config_desc: 'To use Baidu Search API, you need to apply for API Key and Secret Key on Baidu Open Platform first.',
minerU_config_desc: 'MinerU is a high-precision PDF document parsing tool that requires an API Key to use.',
textIn_config_desc: 'TextIn provides intelligent OCR text recognition service with multilingual support.',
link: 'Application Link',
api_key: 'API Key',
baidu_api_key_desc: 'API Key obtained from Baidu Open Platform',
minerU_api_key_desc: 'API Key obtained from MinerU platform',
secret_key: 'Secret Key',
baidu_secret_key_desc: 'Secret Key obtained from Baidu Open Platform',
textIn_secret_key_desc: 'Secret Key obtained from TextIn platform',
search_type: 'Search Type',
pagesize: 'Results Per Page',
pagesize_desc: 'Number of results returned per search (1-50)',
baidu_enable: 'Enable Baidu Search',
baidu_safe_enable: 'Enable Safe Search',
baidu_safe_enable_desc: 'Filter inappropriate content',
api_address: 'API Address',
minerU_api_address_desc: 'Default uses official API address, can be modified for private deployment',
textIn_api_address_desc: 'Default uses official API address',
parsing_mode: 'Parsing Mode',
auto_recognition: 'Auto Recognition',
pure_text_mode: 'Pure Text Mode',
table_priority: 'Table Priority',
image_priority: 'Image Priority',
minerU_timeout_desc: 'PDF parsing timeout (10-300 seconds)',
minerU_enable: 'Enable MinerU',
minerU_extract_images_enable: 'Extract Images',
minerU_extract_images_enable_desc: 'Whether to extract image content from PDF',
app_id: 'APP ID',
textIn_app_id_desc: 'App ID obtained from TextIn platform',
language_identification: 'Recognition Language',
automatic_detection: 'Automatic Detection',
simplified_chinese: 'Simplified Chinese',
traditional_chinese: 'Traditional Chinese',
english: 'English',
japanese: 'Japanese',
korean_language: 'Korean',
pattern_recognition: 'Recognition Mode',
universal_identification: 'Universal Recognition',
high_precision_identification: 'High Precision Recognition',
handwriting_recognition: 'Handwriting Recognition',
formula_recognition: 'Formula Recognition',
textIn_enable: 'Enable TextIn',
return_text_position_enable: 'Return Text Position Info',
return_text_position_enable_desc: 'Whether to return coordinate positions of recognized text',
addCustom: 'Add Custom Tool',
editCustom: 'Edit Custom Tool',
schema: 'Schema',
schemaPlaceholder: 'Enter your OpenAPI schema here',
authentication: 'Authentication Method',
tag: 'Tag',
created_at: 'Created At',
headerName: 'Header Name',
null: 'None',
tagDesc: 'Multiple tags separated by commas',
availableTools: 'Available Tools',
name: 'Name',
desc: 'Description',
method: 'Method',
path: 'Path',
viewDetail: 'View Details'
},
workflow: {
coreNode: 'Core Nodes',
start: 'Start',
end: 'End',
answer: 'Answer',
aiAndCognitiveProcessing: 'AI & Cognitive Processing',
llm: 'Large Language Model (LLM)',
model_selection: 'Model Selection',
model_voting: 'Model Voting',
rag: 'Knowledge Retrieval (RAG)',
classification: 'Smart Classification',
parameter_extraction: 'Parameter Extraction',
flowControl: 'Flow Control',
condition: 'Conditional Branch',
iteration: 'Iteration',
loop: 'Loop',
parallel: 'Parallel Execution',
aggregator: 'Aggregator',
externalInteraction: 'External Interaction',
http_request: 'HTTP Request',
tools: 'Tools',
code_execution: 'Code Execution',
template_rendering: 'Template Rendering',
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
task_planning: 'Task Planning',
reasoning_control: 'Reasoning Control',
self_reflection: 'Self Reflection',
memory_enhancement: 'Memory Enhancement',
agentCollaborationNode: 'Agent Collaboration Nodes',
agent_scheduling: 'Agent Scheduling',
agent_collaboration: 'Agent Collaboration',
agent_arbitration: 'Agent Arbitration',
safetyAndCompliance: 'Safety & Compliance',
sensitive_detection: 'Sensitive Detection',
output_audit: 'Output Audit',
evolutionAndGovernance: 'Evolution & Governance',
self_optimization: 'Self Optimization',
process_evolution: 'Process Evolution',
clickToConfigure: 'Click to configure node parameters',
nodeProperties: 'Node Properties',
empty: "Emmm... The box is empty, there's nothing here~",
nodeName: 'Node Name',
config: {
llm: {
model_id: 'Model',
temperature: 'Temperature',
max_tokens: 'Max Tokens',
},
start: {
variables: 'Input Fields',
string: 'Text',
number: 'Number',
boolean: 'Checkbox',
array: 'Dropdown Options',
object: 'Object',
addVariable: 'Add Variable',
editVariable: 'Edit Variable',
variableType: 'Variable Type',
variableName: 'Variable Name',
description: 'Display Name',
default: 'Default Value',
required: 'Required',
max_length: 'Max Length',
defaultChecked: 'Checked',
notDefaultChecked: 'Not Checked',
options: 'Options',
},
end: {
output: 'Reply'
}
},
clear: 'Clear',
run: 'Run',
save: 'Save',
export: 'Export',
variableConfig: 'Variable Configuration'
},
emotionEngine: {
emotionEngineConfig: 'Emotion Engine Configuration',
emotion_enabled: 'Enable Emotion Engine',
emotion_enabled_desc: 'Automatically analyze emotional tendencies in conversations',
emotion_model_id: 'Emotion Analysis Model',
emotion_model_id_desc: 'Different models vary in accuracy and speed',
emotion_extract_keywords: 'Emotion Keyword Extraction',
emotion_extract_keywords_subTitle: 'Automatically extract emotion-related keywords from conversations',
emotion_extract_keywords_desc: 'Extract emotional keywords like "happy", "disappointed", "excited" to better understand user emotions',
emotion_min_intensity: 'Confidence Threshold',
emotion_min_intensity_desc: 'Higher confidence leads to more accurate recognition, but may miss some information',
emotion_enable_subject: 'Emotion Subject Classification',
emotion_enable_subject_subTitle: 'Identify emotion attribution (self/other/object)',
emotion_enable_subject_desc: 'Distinguish emotion subjects: self (I feel happy), other (he is angry), object (this product is great)',
currentValue: 'Current Value',
emotion_min_intensity_description: 'Confidence Threshold Description',
question: 'What is Confidence Threshold?',
answer: 'Confidence threshold is the "certainty level" standard for emotion engine to judge emotions. When the emotional confidence analyzed by AI is lower than the set threshold, the emotion will not be recorded.',
differentTitle: 'Impact of Different Thresholds',
advantage: 'Advantages',
shortcoming: 'Disadvantages',
scene: 'Applicable Scenarios',
low_title: 'Low Threshold (0.0 - 0.4)',
low_tag: 'Sensitive',
low_advantage: 'Can capture more subtle emotional changes without missing potential emotional signals',
low_shortcoming: 'May cause misjudgments, identifying neutral or unclear expressions as specific emotions',
low_scene: 'Scenarios requiring comprehensive understanding of user emotional fluctuations with low accuracy requirements',
middle_title: 'Medium Threshold (0.5 - 0.7)',
middle_tag: 'Recommended',
middle_advantage: 'Balances accuracy and coverage, can identify obvious emotions without being overly sensitive',
middle_shortcoming: 'May miss some less obvious emotional expressions',
middle_scene: 'Most daily conversation scenarios, suitable for general emotional analysis needs',
high_title: 'High Threshold (0.8 - 1.0)',
high_tag: 'Precise',
high_advantage: 'Only records very clear emotional expressions, extremely high accuracy with low misjudgment rate',
high_shortcoming: 'Will miss a large amount of less obvious emotional information, low data coverage',
high_scene: 'Scenarios requiring extremely high accuracy, such as emotional crisis warnings and important decision references',
configSuggest: 'Configuration Suggestions',
first: 'First Time Use',
first_desc: 'Recommend starting with medium threshold (0.6-0.7), observe for a period and adjust based on actual results',
customer_service: 'Customer Service Scenarios',
customer_service_desc: 'Recommend using lower threshold (0.4-0.6) to timely capture user dissatisfaction',
data_analysis: 'Data Analysis',
data_analysis_desc: 'Recommend using medium threshold (0.6-0.7) to ensure data quality while having sufficient sample size',
risk_warning: 'Risk Warning',
risk_warning_desc: 'Recommend using higher threshold (0.7-0.8) to ensure warning accuracy',
actual_case: 'Actual Case',
user_input: 'User Input',
user_input_message: '"This feature is okay, but there are some minor issues"',
neutral_emotion: 'Neutral Emotion',
neutral_emotion_tag: 'All thresholds will record',
minor_dissatisfaction: 'Minor Dissatisfaction',
minor_dissatisfaction_tag: 'Only low/medium thresholds will record',
expect_improvement: 'Expect Improvement',
expect_improvement_tag: 'Only low threshold will record',
confidence: 'Confidence'
},
emotionDetail: {
wordCloud: 'Emotion Distribution Analysis',
pieces: 'items',
emotionTags: 'High-Frequency Emotion Keywords',
joy: 'Joy',
anger: 'Anger',
sadness: 'Sadness',
fear: 'Fear',
neutral: 'Neutral',
surprise: 'Surprise',
health: 'Emotional Health Index',
positivity_rate: 'Positivity Rate',
stability: 'Stability',
resilience: 'Resilience',
suggestions: 'Personalized Suggestions',
},
reflectionEngine: {
reflectionEngineConfig: 'Reflection Engine Configuration',
reflection_enabled: 'Enable Reflection Engine',
reflection_enabled_desc: 'Transform episodic memory into semantic memory, forming long-term cognition',
reflection_model_id: 'Reflection Model',
reflection_model_id_desc: 'Different models vary in accuracy and speed',
reflection_period_in_hours: 'Iteration Period',
reflection_period_in_hours_desc: 'Determines how often the system performs memory reflection and refinement',
reflexion_range: 'Reflection Range',
partial: 'Partial Reflection (New memories only)',
all: 'Full Reflection (All historical memories)',
reflexion_range_desc: '',
baseline: 'Reflection Baseline',
baseline_desc: '',
TIME: 'Time-based (Temporal relationships)',
FACT: 'Fact-based (Knowledge points)',
HYBRID: 'Fact + Time (Comprehensive dimension)',
quality_assessment: 'Enable Quality Assessment',
quality_assessment_desc: 'Automatically evaluate memory accuracy, completeness and timeliness',
memory_verify: 'Enable Memory Verification',
memory_verify_desc: 'Detect sensitive information and filter inappropriate content',
oneHour: 'Every 1 hour',
threeHours: 'Every 3 hours',
sixHours: 'Every 6 hours',
twelveHours: 'Every 12 hours',
daily: 'Daily',
run: 'Run Debug',
example: 'Raw Data',
exampleText: 'My sister was born in 2025, oh wait, I remembered wrong, she was actually born in 2024. By the way, my ID number is 33252218293749845X.',
runTitle: 'Reflection Test Run',
status: 'Status',
message: 'Message',
conflictDetection: 'Conflict Detection',
reason: 'Conflict Reason',
solution: 'Solution',
qualityAssessment: 'Quality Assessment',
qualityAssessmentObj: {
score: 'Quality Score',
summary: 'Assessment Summary',
},
privacyAudit: 'Privacy Audit',
privacyAuditObj: {
true: 'Yes',
false: 'No',
has_privacy: 'Contains Privacy Information',
privacy_types: 'Privacy Types',
summary: 'Audit Summary',
}
}
},
};

View File

@@ -35,6 +35,9 @@ export const zh = {
knowledgeDocumentDetails: '详情',
userMemoryDetail: '用户记忆详情',
toolManagement: '工具管理',
emotionEngine: '情感引擎',
emotionDetail: '情绪记忆',
selfReflectionEngine: '反思引擎',
},
knowledgeBase: {
home: '首页',
@@ -180,7 +183,6 @@ export const zh = {
fileName: '文件名称',
fileList: '文件列表',
blockPreview: '分块预览',
processingDocuments: '正在处理文档,请稍候...',
chunkContent: '分块内容',
sampleChunk: '示例分块内容',
noFilesSelected: '暂无文件',
@@ -190,10 +192,6 @@ export const zh = {
waiting: '等待中',
startUpload: '共{{count}}个文件 | 开始上传',
startUploading: '开始上传',
startUploadConfirmTitle: '开始处理文档',
startUploadConfirmContent: '文档处理将在后台进行,您可以选择立即返回列表页或停留在此页面查看处理进度。',
returnToList: '返回列表页',
stayOnPage: '停留在此页',
uploadSuccess: '上传成功',
datasetName: '数据集名称',
pleaseEnterDatasetName: '请输入数据集名称',
@@ -759,8 +757,8 @@ export const zh = {
copy: '复制',
copySuccess: '复制成功',
viewDetails: '查看详情',
enabled: '启用',
disabled: '停用',
enabled: '启用',
disabled: '停用',
updateWarning: '更新警告',
deleteWarning: '删除警告',
deleteWarningContent: '确定要删除此{{content}}吗?',
@@ -800,9 +798,7 @@ export const zh = {
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
publicApiCannotRefreshToken: '公共接口不能刷新token',
refreshTokenNotExist: '刷新token不存在',
reset: '重置',
statusEnabled: '已启用',
statusDisabled: '已禁用',
reset: '重置'
},
product: {
applicationManagement: '应用管理',
@@ -1007,6 +1003,8 @@ export const zh = {
active: '活跃',
inactive: '不活跃',
configurationName: '配置名称',
emotionEngine: '情感引擎',
reflectionEngine: '反思引擎'
},
member: {
username: '用户名',
@@ -1067,8 +1065,8 @@ export const zh = {
minimumRetentionDesc: '控制记忆随时间的遗忘速度,值越高时间越短',
forgettingRate: '记忆遗忘率 (λ_mem)',
forgettingRateDesc: '控制记忆遗忘的速度,值越高遗忘越快',
offset: '最小保留度 (offset)',
offsetDesc: '控制记忆保留的最小保留阈值 遗忘这地方改个文字描述',
offset: '偏移量 (offset)',
offsetDesc: '最小保留度的偏移量',
CurrentValue: '当前值',
range: '范围',
forgettingEngineConfigParams: '遗忘引擎配置参数',
@@ -1124,11 +1122,8 @@ export const zh = {
storageType: '存储类型',
rag: 'RAG存储',
ragDesc: '基于向量检索,适合文档问答和语义搜索',
neo4j: '图存储',
neo4j: '图存储',
neo4jDesc: '基于知识图谱,适合关系推理和路径查询',
llmModel: 'LLM 模型',
embeddingModel: 'Embedding 模型',
rerankModel: 'Rerank 模型'
},
memoryExtractionEngine: {
title: '记忆引擎模块配置中心',
@@ -1203,7 +1198,7 @@ export const zh = {
intelligentSemanticPruningSceneDesc: '选择智能语义修剪场景education、online_service、outbound。',
intelligentSemanticPruningThreshold: '智能语义修剪阈值',
intelligentSemanticPruningThresholdDesc: '设置智能语义修剪阈值0-0.9)。',
selfReflexionEngine: '自我反思引擎',
reflectionEngine: '自我反思引擎',
selfReflexionEngineSubTitle: '通过反思和精炼,将情节记忆转化为更深层的语义记忆。',
enableSelfReflexion: '启用自我反思',
iterationPeriod: '迭代周期',
@@ -1299,8 +1294,6 @@ export const zh = {
startANewConversation: '开始新对话',
normalReply: '正常回复',
quickReply: '快速回复',
memoryConversationAnalysisEmpty: '当前没有可用的对话分析内容',
memoryConversationAnalysisEmptySubTitle: '输入用户ID后单击“发送”查看对话记忆'
},
login: {
title: '红熊记忆科学',
@@ -1371,24 +1364,6 @@ export const zh = {
websocketDemoCard: 'WebSocket 演示',
sseDemoCard: 'SSE演示'
},
workflow: {
title: '工作流编辑器',
description: '拖拽节点创建连接,构建您的工作流程。点击节点可进行配置。',
addNode: '添加节点',
deleteNode: '删除选中',
saveWorkflow: '保存工作流',
startNode: '触发节点',
conditionNode: '条件判断',
actionNode: '执行动作',
endNode: '结束节点',
newNode: '新节点',
node: '节点',
nodesCreated: '已创建节点',
loadingNodes: '正在加载节点 {{progress}}%',
loadingFailed: '加载节点失败',
create5kNodes: '创建5000节点',
create10kNodes: '创建10000节点'
},
notFound: {
title: '页面未找到',
description: '请求的页面不存在。',
@@ -1419,5 +1394,391 @@ export const zh = {
active: '活跃',
inactive: '过期'
},
tool: {
mcp: 'MCP 服务',
inner: '内置工具',
custom: '自定义工具',
mcpSearchPlaceholder: '搜索MCP服务...',
addService: '添加MCP服务',
addServiceSuccess: '服务添加成功',
serviceAddress: '服务地址',
lastConnection: '最后连接',
responseTime: '响应时间',
status: {
running: '运行中',
stopped: '已停止',
abnormal: '异常',
unknown: '未知'
},
serviceEndpoint: '服务端点 URL',
serviceEndpointPlaceholder: '服务端点的 URL',
serviceEndpointExtra: 'MCP服务的完整访问地址',
nameAndIcon: '名称和图标',
namePlaceholder: '命名你的 MCP 服务',
serverIdentifier: '服务器标识符',
serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server',
serverIdentifierLength: '最多 24 个字符',
serverIdentifierPattern: '支持小写字母、数字、下划线和连字符',
description: '描述信息',
auth: '认证',
requestHeader: '请求头',
config: '配置',
authType: '认证方式',
noAuth: '无需认证',
apiKey: 'API Key',
basicAuth: 'Basic Auth',
bearerToken: 'Bearer Token',
username: '用户名',
password: '密码',
requestHeaderDesc: '发送到 MCP 服务器的额外 HTTP 请求头',
addRequestHeader: '添加请求头',
editRequestHeader: '编辑请求头',
requestHeaderName: '请求头名称',
requestHeaderValue: '请求头值',
timeout: '超时时间(秒)',
sseReadTimeout: 'SSE 读取超时时间(秒)',
saveAndTest: '保存并测试',
innerSearchPlaceholder: '搜索内置工具',
timeFormat: '时间格式化',
timeZoneConversion: '时区转换',
timestampConversion: '时间戳转换',
timeCalculation: '时间计算',
time_desc: '日期时间处理',
time_features: '提供时间格式转换、时区转换、时间戳计算等功能',
currentTime: '当前时间',
timestamp: '时间戳',
localTime: '本地时间',
utcTime: 'UTC时间',
secondsTimestamp: '时间戳(秒)',
millisecondsTimestamp: '时间戳(毫秒)',
enterTimestamp: '输入时间戳',
conversion: '转换',
conversionResult: '转换结果',
chooseFormatType: '选择格式',
json_desc: '数据格式转换',
json_features: 'JSON格式化、压缩、验证和转换功能',
jsonFormat: 'JSON格式化',
jsonGzip: 'JSON压缩',
jsonCheck: 'JSON验证',
jsonConversion: '格式转换',
jsonEg: '示例JSON',
enterJson: '输入JSON',
jsonPlaceholder: '输入JSON数据例如{"name": "测试", "value": 123}',
clear: '清空',
parse: '粘贴',
format: '格式化',
gzip: '压缩',
check: '验证',
escape: '转义',
outputResult: '输出结果',
validJosn: 'JSON格式正确验证通过',
baidu_desc: '搜索引擎服务',
baidu_features: '集成百度搜索API提供网页搜索、新闻搜索等功能',
webSearch: '网页搜索',
newsSearch: '新闻搜索',
imageSearch: '图片搜索',
realTimeResults: '实时结果',
configStatus: '配置状态',
hasApiKey: 'API 已配置并启用',
needApiKey: '需要配置API Key',
minerU_desc: 'PDF解析工具',
minerU_features: '高精度PDF文档解析工具支持文字、表格、图片提取',
pdfParser: 'PDF解析',
tableExtraction: '表格提取',
imageRecognition: '图片识别',
textExtraction: '文本提取',
textIn_desc: 'OCR文字识别',
textIn_features: '智能OCR文字识别服务支持多语言、手写体识别',
universalOCR: '通用OCR',
handwritingRecognition: '手写识别',
multilingualSupport: '多语言支持',
highPrecisionRecognition: '高精度识别',
configDesc: '配置说明',
baidu_config_desc: '使用百度搜索API需要先在百度开放平台申请API Key和Secret Key。',
minerU_config_desc: 'MinerU是高精度PDF文档解析工具需要API Key才能使用。',
textIn_config_desc: 'TextIn提供智能OCR文字识别服务支持多语言识别。',
link: '申请地址',
api_key: 'API Key',
baidu_api_key_desc: '从百度开放平台获取的API Key',
minerU_api_key_desc: '从MinerU平台获取的API Key',
secret_key: 'Secret Key',
baidu_secret_key_desc: '从百度开放平台获取的Secret Key',
textIn_secret_key_desc: '从TextIn平台获取的Secret Key',
search_type: '搜索类型',
pagesize: '每页结果数',
pagesize_desc: '每次搜索返回的结果数量1-50',
baidu_enable: '启用百度搜索',
baidu_safe_enable: '启用安全搜索',
baidu_safe_enable_desc: '过滤不适宜内容',
api_address: 'API地址',
minerU_api_address_desc: '默认使用官方API地址如有私有部署可修改',
textIn_api_address_desc: '默认使用官方API地址',
parsing_mode: '解析模式',
auto_recognition: '自动识别',
pure_text_mode: '纯文本模式',
table_priority: '表格优先',
image_priority: '图片优先',
minerU_timeout_desc: 'PDF解析超时时间10-300秒',
minerU_enable: '启用MinerU',
minerU_extract_images_enable: '提取图片',
minerU_extract_images_enable_desc: '是否提取PDF中的图片内容',
app_id: 'APP ID',
textIn_app_id_desc: '从TextIn平台获取的App ID',
language_identification: '识别语言',
automatic_detection: '自动检测',
simplified_chinese: '简体中文',
traditional_chinese: '繁体中文',
english: '英文',
japanese: '日文',
korean_language: '韩文',
pattern_recognition: '识别模式',
universal_identification: '通用识别',
high_precision_identification: '高精度识别',
handwriting_recognition: '手写体识别',
formula_recognition: '公式识别',
textIn_enable: '启用TextIn',
return_text_position_enable: '返回文本位置信息',
return_text_position_enable_desc: '是否返回识别文字的坐标位置',
addCustom: '添加自定义工具',
editCustom: '编辑自定义工具',
schema: 'Schema',
schemaPlaceholder: '在此处输入您的 OpenAPI schema',
authentication: '鉴权方式',
tag: '标签',
created_at: '创建时间',
headerName: 'Header 名称',
null: '无',
tagDesc: '多个标签用逗号分隔',
availableTools: '可用工具',
name: '名称',
desc: '描述',
method: '方法',
path: '路径',
viewDetail: '查看详情'
},
workflow: {
coreNode: '核心节点',
start: '开始Start',
end: '结束End',
answer: '回复Answer',
aiAndCognitiveProcessing: 'AI与认知处理',
llm: '大语言模型 (LLM)',
model_selection: '多模型选择',
model_voting: '多模型投票',
rag: '知识检索 (RAG)',
classification: '智能分类',
parameter_extraction: '参数提取',
flowControl: '流程控制',
condition: '条件分支',
iteration: '迭代 (Iteration)',
loop: '循环 (Loop)',
parallel: '并行执行',
aggregator: '聚合器',
externalInteraction: '外部交互',
http_request: 'HTTP请求',
tools: '工具 (Tools)',
code_execution: '代码执行',
template_rendering: '模板渲染',
cognitiveUpgrading: '认知升级(创新)',
task_planning: '任务规划',
reasoning_control: '推理控制',
self_reflection: '自我反思',
memory_enhancement: '记忆增强',
agentCollaborationNode: 'Agent 协作节点',
agent_scheduling: 'Agent 调度',
agent_collaboration: 'Agent 协同',
agent_arbitration: 'Agent 仲裁',
safetyAndCompliance: '安全与合规',
sensitive_detection: '敏感识别',
output_audit: '输出审计',
evolutionAndGovernance: '演化与治理',
self_optimization: '自我优化',
process_evolution: '流程演化',
clickToConfigure: '点击配置节点参数',
nodeProperties: '节点属性',
empty: "Emmm…盒子是空的这里什么都没有",
nodeName: '节点名称',
config: {
llm: {
model_id: '模型',
temperature: '温度',
max_tokens: '最大令牌数',
},
start: {
variables: '输入字段',
string: '文本',
number: '数字',
boolean: '复选框',
array: '下拉选项',
object: '对象',
addVariable: '添加变量',
editVariable: '编辑变量',
variableType: '变量类型',
variableName: '变量名称',
description: '显示名称',
default: '默认值',
required: '必填',
max_length: '最大长度',
defaultChecked: '选中',
notDefaultChecked: '不选中',
options: '选项',
},
end: {
output: '回复'
}
},
clear: '清空',
run: '运行',
save: '保存',
export: '导出',
variableConfig: '变量配置'
},
emotionEngine: {
emotionEngineConfig: '情感引擎配置',
emotion_enabled: '启用情感引擎',
emotion_enabled_desc: '自动分析对话中的情感倾向',
emotion_model_id: '情感分析模型',
emotion_model_id_desc: '不同模型在准确度和速度上有所差异',
emotion_extract_keywords: '情绪关键词提取',
emotion_extract_keywords_subTitle: '自动提取对话中的情绪相关关键词',
emotion_extract_keywords_desc: '提取如"开心"、"失望"、"期待"等情绪关键词,帮助更好地理解用户情绪',
emotion_min_intensity: '置信度阈值',
emotion_min_intensity_desc: '置信度越高,识别越准确,但可能遗漏部分信息',
emotion_enable_subject: '情绪主体分类 ',
emotion_enable_subject_subTitle: '识别情绪归属(自己/他人/物体)',
emotion_enable_subject_desc: '区分情绪主体: self (我感到开心)、other (他很生气)、object (这个产品很棒)',
currentValue: '当前值',
emotion_min_intensity_description: '置信度阈值说明',
question: '什么是置信度阈值?',
answer: '置信度阈值是情感引擎判断情绪时的"确定程度"标准。当 AI 分析出的情感置信度低于设定阈值时,该情感将不会被记录。',
differentTitle: '不同阈值的影响',
advantage: '优点',
shortcoming: '缺点',
scene: '适用场景',
low_title: '低阈值 (0.0 - 0.4)',
low_tag: '灵敏',
low_advantage: '能捕捉到更多细微的情感变化,不会遗漏潜在的情绪信号',
low_shortcoming: '可能产生误判,将中性或不明确的表达识别为特定情感',
low_scene: '需要全面了解用户情绪波动,对准确度要求不高的场景',
middle_title: '中阈值 (0.5 - 0.7)',
middle_tag: '推荐',
middle_advantage: '平衡准确度和覆盖率,既能识别明显情感,也不会过度敏感',
middle_shortcoming: '可能遗漏一些不太明显的情感表达',
middle_scene: '大多数日常对话场景,适合一般性情感分析需求',
high_title: '高阈值 (0.8 - 1.0)',
high_tag: '精准',
high_advantage: '只记录非常明确的情感表达,准确度极高,误判率低',
high_shortcoming: '会遗漏大量不够明显的情感信息,数据覆盖率低',
high_scene: '对准确度要求极高的场景,如情感危机预警、重要决策参考',
configSuggest: '配置建议',
first: '初次使用',
first_desc: '建议从中等阈值0.6-0.7)开始,观察一段时间后根据实际效果调整',
customer_service: '客服场景',
customer_service_desc: '建议使用较低阈值0.4-0.6),及时捕捉用户的不满情绪',
data_analysis: '数据分析',
data_analysis_desc: '建议使用中等阈值0.6-0.7),保证数据质量的同时有足够样本量',
risk_warning: '风险预警',
risk_warning_desc: '建议使用较高阈值0.7-0.8),确保预警的准确性',
actual_case: '实际案例',
user_input: '用户输入',
user_input_message: '"这个功能还行吧,不过有点小问题"',
neutral_emotion: '中性情感',
neutral_emotion_tag: '所有阈值都会记录',
minor_dissatisfaction: '轻微不满',
minor_dissatisfaction_tag: '仅低/中阈值会记录',
expect_improvement: '期待改进',
expect_improvement_tag: '仅低阈值会记录',
confidence: '置信度'
},
emotionDetail: {
wordCloud: '情感分布分析',
pieces: '条',
emotionTags: '高频情绪关键词',
joy: '喜悦',
anger: '愤怒',
sadness: '悲伤',
fear: '恐惧',
neutral: '中性',
surprise: '惊讶',
health: '情绪健康指数',
positivity_rate: '积极率',
stability: '稳定性',
resilience: '恢复力',
suggestions: '个性化建议',
},
reflectionEngine: {
reflectionEngineConfig: '反思引擎配置',
reflection_enabled: '启用反思引擎',
reflection_enabled_desc: '将情节记忆转化为语义记忆,形成长期认知',
reflection_model_id: '反思模型',
reflection_model_id_desc: '不同模型在准确度和速度上有所差异',
reflection_period_in_hours: '迭代周期',
reflection_period_in_hours_desc: '决定系统多久进行一次记忆反思和提炼',
reflexion_range: '反思范围',
partial: '部分反思 (仅新增记忆)',
all: '全部反思 (所有历史记忆)',
reflexion_range_desc: '',
baseline: '反思基线',
baseline_desc: '',
TIME: '基于时间(时序关系)',
FACT: '基于事实(知识点)',
HYBRID: '事实+时间(综合维度)',
quality_assessment: '启用质量评估',
quality_assessment_desc: '自动评估记忆的准确性、完整性和时效性',
memory_verify: '启用记忆审核',
memory_verify_desc: '检测敏感信息并过滤违规内容',
oneHour: '每1个小时',
threeHours: '每3个小时',
sixHours: '每6个小时',
twelveHours: '每12个小时',
daily: '每天',
run: '运行调试',
example: '原始数据',
exampleText: '我妹妹是2025年出生的哦不对我记错了她其实是2024年出生的。对了我的身份证号码是33252218293749845X。',
runTitle: '反思试运行',
status: '状态',
message: '消息',
conflictDetection: '冲突检测',
reason: '冲突原因',
solution: '解决方案',
qualityAssessment: '质量评估',
qualityAssessmentObj: {
score: '质量评分',
summary: '评估摘要',
},
privacyAudit: '隐私审核',
privacyAuditObj: {
true: '是',
false: '否',
has_privacy: '包含隐私信息',
privacy_types: '隐私类型',
summary: '审核摘要',
}
}
},
}

View File

@@ -10,19 +10,19 @@ import routesConfig from './routes.json';
// 递归收集所有路由中的element
function collectElements(routes: RouteConfig[]): Set<string> {
const elements = new Set<string>();
function traverse(routeList: RouteConfig[]) {
routeList.forEach(route => {
// 添加当前路由的element
elements.add(route.element);
// 递归处理子路由
if (route.children && route.children.length > 0) {
traverse(route.children);
}
});
}
traverse(routes);
return elements;
}
@@ -55,10 +55,13 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
ModelManagement: lazy(() => import('@/views/ModelManagement')),
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
EmotionDetail: lazy(() => import('@/views/UserMemoryDetail/pages/EmotionDetail')),
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),
NotFound: lazy(() => import('@/views/NotFound')),
NotFound: lazy(() => import('@/views/NotFound'))
};
// 检查并报告缺失的组件
@@ -88,12 +91,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
// 获取组件
const componentKey = route.element as keyof typeof componentMap;
const Component = componentMap[componentKey];
if (!Component) {
console.error(`Component ${route.element} not found in componentMap`);
return null;
}
// 如果有子路由
if (route.children) {
return (
@@ -102,12 +105,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
</Route>
);
}
// 如果有path属性则为普通路由
if (route.path) {
return <Route key={index} path={route.path} element={<Component />} />;
}
return null;
});
};

View File

@@ -26,6 +26,9 @@
{ "path": "/knowledge-base/:knowledgeBaseId/create-dataset", "element": "CreateDataset" },
{ "path": "/knowledge-base/:knowledgeBaseId/DocumentDetails", "element": "DocumentDetails" },
{ "path": "/api-key", "element": "ApiKeyManagement" },
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
{ "path": "/user-memory/emotion/:id", "element": "EmotionDetail" },
{ "path": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
]

View File

@@ -26,6 +26,19 @@
"sort": 0,
"subs": []
},
{
"id": 4,
"parent": 0,
"code": "tool",
"label": "工具管理",
"i18nKey": "menu.toolManagement",
"path": "/tool",
"enable": true,
"display": true,
"level": 1,
"sort": 1,
"subs": []
},
{
"id": 3,
"parent": 0,
@@ -183,6 +196,32 @@
"level": 1,
"sort": 0,
"subs": null
},
{
"id": 72,
"parent": 7,
"code": "emotionEngine",
"label": "情感引擎",
"i18nKey": "menu.emotionEngine",
"path": "/emotion-engine/:id",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": null
},
{
"id": 72,
"parent": 7,
"code": "selfReflectionEngine",
"label": "反思引擎",
"i18nKey": "menu.selfReflectionEngine",
"path": "/reflection-engine/:id",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": null
}
]
},
@@ -210,7 +249,21 @@
"display": false,
"level": 2,
"sort": 0,
"subs": null
"subs": [
{
"id": 81,
"parent": 8,
"code": "emotionDetail",
"label": "记忆详情",
"i18nKey": "menu.emotionDetail",
"path": "/user-memory/emotion/:id",
"enable": true,
"display": false,
"level": 2,
"sort": 0,
"subs": null
}
]
}
]
},

View File

@@ -171,9 +171,6 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.apiKeyRequestTotal')} value={item.total_requests} />
</Col>
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qps')} value={item.quota_used} />
</Col>
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qpsLimit')} value={item.rate_limit} />
</Col>

View File

@@ -46,7 +46,9 @@ const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProp
...values
})
handleClose()
refresh()
setTimeout(() => {
refresh()
}, 50)
})
}

View File

@@ -69,7 +69,7 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
const response = editVo?.id ? updateApplication(editVo.id, {
...editVo,
...values,
} as Application) : addApplication(values as Application)
}) : addApplication(values)
response.then(() => {
refresh()
handleClose()
@@ -127,7 +127,6 @@ const ApplicationModal = forwardRef<ApplicationModalRef, ApplicationModalProps>(
label: t(`application.${type}`),
labelDesc: t(`application.${type}Desc`),
icon: typeIcons[type],
disabled: editVo?.id || type === 'workflow'
}))}
/>
</FormItem>

View File

@@ -0,0 +1,252 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Form, Slider, Button, Alert, message, Switch, Space } from 'antd';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card';
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
import { getMemoryEmotionConfig, updateMemoryEmotionConfig } from '@/api/memory'
import type { ConfigForm } from './types'
import CustomSelect from '@/components/CustomSelect';
import { getModelListUrl } from '@/api/models'
import Tag from '@/components/Tag'
const configList = [
{
key: 'emotion_enabled',
type: 'switch',
},
{
key: 'emotion_model_id',
type: 'customSelect',
url: getModelListUrl,
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
},
{
key: 'emotion_min_intensity',
type: 'slider',
min: 0,
max: 1,
step: 0.05
},
{
key: 'emotion_extract_keywords',
type: 'switch',
hasSubTitle: true
},
{
key: 'emotion_enable_subject',
type: 'switch',
hasSubTitle: true
},
]
const EmotionEngine: React.FC = () => {
const { t } = useTranslation();
const { id } = useParams();
const [configData, setConfigData] = useState<ConfigForm>({} as ConfigForm);
const [form] = Form.useForm<ConfigForm>();
const [messageApi, contextHolder] = message.useMessage();
const [loading, setLoading] = useState(false)
const values = Form.useWatch([], form);
useEffect(() => {
getConfigData()
}, [id])
const getConfigData = () => {
if (!id) {
return
}
getMemoryEmotionConfig(id)
.then((res) => {
const response = res as ConfigForm
const initialValues = {
...response,
}
setConfigData(initialValues);
form.setFieldsValue(initialValues);
})
.catch(() => {
console.error('Failed to load data');
})
}
const handleReset = () => {
form.setFieldsValue(configData);
}
const handleSave = () => {
if (!id) {
return
}
setLoading(true)
updateMemoryEmotionConfig({
...values,
config_id: id
})
.then(() => {
messageApi.success(t('common.saveSuccess'))
setConfigData({...(values || {})})
})
.finally(() => {
setLoading(false)
})
}
return (
<Row gutter={[16, 16]}>
<Col span={12}>
<RbCard
title={
<div className="rb:flex rb:items-center">
<img src={strategyImpactSimulator} className="rb:w-5 rb:h-5 rb:mr-2" />
{t('emotionEngine.emotionEngineConfig')}
</div>
}
>
<Form
form={form}
layout="vertical"
initialValues={{
offset: 0,
lambda_time: 0.03,
lambda_mem: 0.03,
}}
>
{configList.map(config => {
if (config.type === 'slider') {
return (
<div key={config.key} className=" rb:mb-6">
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
{t(`emotionEngine.${config.key}`)}
</div>
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ">
{t(`emotionEngine.${config.key}_desc`)}
</div>
<Form.Item
name={config.key}
>
<Slider
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
tooltip={{ open: false }} max={config.max} min={config.min} step={config.step} style={{ margin: '0' }} />
</Form.Item>
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
<>{t('emotionEngine.currentValue')}: {values?.[config.key as keyof ConfigForm] || 0}</>
</div>
</div>
)
}
if (config.type === 'customSelect') {
return (
<div key={config.key}>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
{t(`emotionEngine.${config.key}`)}
</div>
<Form.Item
name={config.key}
extra={t(`emotionEngine.${config.key}_desc`)}
>
<CustomSelect
url={config.url as string}
params={config.params}
valueKey='id'
labelKey='name'
hasAll={false}
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
/>
</Form.Item>
</div>
)
}
return (
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
<div>
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`emotionEngine.${config.key}`)}</span>
{config.hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_subTitle`)}</div>}
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_desc`)}</div>
</div>
<Form.Item
name={config.key}
valuePropName="checked"
className="rb:ml-2 rb:mb-0!"
>
<Switch
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} />
</Form.Item>
</div>
)
})}
<Row gutter={16} className="rb:mt-3">
<Col span={12}>
<Button block onClick={handleReset}>{t('common.reset')}</Button>
</Col>
<Col span={12}>
<Button type="primary" loading={loading} block onClick={handleSave}>{t('common.save')}</Button>
</Col>
</Row>
</Form>
</RbCard>
</Col>
<Col span={12}>
<RbCard
title={t('emotionEngine.emotion_min_intensity_description')}
>
<div className="rb:font-medium">{t('emotionEngine.question')}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mt-2">{t('emotionEngine.answer')}</div>
<div className="rb:font-medium rb:mt-4 rb:mb-2">{t('emotionEngine.differentTitle')}</div>
<Space size={16} direction="vertical" className="rb:w-full">
{['low', 'middle', 'high'].map((key, index) => (
<Alert
key={key}
type={(['warning', 'info', 'success'] as const)[index] as 'warning' | 'info' | 'success'}
message={
<div>
<div className="rb:w-full rb:font-medium rb:flex rb:justify-between">
{t(`emotionEngine.${key}_title`)}
<Tag color={(['warning', 'processing', 'success'] as const)[index] as 'warning' | 'processing' | 'success'}>{t(`emotionEngine.${key}_tag`)}</Tag>
</div>
<Space size={8} direction="vertical" className="rb:w-full rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">
<div><span className="rb:font-medium">{t('emotionEngine.advantage')}: </span>{t(`emotionEngine.${key}_advantage`)}</div>
<div><span className="rb:font-medium">{t('emotionEngine.shortcoming')}: </span>{t(`emotionEngine.${key}_shortcoming`)}</div>
<div><span className="rb:font-medium">{t('emotionEngine.scene')}: </span>{t(`emotionEngine.${key}_scene`)}</div>
</Space>
</div>
}
/>
))}
</Space>
<div className="rb:font-medium rb:mt-6 rb:mb-3">{t('emotionEngine.configSuggest')}</div>
<Space size={12} direction="vertical" className="rb:w-full">
{['first', 'customer_service', 'data_analysis', 'risk_warning'].map(key => (
<div className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">{t(`emotionEngine.${key}`)}: {t(`emotionEngine.${key}_desc`)}</div>
))}
</Space>
<div className="rb:font-medium rb:mt-6 rb:mb-3">{t('emotionEngine.actual_case')}</div>
<Space size={12} direction="vertical" className="rb:w-full rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
<div className="rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md">
<span className="rb:font-medium">{t('emotionEngine.user_input')}: </span>
{t('emotionEngine.user_input_message')}
</div>
{['neutral_emotion', 'minor_dissatisfaction', 'expect_improvement'].map((key, index) => (
<div className="rb:flex rb:items-center rb:justify-between rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md">
<div className="rb:w-[50%] rb:flex rb:items-center rb:justify-between rb:text-[12px]">
{t(`emotionEngine.${key}`)}
<span>{t('emotionEngine.confidence')}: {key === 'neutral_emotion' ? 0.85 : key === 'minor_dissatisfaction' ? 0.45 : 0.32}</span>
</div>
<Tag color={(['success', 'warning', 'processing'] as const)[index] as 'warning' | 'processing' | 'success'}>{t(`emotionEngine.${key}_tag`)}</Tag>
</div>
))}
</Space>
</RbCard>
</Col>
{contextHolder}
</Row>
);
};
export default EmotionEngine;

View File

@@ -0,0 +1,48 @@
// 标签表单数据类型
export interface TagFormData {
tagName: string;
type: string;
color: string;
description?: string;
applicableScope?: string[];
semanticExpansion?: string;
isActive?: boolean;
// 扩展字段用于区分编辑和新增操作
isEditing?: boolean;
tagId?: string;
}
// 记忆总览数据类型
export interface MemoryOverviewRecord {
id: number;
memoryID: string,
contentSummary: string;
type: string;
createTime: string;
lastCallTime: string;
retentionDegree: string;
status: string;
}
// 定义组件暴露的方法接口
export interface MemoryOverviewFormRef {
handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void;
}
// 遗忘曲线数据类型
export interface CurveRecord {
memoryID: string;
type: string;
currentRetentionRate: string;
finallyActivated: string;
expectedForgettingTime: string;
reinforcementCount: string;
}
export interface ConfigForm {
config_id: number | string;
emotion_enabled: boolean;
emotion_model_id: string;
emotion_extract_keywords: boolean;
emotion_min_intensity: number;
emotion_enable_subject: boolean;
}

View File

@@ -152,7 +152,7 @@ export const configList: ConfigVo[] = [
},
// 自我反思引擎
// {
// title: 'selfReflexionEngine',
// title: 'reflectionEngine',
// list: [
// // 是否启用反思引擎
// {

View File

@@ -1,11 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
import { List, Button, Space, App } from 'antd';
import { List, Button, Space, App, Tooltip } from 'antd';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import MemoryForm from './components/MemoryForm';
import type { Memory, MemoryFormRef } from '@/views/MemoryManagement/types'
import RbCard from '@/components/RbCard/Card'
import StatusTag from '@/components/StatusTag'
// import StatusTag from '@/components/StatusTag'
import { getMemoryConfigList, deleteMemoryConfig } from '@/api/memory'
import BodyWrapper from '@/components/Empty/BodyWrapper'
import { formatDateTime } from '@/utils/format';
@@ -67,12 +67,18 @@ const MemoryManagement: React.FC = () => {
case 'forgottenEngine':
navigate(`/forgetting-engine/${id}`)
break
case 'emotionEngine':
navigate(`/emotion-engine/${id}`)
break;
case 'reflectionEngine':
navigate(`/reflection-engine/${id}`)
break;
}
}
return (
<>
<div className="rb:text-right rb:mb-[16px]">
<div className="rb:text-right rb:mb-4">
<Button type="primary" onClick={() => handleEdit()}>
{t('memory.createConfiguration')}
</Button>
@@ -80,7 +86,7 @@ const MemoryManagement: React.FC = () => {
<BodyWrapper loading={loading} empty={data.length === 0}>
<List
grid={{ gutter: 16, column: 3 }}
grid={{ gutter: 16, column: 2 }}
loading={loading}
dataSource={data}
renderItem={(item) => (
@@ -88,32 +94,37 @@ const MemoryManagement: React.FC = () => {
<RbCard
title={item.config_name}
>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[17px] rb:font-regular rb:mt-[-4px]">{item.config_desc}</div>
{['memoryExtractionEngine', 'forgottenEngine'].map((key) => (
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-[40px] rb:rounded-[6px] rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:mt-[12px] rb:text-[#5B6167] rb:font-medium"
onClick={() => handleClick(item.config_id, key)}
>
{t(`memory.${key}`)}
<span className='rb:flex rb:items-center rb:justify-end'>
{/* <StatusTag status={item[key] === 'active' ? 'success' : 'error'} text={item[key] === 'active' ? t('memory.active') : t('memory.inactive')} /> */}
<div
className="rb:w-[16px] rb:h-[16px] rb:ml-[-3px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/memory/arrow_right.svg')] rb:group-hover:bg-[url('@/assets/images/memory/arrow_right_hover.svg')]"
></div>
</span>
</div>
))}
<div className={clsx("rb:mt-[16px] rb:text-[12px] rb:leading-[16px] rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center", {
<Tooltip title={item.config_desc}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.config_desc}</div>
</Tooltip>
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
{['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => (
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-10 rb:rounded-md rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:mt-3 rb:text-[#5B6167] rb:font-medium"
onClick={() => handleClick(item.config_id, key)}
>
{t(`memory.${key}`)}
<span className='rb:flex rb:items-center rb:justify-end'>
{/* <StatusTag status={item[key] === 'active' ? 'success' : 'error'} text={item[key] === 'active' ? t('memory.active') : t('memory.inactive')} /> */}
<div
className="rb:w-4 rb:h-4 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/memory/arrow_right.svg')] rb:group-hover:bg-[url('@/assets/images/memory/arrow_right_hover.svg')]"
></div>
</span>
</div>
))}
</div>
<div className={clsx("rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center", {
'rb:justify-between': item.updated_at,
'rb:justify-end': !item.updated_at
})}>
{formatDateTime(item.updated_at, 'YYYY-MM-DD HH:mm:ss')}
<Space size={16}>
<div
className="rb:w-[20px] rb:h-[20px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={() => handleEdit(item)}
></div>
<div
className="rb:w-[20px] rb:h-[20px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDelete(item)}
></div>
</Space>

View File

@@ -0,0 +1,332 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Form, App, Button, Switch, Space, Select } from 'antd';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card';
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
import { getMemoryReflectionConfig, updateMemoryReflectionConfig, pilotRunMemoryReflectionConfig } from '@/api/memory'
import type { ConfigForm, Result, ReflexionData, MemoryVerify, QualityAssessment } from './types'
import CustomSelect from '@/components/CustomSelect';
import { getModelListUrl } from '@/api/models'
import Tag from '@/components/Tag'
const configList = [
// 启用反思引擎
{
key: 'reflection_enabled',
type: 'switch',
},
// 反思模型
{
key: 'reflection_model_id',
type: 'customSelect',
url: getModelListUrl,
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
},
// 迭代周期
{
key: 'reflection_period_in_hours',
type: 'select',
options: [
{ label: 'oneHour', value: '1' },
{ label: 'threeHours', value: '3' },
{ label: 'sixHours', value: '6' },
{ label: 'twelveHours', value: '12' },
{ label: 'daily', value: '24' },
],
},
// 反思范围
{
key: 'reflexion_range',
type: 'select',
hiddenDesc: true,
options: [
{ label: 'partial', value: 'partial' },
{ label: 'all', value: 'all' },
],
},
// 反思基线
{
key: 'baseline',
type: 'select',
hiddenDesc: true,
options: [
{ label: 'TIME', value: 'TIME' },
{ label: 'FACT', value: 'FACT' },
{ label: 'HYBRID', value: 'HYBRID' },
],
},
// 质量评估
{
key: 'quality_assessment',
type: 'switch',
},
// 质量评估
{
key: 'memory_verify',
type: 'switch',
},
]
const SelfReflectionEngine: React.FC = () => {
const { t } = useTranslation();
const { id } = useParams();
const [configData, setConfigData] = useState<ConfigForm>({} as ConfigForm);
const [form] = Form.useForm<ConfigForm>();
const { message } = App.useApp();
const [loading, setLoading] = useState(false)
const [runLoading, setRunLoading] = useState(false)
const [result, setResult] = useState<Result | null>(null)
const values = Form.useWatch([], form);
useEffect(() => {
getConfigData()
}, [id])
const getConfigData = () => {
if (!id) {
return
}
getMemoryReflectionConfig(id)
.then((res) => {
const response = res as ConfigForm
const initialValues = {
...response,
}
console.log('initialValues', initialValues)
setConfigData(initialValues);
form.setFieldsValue(initialValues);
})
.catch(() => {
console.error('Failed to load data');
})
}
const handleReset = () => {
form.setFieldsValue(configData);
}
const handleSave = () => {
if (!id) {
return
}
setLoading(true)
updateMemoryReflectionConfig({
...values,
config_id: id
})
.then(() => {
message.success(t('common.saveSuccess'))
setConfigData({...(values || {})})
})
.finally(() => {
setLoading(false)
})
}
const handleRun = () => {
if (!id) {
return
}
setRunLoading(true)
pilotRunMemoryReflectionConfig({
config_id: id,
dialogue_text: t('reflectionEngine.exampleText')
})
.then((res) => {
setResult(res as Result)
})
.finally(() => {
setRunLoading(false)
})
}
return (
<Row gutter={[16, 16]}>
<Col span={12}>
<RbCard
title={
<div className="rb:flex rb:items-center">
<img src={strategyImpactSimulator} className="rb:w-5 rb:h-5 rb:mr-2" />
{t('reflectionEngine.reflectionEngineConfig')}
</div>
}
>
<Form
form={form}
layout="vertical"
initialValues={{
offset: 0,
lambda_time: 0.03,
lambda_mem: 0.03,
}}
>
{configList.map(config => {
if (config.type === 'customSelect') {
return (
<div key={config.key}>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
{t(`reflectionEngine.${config.key}`)}
</div>
<Form.Item
name={config.key}
extra={t(`reflectionEngine.${config.key}_desc`)}
>
<CustomSelect
url={config.url as string}
params={config.params}
valueKey='id'
labelKey='name'
hasAll={false}
placeholder={t('common.pleaseSelect')}
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'}
/>
</Form.Item>
</div>
)
}
if (config.type === 'select') {
return (
<div key={config.key}>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
{t(`reflectionEngine.${config.key}`)}
</div>
<Form.Item
name={config.key}
extra={t(`reflectionEngine.${config.key}_desc`)}
>
<Select
options={config.options?.map(vo => ({
...vo,
label: t(`reflectionEngine.${vo.label}`),
}))}
placeholder={t('common.pleaseSelect')}
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'}
/>
</Form.Item>
</div>
)
}
return (
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
<div>
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`reflectionEngine.${config.key}`)}</span>
{(config as any).hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`reflectionEngine.${config.key}_subTitle`)}</div>}
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`reflectionEngine.${config.key}_desc`)}</div>
</div>
<Form.Item
name={config.key}
valuePropName="checked"
className="rb:ml-2 rb:mb-0!"
>
<Switch
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'} />
</Form.Item>
</div>
)
})}
<Row gutter={16} className="rb:mt-3">
<Col span={12}>
<Button block onClick={handleReset}>{t('common.reset')}</Button>
</Col>
<Col span={12}>
<Button type="primary" loading={loading} block onClick={handleSave}>{t('common.save')}</Button>
</Col>
</Row>
</Form>
</RbCard>
</Col>
<Col span={12}>
<Space size={16} direction="vertical" className="rb:w-full">
<RbCard
title={t('memoryExtractionEngine.example')}
>
<div className="rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mb-6">
{t('reflectionEngine.exampleText')}
</div>
<Button type="primary" block loading={runLoading} onClick={handleRun}>{t('reflectionEngine.run')}</Button>
</RbCard>
{result && <>
<RbCard
title={t('reflectionEngine.runTitle')}
>
<div
className="rb:flex rb:gap-4 rb:justify-start rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-20 rb:font-medium">{t(`reflectionEngine.baseline`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{result.baseline}
</div>
</div>
</RbCard>
<RbCard
title={t('reflectionEngine.conflictDetection')}
>
<Space size={12} direction="vertical" className="rb:w-full">
{result.reflexion_data.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['reason', 'solution'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-20 rb:font-medium">{t(`reflectionEngine.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof ReflexionData]}
</div>
</div>
))}
</div>
))}
</Space>
</RbCard>
<RbCard
title={t('reflectionEngine.qualityAssessment')}
>
{result.quality_assessments.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['score', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-20 rb:font-medium">{t(`reflectionEngine.qualityAssessmentObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof QualityAssessment]}
</div>
</div>
))}
</div>
))}
</RbCard>
<RbCard
title={t('reflectionEngine.privacyAudit')}
>
{result.memory_verifies.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['has_privacy', 'privacy_types', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-20 rb:font-medium">{t(`reflectionEngine.privacyAuditObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{key === 'has_privacy'
? <Tag color={item[key as keyof MemoryVerify] ? 'success' : 'error'}>{t(`reflectionEngine.privacyAuditObj.${item[key as keyof MemoryVerify]}`)}</Tag>
: key === 'privacy_types' ? (item[key as keyof MemoryVerify] as string[]).join('、')
: item[key as keyof MemoryVerify]
}
</div>
</div>
))}
</div>
))}
</RbCard>
</>}
</Space>
</Col>
</Row>
);
};
export default SelfReflectionEngine;

View File

@@ -0,0 +1,72 @@
// 标签表单数据类型
export interface TagFormData {
tagName: string;
type: string;
color: string;
description?: string;
applicableScope?: string[];
semanticExpansion?: string;
isActive?: boolean;
// 扩展字段用于区分编辑和新增操作
isEditing?: boolean;
tagId?: string;
}
// 记忆总览数据类型
export interface MemoryOverviewRecord {
id: number;
memoryID: string,
contentSummary: string;
type: string;
createTime: string;
lastCallTime: string;
retentionDegree: string;
status: string;
}
// 定义组件暴露的方法接口
export interface MemoryOverviewFormRef {
handleOpen: (memoryOverview?: MemoryOverviewRecord | null) => void;
}
// 遗忘曲线数据类型
export interface CurveRecord {
memoryID: string;
type: string;
currentRetentionRate: string;
finallyActivated: string;
expectedForgettingTime: string;
reinforcementCount: string;
}
export interface ConfigForm {
config_id: number | string;
reflection_enabled: boolean;
reflection_period_in_hours: string;
reflexion_range: string;
baseline: string;
reflection_model_id: string;
memory_verify: boolean;
quality_assessment: boolean;
}
export interface QualityAssessment {
score: number;
summary: string;
}
export interface MemoryVerify {
has_privacy: boolean;
privacy_types: string[];
summary: string;
}
export interface ReflexionData {
reason: string;
solution: string;
}
export interface Result {
baseline: string;
source_data: string;
quality_assessments: QualityAssessment[];
memory_verifies: MemoryVerify[];
reflexion_data: ReflexionData[]
}

View File

@@ -18,6 +18,11 @@ import RelationshipNetwork from './components/RelationshipNetwork'
import MemoryInsight from './components/MemoryInsight'
import Empty from '@/components/Empty'
import WordCloud from './components/WordCloud'
import EmotionTags from './components/EmotionTags'
import Health from './components/Health'
import Suggestions from './components/Suggestions'
const tagColors = ['21, 94, 239', '156, 111, 255', '255, 93, 52', '54, 159, 33']
interface TitleProps {
@@ -102,77 +107,85 @@ const Neo4j: FC = () => {
const name = loading.detail ? '' : data?.name && data?.name !== '' ? data.name : id
return (
<Row gutter={[16, 16]} className="rb:pb-[24px]">
<Row gutter={[16, 16]} className="rb:pb-6">
<Col span={8}>
<RbCard>
<div className="rb:flex rb:items-center">
<div className="rb:flex-[0_0_auto] rb:w-[80px] rb:h-[80px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[80px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:ml-[16px]">
{name}<br/>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] rb:mt-[8px]">{data?.tags?.join(' | ')}</div>
</div>
</div>
<div className="rb:flex rb:gap-[8px] rb:mb-[8px] rb:flex-wrap rb:mt-[25px]">
{data?.hot_tags?.map((tag, tagIndex) => (
<span key={tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-[22px] rb:border"
style={{
backgroundColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.08)`,
borderColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.3)`,
color: `rgba(${tagColors[tagIndex % tagColors.length]}, 1)`,
}}
>
{tag.name}({tag.frequency})
</span>
))}
</div>
<Row gutter={[16, 16]}>
<Col span={24}>
<RbCard>
<div className="rb:flex rb:items-center">
<div className="rb:flex-[0_0_auto] rb:w-[80px] rb:h-[80px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[80px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:ml-[16px]">
{name}<br/>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] rb:mt-[8px]">{data?.tags?.join(' | ')}</div>
</div>
</div>
{/* 记忆总量 */}
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:mb-[25px]">
{t('userMemory.totalNumOfMemories')}
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-[30px] rb:mt-[8px]">{memory || 0}</div>
</div>
<div className="rb:flex rb:gap-[8px] rb:mb-[8px] rb:flex-wrap rb:mt-[25px]">
{data?.hot_tags?.map((tag, tagIndex) => (
<span key={tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-[22px] rb:border"
style={{
backgroundColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.08)`,
borderColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.3)`,
color: `rgba(${tagColors[tagIndex % tagColors.length]}, 1)`,
}}
>
{tag.name}({tag.frequency})
</span>
))}
</div>
{/* 关于我 */}
<>
<Title
type="aboutUs"
title={t('userMemory.aboutMe')}
icon={aboutUs}
t={t}
expanded={expanded.includes('aboutUs')}
onClick={handleTitleClick}
/>
{expanded.includes('aboutUs') && (
{/* 记忆总量 */}
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:mb-[25px]">
{t('userMemory.totalNumOfMemories')}
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-[30px] rb:mt-[8px]">{memory || 0}</div>
</div>
{/* 关于我 */}
<>
{loading.summary
? <Skeleton className="rb:mt-[16px]" />
: summary
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
{summary || '-'}
</div>
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
}
<Title
type="aboutUs"
title={t('userMemory.aboutMe')}
icon={aboutUs}
t={t}
expanded={expanded.includes('aboutUs')}
onClick={handleTitleClick}
/>
{expanded.includes('aboutUs') && (
<>
{loading.summary
? <Skeleton className="rb:mt-[16px]" />
: summary
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
{summary || '-'}
</div>
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
}
</>
)}
</>
)}
</>
{/* 兴趣分布 */}
<>
<Title
type="interestDistribution"
title={t('userMemory.interestDistribution')}
icon={interestDistribution}
t={t}
expanded={expanded.includes('interestDistribution')}
onClick={handleTitleClick}
/>
{/* 兴趣分布 */}
<>
<Title
type="interestDistribution"
title={t('userMemory.interestDistribution')}
icon={interestDistribution}
t={t}
expanded={expanded.includes('interestDistribution')}
onClick={handleTitleClick}
/>
{expanded.includes('interestDistribution') && (
<PieCard />
)}
</>
</RbCard>
{expanded.includes('interestDistribution') && (
<PieCard />
)}
</>
</RbCard>
</Col>
<Col span={24}>
<EmotionTags />
</Col>
</Row>
</Col>
<Col span={16}>
<Row gutter={[16, 16]}>
@@ -182,6 +195,15 @@ const Neo4j: FC = () => {
</Col>
{/* 关系网络 + 记忆详情 */}
<RelationshipNetwork />
<Col span={12}>
<WordCloud />
</Col>
<Col span={12}>
<Health />
</Col>
<Col span={24}>
<Suggestions />
</Col>
</Row>
</Col>
</Row>

View File

@@ -0,0 +1,111 @@
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
import { getWordCloud } from '@/api/memory'
interface TagList {
keywords: Array<{ keyword: string; frequency: number; emotion_type: string; avg_intensity: number; }>;
total_keywords: number;
}
const EmotionTags: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const [tagList, setTagList] = useState<TagList | null>(null)
useEffect(() => {
getEmotionTagData()
}, [id])
const getEmotionTagData = () => {
if (!id) {
return
}
getWordCloud(id)
.then((res) => {
setTagList(res as TagList)
})
}
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
if (!tagList || tagList?.keywords.length === 0) return
const timer = setInterval(() => {
setVisibleCount(prev => {
if (prev >= tagList?.keywords.length) {
clearInterval(timer)
return prev
}
return prev + 1
})
}, 200)
return () => clearInterval(timer)
}, [tagList?.keywords.length])
const getEmotionColor = (emotionType: string) => {
const colors: Record<string, string> = {
joy: '#52c41a',
anger: '#ff4d4f',
sadness: '#1890ff',
fear: '#fa8c16',
neutral: '#8c8c8c',
surprise: '#722ed1'
}
return colors[emotionType] || '#8c8c8c'
}
const emotionStats = tagList?.keywords.reduce((acc, item) => {
acc[item.emotion_type] = (acc[item.emotion_type] || 0) + item.frequency
return acc
}, {} as Record<string, number>) ?? {}
return (
<RbCard
title={t('emotionDetail.emotionTags')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
bodyClassName='rb:p-0! rb:relative'
>
{tagList
? <>
<div className="rb:flex rb:flex-wrap rb:items-center rb:gap-6 rb:text-sm rb:mt-3 rb:p-3 rb:bg-[#F0F3F8]">
{Object.entries(emotionStats).map(([type, count]) => {
console.log(type)
return (
<div key={type} className="rb:flex rb:items-center rb:gap-2">
<div className="rb:w-3 rb:h-3 rb:rounded-full" style={{ backgroundColor: getEmotionColor(type) }}></div>
<span className="rb:text-gray-600">{t(`emotionDetail.${type || 'neutral'}`)} ({count})</span>
</div>
)
})}
</div>
<div className="rb:mt-6 rb:flex rb:items-center rb:flex-wrap rb:gap-3 rb:mb-3 rb:px-6">
{tagList.keywords.slice(0, visibleCount).map((item, index) => (
<div
key={index}
className="rb:flex rb:items-center rb:justify-center rb:animate-fadeIn rb:px-4 rb:py-2 rb:rounded-full rb:text-white rb:font-medium"
style={{
backgroundColor: getEmotionColor(item.emotion_type),
fontSize: `${12 + item.avg_intensity * 8}px`,
animationDelay: `${index * 200}ms`,
height: `${20 + item.avg_intensity * 20}px`,
transition: 'all 0.3s ease-in-out'
}}
>
{item.keyword}
</div>
))}
</div>
</>
: <Empty />
}
</RbCard>
)
}
export default EmotionTags

View File

@@ -0,0 +1,100 @@
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Progress } from 'antd'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
import { getEmotionHealth } from '@/api/memory'
interface Health {
health_score: number;
level: string;
dimensions: {
positivity_rate: {
score: number;
positive_count: number;
negative_count: number;
neutral_count: number;
};
stability: {
score: number;
std_deviation: number;
};
resilience: {
score: number;
recovery_rate: number;
};
};
emotion_distribution: {
joy: number;
sadness: number;
anger: number;
fear: number;
surprise: number;
neutral: number;
};
time_range: string;
}
const Health: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const [health, setHealth] = useState<Health | null>(null)
useEffect(() => {
getWordCloudData()
}, [id])
const getWordCloudData = () => {
if (!id) {
return
}
getEmotionHealth(id)
.then((res) => {
setHealth(res as Health)
})
}
return (
<RbCard
title={t('emotionDetail.health')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
height="100%"
>
{health
? <>
<div className="rb:flex rb:justify-center rb:items-center">
<Progress
size={250}
type="circle"
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
percent={health.health_score}
format={(percent) => `${percent}(${health.level})`}
/>
</div>
{health.dimensions && <>
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.positivity_rate')}</div>
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.positivity_rate.score} />
</div>
<div className="rb:flex rb:items-center rb:gap-3 rb:mt-3">
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.stability')}</div>
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.stability.score} />
</div>
<div className="rb:flex rb:items-center rb:gap-3 rb:mt-3">
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.resilience')}</div>
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.resilience.score} />
</div>
</>}
</>
: <Empty />
}
</RbCard>
)
}
export default Health

View File

@@ -0,0 +1,63 @@
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
import { getEmotionSuggestions } from '@/api/memory'
import RbAlert from '@/components/RbAlert'
interface Suggestions {
health_summary: string;
suggestions: Array<{
type: string;
title: string;
content: string;
priority: string;
actionable_steps: string[];
}>;
}
const Suggestions: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
useEffect(() => {
getSuggestionData()
}, [id])
const getSuggestionData = () => {
if (!id) {
return
}
getEmotionSuggestions(id)
.then((res) => {
setSuggestions(res as Suggestions)
})
}
return (
<RbCard
title={t('emotionDetail.suggestions')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
>
{suggestions
? <>
<RbAlert className="rb:mb-3">{suggestions.health_summary}</RbAlert>
{suggestions.suggestions.map((item, index) => (
<div key={index} className="rb:mb-3">
<div className="rb:font-medium">{index + 1}. {item.title}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:mb-2">{item.content}</div>
{item.actionable_steps.map((vo, idx) => <div key={idx} className="rb:ml-6 rb:text-[12px] rb:text-[#5B6167] rb:mt-1">- {vo}</div>)}
</div>
))}
</>
: <Empty />
}
</RbCard>
)
}
export default Suggestions

View File

@@ -0,0 +1,131 @@
import { type FC, useEffect, useState, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import ReactEcharts from 'echarts-for-react'
import { Progress } from 'antd'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
import { getEmotionTags } from '@/api/memory'
interface WordCloud {
tags: Array<{
emotion_type: string;
count: number;
percentage: number;
avg_intensity: number;
}>;
total_count: number;
}
const WordCloud: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const chartRef = useRef<ReactEcharts>(null);
const resizeScheduledRef = useRef(false)
const [wordCloud, setWordCloud] = useState<WordCloud | null>(null)
useEffect(() => {
getWordCloudData()
}, [id])
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
return () => {
resizeObserver.disconnect()
}
}, [wordCloud])
const getWordCloudData = () => {
if (!id) {
return
}
getEmotionTags(id)
.then((res) => {
setWordCloud(res as WordCloud)
})
}
const radarOption = useMemo(() => {
if (!wordCloud?.tags.length) return {}
// 将avg_intensity转换为1-100范围
const radarData = wordCloud.tags.map(item => ({
name: item.emotion_type,
value: Math.round(item.avg_intensity * 100),
count: item.count,
percentage: item.percentage
}))
return {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const dataIndex = params.dataIndex
const item = radarData[dataIndex]
return `${item.name}<br/>${item.percentage.toFixed(1)}%`
}
},
radar: {
indicator: radarData.map(item => ({
name: t(`emotionDetail.${item.name}`),
max: 100,
min: 1
}))
},
series: [{
type: 'radar',
name: 'Emotion Intensity',
data: [{
value: radarData.map(item => item.value),
name: 'Emotion Intensity'
}]
}]
}
}, [wordCloud])
return (
<RbCard
title={t('emotionDetail.wordCloud')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
height="100%"
>
{wordCloud
? <div className="rb:flex rb:h-100">
<ReactEcharts ref={chartRef} option={radarOption} style={{ width: '50%', height: '100%' }} />
<div className="rb:w-[50%] rb:pl-4 rb:flex rb:flex-col rb:justify-center">
<div className="rb:text-[18px] rb:font-medium rb:mb-4">{wordCloud.total_count}</div>
<div className="rb:space-y-3">
{wordCloud.tags.map(item => (
<div key={item.emotion_type}>
<div className="rb:flex rb:items-center rb:justify-between rb:font-medium">
{t(`emotionDetail.${item.emotion_type}`)}
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('emotionDetail.pieces')}</div>
</div>
<Progress size="small" percent={item.percentage} />
</div>
))}
</div>
</div>
</div>
: <Empty />
}
</RbCard>
)
}
export default WordCloud

View File

@@ -0,0 +1,29 @@
import { type FC } from 'react'
import { Row, Col } from 'antd';
import WordCloud from '../components/WordCloud'
import EmotionTags from '../components/EmotionTags'
import Health from '../components/Health'
import Suggestions from '../components/Suggestions'
const EmotionDetail: FC = () => {
return (
<Row gutter={[16, 16]}>
<Col span={12}>
<WordCloud />
</Col>
<Col span={12}>
<EmotionTags />
</Col>
<Col span={12}>
<Health />
</Col>
<Col span={12}>
<Suggestions />
</Col>
</Row>
)
}
export default EmotionDetail