Merge branch 'develop' into feature/knowledgeBase_yjp
@@ -134,6 +134,77 @@ export const getEmotionSuggestions = (group_id: string) => {
|
||||
export const analyticsRefresh = (end_user_id: string) => {
|
||||
return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
|
||||
}
|
||||
// 遗忘
|
||||
export const getForgetStats = (group_id: string) => {
|
||||
return request.get(`/memory/forget/stats`, { group_id })
|
||||
}
|
||||
// 隐性记忆-偏好
|
||||
export const getImplicitPreferences = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/preferences/${end_user_id}`)
|
||||
}
|
||||
// 隐性记忆-核心特质
|
||||
export const getImplicitPortrait = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/portrait/${end_user_id}`)
|
||||
}
|
||||
// 隐性记忆-兴趣领域分布
|
||||
export const getImplicitInterestAreas = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/interest-areas/${end_user_id}`)
|
||||
}
|
||||
// 隐性记忆-用户习惯分析
|
||||
export const getImplicitHabits = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/habits/${end_user_id}`)
|
||||
}
|
||||
// 短期记忆
|
||||
export const getShortTerm = (end_user_id: string) => {
|
||||
return request.get(`/memory/short/short_term`, { end_user_id })
|
||||
}
|
||||
// 感知记忆-视觉记忆
|
||||
export const getPerceptualLastVisual = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_visual`)
|
||||
}
|
||||
// 感知记忆-音频记忆
|
||||
export const getPerceptualLastListen = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_listen`)
|
||||
}
|
||||
// 感知记忆-文本记忆
|
||||
export const getPerceptualLastText = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_text`)
|
||||
}
|
||||
// 感知记忆-感知记忆时间线
|
||||
export const getPerceptualTimeline = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/timeline`)
|
||||
}
|
||||
// 情景记忆-总览
|
||||
export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => {
|
||||
return request.post(`/memory-storage/classifications/episodic-memory`, data)
|
||||
}
|
||||
export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => {
|
||||
return request.post(`/memory-storage/classifications/episodic-memory-details`, data)
|
||||
}
|
||||
// 关系演化
|
||||
export const getRelationshipEvolution = (data: { id: string; label: string; } ) => {
|
||||
return request.get(`/memory-storage/memory_space/relationship_evolution`, data)
|
||||
}
|
||||
// 共同记忆时间线
|
||||
export const getTimelineMemories = (data: { id: string; label: string; }) => {
|
||||
return request.get(`/memory-storage/memory_space/timeline_memories`, data)
|
||||
}
|
||||
export const getExplicitMemory = (end_user_id: string) => {
|
||||
return request.post(`/memory-storage/classifications/explicit-memory`, { end_user_id })
|
||||
}
|
||||
export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
|
||||
return request.post(`/memory-storage/classifications/explicit-memory-details`, data)
|
||||
}
|
||||
export const getConversations = (end_user: string) => {
|
||||
return request.get(`/memory/work/${end_user}/conversations`)
|
||||
}
|
||||
export const getConversationMessages = (end_user: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user}/messages`, { conversation_id })
|
||||
}
|
||||
export const getConversationDetail = (end_user: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user}/detail`, { conversation_id })
|
||||
}
|
||||
|
||||
|
||||
/*************** end 用户记忆 相关接口 ******************************/
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { AiPromptForm } from '@/views/ApplicationConfig/types'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
|
||||
export const createPromptSessions = () => {
|
||||
return request.post(`/prompt/sessions`)
|
||||
@@ -7,6 +8,6 @@ export const createPromptSessions = () => {
|
||||
export const getPrompt = (session_id: string) => {
|
||||
return request.get(`/prompt/sessions/${session_id}`)
|
||||
}
|
||||
export const updatePromptMessages = (session_id: string, data: AiPromptForm) => {
|
||||
return request.post(`/prompt/sessions/${session_id}/messages`, data)
|
||||
export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage)
|
||||
}
|
||||
BIN
web/src/assets/images/empty/pageLoading.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
14
web/src/assets/images/userMemory/arrow_right_hover.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 5</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆库-用户记忆-详情" transform="translate(-1212, -359)" stroke="#155EEF">
|
||||
<g id="记忆洞察" transform="translate(1044, 79)">
|
||||
<g id="编组-5" transform="translate(168, 280)">
|
||||
<polyline id="路径" points="12.5 7.5 15 10 12.5 12.5"></polyline>
|
||||
<line x1="15" y1="10" x2="2.5" y2="10" id="路径-2"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 117 KiB |
BIN
web/src/assets/images/userMemory/shortTerm.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
14
web/src/assets/images/userMemory/up_border.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>下拉备份</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆管理-交互" transform="translate(-210, -24)" stroke="#212332">
|
||||
<g id="编组-3" transform="translate(20, 20)">
|
||||
<g id="下拉备份" transform="translate(198, 12) scale(1, -1) translate(-198, -12)translate(190, 4)">
|
||||
<rect id="矩形" opacity="0.196382068" fill-rule="nonzero" x="0.5" y="0.5" width="15" height="15" rx="4"></rect>
|
||||
<polyline id="路径" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" points="11 6.5 8 9.5 5 6.5"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 925 B |
19
web/src/assets/images/userMemory/view.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>查看</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="红熊空间-记忆管理-个人记忆-2" transform="translate(-245, -141)" stroke="#5B6167">
|
||||
<g id="实体详情备份" transform="translate(62, 125)">
|
||||
<g id="查看" transform="translate(183, 16)">
|
||||
<g id="编组-22" transform="translate(2, 2)">
|
||||
<path d="M0,4 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L4,0 L4,0" id="路径"></path>
|
||||
<path d="M8,4 L8,1 C8,0.44771525 8.44771525,1.11022302e-16 9,0 L12,0 L12,0" id="路径备份-2" transform="translate(10, 2) scale(-1, 1) translate(-10, -2)"></path>
|
||||
<circle id="椭圆形" cx="8" cy="8" r="3"></circle>
|
||||
<path d="M0,12 L0,9 C0,8.44771525 0.44771525,8 1,8 L4,8 L4,8" id="路径备份" transform="translate(2, 10) scale(1, -1) translate(-2, -10)"></path>
|
||||
<line x1="10.1926589" y1="10.2894584" x2="11.6795155" y2="11.7850158" id="路径-10"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
19
web/src/assets/images/userMemory/view_hover.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>查看</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="红熊空间-记忆管理-个人记忆-2" transform="translate(-245, -141)" stroke="#5B6167">
|
||||
<g id="实体详情备份" transform="translate(62, 125)">
|
||||
<g id="查看" transform="translate(183, 16)">
|
||||
<g id="编组-22" transform="translate(2, 2)">
|
||||
<path d="M0,4 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L4,0 L4,0" id="路径"></path>
|
||||
<path d="M8,4 L8,1 C8,0.44771525 8.44771525,1.11022302e-16 9,0 L12,0 L12,0" id="路径备份-2" transform="translate(10, 2) scale(-1, 1) translate(-10, -2)"></path>
|
||||
<circle id="椭圆形" cx="8" cy="8" r="3"></circle>
|
||||
<path d="M0,12 L0,9 C0,8.44771525 0.44771525,8 1,8 L4,8 L4,8" id="路径备份" transform="translate(2, 10) scale(1, -1) translate(-2, -10)"></path>
|
||||
<line x1="10.1926589" y1="10.2894584" x2="11.6795155" y2="11.7850158" id="路径-10"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
web/src/assets/images/workflow/memory-read.png
Normal file
|
After Width: | Height: | Size: 936 B |
BIN
web/src/assets/images/workflow/memory-write.png
Normal file
|
After Width: | Height: | Size: 568 B |
16
web/src/components/Empty/PageLoading.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LoadingIcon from '@/assets/images/empty/pageLoading.png'
|
||||
import Empty from './index'
|
||||
const PageLoading = ({ size = [240, 210] }: { size?: number | number[] }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Empty
|
||||
url={LoadingIcon}
|
||||
title={t('empty.loadingEmpty')}
|
||||
subTitle={t('empty.loadingEmptyDesc')}
|
||||
size={size}
|
||||
className="rb:h-full"
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PageLoading;
|
||||
@@ -3,23 +3,27 @@ import { Tag } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface StatusTagProps {
|
||||
status: 'success' | 'error' | 'warning',
|
||||
status: 'success' | 'error' | 'warning' | 'default' | 'lightBlue' | 'purple',
|
||||
text: string;
|
||||
}
|
||||
const Colors = {
|
||||
success: 'rb:bg-[#369F21]',
|
||||
error: 'rb:bg-[#FF5D34]',
|
||||
warning: 'rb:bg-[#FF8A4C]',
|
||||
default: 'rb:bg-[#155EEF]',
|
||||
lightBlue: 'rb:bg-[#4DA8FF]',
|
||||
purple: 'rb:bg-[#9C6FFF]'
|
||||
}
|
||||
|
||||
const StatusTag: FC<StatusTagProps> = ({
|
||||
status,
|
||||
text
|
||||
}) => {
|
||||
console.log('status', status)
|
||||
return (
|
||||
<Tag style={{ backgroundColor: '#fff', borderColor: '#DFE4ED' }}>
|
||||
<span className='rb:flex rb:items-center rb:text-[#5B6167] rb:pl-[1px] rb:pr-[1px]'>
|
||||
<span className={clsx(' rb:w-[5px] rb:h-[5px] rb:rounded-full rb:mr-[4px]', Colors[status])}></span>
|
||||
<span className='rb:flex rb:items-center rb:text-[#5B6167] rb:pl-px rb:pr-px'>
|
||||
<span className={clsx(' rb:w-1.25 rb:h-1.25 rb:rounded-full rb:mr-1', Colors[status])}></span>
|
||||
{ text }
|
||||
</span>
|
||||
</Tag>
|
||||
|
||||
@@ -1147,10 +1147,10 @@ export const en = {
|
||||
promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.',
|
||||
|
||||
master: 'Supervisor Mode',
|
||||
master_agent: 'Supervisor Mode',
|
||||
master_agentDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.',
|
||||
handoffs: 'Collaboration Mode',
|
||||
handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.',
|
||||
supervisor: 'Supervisor Mode',
|
||||
supervisorDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.',
|
||||
collaboration: 'Collaboration Mode',
|
||||
collaborationDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.',
|
||||
masterConfig: 'Supervisor Configuration',
|
||||
orchestrationMode: 'Task Assignment Strategy',
|
||||
conditional: 'Intelligent Assignment',
|
||||
@@ -1160,6 +1160,8 @@ export const en = {
|
||||
merge: 'Complete Aggregation',
|
||||
vote: 'Key Information Extraction',
|
||||
priority: 'Structured Integration',
|
||||
addTool: 'Add Tool',
|
||||
tool: 'Tool',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -1204,10 +1206,6 @@ export const en = {
|
||||
|
||||
nodeStatistics: 'Memory Classification',
|
||||
total: 'Total',
|
||||
Chunk: 'Long-term Memory',
|
||||
MemorySummary: 'Episodic Memory',
|
||||
Statement: 'Emotional Memory',
|
||||
ExtractedEntity: 'Short-term Memory',
|
||||
|
||||
PERCEPTUAL_MEMORY: 'Perceptual Memory',
|
||||
WORKING_MEMORY: 'Working Memory',
|
||||
@@ -1217,6 +1215,7 @@ export const en = {
|
||||
IMPLICIT_MEMORY: 'Implicit Memory',
|
||||
EMOTIONAL_MEMORY: 'Emotional Memory',
|
||||
EPISODIC_MEMORY: 'Episodic Memory',
|
||||
FORGETTING_MANAGEMENT: 'Forgetting Management',
|
||||
|
||||
endUserProfile: 'Core Profile',
|
||||
editEndUserProfile: 'Edit',
|
||||
@@ -1234,6 +1233,33 @@ export const en = {
|
||||
key_findings: 'Key Findings',
|
||||
behavior_pattern: 'Behavior Pattern',
|
||||
growth_trajectory: 'Growth Trajectory',
|
||||
personality: 'Personality Traits',
|
||||
core_values: 'Core Values',
|
||||
|
||||
Statement_emotion_keywords: 'Emotion Keywords',
|
||||
Statement_emotion_type: 'Emotion Type',
|
||||
Statement_emotion_subject: 'Emotion Subject',
|
||||
Statement_importance_score: 'Importance Score',
|
||||
|
||||
ExtractedEntity_description: 'Description',
|
||||
ExtractedEntity_name: 'Content',
|
||||
ExtractedEntity_entity_type: 'Type',
|
||||
ExtractedEntity_created_at: 'Created At',
|
||||
ExtractedEntity_aliases: 'Aliases',
|
||||
ExtractedEntity_connect_strngth: 'Connection Strength',
|
||||
ExtractedEntity_importance_score: 'Importance Score',
|
||||
|
||||
associative_memory: 'Associative Memory',
|
||||
unix: 'items',
|
||||
completeMemory: 'Complete Memory',
|
||||
relationshipEvolution: 'Relationship Evolution',
|
||||
timelineMemories: 'Shared Memory Timeline',
|
||||
emotionLine: 'Emotion Changes Over Time',
|
||||
interaction: 'Interaction Frequency & Relationship Stages',
|
||||
timelines_memory: 'All',
|
||||
MemorySummary: 'Long-term Accumulation',
|
||||
Statement: 'Emotional Memory',
|
||||
ExtractedEntity: 'Episodic Memory',
|
||||
},
|
||||
space: {
|
||||
createSpace: 'Create Space',
|
||||
@@ -1809,12 +1835,20 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
"not_contains": 'Does Not Contain',
|
||||
"startwith": 'Starts With',
|
||||
"endwith": 'Ends With',
|
||||
"eq": '==',
|
||||
"ne": '!=',
|
||||
"lt": '<',
|
||||
"le": '<=',
|
||||
"gt": '>',
|
||||
"ge": '>=',
|
||||
"eq": 'Equals',
|
||||
"ne": 'Not Equals',
|
||||
num: {
|
||||
"eq": '=',
|
||||
"ne": '≠',
|
||||
"lt": '<',
|
||||
"le": '≤',
|
||||
"gt": '>',
|
||||
"ge": '≥',
|
||||
},
|
||||
boolean: {
|
||||
"eq": 'Is',
|
||||
"ne": 'Is Not',
|
||||
},
|
||||
else_desc: 'Used to define the logic that should be executed when the if condition is not met.'
|
||||
},
|
||||
'http-request': {
|
||||
@@ -1839,6 +1873,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
status_code: 'Status Code',
|
||||
max_attempts: 'Max Retry Attempts',
|
||||
retry_interval: 'Retry Interval',
|
||||
errorBranch: 'Error Branch',
|
||||
},
|
||||
'jinja-render': {
|
||||
template: 'Code',
|
||||
@@ -1855,12 +1890,17 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
loop: {
|
||||
cycle_vars: 'Loop Variables',
|
||||
condition: 'Loop Termination Condition',
|
||||
max_loop: 'Maximum Loop Count',
|
||||
},
|
||||
assigner: {
|
||||
assignments: 'Variables',
|
||||
cover: 'Overwrite',
|
||||
cover: 'Override',
|
||||
assign: 'Set',
|
||||
clear: 'Clear'
|
||||
clear: 'Clear',
|
||||
add: '+=',
|
||||
subtract: '-=',
|
||||
multiply: '*=',
|
||||
divide: '/=',
|
||||
},
|
||||
iteration: {
|
||||
input: 'Input Variable',
|
||||
@@ -1963,6 +2003,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
},
|
||||
statementDetail: {
|
||||
wordCloud: 'Emotion Distribution Analysis',
|
||||
totalCount: 'Sample Count',
|
||||
pieces: 'items',
|
||||
emotionTags: 'High-Frequency Emotion Keywords',
|
||||
joy: 'Joy',
|
||||
@@ -2147,5 +2188,124 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
orderPayInfo: 'Payment Information',
|
||||
create_time: 'Creation Time',
|
||||
},
|
||||
forgetDetail: {
|
||||
title: 'The forgetting management system helps AI intelligently manage memory lifecycle by automatically identifying low-value memories, setting forgetting strategies, and executing regular cleanup to optimize memory storage space and improve retrieval efficiency.',
|
||||
overviewTitle: 'Core Metrics Overview',
|
||||
totalMemory: 'Total Memory',
|
||||
MemoryHealth: 'Memory Health',
|
||||
riskOfForgetting: 'Forgetting Risk',
|
||||
statement_count: 'Statements',
|
||||
entity_count: 'Entities',
|
||||
summary_count: 'Summaries',
|
||||
chunk_count: 'Chunks',
|
||||
healthStatus: 'Health Status',
|
||||
average: 'Average Activation Value',
|
||||
threshold: 'Threshold Reference:',
|
||||
unhealthy: 'Unhealthy',
|
||||
healthy: 'Healthy',
|
||||
low_nodes: 'Low Activation Nodes',
|
||||
memoryHealthVisualization: 'Memory Health Visualization',
|
||||
activationValueDistribution: 'Activation Value Distribution',
|
||||
forgettingTrend: 'Forgetting Trend (Last 7 Days)',
|
||||
|
||||
nodes_without_activation: 'Observation Zone',
|
||||
low_activation_nodes: 'Forgetting Zone',
|
||||
health_nodes: 'Healthy Zone',
|
||||
average_activation: 'Average Activation Value',
|
||||
merged_count: 'Daily Merged Node Count',
|
||||
|
||||
pending_nodes: 'Risk Node Forgetting Pool',
|
||||
content_summary: 'Content Summary',
|
||||
node_type: 'Node Type',
|
||||
last_access_time: 'Last Activation Time',
|
||||
activation_value: 'Current Activation Value',
|
||||
},
|
||||
episodicDetail: {
|
||||
title: 'Record every important scene you have truly experienced',
|
||||
total_all: 'Total Episodic Memories',
|
||||
all: "All",
|
||||
today: 'Today',
|
||||
this_week: 'This Week',
|
||||
this_month: 'This Month',
|
||||
conversation: "Conversation",
|
||||
project_work: "Project/Work",
|
||||
learning: "Learning",
|
||||
decision: "Decision",
|
||||
important_event: "Important Event",
|
||||
titleKeywordPlaceholder: 'Search episode title or content',
|
||||
curResult: 'Current Filter Results',
|
||||
unix: 'items',
|
||||
created: 'Occurrence Time',
|
||||
episodic_type: 'Episode Type',
|
||||
involved_objects: 'Involved Objects',
|
||||
content_records: 'Episode Content Records',
|
||||
emotion: 'Emotion and State Records',
|
||||
},
|
||||
implicitDetail: {
|
||||
title: 'The invisible forces that shaped me',
|
||||
preferences: 'My Subconscious Preferences',
|
||||
preferencesDetail: 'Association Network',
|
||||
portraitTitle: 'My Subconscious Portrait',
|
||||
portraitSubTitle: 'Personalized insights generated by AI based on your preference tags',
|
||||
portrait: 'Core Traits',
|
||||
aesthetic: 'Aesthetic Driven',
|
||||
creativity: 'Creative Thinking',
|
||||
literature: 'Cultural Sensitivity',
|
||||
technology: 'Technology Affinity',
|
||||
interestAreas: 'Interest Area Distribution',
|
||||
art: 'Art & Design',
|
||||
music: 'Music & Culture',
|
||||
tech: 'Technology & Future',
|
||||
lifestyle: 'Lifestyle',
|
||||
habits: 'User Habit Analysis',
|
||||
habitsSubTitle: 'Habit characteristics identified based on your behavior patterns',
|
||||
context_details: 'Preference Details',
|
||||
supporting_evidence: 'Preference Source',
|
||||
specific_examples: 'Source',
|
||||
},
|
||||
shortTermDetail: {
|
||||
title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.',
|
||||
retrieval_number: 'Retrieval Count',
|
||||
entity: 'Extracted Entities',
|
||||
long_term_number: 'Long-term Candidates',
|
||||
shortTermTitle: 'Deep Retrieval & Extended Answer Area',
|
||||
shortTermSubTitle: 'Stores deep information retrieval performed to answer questions and the extended answers generated from it, including original questions, retrieved information, and generated answers.',
|
||||
longTermTitle: 'Long-term Memory Candidate Pool',
|
||||
longTermSubTitle: 'Aggregates short-term memory, filters and prepares content for storage in long-term memory. This is the "transfer station" and "filter" from short-term to long-term memory.',
|
||||
answer: 'Answer',
|
||||
query: 'Question',
|
||||
noAnswer: 'No reply yet',
|
||||
},
|
||||
perceptualDetail: {
|
||||
last_visual: 'Visual Perception Stream',
|
||||
last_listen: 'Auditory Perception Stream',
|
||||
last_text: 'Text Perception',
|
||||
summary: 'Summary',
|
||||
keywords: 'Keywords',
|
||||
topic: 'Topic',
|
||||
domain: 'Domain',
|
||||
scene: 'Scene',
|
||||
speaker_count: 'Number of Speakers',
|
||||
section_count: 'Number of Sections',
|
||||
timeLine: 'Perception Timeline',
|
||||
lastInfo: 'Real-time Perception Dashboard',
|
||||
},
|
||||
explicitDetail: {
|
||||
episodic_memories: 'Episodic Memories',
|
||||
semantic_memories: 'Semantic Memories',
|
||||
content: 'Core Description',
|
||||
created_at: 'Created At',
|
||||
emotion: 'Emotion',
|
||||
core_definition: 'Core Definition',
|
||||
detailed_notes: 'Detailed Notes',
|
||||
},
|
||||
workingDetail: {
|
||||
conversationStream: 'Real-time Conversation Stream',
|
||||
refresh: 'Refresh',
|
||||
successfulTitle: 'Successful Experience',
|
||||
question: 'Lessons Learned',
|
||||
summary: 'Core Insights',
|
||||
none: 'None'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -636,10 +636,10 @@ export const zh = {
|
||||
promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。',
|
||||
|
||||
master: '主管模式',
|
||||
master_agent: '主管模式',
|
||||
master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。',
|
||||
handoffs: '协作模式',
|
||||
handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。',
|
||||
supervisor: '主管模式',
|
||||
supervisorDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。',
|
||||
collaboration: '协作模式',
|
||||
collaborationDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。',
|
||||
masterConfig: '主管配置',
|
||||
orchestrationMode: '任务分配策略',
|
||||
conditional: '智能分配',
|
||||
@@ -649,6 +649,8 @@ export const zh = {
|
||||
merge: '完整汇总',
|
||||
vote: '关键信息提取',
|
||||
priority: '结构化整合',
|
||||
addTool: '添加工具',
|
||||
tool: '工具',
|
||||
},
|
||||
// 角色管理相关翻译
|
||||
role: {
|
||||
@@ -1283,11 +1285,6 @@ export const zh = {
|
||||
nodeStatistics: '记忆分类',
|
||||
total: '总计',
|
||||
|
||||
Chunk: '长期记忆',
|
||||
MemorySummary: '情景记忆',
|
||||
Statement: '情绪记忆',
|
||||
ExtractedEntity: '短期记忆',
|
||||
|
||||
PERCEPTUAL_MEMORY: '感知记忆',
|
||||
WORKING_MEMORY: '工作记忆',
|
||||
SHORT_TERM_MEMORY: '短期记忆',
|
||||
@@ -1296,6 +1293,7 @@ export const zh = {
|
||||
IMPLICIT_MEMORY: '隐性记忆',
|
||||
EMOTIONAL_MEMORY: '情绪记忆',
|
||||
EPISODIC_MEMORY: '情景记忆',
|
||||
FORGETTING_MANAGEMENT: '遗忘',
|
||||
|
||||
endUserProfile: '核心档案',
|
||||
editEndUserProfile: '编辑',
|
||||
@@ -1315,6 +1313,33 @@ export const zh = {
|
||||
key_findings: '关键发现',
|
||||
behavior_pattern: '行为模式',
|
||||
growth_trajectory: '成长轨迹',
|
||||
personality: '性格特点',
|
||||
core_values: '核心价值观',
|
||||
|
||||
Statement_emotion_keywords: '情感关键词',
|
||||
Statement_emotion_type: '情感类型',
|
||||
Statement_emotion_subject: '情感主体',
|
||||
Statement_importance_score: '重要性评分',
|
||||
|
||||
ExtractedEntity_description: '描述',
|
||||
ExtractedEntity_name: '内容',
|
||||
ExtractedEntity_entity_type: '类型',
|
||||
ExtractedEntity_created_at: '创建时间',
|
||||
ExtractedEntity_aliases: '别名',
|
||||
ExtractedEntity_connect_strngth: '连接强度',
|
||||
ExtractedEntity_importance_score: '重要性评分',
|
||||
|
||||
associative_memory: '关联记忆',
|
||||
unix: '个',
|
||||
completeMemory: '完整记忆',
|
||||
relationshipEvolution: '关系演化',
|
||||
timelineMemories: '共同记忆时间线',
|
||||
emotionLine: '情绪随时间变化',
|
||||
interaction: '互动频率 & 关系阶段',
|
||||
timelines_memory: '全部',
|
||||
MemorySummary: '长期沉淀',
|
||||
Statement: '情绪记忆',
|
||||
ExtractedEntity: '情景记忆',
|
||||
},
|
||||
space: {
|
||||
createSpace: '创建空间',
|
||||
@@ -1909,12 +1934,20 @@ export const zh = {
|
||||
"not_contains": '不包含',
|
||||
"startwith": '开始是',
|
||||
"endwith": '结束是',
|
||||
"eq": '==',
|
||||
"ne": '!=',
|
||||
"lt": '<',
|
||||
"le": '<=',
|
||||
"gt": '>',
|
||||
"ge": '>=',
|
||||
"eq": '是',
|
||||
"ne": '不是',
|
||||
num: {
|
||||
"eq": '=',
|
||||
"ne": '≠',
|
||||
"lt": '<',
|
||||
"le": '≤',
|
||||
"gt": '>',
|
||||
"ge": '≥',
|
||||
},
|
||||
boolean: {
|
||||
"eq": '是',
|
||||
"ne": '不是',
|
||||
},
|
||||
else_desc: '用于定义当 if 条件不满足时应执行的逻辑。'
|
||||
},
|
||||
'http-request': {
|
||||
@@ -1939,6 +1972,7 @@ export const zh = {
|
||||
status_code: '状态码',
|
||||
max_attempts: '最大重试次数',
|
||||
retry_interval: '重试间隔',
|
||||
errorBranch: '异常分支',
|
||||
},
|
||||
'jinja-render': {
|
||||
template: '代码',
|
||||
@@ -1955,12 +1989,17 @@ export const zh = {
|
||||
loop: {
|
||||
cycle_vars: '循环变量',
|
||||
condition: '循环终止条件',
|
||||
max_loop: '最大循环次数',
|
||||
},
|
||||
assigner: {
|
||||
assignments: '变量',
|
||||
cover: '覆盖',
|
||||
assign: '设置',
|
||||
clear: '清空'
|
||||
clear: '清空',
|
||||
add: '+=',
|
||||
subtract: '-=',
|
||||
multiply: '*=',
|
||||
divide: '/=',
|
||||
},
|
||||
iteration: {
|
||||
input: '输入变量',
|
||||
@@ -2063,6 +2102,7 @@ export const zh = {
|
||||
},
|
||||
statementDetail: {
|
||||
wordCloud: '情感分布分析',
|
||||
totalCount: '样本数',
|
||||
pieces: '条',
|
||||
emotionTags: '高频情绪关键词',
|
||||
joy: '喜悦',
|
||||
@@ -2247,5 +2287,124 @@ export const zh = {
|
||||
orderPayInfo: '支付信息',
|
||||
create_time: '创建时间',
|
||||
},
|
||||
forgetDetail: {
|
||||
title: '遗忘管理系统帮助AI智能管理记忆生命周期,通过自动识别低价值记忆、设置遗忘策略和执行定期清理,优化记忆库存储空间,提升检索效率。',
|
||||
overviewTitle: '核心指标概览',
|
||||
totalMemory: '记忆总量',
|
||||
MemoryHealth: '记忆健康度',
|
||||
riskOfForgetting: '遗忘风险',
|
||||
statement_count: '陈述',
|
||||
entity_count: '实体',
|
||||
summary_count: '摘要',
|
||||
chunk_count: '片段',
|
||||
healthStatus: '健康状态',
|
||||
average: '平均激活值',
|
||||
threshold: '阈值参考:',
|
||||
unhealthy: '不健康',
|
||||
healthy: '健康',
|
||||
low_nodes: '低激活节点',
|
||||
memoryHealthVisualization: '记忆健康可视化',
|
||||
activationValueDistribution: '激活值分布',
|
||||
forgettingTrend: '遗忘趋势(近7天)',
|
||||
|
||||
nodes_without_activation: '观察区',
|
||||
low_activation_nodes: '遗忘区',
|
||||
health_nodes: '健康区',
|
||||
average_activation: '平均激活值',
|
||||
merged_count: '每日融合节点数',
|
||||
|
||||
pending_nodes: '风险节点遗忘池',
|
||||
content_summary: '内容摘要',
|
||||
node_type: '节点类型',
|
||||
last_access_time: '最后激活时间',
|
||||
activation_value: '当前激活值',
|
||||
},
|
||||
episodicDetail: {
|
||||
title: '记录你真实经历过的每一个重要场景',
|
||||
total_all: '情景记忆总数',
|
||||
all: "全部",
|
||||
today: '今天',
|
||||
this_week: '本周',
|
||||
this_month: '本月',
|
||||
conversation: "对话",
|
||||
project_work: "项目/工作",
|
||||
learning: "学习",
|
||||
decision: "决策",
|
||||
important_event: "重要事件",
|
||||
titleKeywordPlaceholder: '搜索情景标题或内容',
|
||||
curResult: '当前筛选结果',
|
||||
unix: '条',
|
||||
created: '发生时间',
|
||||
episodic_type: '情景类型',
|
||||
involved_objects: '涉及对象',
|
||||
content_records: '情景内容记录',
|
||||
emotion: '情绪与状态记录',
|
||||
},
|
||||
implicitDetail: {
|
||||
title: '那些塑造了我的无形力量',
|
||||
preferences: '我的潜意识偏好',
|
||||
preferencesDetail: '的联想网络',
|
||||
portraitTitle: '我的潜意识画像',
|
||||
portraitSubTitle: '基于您的偏好标签,AI为您生成的个性化洞察',
|
||||
portrait: '核心特质',
|
||||
aesthetic: '审美驱动',
|
||||
creativity: '创造性思维',
|
||||
literature: '文化敏感度',
|
||||
technology: '技术亲和力',
|
||||
interestAreas: '兴趣领域分布',
|
||||
art: '艺术与设计',
|
||||
music: '音乐与文化',
|
||||
tech: '科技与未来',
|
||||
lifestyle: '生活方式',
|
||||
habits: '用户习惯分析',
|
||||
habitsSubTitle: '基于您的行为模式识别的习惯特征',
|
||||
context_details: '偏好详情',
|
||||
supporting_evidence: '偏好来源',
|
||||
specific_examples: '来源',
|
||||
},
|
||||
shortTermDetail: {
|
||||
title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。',
|
||||
retrieval_number: '检索次数',
|
||||
entity: '提取实体',
|
||||
long_term_number: '长期候选',
|
||||
shortTermTitle: '深度检索与扩展答案区',
|
||||
shortTermSubTitle: '存放为回答问题而进行的深度信息检索和由此生成的扩展答案,包含原始问题、检索信息和生成答案。',
|
||||
longTermTitle: '长期记忆候选池',
|
||||
longTermSubTitle: '聚合短期记忆,筛选并准备存入长期记忆的内容。这是从短时记忆到长时记忆的"中转站"和"过滤器"。',
|
||||
answer: '回答',
|
||||
query: '问题',
|
||||
noAnswer: '暂无回复',
|
||||
},
|
||||
perceptualDetail: {
|
||||
last_visual: '视觉感知流',
|
||||
last_listen: '听觉感知流',
|
||||
last_text: '文本感知',
|
||||
summary: '摘要',
|
||||
keywords: '关键词',
|
||||
topic: '主题',
|
||||
domain: '领域',
|
||||
scene: '场景',
|
||||
speaker_count: '对话人数',
|
||||
section_count: '段落数',
|
||||
timeLine: '感知时间线',
|
||||
lastInfo: '实时感知仪表盘',
|
||||
},
|
||||
explicitDetail: {
|
||||
episodic_memories: '情景记忆',
|
||||
semantic_memories: '语义记忆',
|
||||
content: '核心描述',
|
||||
created_at: '创建时间',
|
||||
emotion: '情绪',
|
||||
core_definition: '核心定义',
|
||||
detailed_notes: '详细笔记',
|
||||
},
|
||||
workingDetail: {
|
||||
conversationStream: '实时对话流',
|
||||
refresh: '刷新',
|
||||
successfulTitle: '成功经验',
|
||||
question: '踩过的坑',
|
||||
summary: '核心洞察',
|
||||
none: '无'
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -59,6 +59,8 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
|
||||
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
|
||||
StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')),
|
||||
ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')),
|
||||
MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')),
|
||||
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
|
||||
OrderPayment: lazy(() => import('@/views/OrderPayment')),
|
||||
OrderHistory: lazy(() => import('@/views/OrderHistory')),
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
|
||||
{ "path": "/conversation/:token", "element": "Conversation" },
|
||||
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" },
|
||||
{ "path": "/statement/:id", "element": "StatementDetail" }
|
||||
{ "path": "/statement/:id", "element": "StatementDetail" },
|
||||
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ import type {
|
||||
Variable,
|
||||
MemoryConfig,
|
||||
AiPromptModalRef,
|
||||
Source
|
||||
Source,
|
||||
ToolOption
|
||||
} from './types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { getModelList } from '@/api/models';
|
||||
@@ -31,6 +32,7 @@ import { memoryConfigListUrl } from '@/api/memory'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import aiPrompt from '@/assets/images/application/aiPrompt.png'
|
||||
import AiPromptModal from './components/AiPromptModal'
|
||||
import ToolList from './components/ToolList'
|
||||
|
||||
const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => {
|
||||
return (
|
||||
@@ -47,12 +49,12 @@ const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const SwitchWrapper: FC<{ title: string, desc: string, name: string }> = ({ title, desc, name }) => {
|
||||
const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[]; needTransition?: boolean; }> = ({ title, desc, name, needTransition = true }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<LabelWrapper title={t(`application.${title}`)}>
|
||||
<DescWrapper desc={t(`application.${desc}`)} className="rb:mt-2" />
|
||||
<LabelWrapper title={needTransition ? t(`application.${title}`) : title}>
|
||||
{desc && <DescWrapper desc={needTransition ? t(`application.${desc}`) : desc} className="rb:mt-2" />}
|
||||
</LabelWrapper>
|
||||
<Form.Item
|
||||
name={name}
|
||||
@@ -100,17 +102,18 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
const [formData, setFormData] = useState<{
|
||||
default_model_config_id?: string,
|
||||
model_parameters?: Config['model_parameters'],
|
||||
tools: ToolOption[],
|
||||
} | null>(null)
|
||||
const values = Form.useWatch<{
|
||||
memoryEnabled: boolean;
|
||||
memory_content?: string | number;
|
||||
webSearch: boolean;
|
||||
} & Config>([], form)
|
||||
|
||||
const [knowledgeConfig, setKnowledgeConfig] = useState<KnowledgeConfig>({ knowledge_bases: [] })
|
||||
const [variableList, setVariableList] = useState<Variable[]>([])
|
||||
const [isSave, setIsSave] = useState(false)
|
||||
const initialized = useRef(false)
|
||||
const [toolList, setToolList] = useState<ToolOption[]>([])
|
||||
|
||||
// 初始化完成标记
|
||||
useEffect(() => {
|
||||
@@ -139,6 +142,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
if (isSave) return
|
||||
setIsSave(true)
|
||||
}, [values])
|
||||
useEffect(() => {
|
||||
if (!initialized.current) return
|
||||
if (isSave) return
|
||||
setIsSave(true)
|
||||
}, [toolList])
|
||||
|
||||
useEffect(() => {
|
||||
getModels()
|
||||
@@ -149,17 +157,21 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
setLoading(true)
|
||||
getApplicationConfig(id as string).then(res => {
|
||||
const response = res as Config
|
||||
setData(response)
|
||||
setData({
|
||||
...response,
|
||||
tools: Array.isArray(response.tools) ? response.tools : []
|
||||
})
|
||||
const { memory, tools } = response
|
||||
form.setFieldsValue({
|
||||
...response,
|
||||
memoryEnabled: memory?.enabled || false,
|
||||
memory_content: memory?.memory_content ? Number(memory?.memory_content) : undefined,
|
||||
webSearch: tools?.web_search?.enabled || false,
|
||||
tools: Array.isArray(tools) ? tools : []
|
||||
})
|
||||
setFormData({
|
||||
default_model_config_id: response.default_model_config_id,
|
||||
model_parameters: response.model_parameters || {},
|
||||
tools: Array.isArray(tools) ? tools : []
|
||||
})
|
||||
if (response?.knowledge_retrieval?.knowledge_bases?.length) {
|
||||
getDefaultKnowledgeList(response)
|
||||
@@ -260,8 +272,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
// 保存Agent配置
|
||||
const handleSave = (flag = true) => {
|
||||
if (!isSave || !data) return Promise.resolve()
|
||||
const { memoryEnabled, memory_content, webSearch, ...rest } = values
|
||||
const { memoryEnabled, memory_content, ...rest } = values
|
||||
const { knowledge_bases = [], ...knowledgeRest } = knowledgeConfig || {}
|
||||
|
||||
|
||||
// 从原数据中获取memory的其他必要属性
|
||||
const originalMemory = data.memory || ({} as MemoryConfig)
|
||||
@@ -285,15 +298,14 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
...(item.config || {})
|
||||
}))
|
||||
} as KnowledgeConfig : null,
|
||||
tools: {
|
||||
web_search: {
|
||||
enabled: webSearch,
|
||||
config: {
|
||||
web_search: webSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
tools: toolList.map(vo => ({
|
||||
tool_id: vo.tool_id,
|
||||
operation: vo.operation,
|
||||
enabled: vo.enabled
|
||||
}))
|
||||
}
|
||||
|
||||
console.log('params', rest, params)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
saveAgentConfig(data.app_id, params)
|
||||
@@ -342,6 +354,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
const updatePrompt = (value: string) => {
|
||||
form.setFieldValue('system_prompt', value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Spin fullscreen></Spin>}
|
||||
@@ -410,14 +423,12 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
data={data?.variables}
|
||||
onUpdate={setVariableList}
|
||||
/>
|
||||
|
||||
{/* 工具配置 */}
|
||||
<Card title={t('application.toolConfiguration')}>
|
||||
<Space size={24} direction='vertical' style={{ width: '100%' }}>
|
||||
<SwitchWrapper title="webSearch" desc="webSearchDesc" name="webSearch" />
|
||||
{/* <SwitchWrapper title="codeExecutor" desc="codeExecutorDesc" name="codeExecutor" />
|
||||
<SwitchWrapper title="imageGeneration" desc="imageGenerationDesc" name="imageGeneration" /> */}
|
||||
</Space>
|
||||
</Card>
|
||||
<ToolList
|
||||
data={data?.tools || []}
|
||||
onUpdate={setToolList}
|
||||
/>
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
@@ -42,7 +42,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
|
||||
const handleSave = (flag = true) => {
|
||||
if (!data) return Promise.resolve()
|
||||
if (!values.default_model_config_id) {
|
||||
if (!values.default_model_config_id && values.orchestration_mode === 'supervisor') {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('application.model') }))
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -138,17 +138,16 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
</div>
|
||||
<Form form={form} layout="vertical">
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card title={t('application.handoffs')}>
|
||||
<Card title={t('application.collaboration')}>
|
||||
<Form.Item
|
||||
name={['execution_config', 'routing_mode']}
|
||||
name="orchestration_mode"
|
||||
noStyle
|
||||
>
|
||||
<RadioGroupCard
|
||||
options={['master_agent', 'handoffs'].map((type) => ({
|
||||
options={['supervisor', 'collaboration'].map((type) => ({
|
||||
value: type,
|
||||
label: t(`application.${type}`),
|
||||
labelDesc: t(`application.${type}Desc`),
|
||||
disabled: type === 'handoffs'
|
||||
}))}
|
||||
allowClear={false}
|
||||
/>
|
||||
@@ -192,7 +191,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card title={t('application.masterConfig')}>
|
||||
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
|
||||
<Form.Item
|
||||
label={t('application.model')}
|
||||
required={true}
|
||||
@@ -218,11 +217,11 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="orchestration_mode"
|
||||
name={['execution_config',"sub_agent_execution_mode"]}
|
||||
label={t('application.orchestrationMode')}
|
||||
>
|
||||
<Select
|
||||
options={['conditional', 'sequential', 'parallel'].map((type) => ({
|
||||
options={['sequential', 'parallel'].map((type) => ({
|
||||
value: type,
|
||||
label: t(`application.${type}`),
|
||||
}))}
|
||||
@@ -239,7 +238,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Card>}
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
@@ -16,6 +16,8 @@ import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpt
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import AiPromptVariableModal from './AiPromptVariableModal'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import Editor from './Editor'
|
||||
|
||||
interface AiPromptModalProps {
|
||||
refresh: (value: string) => void;
|
||||
@@ -35,7 +37,8 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
const [variables, setVariables] = useState<string[]>([])
|
||||
const [promptSession, setPromptSession] = useState<string | null>(null)
|
||||
const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null)
|
||||
const currentPromptRef = useRef<any>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
const currentPromptValueRef = useRef<string>('')
|
||||
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
@@ -78,16 +81,55 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'user', content: messageContent}]
|
||||
})
|
||||
form.setFieldsValue({ message: undefined })
|
||||
updatePromptMessages(promptSession, values)
|
||||
.then(res => {
|
||||
const response = res as { prompt: string; desc: string; variables: string[] }
|
||||
form.setFieldsValue({ current_prompt: response.prompt })
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'assistant', content: response.desc }]
|
||||
})
|
||||
setVariables(response.variables)
|
||||
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.map(item => {
|
||||
const { content, desc, variables } = item.data as { content: string; desc: string; variables: string[] };
|
||||
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
currentPromptValueRef.current = ''
|
||||
if (editorRef.current?.clear) {
|
||||
editorRef.current.clear();
|
||||
}
|
||||
break;
|
||||
case 'message':
|
||||
if (content) {
|
||||
currentPromptValueRef.current += content;
|
||||
if (editorRef.current?.appendText) {
|
||||
editorRef.current.appendText(content);
|
||||
editorRef.current.scrollToBottom();
|
||||
} else {
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
}
|
||||
}
|
||||
if (desc) {
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'assistant', content: desc }]
|
||||
})
|
||||
}
|
||||
if (variables) {
|
||||
setVariables(variables)
|
||||
}
|
||||
break;
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
// 流结束时同步表单值
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
break
|
||||
}
|
||||
})
|
||||
};
|
||||
updatePromptMessages(promptSession, values, handleStreamMessage)
|
||||
// .then(res => {
|
||||
// const response = res as { prompt: string; desc: string; variables: string[] }
|
||||
// form.setFieldsValue({ current_prompt: response.prompt })
|
||||
// setChatList(prev => {
|
||||
// return [...prev, { role: 'assistant', content: response.desc }]
|
||||
// })
|
||||
// setVariables(response.variables)
|
||||
// })
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
@@ -101,18 +143,8 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
aiPromptVariableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleVariableApply = (value: string) => {
|
||||
const textArea = currentPromptRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
const cursorPosition = textArea.selectionStart
|
||||
const currentValue = values.current_prompt || ''
|
||||
const newValue = currentValue.slice(0, cursorPosition) + value + currentValue.slice(cursorPosition)
|
||||
form.setFieldValue('current_prompt', newValue)
|
||||
|
||||
// 设置新的光标位置
|
||||
setTimeout(() => {
|
||||
textArea.focus()
|
||||
textArea.setSelectionRange(cursorPosition + value.length, cursorPosition + value.length)
|
||||
}, 0)
|
||||
if (editorRef.current?.insertText) {
|
||||
editorRef.current.insertText(value)
|
||||
} else {
|
||||
form.setFieldValue('current_prompt', (values.current_prompt || '') + value)
|
||||
}
|
||||
@@ -191,7 +223,11 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="current_prompt">
|
||||
<Input.TextArea ref={currentPromptRef} className="rb:bg-[#FBFDFF]! rb:h-100.5!" />
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
className="rb:h-100.5 "
|
||||
onChange={(value) => form.setFieldValue('current_prompt', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
|
||||
<Button block disabled={!values?.current_prompt} onClick={handleCopy}>{t('common.copy')}</Button>
|
||||
|
||||
129
web/src/views/ApplicationConfig/components/Editor/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import {forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||
import { $getSelection, $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode, $isTextNode } from 'lexical';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import InitialValuePlugin from './plugin/InitialValuePlugin'
|
||||
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
||||
import InsertTextPlugin from './plugin/InsertTextPlugin';
|
||||
|
||||
export interface EditorRef {
|
||||
insertText: (text: string) => void;
|
||||
appendText: (text: string) => void;
|
||||
clear: () => void;
|
||||
scrollToBottom: () => void;
|
||||
}
|
||||
|
||||
interface LexicalEditorProps {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
paragraph: 'editor-paragraph',
|
||||
text: {
|
||||
bold: 'editor-text-bold',
|
||||
italic: 'editor-text-italic',
|
||||
},
|
||||
};
|
||||
|
||||
const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
||||
className = '',
|
||||
value,
|
||||
placeholder = "请输入内容...",
|
||||
onChange,
|
||||
}, ref) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
insertText: (text: string) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (selection) {
|
||||
selection.insertText(text);
|
||||
}
|
||||
});
|
||||
},
|
||||
appendText: (text: string) => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const lastChild = root.getLastChild();
|
||||
if (lastChild && $isParagraphNode(lastChild)) {
|
||||
const lastTextNode = lastChild.getLastChild();
|
||||
if (lastTextNode && $isTextNode(lastTextNode)) {
|
||||
const currentText = lastTextNode.getTextContent();
|
||||
lastTextNode.setTextContent(currentText + text);
|
||||
} else {
|
||||
const textNode = $createTextNode(text);
|
||||
lastChild.append(textNode);
|
||||
}
|
||||
} else {
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(text);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
}
|
||||
});
|
||||
},
|
||||
clear: () => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
root.append(paragraph);
|
||||
});
|
||||
},
|
||||
scrollToBottom: () => {
|
||||
const editorElement = editor.getRootElement();
|
||||
if (editorElement) {
|
||||
editorElement.scrollTop = editorElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
}), [editor]);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
className={clsx("rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:overflow-auto", className)}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
<div className="rb:absolute rb:px-4 rb:py-5 rb:text-[14px] rb:text-[#5B6167] rb:leading-5 rb:pointer-none">
|
||||
{placeholder}
|
||||
</div>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<LineBreakPlugin onChange={onChange} />
|
||||
<InitialValuePlugin value={value} />
|
||||
<InsertTextPlugin />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Editor = forwardRef<EditorRef, LexicalEditorProps>((props, ref) => {
|
||||
const initialConfig = {
|
||||
namespace: 'Editor',
|
||||
theme,
|
||||
nodes: [],
|
||||
onError: (error: Error) => {
|
||||
console.error(error);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<EditorContent {...props} ref={ref} />
|
||||
</LexicalComposer>
|
||||
);
|
||||
});
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
// 设置初始值的插件
|
||||
const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const lastValueRef = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
// 只有当value真正发生变化时才更新
|
||||
if (lastValueRef.current !== value) {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const currentText = root.getTextContent();
|
||||
|
||||
// 如果当前内容和新值相同,则不更新
|
||||
if (currentText === (value || '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
root.clear();
|
||||
if (value) {
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(value);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
} else {
|
||||
// 当value为undefined或空时,创建一个空段落
|
||||
const paragraph = $createParagraphNode();
|
||||
root.append(paragraph);
|
||||
}
|
||||
});
|
||||
lastValueRef.current = value;
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default InitialValuePlugin
|
||||
@@ -0,0 +1,24 @@
|
||||
import { forwardRef, useImperativeHandle } from 'react';
|
||||
import { $getSelection } from 'lexical';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import type { EditorRef } from '../index'
|
||||
|
||||
// 插入文本的插件
|
||||
const InsertTextPlugin = forwardRef<EditorRef>((_, ref) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
insertText: (text: string) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (selection) {
|
||||
selection.insertText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [editor]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default InsertTextPlugin;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { type FC, useEffect } from 'react';
|
||||
import { $getRoot } from 'lexical';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
// 处理换行的插件
|
||||
const LineBreakPlugin: FC<{ onChange?: (value: string) => void }> = ({ onChange }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const textContent = root.getTextContent();
|
||||
// 将\n转换为实际换行
|
||||
const processedContent = textContent.replace(/\\n/g, '\n');
|
||||
onChange?.(processedContent);
|
||||
});
|
||||
});
|
||||
}, [editor, onChange]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default LineBreakPlugin;
|
||||
@@ -31,10 +31,6 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig)
|
||||
setEditConfig({ ...(data || {}) })
|
||||
const knowledge_bases = [...(data.knowledge_bases || [])]
|
||||
setKnowledgeList(knowledge_bases)
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
knowledge_bases: knowledge_bases,
|
||||
}))
|
||||
}
|
||||
}, [data])
|
||||
|
||||
@@ -47,10 +43,10 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig)
|
||||
const handleDeleteKnowledge = (id: string) => {
|
||||
const list = knowledgeList.filter(item => item.id !== id)
|
||||
setKnowledgeList([...list])
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
onUpdate({
|
||||
...editConfig,
|
||||
knowledge_bases: [...list],
|
||||
}))
|
||||
})
|
||||
}
|
||||
const handleEditKnowledge = (item: KnowledgeBase) => {
|
||||
knowledgeConfigModalRef.current?.handleOpen(item)
|
||||
@@ -69,10 +65,10 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig)
|
||||
list = [...values as KnowledgeBase[]]
|
||||
}
|
||||
setKnowledgeList([...list])
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
onUpdate({
|
||||
...editConfig,
|
||||
knowledge_bases: [...list],
|
||||
}))
|
||||
})
|
||||
} else if (type === 'knowledgeConfig') {
|
||||
const index = knowledgeList.findIndex(item => item.id === (values as KnowledgeBase).kb_id)
|
||||
const list = [...knowledgeList]
|
||||
@@ -81,18 +77,19 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig)
|
||||
config: {...values as KnowledgeConfigForm}
|
||||
}
|
||||
setKnowledgeList([...list])
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
onUpdate({
|
||||
...editConfig,
|
||||
knowledge_bases: [...list],
|
||||
}))
|
||||
})
|
||||
} else if (type === 'rerankerConfig') {
|
||||
setEditConfig(prev => ({ ...prev, ...(values as RerankerConfig) }))
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
...values,
|
||||
reranker_id: values.rerank_model ? values.reranker_id : undefined,
|
||||
reranker_top_k: values.rerank_model ? values.reranker_top_k : undefined,
|
||||
}))
|
||||
const rerankerValues = values as RerankerConfig
|
||||
setEditConfig(prev => ({ ...prev, ...rerankerValues }))
|
||||
onUpdate({
|
||||
...editConfig,
|
||||
...rerankerValues,
|
||||
reranker_id: rerankerValues.rerank_model ? rerankerValues.reranker_id : undefined,
|
||||
reranker_top_k: rerankerValues.rerank_model ? rerankerValues.reranker_top_k : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -102,8 +99,8 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig)
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={() => handleKnowledgeConfig()}>{t('application.globalConfig')}</Button>
|
||||
}
|
||||
>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-[12px]">
|
||||
<div className="rb:font-medium rb:leading-[20px]">{t('application.associatedKnowledgeBase')}</div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<div className="rb:font-medium rb:leading-5">{t('application.associatedKnowledgeBase')}</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddKnowledge}>+{t('application.addKnowledgeBase')}</Button>
|
||||
</div>
|
||||
|
||||
@@ -115,21 +112,21 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig)
|
||||
dataSource={knowledgeList}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
|
||||
<div className="rb:font-medium rb:leading-[16px]">
|
||||
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.name}
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-[8px]">
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[20px]">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-[24px] rb:h-[24px] rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-[24px] rb:h-[24px] rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
|
||||
149
web/src/views/ApplicationConfig/components/ToolList.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, List, Switch } from 'antd'
|
||||
import Card from './Card'
|
||||
import type {
|
||||
ToolModalRef,
|
||||
ToolOption
|
||||
} from '../types'
|
||||
import Empty from '@/components/Empty'
|
||||
import ToolModal from './ToolModal'
|
||||
import { getToolMethods, getToolDetail } from '@/api/tools'
|
||||
|
||||
const ToolList: FC<{ data: ToolOption[]; onUpdate: (config: ToolOption[]) => void}> = ({data, onUpdate}) => {
|
||||
const { t } = useTranslation()
|
||||
const toolModalRef = useRef<ToolModalRef>(null)
|
||||
const [toolList, setToolList] = useState<ToolOption[]>([])
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const processedData = data.map(async (item) => {
|
||||
if (!item.label && item.tool_id) {
|
||||
try {
|
||||
const [toolDetail, methods] = await Promise.all([
|
||||
getToolDetail(item.tool_id),
|
||||
getToolMethods(item.tool_id)
|
||||
])
|
||||
|
||||
switch ((toolDetail as any).tool_type) {
|
||||
case 'mcp':
|
||||
const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
|
||||
return {
|
||||
...item,
|
||||
label: mcpFilterItem?.description,
|
||||
method_id: mcpFilterItem?.method_id,
|
||||
value: mcpFilterItem?.name,
|
||||
description: mcpFilterItem?.description,
|
||||
parameters: mcpFilterItem?.parameters
|
||||
}
|
||||
break
|
||||
case 'builtin':
|
||||
if ((methods as any[]).length > 1) {
|
||||
const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
|
||||
return {
|
||||
...item,
|
||||
label: builtinFilterItem?.description,
|
||||
method_id: builtinFilterItem?.method_id,
|
||||
value: builtinFilterItem?.name,
|
||||
description: builtinFilterItem?.description,
|
||||
parameters: builtinFilterItem?.parameters
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
label: (methods as any[])[0]?.description,
|
||||
method_id: (methods as any[])[0]?.method_id,
|
||||
value: (methods as any[])[0]?.name,
|
||||
description: (methods as any[])[0]?.description,
|
||||
parameters: (methods as any[])[0]?.parameters
|
||||
}
|
||||
break
|
||||
default:
|
||||
const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation)
|
||||
return {
|
||||
...item,
|
||||
label: customFilterItem?.name,
|
||||
method_id: customFilterItem?.method_id,
|
||||
value: customFilterItem?.name,
|
||||
description: customFilterItem?.description,
|
||||
parameters: customFilterItem?.parameters
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
Promise.all(processedData).then(setToolList)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
console.log('toolList', toolList)
|
||||
|
||||
const handleAddTool = () => {
|
||||
toolModalRef.current?.handleOpen()
|
||||
}
|
||||
const updateTools = (tool: ToolOption) => {
|
||||
const list = [...toolList, tool]
|
||||
setToolList(list)
|
||||
onUpdate(list)
|
||||
}
|
||||
const handleDeleteTool = (index: number) => {
|
||||
const list = toolList.filter((_item, idx) => idx !== index)
|
||||
setToolList([...list])
|
||||
onUpdate(list)
|
||||
}
|
||||
const handleChangeEnabled = (index: number) => {
|
||||
const list = toolList.map((item, idx) => {
|
||||
if (idx === index) {
|
||||
return {
|
||||
...item,
|
||||
enabled: !item.enabled
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
setToolList([...list])
|
||||
onUpdate(list)
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
title={t('application.toolConfiguration')}
|
||||
extra={
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}>+{t('application.addTool')}</Button>
|
||||
}
|
||||
>
|
||||
|
||||
{toolList.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={toolList}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.label}
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteTool(index)}
|
||||
></div>
|
||||
<Switch checked={item.enabled} onChange={() => handleChangeEnabled(index)} />
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
<ToolModal
|
||||
ref={toolModalRef}
|
||||
refresh={updateTools}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default ToolList
|
||||
145
web/src/views/ApplicationConfig/components/ToolModal.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Cascader, type CascaderProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ToolModalRef, ToolOption } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getToolMethods, getTools } from '@/api/tools'
|
||||
import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ToolModalProps {
|
||||
refresh: (tool: ToolOption) => void;
|
||||
}
|
||||
|
||||
const ToolModal = forwardRef<ToolModalRef, ToolModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [optionList, setOptionList] = useState<ToolOption[]>([
|
||||
{ value: 'mcp', label: t('tool.mcp'), isLeaf: false },
|
||||
{ value: 'builtin', label: t('tool.inner'), isLeaf: false },
|
||||
{ value: 'custom', label: t('tool.custom'), isLeaf: false },
|
||||
])
|
||||
const [selectdTools, setSelectedTools] = useState<ToolOption[]>([])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setSelectedTools([])
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
form.resetFields();
|
||||
setSelectedTools([])
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
setLoading(false)
|
||||
let operation: any = undefined
|
||||
if (selectdTools[0].value === 'mcp' || (selectdTools[0].value === 'builtin' && selectdTools[1]?.children && selectdTools[1].children.length > 1)) {
|
||||
operation = selectdTools[2].value
|
||||
} else if (selectdTools[0].value === 'custom') {
|
||||
operation = selectdTools[2].method_id
|
||||
}
|
||||
|
||||
const tool = {
|
||||
...selectdTools[2],
|
||||
label: selectdTools[0].value === 'custom' ? selectdTools[2].label : selectdTools[2].description,
|
||||
tool_id: selectdTools[1].value as string,
|
||||
enabled: true
|
||||
}
|
||||
if (operation) {
|
||||
tool.operation = operation
|
||||
}
|
||||
refresh(tool)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
const loadData = (selectedOptions: ToolOption[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
if (selectedOptions.length === 1) {
|
||||
getTools({ tool_type: targetOption.value as ToolType })
|
||||
.then(res => {
|
||||
const response = res as ToolItem[]
|
||||
targetOption.children = response.map((vo: any) => {
|
||||
return {
|
||||
value: vo.id,
|
||||
label: vo.name,
|
||||
isLeaf: response.length === 0,
|
||||
}
|
||||
})
|
||||
setOptionList([...optionList])
|
||||
})
|
||||
} else {
|
||||
getToolMethods(targetOption.value as string)
|
||||
.then(res => {
|
||||
const response = res as Array<{ method_id: string; name: string }>
|
||||
targetOption.children = response.map((vo: any) => {
|
||||
return {
|
||||
value: vo.name,
|
||||
label: vo.name,
|
||||
description: vo.description,
|
||||
isLeaf: true,
|
||||
method_id: vo.method_id,
|
||||
parameters: vo.parameters
|
||||
}
|
||||
})
|
||||
setOptionList([...optionList])
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: CascaderProps<ToolOption>['onChange'] = (_value, selectedOptions) => {
|
||||
console.log('selectedOptions', selectedOptions)
|
||||
setSelectedTools(selectedOptions)
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t(`application.addTool`)}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="agent_id"
|
||||
label={t('application.tool')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') },
|
||||
]}
|
||||
>
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={optionList}
|
||||
loadData={loadData}
|
||||
onChange={handleChange}
|
||||
changeOnSelect={false}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ToolModal;
|
||||
@@ -78,14 +78,7 @@ export interface Config extends MultiAgentConfig {
|
||||
knowledge_retrieval: KnowledgeConfig | null;
|
||||
memory?: MemoryConfig;
|
||||
variables: Variable[];
|
||||
tools: {
|
||||
web_search: {
|
||||
enabled: boolean;
|
||||
config: {
|
||||
web_search: boolean;
|
||||
}
|
||||
}
|
||||
};
|
||||
tools: ToolOption[];
|
||||
is_active: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
@@ -95,11 +88,11 @@ export interface MultiAgentConfig {
|
||||
app_id: string;
|
||||
default_model_config_id?: string;
|
||||
model_parameters: ModelConfig;
|
||||
orchestration_mode: 'conditional' | 'sequential' | 'parallel';
|
||||
sub_agents?: SubAgentItem[];
|
||||
routing_rules: null;
|
||||
orchestration_mode: 'supervisor' | 'collaboration';
|
||||
execution_config: {
|
||||
routing_mode: 'master' | 'handoffs'
|
||||
sub_agent_execution_mode: 'sequential' | 'parallel';
|
||||
};
|
||||
aggregation_strategy: 'merge' | 'vote' | 'priority'
|
||||
}
|
||||
@@ -211,4 +204,31 @@ export interface AiPromptForm {
|
||||
model_id?: string;
|
||||
message?: string;
|
||||
current_prompt?: string;
|
||||
}
|
||||
export interface ToolModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
export interface ToolOption {
|
||||
value?: string | number | null;
|
||||
label?: React.ReactNode;
|
||||
description?: string;
|
||||
children?: ToolOption[];
|
||||
isLeaf?: boolean;
|
||||
method_id?: string;
|
||||
operation?: string;
|
||||
parameters?: Parameter[];
|
||||
tool_id?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
export interface Parameter {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
default: any;
|
||||
enum: null | string[];
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
pattern: null | string;
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
Col,
|
||||
Tag,
|
||||
List,
|
||||
Space
|
||||
Flex
|
||||
} from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
|
||||
@@ -103,9 +102,9 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getS
|
||||
<div className="rb:h-full rb:flex rb:flex-col rb:justify-between">
|
||||
<div className="rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167]">
|
||||
{t(`tool.${item.config_data.tool_class}_features`)} <br />
|
||||
<Space size={4} className="rb:mt-2">
|
||||
<Flex gap={4} wrap className="rb:mt-2 rb:w-full">
|
||||
{InnerConfigData[item.config_data.tool_class].features.map(vo => <Tag key={vo} color="default">{ t(`tool.${vo}`) }</Tag>) }
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{item.config_data.tool_class === 'DateTimeTool'
|
||||
? <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
|
||||
@@ -104,15 +104,15 @@ export default function UserMemory() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16} className="rb:mb-[16px]">
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
{countList.map(key => (
|
||||
<Col key={key} span={6}>
|
||||
<div className="rb:bg-[#FBFDFF] rb:border-[1px] rb:border-[#DFE4ED] rb:rounded-[12px] rb:p-[18px_20px_20px_20px]">
|
||||
<div className="rb:text-[28px] rb:font-extrabold rb:leading-[35px] rb:flex rb:items-center rb:justify-between rb:mb-[12px]">
|
||||
<div className="rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:p-[18px_20px_20px_20px]">
|
||||
<div className="rb:text-[28px] rb:font-extrabold rb:leading-8.75 rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''}
|
||||
<img className="rb:w-[24px] rb:h-[24px]" src={IconList[key]} />
|
||||
<img className="rb:w-6 rb:h-6" src={IconList[key]} />
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">{t(`userMemory.${key}`)}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`userMemory.${key}`)}</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
@@ -140,22 +140,22 @@ export default function UserMemory() {
|
||||
return (
|
||||
<List.Item key={index}>
|
||||
<div
|
||||
className="rb:p-[20px] rb:rounded-[12px] rb:border-[1px] rb:border-[#DFE4ED] rb:cursor-pointer"
|
||||
className="rb:p-5 rb:rounded-xl rb:border rb:border-[#DFE4ED] rb:cursor-pointer"
|
||||
style={{
|
||||
background: bgList[index % bgList.length],
|
||||
}}
|
||||
onClick={() => handleViewDetail(end_user.id)}
|
||||
>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:w-[48px] rb:h-[48px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[48px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
|
||||
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-[24px] rb:ml-[12px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
|
||||
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-6 rb:ml-3 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{name || '-'}<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-[12px] rb:mt-[28px] rb:mb-[28px]">
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-3 rb:mt-7 rb:mb-7">
|
||||
<div className="rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{memory_num.total || 0}</div>
|
||||
<div className="rb:break-words">{t(`userMemory.knowledgeEntryCount`)}</div>
|
||||
<div className="rb:text-[24px] rb:leading-7.5 rb:font-extrabold">{memory_num.total || 0}</div>
|
||||
<div className="rb:wrap-break-word">{t(`userMemory.knowledgeEntryCount`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useRef, useState } from 'react'
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Space, Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -25,7 +25,7 @@ const Neo4j: FC = () => {
|
||||
const aboutMeRef = useRef<AboutMeRef>(null)
|
||||
|
||||
const handleNameUpdate = (data: { other_name?: string; id: string }) => {
|
||||
setName(data.other_name ?? data.id)
|
||||
setName(data.other_name && data.other_name !== '' ? data.other_name : data.id)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
|
||||
@@ -5,29 +5,37 @@ import { Skeleton } from 'antd';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import RbAlert from '@/components/RbAlert';
|
||||
import {
|
||||
getUserSummary,
|
||||
} from '@/api/memory'
|
||||
import type { AboutMeRef } from '../types'
|
||||
|
||||
|
||||
interface Data {
|
||||
user_summary: string;
|
||||
personality: string;
|
||||
core_values: string;
|
||||
one_sentence: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
const AboutMe = forwardRef<AboutMeRef>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<string | null>(null)
|
||||
const [data, setData] = useState<Data>({} as Data)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getUserSummary(id)
|
||||
.then((res) => {
|
||||
setData((res as { summary?: string }).summary || null)
|
||||
setData((res as Data) || null)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
@@ -40,14 +48,34 @@ const AboutMe = forwardRef<AboutMeRef>((_props, ref) => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.aboutMe')}
|
||||
title={t('userMemory.aboutMe')}
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: data
|
||||
? <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167]">
|
||||
{data || '-'}
|
||||
</div>
|
||||
: Object.keys(data).filter(key => data[key] !== null).length > 0
|
||||
? <>
|
||||
{data.user_summary &&
|
||||
<div className="rb:font-regular rb:leading-5 rb:text-[#5B6167]">
|
||||
{data.user_summary}
|
||||
</div>
|
||||
}
|
||||
{data.personality && <>
|
||||
<div className="rb:pt-4 rb:font-medium rb:leading-5 rb:mb-2">{t('userMemory.personality')}</div>
|
||||
<div className="rb:font-regular rb:leading-5 rb:text-[#5B6167]">
|
||||
{data.personality}
|
||||
</div>
|
||||
</>}
|
||||
{data.core_values && <>
|
||||
<div className="rb:pt-4 rb:font-medium rb:leading-5 rb:mb-2">{t('userMemory.core_values')}</div>
|
||||
<div className="rb:font-regular rb:leading-5 rb:text-[#5B6167]">
|
||||
{data.core_values}
|
||||
</div>
|
||||
</>}
|
||||
{data.one_sentence &&
|
||||
<RbAlert className="rb:mt-4">{data.one_sentence}</RbAlert>
|
||||
}
|
||||
</>
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
|
||||
interface ActivationMetricsPieCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
loading: boolean;
|
||||
}
|
||||
const Colors = ['#155EEF', '#FFB048', '#FF5D34']
|
||||
|
||||
const ActivationMetricsPieCard: FC<ActivationMetricsPieCardProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
const resizeScheduledRef = useRef(false)
|
||||
|
||||
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()
|
||||
}
|
||||
}, [chartData])
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('forgetDetail.activationValueDistribution')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <ReactEcharts
|
||||
option={{
|
||||
color: Colors,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontSize: 12,
|
||||
width: 27,
|
||||
height: 16,
|
||||
},
|
||||
formatter: '{d}%',
|
||||
padding: [8, 5],
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#DFE4ED',
|
||||
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
|
||||
},
|
||||
legend: {
|
||||
bottom: 14 ,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: ['50%', '90%'],
|
||||
avoidLabelOverlap: false,
|
||||
percentPrecision: 0,
|
||||
padAngle: 4,
|
||||
width: 200,
|
||||
height: 200,
|
||||
left: 143,
|
||||
itemStyle: {
|
||||
borderRadius: 0
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#212332',
|
||||
formatter: '{d}%\n{b}',
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '400px' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivationMetricsPieCard
|
||||
169
web/src/views/UserMemoryDetail/components/EmotionLine.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import Empty from '@/components/Empty'
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import type { Emotion } from './GraphDetail'
|
||||
|
||||
interface EmotionLineProps {
|
||||
chartData: Emotion[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Colors = ['#369F21', '#155EEF', '#FF5D34']
|
||||
|
||||
const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
const getSeries = () => {
|
||||
const emotionTypes = [...new Set(chartData.map(item => item.emotion_type))]
|
||||
const timePoints = [...new Set(chartData.map(item => item.created_at))].sort()
|
||||
|
||||
return emotionTypes.map((emotionType, index) => {
|
||||
const emotionData = chartData.filter(item => item.emotion_type === emotionType)
|
||||
const dataMap = new Map(emotionData.map(item => [item.created_at, item.emotion_intensity]))
|
||||
const seriesData = timePoints.map(time => dataMap.get(time) || 0)
|
||||
|
||||
return {
|
||||
name: emotionType,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: Colors[index % Colors.length]
|
||||
},
|
||||
itemStyle: {
|
||||
color: Colors[index % Colors.length]
|
||||
},
|
||||
areaStyle: {
|
||||
color: Colors[index % Colors.length],
|
||||
opacity: 0.08
|
||||
},
|
||||
data: seriesData
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{t('userMemory.emotionLine')}</div>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: Colors,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
}
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
let result = `${params[0].axisValue}<br/>`
|
||||
params.forEach((param: any) => {
|
||||
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
bottom: 2,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 16,
|
||||
left: 30,
|
||||
right: 36,
|
||||
bottom: 48,
|
||||
// containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [...new Set(chartData.map(item => item.created_at))].sort(),
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
min: 0
|
||||
},
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmotionLine
|
||||
@@ -1,6 +1,8 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import * as echarts from 'echarts'
|
||||
import 'echarts-wordcloud'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -13,40 +15,23 @@ interface TagList {
|
||||
const EmotionTags: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const chartRef = useRef<HTMLDivElement>(null)
|
||||
const chartInstance = useRef<echarts.ECharts | null>(null)
|
||||
const [data, setData] = useState<TagList | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getEmotionTagData()
|
||||
}, [id])
|
||||
|
||||
const getEmotionTagData = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
if (!id) return
|
||||
getWordCloud(id)
|
||||
.then((res) => {
|
||||
setData(res as TagList)
|
||||
})
|
||||
}
|
||||
|
||||
const [visibleCount, setVisibleCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data?.keywords.length === 0) return
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setVisibleCount(prev => {
|
||||
if (prev >= data?.keywords.length) {
|
||||
clearInterval(timer)
|
||||
return prev
|
||||
}
|
||||
return prev + 1
|
||||
})
|
||||
}, 200)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [data?.keywords.length])
|
||||
|
||||
const getEmotionColor = (emotionType: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
joy: '#52c41a',
|
||||
@@ -59,6 +44,56 @@ const EmotionTags: FC = () => {
|
||||
return colors[emotionType] || '#8c8c8c'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !data?.keywords.length) return
|
||||
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
}
|
||||
|
||||
chartInstance.current = echarts.init(chartRef.current)
|
||||
|
||||
const wordCloudData = data.keywords.map((item) => ({
|
||||
name: item.keyword,
|
||||
value: item.frequency,
|
||||
textStyle: {
|
||||
color: getEmotionColor(item.emotion_type)
|
||||
}
|
||||
}))
|
||||
|
||||
const option = {
|
||||
series: [{
|
||||
type: 'wordCloud',
|
||||
gridSize: 8,
|
||||
sizeRange: [14, 60],
|
||||
rotationRange: [-45, 45],
|
||||
shape: 'pentagon',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
textStyle: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
emphasis: {
|
||||
textStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#333'
|
||||
}
|
||||
},
|
||||
data: wordCloudData
|
||||
}]
|
||||
}
|
||||
|
||||
chartInstance.current.setOption(option)
|
||||
|
||||
return () => {
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
chartInstance.current = null
|
||||
}
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const emotionStats = data?.keywords.reduce((acc, item) => {
|
||||
acc[item.emotion_type] = (acc[item.emotion_type] || 0) + item.frequency
|
||||
return acc
|
||||
@@ -68,41 +103,25 @@ const EmotionTags: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.emotionTags')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0! rb:pb-3! rb:relative'
|
||||
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
|
||||
bodyClassName="rb:p-0!"
|
||||
>
|
||||
{data?.keywords && data?.keywords.length > 0
|
||||
? <>
|
||||
<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]">
|
||||
? <div>
|
||||
<div ref={chartRef} className="rb:mt-6 rb:px-6" style={{ height: '320px', width: '100%' }} />
|
||||
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center rb:gap-10 rb:text-sm rb:mt-3 rb:p-3 rb:bg-[#F0F3F8] rb:rounded-[0_0_8px_8px]">
|
||||
{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(`statementDetail.${type || 'neutral'}`)} ({count}个)</span>
|
||||
<span className="rb:leading-5">{t(`statementDetail.${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">
|
||||
{data.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 size={88} />
|
||||
</div>
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -68,6 +68,7 @@ const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ onD
|
||||
onClick={handleEdit}
|
||||
></div>
|
||||
}
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Descriptions, Skeleton } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getExplicitMemoryDetails } from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import type { ExplicitDetailModalRef, EpisodicMemory, SemanticMemory } from '../pages/ExplicitDetail'
|
||||
|
||||
|
||||
interface Data {
|
||||
memory_type: 'episodic' | 'semantic';
|
||||
title: string;
|
||||
content: string;
|
||||
emotion: string;
|
||||
created_at: number;
|
||||
|
||||
name: string;
|
||||
core_definition: string;
|
||||
detailed_notes: string;
|
||||
}
|
||||
const ExplicitDetailModal = forwardRef<ExplicitDetailModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Data>({} as Data)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
setData({} as Data)
|
||||
};
|
||||
|
||||
const handleOpen = (vo: EpisodicMemory | SemanticMemory) => {
|
||||
setLoading(true)
|
||||
getExplicitMemoryDetails({
|
||||
end_user_id: id as string,
|
||||
memory_id: vo.id
|
||||
})
|
||||
.then(res => {
|
||||
setVisible(true);
|
||||
setData(res as Data)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
};
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
return (
|
||||
<RbModal
|
||||
title={data.name || data.title}
|
||||
open={visible}
|
||||
footer={null}
|
||||
onCancel={handleClose}
|
||||
>
|
||||
{loading ? <Skeleton active />
|
||||
: <Descriptions column={data.memory_type === 'semantic' ? 1 : 2} classNames={{ label: 'rb:w-20' }}>
|
||||
{data.emotion && <Descriptions.Item label={t('explicitDetail.emotion')}>
|
||||
<div className="rb:flex rb:items-center rb:gap-2">
|
||||
<div className="rb:w-3 rb:h-3 rb:rounded-full" style={{ backgroundColor: getEmotionColor(data.emotion) }}></div>
|
||||
<span className="rb:text-gray-600">{t(`statementDetail.${data.emotion || 'neutral'}`)}</span>
|
||||
</div>
|
||||
</Descriptions.Item>}
|
||||
{data.core_definition && <Descriptions.Item label={t('explicitDetail.core_definition')}>
|
||||
{data.core_definition}
|
||||
</Descriptions.Item>}
|
||||
{data.detailed_notes && <Descriptions.Item label={t('explicitDetail.detailed_notes')}>
|
||||
{data.detailed_notes}
|
||||
</Descriptions.Item>}
|
||||
{data.created_at && <Descriptions.Item label={t('explicitDetail.created_at')}>
|
||||
{formatDateTime(data.created_at)}
|
||||
</Descriptions.Item>}
|
||||
{data.content && <Descriptions.Item span="filled" label={t('explicitDetail.content')}>
|
||||
{data.content}
|
||||
</Descriptions.Item>}
|
||||
</Descriptions>
|
||||
}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ExplicitDetailModal;
|
||||
140
web/src/views/UserMemoryDetail/components/GraphDetail.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useState, forwardRef, useImperativeHandle, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col, Tabs, Space, Skeleton } from 'antd'
|
||||
|
||||
import { getRelationshipEvolution, getTimelineMemories } from '@/api/memory'
|
||||
import type { Node, GraphDetailRef } from '../types'
|
||||
import RbDrawer from '@/components/RbDrawer'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import EmotionLine from './EmotionLine'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import InteractionBar from './InteractionBar'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
export interface Emotion {
|
||||
emotion_intensity: number;
|
||||
emotion_type: string;
|
||||
created_at: string | number;
|
||||
}
|
||||
export interface Interaction {
|
||||
created_at: string | number;
|
||||
count: number;
|
||||
}
|
||||
interface TimelineMemory {
|
||||
text: string;
|
||||
type: string;
|
||||
created_at: number | string;
|
||||
}
|
||||
interface Timeline {
|
||||
MemorySummary: TimelineMemory[];
|
||||
Statement: TimelineMemory[];
|
||||
ExtractedEntity: TimelineMemory[];
|
||||
timelines_memory: TimelineMemory[];
|
||||
}
|
||||
|
||||
const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false);
|
||||
const [vo, setVo] = useState<Node | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [emotionData, setEmotionData] = useState<Emotion[]>([])
|
||||
const [interactionData, setInteractionData] = useState<Interaction[]>([])
|
||||
const [activeTab, setActiveTab] = useState('timelines_memory')
|
||||
const [timelineLoading, setTimelineLoading] = useState(false)
|
||||
const [timelineMemories, setTimelineMemories] = useState<Timeline>({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []})
|
||||
|
||||
const handleCancel = () => {
|
||||
setVo(null)
|
||||
setOpen(false)
|
||||
}
|
||||
const handleOpen = (vo: Node) => {
|
||||
setActiveTab('timelines_memory')
|
||||
setOpen(true)
|
||||
setVo(vo)
|
||||
getRelationshipEvolutionData(vo)
|
||||
getTimelineMemoriesData(vo)
|
||||
}
|
||||
const getRelationshipEvolutionData = (vo: Node) => {
|
||||
if (!vo.id || !vo.label) return
|
||||
setLoading(true)
|
||||
getRelationshipEvolution({ id: vo.id as string, label: vo.label })
|
||||
.then(res => {
|
||||
const { emotion, interaction } = res as { emotion: Emotion[]; interaction: Interaction[] } || {}
|
||||
setEmotionData(emotion)
|
||||
setInteractionData(interaction)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
const getTimelineMemoriesData = (vo: Node) => {
|
||||
if (!vo.id || !vo.label) return
|
||||
setTimelineLoading(true)
|
||||
getTimelineMemories({ id: vo.id as string, label: vo.label })
|
||||
.then(res => {
|
||||
setTimelineMemories(res as Timeline)
|
||||
})
|
||||
.finally(() => setTimelineLoading(false))
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
const activeContent = useMemo(() => {
|
||||
return timelineMemories[activeTab as keyof Timeline] || []
|
||||
}, [activeTab, timelineMemories])
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={vo?.name}
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
width={1000}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div>
|
||||
<RbCard>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<EmotionLine chartData={emotionData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InteractionBar chartData={interactionData} loading={loading} />
|
||||
</Col>
|
||||
</Row>
|
||||
</RbCard>
|
||||
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div>
|
||||
<RbCard>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={['timelines_memory', 'ExtractedEntity', 'Statement', 'MemorySummary'].map(key => ({
|
||||
label: t(`userMemory.${key}`),
|
||||
key
|
||||
}))}
|
||||
onChange={(key: string) => setActiveTab(key)}
|
||||
/>
|
||||
{timelineLoading
|
||||
? <Skeleton active />
|
||||
: !activeContent || activeContent.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <Space size={16} direction="vertical" className="rb:w-full">
|
||||
{activeContent.map((vo, index) => (
|
||||
<RbCard
|
||||
key={index}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
title={vo.text}
|
||||
>
|
||||
<div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div>
|
||||
<Tag className="rb:mt-2">{vo.type}</Tag>
|
||||
</RbCard>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
|
||||
|
||||
</RbCard>
|
||||
</RbDrawer>
|
||||
)
|
||||
})
|
||||
export default GraphDetail
|
||||
84
web/src/views/UserMemoryDetail/components/Habits.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Space, Progress } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty'
|
||||
import {
|
||||
getImplicitHabits,
|
||||
} from '@/api/memory'
|
||||
|
||||
interface HabitsItem {
|
||||
habit_description: string;
|
||||
frequency_pattern: string;
|
||||
time_context: string;
|
||||
confidence_level: number;
|
||||
supporting_summaries: string[];
|
||||
first_observed: string;
|
||||
last_observed: string;
|
||||
is_current: boolean;
|
||||
specific_examples: string[];
|
||||
}
|
||||
|
||||
const Habits: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<HabitsItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getImplicitHabits(id).then((res) => {
|
||||
const response = res as HabitsItem[]
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.habits')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.habitsSubTitle')}</div>
|
||||
<RbCard>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty size={88} />
|
||||
: <Space size={12} direction="vertical" className="rb:w-full!">
|
||||
{data.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div>
|
||||
<div className="rb:mb-1">{vo.habit_description}</div>
|
||||
<div className="rb:mb-1 rb:text-[#5B6167]">{vo.time_context}</div>
|
||||
</div>
|
||||
<div className="rb:text-[24px] rb:font-medium">{vo.confidence_level}%</div>
|
||||
</div>
|
||||
|
||||
{vo.specific_examples.length > 0 && <>
|
||||
<div className="rb:mt-3 rb:mb-2">{t('implicitDetail.specific_examples')}</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
{vo.specific_examples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">- {item}</div>
|
||||
))}
|
||||
</div>
|
||||
</>}
|
||||
<Progress percent={vo.confidence_level} showInfo={false} className="rb:mt-3" />
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
</RbCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Habits
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Progress } from 'antd'
|
||||
import { Row, Col, Progress } from 'antd'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -58,37 +59,95 @@ const Health: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.health')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
|
||||
bodyClassName="rb:px-[28px]! rb:py-[16px]!"
|
||||
>
|
||||
{health?.health_score && health?.health_score > 0
|
||||
? <>
|
||||
<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>
|
||||
<Row gutter={59}>
|
||||
<Col span={12}>
|
||||
<div className="rb:flex rb:justify-center rb:items-center">
|
||||
<ReactEcharts
|
||||
option={{
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['65%', '80%'],
|
||||
center: ['50%', '50%'],
|
||||
startAngle: 90,
|
||||
data: [
|
||||
{
|
||||
value: health.health_score,
|
||||
name: health.level,
|
||||
itemStyle: {
|
||||
color: '#155EEF',
|
||||
borderRadius: [10, 10, 10, 10]
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 100 - health.health_score,
|
||||
name: '',
|
||||
itemStyle: {
|
||||
color: '#DFE4ED',
|
||||
borderRadius: [10, 10, 10, 10]
|
||||
}
|
||||
}
|
||||
],
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center',
|
||||
formatter: '{score|' + health.health_score + '}\n{level|' + health.level + '}',
|
||||
rich: {
|
||||
score: {
|
||||
fontSize: 36,
|
||||
fontWeight: 'bold',
|
||||
color: '#212332',
|
||||
lineHeight: 36
|
||||
},
|
||||
level: {
|
||||
fontSize: 14,
|
||||
color: '#5B6167',
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
labelLine: { show: false },
|
||||
emphasis: { disabled: true },
|
||||
itemStyle: {
|
||||
borderRadius: 10
|
||||
}
|
||||
}]
|
||||
}}
|
||||
style={{ height: '200px', width: '200px' }}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{health.dimensions && <div className="rb:space-y-7">
|
||||
<div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
|
||||
{t('statementDetail.positivity_rate')}
|
||||
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.positivity_rate.score}%</div>
|
||||
</div>
|
||||
<Progress strokeColor="#155EEF" percent={health.dimensions.positivity_rate.score} showInfo={false} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
|
||||
{t('statementDetail.stability')}
|
||||
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.stability.score}%</div>
|
||||
</div>
|
||||
<Progress strokeColor="#155EEF" percent={health.dimensions.stability.score} showInfo={false} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
|
||||
{t('statementDetail.resilience')}
|
||||
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.resilience.score}%</div>
|
||||
</div>
|
||||
<Progress strokeColor="#155EEF" percent={health.dimensions.resilience.score} showInfo={false} />
|
||||
</div>
|
||||
</div>}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{health.dimensions && <>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.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('statementDetail.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('statementDetail.resilience')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.resilience.score} />
|
||||
</div>
|
||||
</>}
|
||||
</>
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
|
||||
119
web/src/views/UserMemoryDetail/components/InteractionBar.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import Empty from '@/components/Empty'
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import type { Interaction } from './GraphDetail'
|
||||
|
||||
interface InteractionBarProps {
|
||||
chartData: Interaction[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Colors = ['#155EEF', '#369F21', '#FF5D34']
|
||||
const InteractionBar: FC<InteractionBarProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const series = [{
|
||||
name: 'Interaction Count',
|
||||
type: 'bar',
|
||||
data: chartData.map(item => item.count)
|
||||
}]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{t('userMemory.interaction')}</div>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <ReactEcharts
|
||||
option={{
|
||||
color: Colors,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
}
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: 16,
|
||||
left: 30,
|
||||
right: 36,
|
||||
bottom: 48,
|
||||
// containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.created_at),
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
min: 0
|
||||
},
|
||||
series
|
||||
}}
|
||||
style={{ height: '265px', width: '100%' }}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InteractionBar
|
||||
74
web/src/views/UserMemoryDetail/components/InterestAreas.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getImplicitInterestAreas,
|
||||
} from '@/api/memory'
|
||||
|
||||
interface Item {
|
||||
category_name: string;
|
||||
percentage: number;
|
||||
evidence: string[];
|
||||
trending_direction: string | null;
|
||||
}
|
||||
interface InterestAreasItem {
|
||||
user_id: string;
|
||||
analysis_timestamp: number | string;
|
||||
total_summaries_analyzed: number;
|
||||
tech: Item;
|
||||
lifestyle: Item;
|
||||
music: Item;
|
||||
art: Item;
|
||||
}
|
||||
|
||||
const InterestAreas: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<InterestAreasItem>({} as InterestAreasItem)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getImplicitInterestAreas(id).then((res) => {
|
||||
const response = res as InterestAreasItem
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('implicitDetail.interestAreas')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div>
|
||||
{(['art', 'music', 'tech', 'lifestyle'] as const).map((key) => {
|
||||
return (
|
||||
<div key={key} >
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-1">{t(`implicitDetail.${key}`)}</div>
|
||||
{data[key]?.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress percent={data[key]?.percentage || 0} showInfo={false} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default InterestAreas
|
||||
@@ -63,6 +63,7 @@ const InterestDistribution: FC = () => {
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.interestDistribution')}
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
|
||||
@@ -50,6 +50,7 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||
<RbCard
|
||||
title={t('userMemory.memoryInsight')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Skeleton } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getNodeStatistics,
|
||||
} from '@/api/memory'
|
||||
@@ -15,11 +14,25 @@ const BG_LIST = [
|
||||
'rb:bg-[linear-gradient(316deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(316deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(314deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(314deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[]',
|
||||
'rb:bg-[linear-gradient(332deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(313deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(332deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
]
|
||||
const typeList = [
|
||||
{ key: 'PERCEPTUAL_MEMORY', bg: 0 },
|
||||
{ key: 'WORKING_MEMORY', bg: 1 },
|
||||
{ key: 'EMOTIONAL_MEMORY', bg: 2 },
|
||||
{ key: 'SHORT_TERM_MEMORY', bg: 3 },
|
||||
{
|
||||
key: 'LONG_TERM_MEMORY',
|
||||
bg: 4,
|
||||
children: [
|
||||
{ key: 'IMPLICIT_MEMORY' },
|
||||
{ key: 'EPISODIC_MEMORY' },
|
||||
{ key: 'EXPLICIT_MEMORY' }
|
||||
]
|
||||
},
|
||||
{ key: 'FORGETTING_MANAGEMENT', bg: 5 },
|
||||
]
|
||||
|
||||
const NodeStatistics: FC = () => {
|
||||
@@ -34,8 +47,7 @@ const NodeStatistics: FC = () => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
@@ -52,43 +64,60 @@ const NodeStatistics: FC = () => {
|
||||
})
|
||||
}
|
||||
const handleViewDetail = (type: string) => {
|
||||
switch (type) {
|
||||
case 'EMOTIONAL_MEMORY':
|
||||
navigate(`/statement/${id}`)
|
||||
break
|
||||
}
|
||||
navigate(`/user-memory/detail/${id}/${type}`)
|
||||
}
|
||||
const renderCard = (key: string, bgIndex: number | null, isChild: boolean = false) => {
|
||||
const item = data.find((item) => item.type === key)
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"rb:flex rb:flex-col rb:justify-between rb:group rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:pt-3 rb:px-4 rb:pb-5 rb:cursor-pointer",
|
||||
{
|
||||
'rb:h-45': !isChild,
|
||||
'rb:h-31': isChild
|
||||
},
|
||||
typeof bgIndex === 'number' ? BG_LIST[bgIndex] : 'rb:bg-[#FBFDFF]'
|
||||
)}
|
||||
onClick={() => handleViewDetail(key)}
|
||||
>
|
||||
<div>
|
||||
<div className={clsx("rb:text-[#5B6167] rb:leading-5 rb:font-regular", {
|
||||
'rb:mb-2': !isChild,
|
||||
'rb:mb-1': isChild
|
||||
})}>
|
||||
{t(`userMemory.${key}`)}
|
||||
</div>
|
||||
<div className="rb:w-3 rb:h-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/arrow_right.svg')] rb:group-hover:bg-[url('@/assets/images/userMemory/arrow_right_hover.svg')]"></div>
|
||||
</div>
|
||||
<div className="rb:text-[28px] rb:leading-8.75 rb:font-extrabold">{item?.count ?? 0}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={<>{t('userMemory.nodeStatistics')} <span className="rb:text-[#5B6167] rb:font-normal!">({t('userMemory.total')}: {total})</span></>}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data && data.length > 0
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-8 rb:gap-3`}>
|
||||
{data.map((vo, index) => (
|
||||
<div
|
||||
key={vo.type}
|
||||
className={clsx("rb:flex rb:flex-col rb:justify-between rb:group rb:border rb:border-[#DFE4ED] rb:h-45 rb:rounded-lg rb:pt-3 rb:px-4 rb:pb-5", {
|
||||
'rb:cursor-pointer': vo.type === 'EMOTIONAL_MEMORY'
|
||||
}, BG_LIST[index])}
|
||||
onClick={() => handleViewDetail(vo.type)}
|
||||
>
|
||||
<div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">
|
||||
{t(`userMemory.${vo.type}`)}
|
||||
</div>
|
||||
{vo.type === 'EMOTIONAL_MEMORY' && <div
|
||||
className="rb:w-3 rb:h-3 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/home/arrow_top_right.svg')] rb:group-hover:bg-[url('@/assets/images/home/arrow_top_right_hover.svg')]"
|
||||
></div>}
|
||||
? <Skeleton active />
|
||||
: <div className="rb:w-full rb:grid rb:grid-cols-8 rb:gap-3">
|
||||
{typeList.map((vo) => {
|
||||
if (!vo.children) {
|
||||
return <div key={vo.key}>{renderCard(vo.key, vo.bg)}</div>
|
||||
}
|
||||
return (
|
||||
<div key={vo.key} className={clsx("rb:col-span-3 rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3", BG_LIST[vo.bg])}>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-3">{t(`userMemory.${vo.key}`)}</div>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-3">
|
||||
{vo.children.map((child) => <div key={child.key}>{renderCard(child.key, null, true)}</div>)}
|
||||
</div>
|
||||
<div className="rb:text-[28px] rb:leading-8.75 rb:font-extrabold">{vo.count ?? 0}</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const { Header } = Layout;
|
||||
interface ConfigHeaderProps {
|
||||
name?: string;
|
||||
operation?: ReactNode;
|
||||
source?: 'detail' | 'statement'
|
||||
source?: 'detail' | 'node'
|
||||
}
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
|
||||
120
web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Space, Tooltip, Image } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getPerceptualLastVisual,
|
||||
getPerceptualLastListen,
|
||||
getPerceptualLastText,
|
||||
} from '@/api/memory'
|
||||
|
||||
interface PerceptualLastInfoItem {
|
||||
id: string;
|
||||
file_name: string;
|
||||
file_ext: string;
|
||||
file_path: string;
|
||||
storage_type: number;
|
||||
summary: string;
|
||||
keywords: string[];
|
||||
topic: string;
|
||||
domain: string;
|
||||
created_time: number | string;
|
||||
scene: string[]
|
||||
speaker_count: number;
|
||||
section_count: number;
|
||||
}
|
||||
|
||||
const KEYS = {
|
||||
last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'],
|
||||
last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'],
|
||||
last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'],
|
||||
}
|
||||
|
||||
const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' }> = ({ type }) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<PerceptualLastInfoItem>({} as PerceptualLastInfoItem)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id, type])
|
||||
const getData = () => {
|
||||
if (!id || !type) return
|
||||
setLoading(true)
|
||||
const request = type === 'last_visual'
|
||||
? getPerceptualLastVisual(id)
|
||||
: type === 'last_listen'
|
||||
? getPerceptualLastListen(id)
|
||||
: getPerceptualLastText(id)
|
||||
request.then((res) => {
|
||||
const response = res as PerceptualLastInfoItem
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t(`perceptualDetail.${type}`)}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:h-36 rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:overflow-hidden">
|
||||
{data.file_path ? (
|
||||
type === 'last_visual' ? (
|
||||
/\.(mp4|webm|ogg|mov)$/i.test(data.file_name) ? (
|
||||
<video controls className="rb:max-w-full rb:max-h-full">
|
||||
<source src={data.file_path} />
|
||||
</video>
|
||||
) : /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name) ? (
|
||||
<Image src={data.file_path} alt={data.file_name} />
|
||||
// <img src={data.file_path} alt={data.file_name} className="rb:max-w-full rb:max-h-full rb:object-contain" />
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
)
|
||||
) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? (
|
||||
<audio controls className="rb:w-full">
|
||||
<source src={data.file_path} />
|
||||
</audio>
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="rb:text-gray-400">No file</div>
|
||||
)}
|
||||
</div>
|
||||
<Space size={4} direction="vertical" className="rb:w-full rb:mt-3">
|
||||
{KEYS[type].map(key => {
|
||||
const value = (data as any)[key]
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:justify-between rb:items-center rb:gap-3">
|
||||
<div className="rb:text-[#5B6167]">{t(`perceptualDetail.${key}`)}</div>
|
||||
{key === 'summary' ? (
|
||||
<Tooltip title={value}>
|
||||
<div className="rb:flex-1 rb:text-right rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
: <div className="rb:flex-1 rb:text-right">
|
||||
{typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default PerceptualLastInfo
|
||||
77
web/src/views/UserMemoryDetail/components/Portrait.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getImplicitPortrait,
|
||||
} from '@/api/memory'
|
||||
|
||||
interface Item {
|
||||
dimension_name: string;
|
||||
percentage: number;
|
||||
evidence: string[];
|
||||
reasoning: string;
|
||||
confidence_level: string;
|
||||
}
|
||||
interface PortraitItem {
|
||||
user_id: string;
|
||||
analysis_timestamp: number | string;
|
||||
total_summaries_analyzed: number;
|
||||
historical_trends: null;
|
||||
creativity: Item;
|
||||
aesthetic: Item;
|
||||
technology: Item;
|
||||
literature: Item;
|
||||
}
|
||||
|
||||
const Portrait: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<PortraitItem>({} as PortraitItem)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getImplicitPortrait(id).then((res) => {
|
||||
const response = res as PortraitItem
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('implicitDetail.portrait')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div className="rb:mt-1">
|
||||
{(['aesthetic', 'creativity', 'literature', 'technology'] as const).map((key) => {
|
||||
const item = data[key] as Item
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-1">{t(`implicitDetail.${key}`)}</div>
|
||||
{item?.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress percent={item?.percentage || 0} showInfo={false} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default Portrait
|
||||
183
web/src/views/UserMemoryDetail/components/Preferences.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { type FC, useEffect, useState, useRef, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Skeleton } from 'antd'
|
||||
import * as echarts from 'echarts'
|
||||
import 'echarts-wordcloud'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getImplicitPreferences } from '@/api/memory'
|
||||
|
||||
interface PreferenceItem {
|
||||
tag_name: string;
|
||||
confidence_score: number;
|
||||
supporting_evidence: string[];
|
||||
context_details: string;
|
||||
created_at: number | string;
|
||||
updated_at: number | string;
|
||||
conversation_references: string[];
|
||||
category: string;
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = ['#FF5D34', '#155EEF', '#9C6FFF', '#369F21', '#4DA8FF', '#FF8C00', '#32CD32', '#FF69B4', '#20B2AA', '#DDA0DD']
|
||||
|
||||
const generateCategoryColors = (categories: string[]) => {
|
||||
const colors: Record<string, string> = {}
|
||||
categories.forEach((category, index) => {
|
||||
colors[category] = DEFAULT_COLORS[index % DEFAULT_COLORS.length]
|
||||
})
|
||||
return colors
|
||||
}
|
||||
|
||||
const Preferences: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const chartRef = useRef<HTMLDivElement>(null)
|
||||
const chartInstance = useRef<echarts.ECharts | null>(null)
|
||||
const [selectedWord, setSelectedWord] = useState<number | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<PreferenceItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setSelectedWord(null)
|
||||
getImplicitPreferences(id)
|
||||
.then((res) => {
|
||||
setData(res as PreferenceItem[])
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const uniqueCategories = [...new Set(data.map(item => item.category).filter(Boolean))]
|
||||
const categoryColors = generateCategoryColors(uniqueCategories)
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
return categoryColors[category] || '#4DA8FF'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !data.length) return
|
||||
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
}
|
||||
|
||||
chartInstance.current = echarts.init(chartRef.current)
|
||||
|
||||
const wordCloudData = data.map((item, index) => ({
|
||||
name: item.tag_name,
|
||||
value: Math.round(item.confidence_score * 100),
|
||||
itemIndex: index,
|
||||
textStyle: {
|
||||
color: getCategoryColor(item.category)
|
||||
}
|
||||
}))
|
||||
|
||||
const option = {
|
||||
series: [{
|
||||
type: 'wordCloud',
|
||||
gridSize: 8,
|
||||
sizeRange: [14, 60],
|
||||
rotationRange: [-45, 45],
|
||||
shape: 'pentagon',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
textStyle: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
emphasis: {
|
||||
textStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#333'
|
||||
}
|
||||
},
|
||||
data: wordCloudData
|
||||
}]
|
||||
}
|
||||
|
||||
chartInstance.current.setOption(option)
|
||||
|
||||
chartInstance.current.on('click', (params) => {
|
||||
const clickedIndex = (params.data as any).itemIndex
|
||||
if (selectedWord !== clickedIndex) {
|
||||
setSelectedWord(clickedIndex)
|
||||
}
|
||||
|
||||
// Highlight selected word without redrawing
|
||||
chartInstance.current?.dispatchAction({
|
||||
type: 'highlight',
|
||||
dataIndex: clickedIndex
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.dispose()
|
||||
chartInstance.current = null
|
||||
}
|
||||
}
|
||||
}, [data])
|
||||
|
||||
|
||||
console.log(selectedWord, data)
|
||||
|
||||
const detailTitle = useMemo(() => {
|
||||
return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')}</> : ''
|
||||
}, [selectedWord, data, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={t('implicitDetail.preferences')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0! rb:pb-3! rb:relative rb:h-[350px]'
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active className="rb:px-4" />
|
||||
: data && data.length > 0
|
||||
? <div ref={chartRef} className="rb:mt-6 rb:px-6" style={{ height: '350px' }} />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={detailTitle}
|
||||
headerType="borderless"
|
||||
height="100%"
|
||||
bodyClassName='rb:p-3! rb:h-[326px]'
|
||||
>
|
||||
{selectedWord === null
|
||||
? <Empty size={88} className="rb:h-full!" />
|
||||
: <>
|
||||
<div className="rb:leading-5 rb:mb-1 rb:font-medium">{t('implicitDetail.context_details')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{data[selectedWord].context_details}</div>
|
||||
|
||||
<div className="rb:leading-5 rb:mt-3 rb:font-medium">{t('implicitDetail.supporting_evidence')}</div>
|
||||
{data[selectedWord].supporting_evidence.map((vo, index) => <div key={index} className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">-{vo}</div>)}
|
||||
</>
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Preferences
|
||||
@@ -0,0 +1,191 @@
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import * as echarts from 'echarts';
|
||||
import Empty from '@/components/Empty'
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
|
||||
interface RecentTrendsLineCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
seriesList: string[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Colors = ['#155EEF', '#FF5D34']
|
||||
|
||||
const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, seriesList, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
const getSeries = () => {
|
||||
return seriesList.map((key, index) => ({
|
||||
name: key === 'merged_count' ? t('forgetDetail.merged_count') : t('forgetDetail.average_activation'),
|
||||
type: 'line',
|
||||
yAxisIndex: key === 'merged_count' ? 0 : 1,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: Colors[index]
|
||||
},
|
||||
itemStyle: {
|
||||
color: Colors[index]
|
||||
},
|
||||
areaStyle: {
|
||||
color: Colors[index],
|
||||
opacity: 0.08
|
||||
},
|
||||
data: chartData.map(item => item[key])
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('forgetDetail.forgettingTrend')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: Colors,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
}
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
let result = `${params[0].axisValue}<br/>`
|
||||
params.forEach((param: any) => {
|
||||
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
bottom: 2,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 16,
|
||||
left: 30,
|
||||
right: 36,
|
||||
bottom: 48,
|
||||
// containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.date),
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
color: Colors[0],
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: Colors[0]
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: Colors[1],
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
formatter: '{value}'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: Colors[1]
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
min: 0
|
||||
}
|
||||
],
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentTrendsLineCard
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Col, Row } from 'antd'
|
||||
import { Col, Row, Space, Button } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
|
||||
import type { Node, Edge, GraphData } from '../types'
|
||||
import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties, GraphDetailRef } from '../types'
|
||||
import {
|
||||
getMemorySearchEdges,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
import GraphDetail from '../components/GraphDetail'
|
||||
|
||||
const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048']
|
||||
const RelationshipNetwork:FC = () => {
|
||||
@@ -24,6 +26,7 @@ const RelationshipNetwork:FC = () => {
|
||||
const [categories, setCategories] = useState<{ name: string }[]>([])
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
|
||||
// const [fullScreen, setFullScreen] = useState<boolean>(false)
|
||||
const graphDetailRef = useRef<GraphDetailRef>(null)
|
||||
|
||||
console.log('categories', categories)
|
||||
// 关系网络
|
||||
@@ -136,6 +139,11 @@ const RelationshipNetwork:FC = () => {
|
||||
|
||||
console.log('selectedNode', selectedNode)
|
||||
|
||||
const handleViewAll = () => {
|
||||
if (!selectedNode) return
|
||||
graphDetailRef.current?.handleOpen(selectedNode)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
{/* 关系网络 */}
|
||||
@@ -143,6 +151,7 @@ const RelationshipNetwork:FC = () => {
|
||||
<RbCard
|
||||
title={t('userMemory.relationshipNetwork')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
// extra={
|
||||
// <div
|
||||
// onClick={handleFullScreen}
|
||||
@@ -239,15 +248,22 @@ const RelationshipNetwork:FC = () => {
|
||||
<RbCard
|
||||
title={t('userMemory.memoryDetails')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:min-h-[46px]!"
|
||||
bodyClassName='rb:p-0!'
|
||||
extra={selectedNode && <Button type="text" onClick={handleViewAll}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/view.svg')] rb:hover:bg-[url('@/assets/images/userMemory/view_hover.svg')]"
|
||||
></div>
|
||||
{t('userMemory.completeMemory')}
|
||||
</Button>}
|
||||
>
|
||||
<div className="rb:h-133.5">
|
||||
<div className="rb:h-133.5 rb:overflow-y-auto">
|
||||
{!selectedNode
|
||||
? <Empty
|
||||
url={detailEmpty}
|
||||
subTitle={t('userMemory.memoryDetailEmptyDesc')}
|
||||
className="rb:h-full rb:mx-10 rb:text-center"
|
||||
size={90}
|
||||
size={[197.81, 150]}
|
||||
/>
|
||||
: <>
|
||||
<div className="rb:bg-[#F6F8FC] rb:border-t rb:border-b rb:border-[#DFE4ED] rb:font-medium rb:py-2 rb:px-4 rb:h-10">{selectedNode.name}</div>
|
||||
@@ -267,9 +283,52 @@ const RelationshipNetwork:FC = () => {
|
||||
</>
|
||||
<div className="rb:font-medium rb:mb-2 rb:mt-4">
|
||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.created_at')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4">
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{dayjs(selectedNode?.properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
|
||||
{selectedNode?.properties.associative_memory > 0 && <div className="rb:mt-4">
|
||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.associative_memory')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
<span className="rb:text-[#155EEF] rb:font-medium">{selectedNode?.properties.associative_memory}</span> {t('userMemory.unix')}{t('userMemory.associative_memory')}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{selectedNode.label === 'Statement' && <>
|
||||
{(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => {
|
||||
const statementProps = selectedNode.properties as StatementNodeProperties;
|
||||
if ((key === 'emotion_keywords' && statementProps[key]?.length > 0) || statementProps[key]) {
|
||||
return (
|
||||
<div className="rb:mt-4" key={key}>
|
||||
{t(`userMemory.Statement_${key}`)}
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{key === 'emotion_keywords'
|
||||
? <Space>{statementProps.emotion_keywords.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Space>
|
||||
: statementProps[key]
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</>}
|
||||
{selectedNode.label === 'ExtractedEntity' && <>
|
||||
{(['name', 'entity_type', 'aliases', 'connect_strngth', 'importance_score'] as const).map(key => {
|
||||
const entityProps = selectedNode.properties as ExtractedEntityNodeProperties;
|
||||
if (entityProps[key]) {
|
||||
return (
|
||||
<div className="rb:mt-4" key={key}>
|
||||
{t(`userMemory.ExtractedEntity_${key}`)}
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{entityProps[key]}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -277,6 +336,8 @@ const RelationshipNetwork:FC = () => {
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
<GraphDetail ref={graphDetailRef} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,18 +41,24 @@ const Suggestions: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.suggestions')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
|
||||
bodyClassName="rb:px-[16px]! rb:pt-[20px]! rb:pb-[24px]!"
|
||||
>
|
||||
{suggestions?.suggestions && suggestions?.suggestions.length > 0
|
||||
? <>
|
||||
<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>
|
||||
))}
|
||||
<div className="rb:space-y-8">
|
||||
{suggestions.suggestions.map((item, index) => (
|
||||
<div key={index}>
|
||||
<div className="rb:font-medium">{index + 1}. {item.title}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:mb-2 rb:leading-5">{item.content}</div>
|
||||
|
||||
<ul className="rb:list-disc rb:ml-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
|
||||
{item.actionable_steps.map((vo, idx) => <li key={idx}>{vo}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
|
||||
82
web/src/views/UserMemoryDetail/components/Timeline.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress, Space, Tooltip, Divider } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getPerceptualTimeline
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
interface TimelineItem {
|
||||
id: string;
|
||||
perceptual_type: number;
|
||||
file_path: string;
|
||||
file_name: string;
|
||||
summary: string;
|
||||
storage_type: number;
|
||||
created_time: string | number;
|
||||
}
|
||||
|
||||
const KEYS = {
|
||||
last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'],
|
||||
last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'],
|
||||
last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'],
|
||||
}
|
||||
|
||||
const perceptual_type: Record<number, string> = {
|
||||
1: 'last_visual',
|
||||
2: 'last_listen',
|
||||
3: 'last_text',
|
||||
}
|
||||
|
||||
const Timeline: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<TimelineItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getPerceptualTimeline(id).then((res) => {
|
||||
const response = res as { memories: TimelineItem[] }
|
||||
setData(response.memories || [])
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty />
|
||||
: <Space size={8} direction="vertical" className="rb:w-full">
|
||||
{data.map((vo, index) => (
|
||||
<div key={vo.id} className="rb:flex rb:gap-6 rb:min-h-16">
|
||||
<div className="rb:text-[#155EEF] rb:leading-5 rb:font-medium rb:flex rb:flex-col rb:gap-2 rb:items-center">
|
||||
{formatDateTime(vo.created_time)}
|
||||
{index !== data.length - 1 && <Divider type="vertical" className="rb:flex-1 rb:w-px rb:border-[#155EEF]!" />}
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-between rb:flex-1 rb:mb-4">
|
||||
<div className="rb:w-150 rb:leading-5">{vo.summary}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium">{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default Timeline
|
||||
@@ -2,7 +2,7 @@ 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 { Progress, Row, Col } from 'antd'
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -101,27 +101,35 @@ const WordCloud: FC = () => {
|
||||
<RbCard
|
||||
title={t('statementDetail.wordCloud')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
height="100%"
|
||||
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
|
||||
bodyClassName="rb:px-[28px]! rb:py-[16px]!"
|
||||
>
|
||||
{wordCloud?.total_count && wordCloud?.total_count > 0
|
||||
? <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">
|
||||
? <Row gutter={50}>
|
||||
<Col span={12}>
|
||||
<ReactEcharts ref={chartRef} option={radarOption} style={{ width: '100%', height: 'calc(100% - 100px)' }} />
|
||||
<div className="rb:mb-4 rb:text-center rb:bg-[#F5F7FC] rb:rounded-lg rb:p-2.5 rb:mt-4">
|
||||
<span className="rb:text-[#155EEF] rb:text-[28px] rb:font-bold rb:leading-8">{wordCloud.total_count}</span><br />
|
||||
<span className="rb:text-[#5B6167] rb:leading-5">{t('statementDetail.totalCount')}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="rb:space-y-5">
|
||||
{wordCloud.tags.map(item => (
|
||||
<div key={item.emotion_type}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:font-medium">
|
||||
{t(`statementDetail.${item.emotion_type}`)}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('statementDetail.pieces')}</div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div>
|
||||
<span className="rb:font-medium">{t(`statementDetail.${item.emotion_type}`)}</span>
|
||||
<span className="rb:font-regular rb:text-[#5B6167]"> ( {item.count} {t('statementDetail.pieces')} )</span>
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{item.percentage.toFixed(1)}%</div>
|
||||
</div>
|
||||
<Progress size="small" percent={item.percentage} />
|
||||
<Progress strokeColor="#155EEF" percent={item.percentage} showInfo={false} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
249
web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Select, Form, Space, Skeleton, Input } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getEpisodicOverview,
|
||||
getEpisodicDetail,
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
interface EpisodicMemory {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
created_at: number;
|
||||
}
|
||||
interface EpisodicOverviewData {
|
||||
total: number;
|
||||
total_all: number;
|
||||
episodic_memories: EpisodicMemory[]
|
||||
}
|
||||
interface EpisodicMemoryDetail {
|
||||
id: string;
|
||||
created_at: number;
|
||||
involved_objects: string[];
|
||||
episodic_type: string;
|
||||
content_records: string[];
|
||||
emotion: string;
|
||||
}
|
||||
|
||||
const TAG_COLORS: Record<string, "processing" | "success" | "warning" | "error" | "default"> = {
|
||||
conversation: "processing",
|
||||
project_work: "success",
|
||||
learning: "warning",
|
||||
decision: "warning",
|
||||
important_event: "error",
|
||||
}
|
||||
const BG_COLORS: Record<string, string> = {
|
||||
conversation: "rb:bg-[#155EEF]",
|
||||
project_work: "rb:bg-[#369F21]",
|
||||
learning: "rb:bg-[#FF5D34]",
|
||||
decision: "rb:bg-[#FF5D34]",
|
||||
important_event: "rb:bg-[#5B6167]",
|
||||
}
|
||||
|
||||
// Map display types to internal keys
|
||||
const getTypeKey = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'Learning': 'learning',
|
||||
'Project/Work': 'project_work',
|
||||
'Conversation': 'conversation',
|
||||
'Decision': 'decision',
|
||||
'Important Event': 'important_event',
|
||||
}
|
||||
return typeMap[type] || type.toLowerCase().replace(/[^a-z0-9]/g, '_')
|
||||
}
|
||||
const EpisodicDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<EpisodicOverviewData>({} as EpisodicOverviewData)
|
||||
const values = Form.useWatch([], form)
|
||||
const [detailLoading, setDetailLoading] = useState<boolean>(false)
|
||||
const [detail, setDetail] = useState<EpisodicMemoryDetail | null>(null)
|
||||
const [selected, setSelected] = useState<EpisodicMemory | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
setSelected(null)
|
||||
setDetail(null)
|
||||
getEpisodicOverview({
|
||||
end_user_id: id,
|
||||
...values
|
||||
}).then((res) => {
|
||||
const response = res as EpisodicOverviewData
|
||||
setData(response)
|
||||
if (response.episodic_memories.length > 0) {
|
||||
setSelected(response.episodic_memories[0])
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [values])
|
||||
|
||||
useEffect(() => {
|
||||
getDetail()
|
||||
}, [selected])
|
||||
|
||||
const getDetail = () => {
|
||||
if (!selected || !selected.id) return
|
||||
|
||||
setDetailLoading(true)
|
||||
getEpisodicDetail({
|
||||
end_user_id: id as string,
|
||||
summary_id: selected.id
|
||||
})
|
||||
.then(res => {
|
||||
setDetail(res as EpisodicMemoryDetail)
|
||||
})
|
||||
.finally(() => {
|
||||
setDetailLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:text-[#FFFFFF] rb:leading-5 rb:h-30 rb:p-5 rb:bg-[url('@/assets/images/userMemory/shortTerm.png')] rb:bg-cover rb:mb-6">
|
||||
<div className="rb:max-w-135">{t('episodicDetail.title')}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-4">
|
||||
<div className="rb:bg-[rgba(255,255,255,0.2)] rb:rounded-lg rb:p-3.5 rb:text-[12px] rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{data.total_all ?? 0}</div>
|
||||
{t(`episodicDetail.total_all`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form form={form} initialValues={{ time_range: 'all', episodic_type: 'all' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="time_range">
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'today', label: t('episodicDetail.today') },
|
||||
{ value: 'this_week', label: t('episodicDetail.this_week') },
|
||||
{ value: 'this_month', label: t('episodicDetail.this_month') },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="episodic_type">
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'conversation', label: t('episodicDetail.conversation') },
|
||||
{ value: 'project_work', label: t('episodicDetail.project_work') },
|
||||
{ value: 'learning', label: t('episodicDetail.learning') },
|
||||
{ value: 'decision', label: t('episodicDetail.decision') },
|
||||
{ value: 'important_event', label: t('episodicDetail.important_event') },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="title_keyword">
|
||||
<Input placeholder={t('episodicDetail.titleKeywordPlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={<>{t('episodicDetail.curResult')}<span className="rb:text-[#5B6167] rb:font-regular!"> ({data.total || 0}{t('episodicDetail.unix')})</span></>}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.episodic_memories || data.episodic_memories.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{data.episodic_memories.map((vo, index) => (
|
||||
<div
|
||||
key={vo.id}
|
||||
className={clsx("rb:cursor-pointer rb:flex rb:items-center rb:bg-[#FFFFFF] rb:border rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5", {
|
||||
'rb:border-[#DFE4ED] rb:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.16)]': selected?.id !== vo.id,
|
||||
'rb:border-[#155EEF]': selected?.id === vo.id,
|
||||
})}
|
||||
onClick={() => setSelected(vo)}
|
||||
>
|
||||
<div className={clsx("rb:bg-[#369F21] rb:rounded-lg rb:text-[#FFFFFF] rb:size-6 rb:text-[12px] rb:leading-6 rb:text-center rb:mr-3", BG_COLORS[getTypeKey(vo.type)])}>{index + 1}</div>
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">{vo.title} <Tag color={TAG_COLORS[getTypeKey(vo.type)]}>{t(`episodicDetail.${getTypeKey(vo.type)}`)}</Tag></div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px]">{formatDateTime(vo.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={selected?.title}
|
||||
headerType="borderless"
|
||||
>
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: !selected || !detail
|
||||
? <Empty className="rb:mt-14" />
|
||||
: (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5">
|
||||
<Row gutter={[12, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="rb:text-[#5B6167]">{t('episodicDetail.created')}<br />{formatDateTime(detail.created_at)}</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="rb:text-[#5B6167]">{t('episodicDetail.episodic_type')}<br />{detail.episodic_type}</div>
|
||||
</Col>
|
||||
{detail.involved_objects.length > 0 && <Col span={24}>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('episodicDetail.involved_objects')}</div>
|
||||
<Space size={8}>{detail.involved_objects.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Space>
|
||||
</Col>}
|
||||
</Row>
|
||||
</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5">
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('episodicDetail.content_records')}</div>
|
||||
{detail.content_records.map((vo, index) => <div key={index} className="rb:text-[#5B6167] rb:leading-5">- {vo}</div>)}
|
||||
</div>
|
||||
<RbAlert>
|
||||
{t('episodicDetail.emotion')}: {t(`statementDetail.${detail.emotion}`)}
|
||||
</RbAlert>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default EpisodicDetail
|
||||
109
web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { List, Skeleton, Row, Col } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getExplicitMemory,
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Empty from '@/components/Empty'
|
||||
import ExplicitDetailModal from '../components/ExplicitDetailModal'
|
||||
|
||||
export interface EpisodicMemory {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface SemanticMemory {
|
||||
id: string;
|
||||
name: string;
|
||||
entity_type: string;
|
||||
core_definition: string;
|
||||
created_at: number;
|
||||
}
|
||||
interface Data {
|
||||
episodic_memories: EpisodicMemory[];
|
||||
semantic_memories: SemanticMemory[]
|
||||
}
|
||||
|
||||
export interface ExplicitDetailModalRef {
|
||||
handleOpen: (vo: EpisodicMemory | SemanticMemory) => void;
|
||||
}
|
||||
|
||||
const ExplicitDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const explicitDetailModalRef = useRef<ExplicitDetailModalRef>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<Data>({ episodic_memories: [], semantic_memories: [] })
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getExplicitMemory(id).then((res) => {
|
||||
const response = res as Data
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleView = (item: EpisodicMemory | SemanticMemory) => {
|
||||
explicitDetailModalRef.current?.handleOpen(item)
|
||||
}
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-3 rb:rounded-md rb:mb-4">{t('explicitDetail.episodic_memories')}</div>
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: data.episodic_memories?.length > 0 ? (
|
||||
<Row gutter={16}>
|
||||
{data.episodic_memories.map(item => (
|
||||
<Col key={item.id} span={6}>
|
||||
<RbCard
|
||||
title={item.title}
|
||||
className="rb:h-full! rb:cursor-pointer"
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
<div>{formatDateTime(item.created_at)}</div>
|
||||
<div>{item.content}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : <Empty />}
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('explicitDetail.semantic_memories')}</div>
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: data.semantic_memories?.length > 0 ? (
|
||||
<Row gutter={16}>
|
||||
{data.semantic_memories.map(item => (
|
||||
<Col key={item.id} span={6}>
|
||||
<RbCard
|
||||
title={item.name}
|
||||
className="rb:h-full! rb:cursor-pointer"
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
<div>{item.core_definition}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : <Empty />}
|
||||
|
||||
<ExplicitDetailModal
|
||||
ref={explicitDetailModalRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ExplicitDetail
|
||||
158
web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Progress } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getForgetStats,
|
||||
} from '@/api/memory'
|
||||
import type { ForgetData } from '../types'
|
||||
import ActivationMetricsPieCard from '../components/ActivationMetricsPieCard'
|
||||
import RecentTrendsLineCard from '../components/RecentTrendsLineCard'
|
||||
import Table from '@/components/Table'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import StatusTag from '@/components/StatusTag'
|
||||
|
||||
const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warning' | 'error' | 'lightBlue'> = {
|
||||
statement: 'success',
|
||||
entity: 'purple',
|
||||
summary: 'default',
|
||||
chunk: 'warning',
|
||||
}
|
||||
|
||||
const ForgetDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<ForgetData>({} as ForgetData)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getForgetStats(id).then((res) => {
|
||||
const response = res as ForgetData
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const chartData = useMemo(() => {
|
||||
const { activation_metrics } = data
|
||||
if (!activation_metrics) return []
|
||||
|
||||
let health_nodes = (activation_metrics.total_nodes || 0) - (activation_metrics.low_activation_nodes || 0) - (activation_metrics.nodes_without_activation || 0)
|
||||
|
||||
return [
|
||||
{ name: t('forgetDetail.health_nodes'), value: health_nodes },
|
||||
{ name: t('forgetDetail.nodes_without_activation'), value: activation_metrics.nodes_without_activation || 0 },
|
||||
{ name: t('forgetDetail.low_activation_nodes'), value: activation_metrics.low_activation_nodes || 0 },
|
||||
]
|
||||
|
||||
}, [data.activation_metrics, t])
|
||||
|
||||
const seriesList = useMemo(() => {
|
||||
const { recent_trends = [] } = data
|
||||
if (!recent_trends || recent_trends.length === 0) return { chartData: [], seriesList: [] }
|
||||
|
||||
return {
|
||||
chartData: recent_trends,
|
||||
seriesList: ['merged_count', 'average_activation']
|
||||
}
|
||||
}, [data.recent_trends])
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('forgetDetail.title')}</div>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.totalMemory')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data?.activation_metrics?.total_nodes ?? 0}</div>
|
||||
<div className="rb:mt-4 rb:grid rb:grid-cols-2 rb:gap-x-2 rb:gap-y-5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2">
|
||||
{['statement_count', 'entity_count', 'summary_count', 'chunk_count'].map((key, index) => (
|
||||
<div key={index}>
|
||||
<div className="rb:text-[16px] rb:font-bold rb:leading-5.5">{data?.node_distribution?.[key as keyof typeof data.node_distribution] ?? 0}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mt-1">{t(`forgetDetail.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.MemoryHealth')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data?.activation_metrics?.average_activation_value ?? 0}</div>
|
||||
<Progress className="rb:mt-px" showInfo={false} percent={data?.activation_metrics?.average_activation_value ?? 0} />
|
||||
<div className="rb:mt-4 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{t('forgetDetail.healthStatus')}</div>
|
||||
<div className="rb:text-[20px] rb:font-semibold rb:leading-7">{data?.activation_metrics?.average_activation_value > data.activation_metrics?.forgetting_threshold ? t('forgetDetail.healthy') : t('forgetDetail.unhealthy')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:mt-2">
|
||||
{t('forgetDetail.average')}<br />
|
||||
{t('forgetDetail.threshold')}{data.activation_metrics?.forgetting_threshold ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.riskOfForgetting')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data.activation_metrics?.low_activation_nodes ?? 0}</div>
|
||||
<div className="rb:mb-31.5 rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4 rb:font-regular rb:mt-1">{t('forgetDetail.low_nodes')}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.memoryHealthVisualization')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<ActivationMetricsPieCard chartData={chartData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RecentTrendsLineCard chartData={seriesList.chartData} seriesList={seriesList.seriesList} loading={loading} />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.pending_nodes')}</div>
|
||||
<Table
|
||||
rowKey='node_id'
|
||||
initialData={data.pending_nodes ?? []}
|
||||
columns={[
|
||||
{
|
||||
title: t('forgetDetail.content_summary'),
|
||||
dataIndex: 'content_summary',
|
||||
key: 'content_summary',
|
||||
width: 340,
|
||||
render: (content_summary) => <div className="rb:wrap-break-word rb:line-clamp-2">{content_summary}</div>
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.node_type'),
|
||||
dataIndex: 'node_type',
|
||||
key: 'node_type',
|
||||
render: (node_type: string) => {
|
||||
return <StatusTag status={statusTagColors[node_type] || 'default'} text={node_type} />}
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.last_access_time'),
|
||||
dataIndex: 'last_access_time',
|
||||
key: 'last_access_time',
|
||||
render: (last_access_time) => formatDateTime(last_access_time, 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.activation_value'),
|
||||
dataIndex: 'activation_value',
|
||||
key: 'activation_value',
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ForgetDetail
|
||||
34
web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col } from 'antd'
|
||||
|
||||
import Preferences from '../components/Preferences'
|
||||
import Portrait from '../components/Portrait'
|
||||
import InterestAreas from '../components/InterestAreas'
|
||||
import Habits from '../components/Habits'
|
||||
|
||||
const ImplicitDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('implicitDetail.title')}</div>
|
||||
|
||||
<Preferences />
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.portraitTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div>
|
||||
<Row gutter={[16, 16]} className="rb:mt-4">
|
||||
<Col span={12}>
|
||||
<Portrait />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InterestAreas />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Habits />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ImplicitDetail
|
||||
32
web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col } from 'antd'
|
||||
|
||||
import PerceptualLastInfo from '../components/PerceptualLastInfo'
|
||||
import Timeline from '../components/Timeline'
|
||||
|
||||
const PerceptualDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('perceptualDetail.lastInfo')}</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_visual" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_listen" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_text" />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('perceptualDetail.timeLine')}</div>
|
||||
<Timeline />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PerceptualDetail
|
||||
114
web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Space, Skeleton } from 'antd'
|
||||
import {
|
||||
getShortTerm,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
interface ShortTermItem {
|
||||
retrieval: Array<{ query: string; retrieval: string[]; }>;
|
||||
message: string;
|
||||
answer: string;
|
||||
}
|
||||
interface LongTermItem {
|
||||
query: string;
|
||||
retrieval: string;
|
||||
}
|
||||
interface ShortData {
|
||||
short_term: ShortTermItem[];
|
||||
long_term: LongTermItem[];
|
||||
entity: number;
|
||||
retrieval_number: number;
|
||||
long_term_number: number;
|
||||
}
|
||||
const ShortTermDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<ShortData>({} as ShortData)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getShortTerm(id).then((res) => {
|
||||
const response = res as ShortData
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:text-[#FFFFFF] rb:leading-5 rb:h-30 rb:p-5 rb:bg-[url('@/assets/images/userMemory/shortTerm.png')] rb:bg-cover">
|
||||
<div className="rb:max-w-135">{t('shortTermDetail.title')}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
{(['retrieval_number', 'entity', 'long_term_number'] as const).map(key => (
|
||||
<div key={key} className="rb:bg-[rgba(255,255,255,0.2)] rb:rounded-lg rb:p-3.5 rb:text-[12px] rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{(data as any)[key] ?? 0}</div>
|
||||
{t(`shortTermDetail.${key}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('shortTermDetail.shortTermTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('shortTermDetail.shortTermSubTitle')}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.short_term || data.short_term.length === 0
|
||||
? <Empty />
|
||||
:data.short_term?.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-6 rb:py-3">
|
||||
<div className="rb:font-medium rb:text-[16px] rb:leading-5.5 rb:mb-3">{vo.message}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{vo.retrieval.map((item, index) => (
|
||||
<div key={index} className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">
|
||||
<div className="rb:font-medium rb:mb-3">{t('shortTermDetail.query')}: {item.query}</div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('shortTermDetail.answer')}:</div>
|
||||
{item.retrieval.length > 0 ? item.retrieval.map((retrieval, retrievalIdx) => (
|
||||
<div key={retrievalIdx} className="rb:text-[#5B6167] rb:text-[12px]">- {retrieval}</div>
|
||||
)) : <div className="rb:text-[#5B6167] rb:text-[12px]">{t('shortTermDetail.noAnswer')}</div>}
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('shortTermDetail.answer')}</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">{vo.answer}</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('shortTermDetail.longTermTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('shortTermDetail.shortTermSubTitle')}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.long_term || data.long_term.length === 0
|
||||
? <Empty />
|
||||
: data.long_term?.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-6 rb:py-3">
|
||||
<div className="rb:mb-1 rb:font-medium rb:leading-5.5">{vo.query}</div>
|
||||
<div className="rb:mt-1 rb:leading-5 rb:text-[#5B6167] rb:text-[12px]">{vo.retrieval}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ShortTermDetail
|
||||
@@ -1,53 +1,26 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { type FC } from 'react'
|
||||
import { Row, Col, Space } from 'antd';
|
||||
|
||||
import WordCloud from '../components/WordCloud'
|
||||
import EmotionTags from '../components/EmotionTags'
|
||||
import Health from '../components/Health'
|
||||
import Suggestions from '../components/Suggestions'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
|
||||
|
||||
const StatementDetail: FC = () => {
|
||||
const { id } = useParams()
|
||||
const [name, setName] = useState<string>('')
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
getEndUserProfile(id).then((res) => {
|
||||
const response = res as { other_name: string; id: string; }
|
||||
setName(response.other_name ?? response.id)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<PageHeader
|
||||
name={name}
|
||||
source="statement"
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
209
web/src/views/UserMemoryDetail/pages/WorkingDetail.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Select, Form, Space, Skeleton, Input, Button, Divider } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getConversations,
|
||||
getConversationMessages,
|
||||
getConversationDetail,
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import PageLoading from '@/components/Empty/PageLoading'
|
||||
|
||||
interface Conversation {
|
||||
title: string;
|
||||
id: string;
|
||||
}
|
||||
interface Detail {
|
||||
theme: string;
|
||||
theme_intro: string;
|
||||
summary: string;
|
||||
question: string[];
|
||||
takeaways: string[];
|
||||
info_score: number;
|
||||
}
|
||||
|
||||
const WorkingDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<Conversation[]>([])
|
||||
const [messagesLoading, setMessagesLoading] = useState<boolean>(false)
|
||||
const [messages, setMessages] = useState<ChatItem[]>([])
|
||||
const [detailLoading, setDetailLoading] = useState<boolean>(false)
|
||||
const [detail, setDetail] = useState<Detail | null>(null)
|
||||
const [selected, setSelected] = useState<Conversation | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
setSelected(null)
|
||||
setDetail(null)
|
||||
setData([])
|
||||
getConversations(id).then((res) => {
|
||||
const response = res as Conversation[]
|
||||
setData(response)
|
||||
setSelected(response[0] || null)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !selected || !selected.id) return
|
||||
getDetail(selected.id)
|
||||
}, [id, selected])
|
||||
|
||||
const getDetail = (conversationId: string) => {
|
||||
if (!id || !conversationId) return
|
||||
|
||||
setDetail(null)
|
||||
setMessages([])
|
||||
setDetailLoading(true)
|
||||
setMessagesLoading(true)
|
||||
getConversationMessages(id, conversationId)
|
||||
.then(res => {
|
||||
setMessages(res as ChatItem[])
|
||||
})
|
||||
.finally(() => {
|
||||
setMessagesLoading(false)
|
||||
})
|
||||
getConversationDetail(id, conversationId)
|
||||
.then(res => {
|
||||
setDetail(res as Detail)
|
||||
})
|
||||
.finally(() => {
|
||||
setDetailLoading(false)
|
||||
})
|
||||
}
|
||||
const timeRange = useMemo(() => {
|
||||
const times = messages.filter(m => m.created_at).map(m => Number(m.created_at))
|
||||
if (times.length === 0) return ''
|
||||
const minTime = Math.min(...times)
|
||||
const maxTime = Math.max(...times)
|
||||
return `${formatDateTime(minTime, 'YYYY.MM')} - ${formatDateTime(maxTime, 'YYYY.MM')}`
|
||||
}, [messages])
|
||||
|
||||
return (
|
||||
<div className="rb:h-[calc(100vh-64px)]! rb:w-full rb:-mx-4 rb:-my-3">
|
||||
{loading
|
||||
? <PageLoading />
|
||||
: data.length === 0
|
||||
? <Empty />
|
||||
:(
|
||||
<Row gutter={16} className="rb:h-full">
|
||||
<Col span={5}>
|
||||
<div className="rb:h-full! rb:border-r rb:border-[#EAECEE] rb:py-3 rb:px-4">
|
||||
{data.map(item => (
|
||||
<div key={item.id} className="rb:mb-3">
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
|
||||
})}
|
||||
onClick={() => getDetail(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Col>
|
||||
{selected && <>
|
||||
<Col span={19}>
|
||||
<div className="rb:text-[18px] rb:font-medium rb:leading-6 rb:mt-4">{selected.title}</div>
|
||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:leading-5">{timeRange}</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={t('workingDetail.conversationStream')}
|
||||
extra={<Button className="rb:h-6!" onClick={() => getDetail(selected.id)}>{t('workingDetail.refresh')}</Button>}
|
||||
className="rb:mt-4!"
|
||||
headerClassName='rb:bg-[#F6F8FC]! rb:border-b! rb:border-b-[#DFE4ED]! rb:min-h-11!'
|
||||
headerType="borderless"
|
||||
bodyClassName="rb:h-[calc(100vh-210px)]"
|
||||
>
|
||||
{messagesLoading
|
||||
? <Skeleton active />
|
||||
: messages.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<ChatContent
|
||||
classNames="rb:h-[calc(100vh-244px)]"
|
||||
data={messages}
|
||||
streamLoading={false}
|
||||
labelFormat={(item) => formatDateTime(item.created_at)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard className="rb:mt-4!" bodyClassName="rb:h-[calc(100vh-166px)] rb:overflow-y-auto">
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: detail
|
||||
? <>
|
||||
<>
|
||||
<div className="rb:text-[#369F21] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.successfulTitle')}</div>
|
||||
|
||||
{detail.takeaways.length > 0
|
||||
? (
|
||||
<ul className="rb:text-[#5B6167] rb:leading-5.5 rb:list-disc rb:ml-4">
|
||||
{detail.takeaways.map(vo => <li>{vo}</li>)}
|
||||
</ul>
|
||||
)
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
|
||||
<>
|
||||
<Divider />
|
||||
<div className="rb:text-[#FF5D34] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.question')}</div>
|
||||
|
||||
{detail.question.length > 0
|
||||
? (
|
||||
<ul className="rb:text-[#5B6167] rb:leading-5.5 rb:list-disc rb:ml-4">
|
||||
{detail.question.map(vo => <li>{vo}</li>)}
|
||||
</ul>
|
||||
)
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
|
||||
<>
|
||||
<Divider />
|
||||
<div className="rb:text-[#369F21] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.summary')}</div>
|
||||
{detail.summary
|
||||
? <RbAlert className="rb:text-[#212332]! rb:text-[14px]! rb:leading-5.5! rb:p-3!">{detail.summary}</RbAlert>
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</>
|
||||
</>
|
||||
: <Empty />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</>}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default WorkingDetail
|
||||
74
web/src/views/UserMemoryDetail/pages/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dropdown } from 'antd'
|
||||
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import StatementDetail from './StatementDetail'
|
||||
import ForgetDetail from './ForgetDetail'
|
||||
import ImplicitDetail from './ImplicitDetail'
|
||||
import ShortTermDetail from './ShortTermDetail'
|
||||
import PerceptualDetail from './PerceptualDetail'
|
||||
import EpisodicDetail from './EpisodicDetail'
|
||||
import ExplicitDetail from './ExplicitDetail'
|
||||
import WorkingDetail from './WorkingDetail'
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
|
||||
const Detail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id, type } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState<string>('')
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
getEndUserProfile(id).then((res) => {
|
||||
const response = res as { other_name: string; id: string; }
|
||||
setName(response.other_name || response.id)
|
||||
})
|
||||
}
|
||||
const items = useMemo(() => {
|
||||
return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGETTING_MANAGEMENT']
|
||||
.map(key => ({ key, label: t(`userMemory.${key}`) }))
|
||||
}, [t])
|
||||
const onClick = ({ key }: { key: string }) => {
|
||||
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<PageHeader
|
||||
name={name}
|
||||
source="node"
|
||||
operation={
|
||||
<Dropdown menu={{ items, onClick, selectedKeys: type ? [type] : [] }}>
|
||||
<div className="rb:cursor-pointer rb:group rb:flex rb:items-center rb:gap-1">
|
||||
- {type ? t(`userMemory.${type}`) : ''}
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/up_border.svg')] rb:transform-[rotate(180deg)] rb:group-hover:transform-[rotate(0deg)]"
|
||||
></div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail />}
|
||||
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
|
||||
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
||||
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />} {/** TODO */}
|
||||
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
|
||||
{type === 'WORKING_MEMORY' && <WorkingDetail />} {/** TODO */}
|
||||
{type === 'EXPLICIT_MEMORY' && <ExplicitDetail />} {/** TODO */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
@@ -44,6 +44,7 @@ export interface Data {
|
||||
export interface BaseProperties {
|
||||
content: string;
|
||||
created_at: number;
|
||||
associative_memory: number;
|
||||
}
|
||||
export interface StatementNodeProperties {
|
||||
temporal_info: string;
|
||||
@@ -51,12 +52,21 @@ export interface StatementNodeProperties {
|
||||
statement: string;
|
||||
valid_at: string;
|
||||
created_at: number;
|
||||
emotion_keywords: string[];
|
||||
emotion_type: string;
|
||||
emotion_subject: string;
|
||||
importance_score: number;
|
||||
associative_memory: number;
|
||||
}
|
||||
export interface ExtractedEntityNodeProperties {
|
||||
description: string;
|
||||
name: string;
|
||||
entity_type: string;
|
||||
created_at: number;
|
||||
aliases: string;
|
||||
connect_strngth: string;
|
||||
importance_score: number;
|
||||
associative_memory: number;
|
||||
}
|
||||
export interface MemorySummaryNode {
|
||||
id: string;
|
||||
@@ -72,7 +82,7 @@ export interface MemorySummaryNode {
|
||||
created_at: number;
|
||||
}
|
||||
caption: string;
|
||||
|
||||
associative_memory: number;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
@@ -140,4 +150,41 @@ export interface AboutMeRef {
|
||||
}
|
||||
export interface EndUserProfileRef {
|
||||
data: EndUser | null
|
||||
}
|
||||
|
||||
|
||||
export interface ForgetData {
|
||||
activation_metrics: {
|
||||
total_nodes: number;
|
||||
nodes_with_activation: number;
|
||||
nodes_without_activation: number;
|
||||
average_activation_value: number;
|
||||
low_activation_nodes: number;
|
||||
timestamp: number;
|
||||
forgetting_threshold: number;
|
||||
},
|
||||
node_distribution: {
|
||||
statement_count: number;
|
||||
entity_count: number;
|
||||
summary_count: number;
|
||||
chunk_count: number;
|
||||
},
|
||||
recent_trends: {
|
||||
date: string;
|
||||
merged_count: number;
|
||||
average_activation: number;
|
||||
total_nodes: number;
|
||||
execution_time: number;
|
||||
}[],
|
||||
pending_nodes: {
|
||||
node_id: string;
|
||||
node_type: string;
|
||||
content_summary: string;
|
||||
activation_value: number;
|
||||
last_access_time: number;
|
||||
}[],
|
||||
timestamp: number;
|
||||
}
|
||||
export interface GraphDetailRef {
|
||||
handleOpen: (vo: Node) => void
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, Checkbox, InputNumber } from 'antd';
|
||||
import { Form, Input, Select, InputNumber } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ChatVariableModalRef } from './types'
|
||||
@@ -26,7 +26,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
const [form] = Form.useForm<ChatVariable>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined)
|
||||
const typeValue = Form.useWatch('type', form);
|
||||
const type = Form.useWatch('type', form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
@@ -39,7 +39,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
const handleOpen = (variable?: ChatVariable, index?: number) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
form.setFieldsValue(variable)
|
||||
const { default: _, ...rest } = variable
|
||||
form.setFieldsValue({ ...rest })
|
||||
setEditIndex(index)
|
||||
} else {
|
||||
form.resetFields();
|
||||
@@ -49,7 +50,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then((values) => {
|
||||
refresh({ ...values }, editIndex)
|
||||
refresh({ ...values, default: values.defaultValue }, editIndex)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
@@ -90,52 +91,36 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={() => form.setFieldValue('default', undefined)}
|
||||
onChange={() => form.setFieldValue('defaultValue', undefined)}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="default"
|
||||
<Form.Item
|
||||
name="defaultValue"
|
||||
label={t('workflow.config.parameter-extractor.default')}
|
||||
>
|
||||
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}>
|
||||
{({ getFieldValue }) => {
|
||||
const type = getFieldValue('type');
|
||||
if (type === 'number') {
|
||||
return <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />;
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return (
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: true, label: 'true' },
|
||||
{ value: false, label: 'false' }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Input placeholder={t('common.enter')} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
|
||||
{type === 'number'
|
||||
? <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
|
||||
: type === 'boolean'
|
||||
? <Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: true, label: 'true' },
|
||||
{ value: false, label: 'false' }
|
||||
]}
|
||||
/>
|
||||
: <Input placeholder={t('common.enter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('workflow.config.parameter-extractor.desc')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="required"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>{t('workflow.config.parameter-extractor.required')}</Checkbox>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ const AddChatVariable = forwardRef<AddChatVariableRef, AddChatVariableProps>(({
|
||||
}
|
||||
const handleSave = (value: ChatVariable, index?: number) => {
|
||||
const list = [...variables]
|
||||
if (index && index > -1) {
|
||||
if (typeof index === 'number' && index > -1) {
|
||||
list[index] = value
|
||||
} else {
|
||||
list.push(value)
|
||||
@@ -75,17 +75,15 @@ const AddChatVariable = forwardRef<AddChatVariableRef, AddChatVariableProps>(({
|
||||
dataSource={variables}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:group rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div key={index} className="rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:leading-4">
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
|
||||
</div>
|
||||
<span className="rb:block rb:group-hover:hidden rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.description}</div>
|
||||
<Space size={12} className="rb:hidden! rb:group-hover:flex! rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<Space size={12} className="rb:flex rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
|
||||
@@ -11,8 +11,8 @@ export interface VariableFormData {
|
||||
name: string;
|
||||
type: ChatVariable['type'];
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
defaultValue?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface ChatVariableModalRef {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { type FC, useState, useEffect } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
@@ -23,6 +23,7 @@ interface LexicalEditorProps {
|
||||
options: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
height?: number;
|
||||
enableJinja2?: boolean;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -33,6 +34,15 @@ const theme = {
|
||||
},
|
||||
};
|
||||
|
||||
const jinja2Theme = {
|
||||
...theme,
|
||||
code: 'jinja2-expression',
|
||||
text: {
|
||||
...theme.text,
|
||||
code: 'jinja2-inline',
|
||||
},
|
||||
};
|
||||
|
||||
const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
@@ -40,19 +50,62 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
options,
|
||||
variant = 'borderless',
|
||||
height = 60,
|
||||
enableJinja2 = false,
|
||||
}) => {
|
||||
|
||||
const [_count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (enableJinja2) {
|
||||
const styleId = 'jinja2-styles';
|
||||
let existingStyle = document.getElementById(styleId);
|
||||
|
||||
if (!existingStyle) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
.jinja2-expression {
|
||||
background-color: #f6f8fa !important;
|
||||
border: 1px solid #d1d9e0 !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 2px 4px !important;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||
font-size: 13px !important;
|
||||
color: #0969da !important;
|
||||
}
|
||||
.jinja2-inline {
|
||||
background-color: #f6f8fa !important;
|
||||
padding: 1px 3px !important;
|
||||
border-radius: 2px !important;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||
font-size: 13px !important;
|
||||
color: #0969da !important;
|
||||
}
|
||||
.editor-paragraph {
|
||||
margin: 0;
|
||||
}
|
||||
.editor-paragraph:has-text('{') .editor-text,
|
||||
.editor-paragraph:has-text('[') .editor-text {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}, [enableJinja2]);
|
||||
const initialConfig = {
|
||||
namespace: 'AutocompleteEditor',
|
||||
theme,
|
||||
nodes: [
|
||||
theme: enableJinja2 ? jinja2Theme : theme,
|
||||
nodes: enableJinja2 ? [
|
||||
// 当启用jinja2时,不使用VariableNode,使用普通文本
|
||||
] : [
|
||||
// HeadingNode,
|
||||
// QuoteNode,
|
||||
// ListItemNode,
|
||||
// ListNode,
|
||||
// LinkNode,
|
||||
// CodeNode,
|
||||
VariableNode
|
||||
VariableNode,
|
||||
],
|
||||
onError: (error: Error) => {
|
||||
console.error(error);
|
||||
@@ -96,9 +149,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
<AutocompletePlugin options={options} />
|
||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} options={options} />
|
||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface Suggestion {
|
||||
disabled?: boolean; // 标记是否禁用
|
||||
}
|
||||
|
||||
const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -82,7 +82,32 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
}, [editor]);
|
||||
|
||||
const insertMention = (suggestion: Suggestion) => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
||||
if (enableJinja2) {
|
||||
// 在jinja2模式下,插入{{variable}}格式的文本
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const anchorOffset = selection.anchor.offset;
|
||||
const nodeText = anchorNode.getTextContent();
|
||||
|
||||
// 移除触发字符'/'
|
||||
const textBefore = nodeText.substring(0, anchorOffset - 1);
|
||||
const textAfter = nodeText.substring(anchorOffset);
|
||||
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
|
||||
|
||||
anchorNode.setTextContent(newText);
|
||||
|
||||
// 设置光标位置到插入文本之后
|
||||
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
|
||||
selection.anchor.offset = newOffset;
|
||||
selection.focus.offset = newOffset;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 普通模式下使用VariableNode
|
||||
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
||||
}
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,18 +14,23 @@ const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number
|
||||
let serializedContent = '';
|
||||
|
||||
// Traverse all nodes and serialize properly
|
||||
const paragraphs: string[] = [];
|
||||
root.getChildren().forEach(child => {
|
||||
if ($isParagraphNode(child)) {
|
||||
let paragraphContent = '';
|
||||
child.getChildren().forEach(node => {
|
||||
if ($isVariableNode(node)) {
|
||||
serializedContent += node.getTextContent();
|
||||
paragraphContent += node.getTextContent();
|
||||
} else {
|
||||
serializedContent += node.getTextContent();
|
||||
paragraphContent += node.getTextContent();
|
||||
}
|
||||
});
|
||||
paragraphs.push(paragraphContent);
|
||||
}
|
||||
});
|
||||
|
||||
serializedContent = paragraphs.join('\n');
|
||||
|
||||
setCount(serializedContent.length);
|
||||
onChange?.(serializedContent);
|
||||
});
|
||||
|
||||
@@ -8,14 +8,31 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
||||
interface InitialValuePluginProps {
|
||||
value: string;
|
||||
options?: Suggestion[];
|
||||
enableJinja2?: boolean;
|
||||
}
|
||||
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const initializedRef = useRef(false);
|
||||
const prevValueRef = useRef<string>('');
|
||||
const isUserInputRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initializedRef.current && value) {
|
||||
// 监听编辑器变化,标记是否为用户输入
|
||||
const removeListener = editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const textContent = root.getTextContent();
|
||||
if (textContent !== prevValueRef.current) {
|
||||
isUserInputRef.current = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return removeListener;
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== prevValueRef.current && !isUserInputRef.current) {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
root.clear();
|
||||
@@ -26,8 +43,13 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
parts.forEach(part => {
|
||||
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
|
||||
const contextMatch = part.match(/^\{\{context\}\}$/);
|
||||
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
||||
|
||||
if (enableJinja2) {
|
||||
paragraph.append($createTextNode(part));
|
||||
return;
|
||||
}
|
||||
|
||||
// 匹配{{context}}格式
|
||||
if (contextMatch) {
|
||||
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
||||
if (contextSuggestion) {
|
||||
@@ -38,7 +60,19 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
return
|
||||
}
|
||||
|
||||
// 匹配普通变量{{nodeId.label}}格式
|
||||
if (conversationMatch) {
|
||||
const [_, variableName] = conversationMatch;
|
||||
const conversationSuggestion = options.find(s =>
|
||||
s.group === 'CONVERSATION' && s.label === variableName
|
||||
);
|
||||
if (conversationSuggestion) {
|
||||
paragraph.append($createVariableNode(conversationSuggestion));
|
||||
} else {
|
||||
paragraph.append($createTextNode(part));
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const [_, nodeId, label] = match;
|
||||
|
||||
@@ -60,11 +94,12 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
});
|
||||
|
||||
root.append(paragraph);
|
||||
});
|
||||
|
||||
initializedRef.current = true;
|
||||
}, { discrete: true });
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
prevValueRef.current = value;
|
||||
isUserInputRef.current = false;
|
||||
}, [value, options, editor, enableJinja2]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical';
|
||||
|
||||
const JsonHighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
// Check if text contains JSON-like patterns
|
||||
if (containsJsonPatterns(text)) {
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
// Split text into tokens and create new nodes with appropriate classes
|
||||
const tokens = tokenizeJson(text);
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
|
||||
// Set format based on token type
|
||||
switch (token.type) {
|
||||
case 'string':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #032f62');
|
||||
break;
|
||||
case 'number':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #005cc5');
|
||||
break;
|
||||
case 'boolean':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #d73a49');
|
||||
break;
|
||||
case 'null':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #6f42c1');
|
||||
break;
|
||||
case 'key':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #22863a; font-weight: bold');
|
||||
break;
|
||||
case 'punctuation':
|
||||
newNode.setFormat('code');
|
||||
newNode.setStyle('color: #24292e');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
// Replace the original text node with the new highlighted nodes
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function containsJsonPatterns(text: string): boolean {
|
||||
// Check for JSON-like patterns
|
||||
return /[{}\[\]:,]/.test(text) ||
|
||||
/"[^"]*"/.test(text) ||
|
||||
/\b\d+(\.\d+)?\b/.test(text) ||
|
||||
/\b(true|false|null)\b/.test(text);
|
||||
}
|
||||
|
||||
function tokenizeJson(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match;
|
||||
|
||||
if (string) {
|
||||
// Check if it's a key (followed by colon)
|
||||
const afterMatch = text.slice(match.index + fullMatch.length).trim();
|
||||
if (afterMatch.startsWith(':')) {
|
||||
tokens.push({ text: fullMatch, type: 'key' });
|
||||
} else {
|
||||
tokens.push({ text: fullMatch, type: 'string' });
|
||||
}
|
||||
} else if (punctuation) {
|
||||
tokens.push({ text: fullMatch, type: 'punctuation' });
|
||||
} else if (number) {
|
||||
tokens.push({ text: fullMatch, type: 'number' });
|
||||
} else if (boolean) {
|
||||
if (fullMatch === 'null') {
|
||||
tokens.push({ text: fullMatch, type: 'null' });
|
||||
} else {
|
||||
tokens.push({ text: fullMatch, type: 'boolean' });
|
||||
}
|
||||
} else if (whitespace || other) {
|
||||
tokens.push({ text: fullMatch, type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default JsonHighlightPlugin;
|
||||
@@ -13,13 +13,15 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const handleNodeSelect = (selectedNodeType: any) => {
|
||||
const parentBBox = node.getBBox();
|
||||
const cycleId = data.cycle;
|
||||
|
||||
|
||||
const id = `${selectedNodeType.type.replace(/-/g, '_') }_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const newNode = graph.addNode({
|
||||
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||
x: parentBBox.x,
|
||||
y: parentBBox.y,
|
||||
id,
|
||||
data: {
|
||||
id: `${selectedNodeType.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
id,
|
||||
type: selectedNodeType.type,
|
||||
icon: selectedNodeType.icon,
|
||||
name: t(`workflow.${selectedNodeType.type}`),
|
||||
|
||||
@@ -75,12 +75,15 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const parentBBox = node.getBBox();
|
||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||
|
||||
|
||||
const cycleStartNodeId = `cycle_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const cycleStartNode = graph.addNode({
|
||||
...graphNodeLibrary.cycleStart,
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
id: cycleStartNodeId,
|
||||
data: {
|
||||
id: cycleStartNodeId,
|
||||
type: 'cycle-start',
|
||||
parentId: node.id,
|
||||
isDefault: true, // 标记为默认节点,不可删除
|
||||
|
||||
@@ -43,12 +43,14 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
const newY = sourceBBox.y;
|
||||
|
||||
// 创建新节点
|
||||
const id = `${selectedNodeType.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const newNode = graph.addNode({
|
||||
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||
x: newX,
|
||||
y: newY,
|
||||
id,
|
||||
data: {
|
||||
id: `${selectedNodeType.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
id,
|
||||
type: selectedNodeType.type,
|
||||
icon: selectedNodeType.icon,
|
||||
name: t(`workflow.${selectedNodeType.type}`),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col, Select } from 'antd'
|
||||
import { Form, Input, Row, Col, Select, InputNumber, Radio } from 'antd'
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
@@ -11,6 +11,23 @@ interface AssignmentListProps {
|
||||
options: Suggestion[];
|
||||
}
|
||||
|
||||
const operationsObj = {
|
||||
number: [
|
||||
{ value: 'cover', label: 'workflow.config.assigner.cover' },
|
||||
{ value: 'clear', label: 'workflow.config.assigner.clear' },
|
||||
{ value: 'assign', label: 'workflow.config.assigner.assign' },
|
||||
{ value: 'add', label: 'workflow.config.assigner.add' },
|
||||
{ value: 'subtract', label: 'workflow.config.assigner.subtract' },
|
||||
{ value: 'multiply', label: 'workflow.config.assigner.multiply' },
|
||||
{ value: 'divide', label: 'workflow.config.assigner.divide' },
|
||||
],
|
||||
default: [
|
||||
{ value: 'cover', label: 'workflow.config.assigner.cover' },
|
||||
{ value: 'clear', label: 'workflow.config.assigner.clear' },
|
||||
{ value: 'assign', label: 'workflow.config.assigner.assign' },
|
||||
],
|
||||
}
|
||||
|
||||
const AssignmentList: FC<AssignmentListProps> = ({
|
||||
parentName,
|
||||
options = [],
|
||||
@@ -27,6 +44,11 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
<PlusOutlined onClick={() => add({ operation: 'cover'})} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
const dataType = selectedOption?.dataType;
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
@@ -40,6 +62,10 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'operation'], undefined);
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -50,11 +76,11 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'cover', label: t('workflow.config.assigner.cover') },
|
||||
{ value: 'clear', label: t('workflow.config.assigner.clear') },
|
||||
{ value: 'assign', label: t('workflow.config.assigner.assign') },
|
||||
]}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={operationOptions.map(op => ({
|
||||
...op,
|
||||
label: t(op.label)
|
||||
}))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
@@ -77,20 +103,38 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
rules={[{ required: true, message: 'Missing last name' }]}
|
||||
>
|
||||
{operation === 'assign' ? (
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<VariableSelect
|
||||
{dataType === 'number' && operation === 'cover'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
)}
|
||||
: dataType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
/>
|
||||
: operation === 'assign'
|
||||
? <>
|
||||
{dataType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
: <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider } from 'antd'
|
||||
import { Form, Button, Select, Space, Row, Col, Divider, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
@@ -9,37 +9,50 @@ import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>) => void;
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void;
|
||||
options: Suggestion[];
|
||||
name: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
}
|
||||
const operatorList = [
|
||||
"empty",
|
||||
"not_empty",
|
||||
"contains",
|
||||
"not_contains",
|
||||
"startwith",
|
||||
"endwith",
|
||||
"eq",
|
||||
"ne",
|
||||
"lt",
|
||||
"le",
|
||||
"gt",
|
||||
"ge"
|
||||
]
|
||||
const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
default: [
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
{ value: 'startwith', label: 'workflow.config.if-else.startwith' },
|
||||
{ value: 'endwith', label: 'workflow.config.if-else.endwith' },
|
||||
{ value: 'eq', label: 'workflow.config.if-else.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.ne' },
|
||||
],
|
||||
number: [
|
||||
{ value: 'eq', label: 'workflow.config.if-else.num.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.num.ne' },
|
||||
{ value: 'lt', label: 'workflow.config.if-else.num.lt' },
|
||||
{ value: 'le', label: 'workflow.config.if-else.num.le' },
|
||||
{ value: 'gt', label: 'workflow.config.if-else.num.gt' },
|
||||
{ value: 'ge', label: 'workflow.config.if-else.num.ge' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
boolean: [
|
||||
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
]
|
||||
}
|
||||
|
||||
const CaseList: FC<CaseListProps> = ({
|
||||
value = [],
|
||||
options,
|
||||
name,
|
||||
onChange,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
||||
if (!selectedNode || !graphRef?.current) return;
|
||||
@@ -175,29 +188,49 @@ const CaseList: FC<CaseListProps> = ({
|
||||
});
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleChangeLogicalOperator = (index: number) => {
|
||||
const newValue = [...value]
|
||||
newValue[index] = {
|
||||
...newValue[index],
|
||||
logical_operator: newValue[index].logical_operator === 'and' ? 'or' : 'and'
|
||||
}
|
||||
onChange && onChange(newValue)
|
||||
}
|
||||
const currentValue = form.getFieldValue([name, index, 'logical_operator']);
|
||||
form.setFieldValue([name, index, 'logical_operator'], currentValue === 'and' ? 'or' : 'and');
|
||||
};
|
||||
|
||||
const handleLeftFieldChange = (caseIndex: number, conditionIndex: number, newValue: string) => {
|
||||
form.setFieldsValue({
|
||||
[name]: {
|
||||
[caseIndex]: {
|
||||
expressions: {
|
||||
[conditionIndex]: {
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
right: undefined,
|
||||
input_type: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddCase = (addCaseFunc: Function) => {
|
||||
addCaseFunc({ logical_operator: 'and', expressions: [] });
|
||||
setTimeout(() => {
|
||||
updateNodePorts((value?.length || 0) + 1);
|
||||
const currentCases = form.getFieldValue(name) || [];
|
||||
updateNodePorts(currentCases.length);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRemoveCase = (removeCaseFunc: Function, fieldName: number, caseIndex: number) => {
|
||||
removeCaseFunc(fieldName);
|
||||
setTimeout(() => {
|
||||
updateNodePorts((value?.length || 1) - 1, caseIndex);
|
||||
const currentCases = form.getFieldValue(name) || [];
|
||||
updateNodePorts(currentCases.length, caseIndex);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleInputTypeChange = (caseIndex: number, conditionIndex: number) => {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
@@ -207,6 +240,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<div key={caseField.key}>
|
||||
<Form.List name={[caseField.name, 'expressions']}>
|
||||
{(conditionFields, { add: addCondition, remove: removeCondition }) => {
|
||||
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
|
||||
return (
|
||||
<div className={clsx("rb:relative rb:mb-4 rb:border rb:border-gray-200 rb:rounded rb:p-3 rb:pl-5")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
@@ -218,7 +252,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<Space>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => addCondition()}
|
||||
onClick={() => addCondition({})}
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addCase')}
|
||||
@@ -234,15 +268,22 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={() => handleChangeLogicalOperator(caseIndex)}>{value?.[caseIndex].logical_operator}</Button>
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{conditionFields.map((conditionField, conditionIndex) => {
|
||||
const currentOperator = value?.[caseIndex]?.expressions?.[conditionIndex]?.comparison_operator;
|
||||
const cases = form.getFieldValue(name) || [];
|
||||
const currentCase = cases[caseIndex] || {};
|
||||
const currentExpression = currentCase.expressions?.[conditionIndex] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
@@ -257,18 +298,20 @@ const CaseList: FC<CaseListProps> = ({
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'comparison_operator']} noStyle>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
options={operatorList.map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -280,11 +323,52 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!hideRightField && (
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!hideRightField && <>
|
||||
{leftFieldType === 'number'
|
||||
? <Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
: <Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Editor options={options} />
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider } from 'antd'
|
||||
import { Form, Button, Select, Row, Col, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
@@ -10,7 +9,7 @@ import Editor from '../../Editor'
|
||||
|
||||
interface Case {
|
||||
logical_operator: 'and' | 'or';
|
||||
expressions: Array<{ left: string; comparison_operator: string; right: string; }>
|
||||
expressions: Array<{ left: string; operator: string; right: string; input_type: string; }>
|
||||
}
|
||||
|
||||
interface CaseListProps {
|
||||
@@ -22,36 +21,65 @@ interface CaseListProps {
|
||||
graphRef?: any;
|
||||
addBtnText?: string;
|
||||
}
|
||||
const operatorList = [
|
||||
"empty",
|
||||
"not_empty",
|
||||
"contains",
|
||||
"not_contains",
|
||||
"startwith",
|
||||
"endwith",
|
||||
"eq",
|
||||
"ne",
|
||||
"lt",
|
||||
"le",
|
||||
"gt",
|
||||
"ge"
|
||||
]
|
||||
const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
default: [
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
{ value: 'startwith', label: 'workflow.config.if-else.startwith' },
|
||||
{ value: 'endwith', label: 'workflow.config.if-else.endwith' },
|
||||
{ value: 'eq', label: 'workflow.config.if-else.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.ne' },
|
||||
],
|
||||
number: [
|
||||
{ value: 'eq', label: 'workflow.config.if-else.num.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.num.ne' },
|
||||
{ value: 'lt', label: 'workflow.config.if-else.num.lt' },
|
||||
{ value: 'le', label: 'workflow.config.if-else.num.le' },
|
||||
{ value: 'gt', label: 'workflow.config.if-else.num.gt' },
|
||||
{ value: 'ge', label: 'workflow.config.if-else.num.ge' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
boolean: [
|
||||
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
]
|
||||
}
|
||||
|
||||
const ConditionList: FC<CaseListProps> = ({
|
||||
value,
|
||||
options,
|
||||
parentName,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const handleLeftFieldChange = (index: number, newValue: string) => {
|
||||
form.setFieldsValue({
|
||||
[parentName]: {
|
||||
expressions: {
|
||||
[index]: {
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
right: undefined,
|
||||
input_type: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputTypeChange = (index: number) => {
|
||||
form.setFieldValue([parentName, 'expressions', index, 'right'], undefined);
|
||||
};
|
||||
|
||||
const handleChangeLogicalOperator = () => {
|
||||
if (!value) return;
|
||||
onChange && onChange({
|
||||
logical_operator: value.logical_operator === 'and' ? 'or' : 'and',
|
||||
expressions: value.expressions || []
|
||||
})
|
||||
}
|
||||
const currentValue = form.getFieldValue([parentName, 'logical_operator']);
|
||||
form.setFieldValue([parentName, 'logical_operator'], currentValue === 'and' ? 'or' : 'and');
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[parentName, 'expressions']}>
|
||||
@@ -59,8 +87,16 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<div>
|
||||
<div className="rb:relative">
|
||||
{fields.map((field, index) => {
|
||||
const currentOperator = value?.expressions?.[index]?.comparison_operator;
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:mb-3">
|
||||
@@ -68,7 +104,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{value?.logical_operator}</Button>
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>)}
|
||||
@@ -82,16 +118,17 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(index, val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||
<Form.Item name={[field.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
options={operatorList.map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
@@ -104,14 +141,57 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{!hideRightField && (
|
||||
<Col span={24}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{!hideRightField && <>
|
||||
{leftFieldType === 'number'
|
||||
? <Col span={24}><Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={() => handleInputTypeChange(index)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row></Col>
|
||||
: <Col span={24}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Editor options={options} />
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
</>}
|
||||
|
||||
</Row>
|
||||
</div>
|
||||
@@ -122,7 +202,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({ left: '', comparison_operator: '', right: '' })}
|
||||
onClick={() => add({ left: '', operator: '', right: '' })}
|
||||
className="rb:w-full rb:ml-6 rb:mt-2"
|
||||
icon={<span>+</span>}
|
||||
>
|
||||
|
||||
@@ -65,7 +65,7 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
label: `${childData.name || childData.type}.${key}`,
|
||||
type: 'output',
|
||||
dataType: 'string',
|
||||
value: `{{${childData.id}.${key}}}`,
|
||||
value: `${childData.id}.${key}`,
|
||||
nodeData: childData
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name={[name,0, 'key']}
|
||||
noStyle
|
||||
>
|
||||
{t('workflow.config.var-aggregator.variable')}
|
||||
@@ -34,9 +33,8 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name={[name, 0, 'value']}
|
||||
name={name}
|
||||
noStyle
|
||||
rules={[{ required: true, message: 'Missing last name' }]}
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
@@ -76,7 +74,6 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
rules={[{ required: true, message: 'Missing last name' }]}
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
|
||||
@@ -28,12 +28,18 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
|
||||
const handleOpen = (data?: HttpRequestConfigForm['auth']) => {
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
const initialValues = {
|
||||
auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key',
|
||||
auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type,
|
||||
header: data.header,
|
||||
api_key: data.api_key
|
||||
})
|
||||
}
|
||||
form.setFieldValue('auth', initialValues.auth)
|
||||
if (initialValues.auth !== 'none') {
|
||||
setTimeout(() => {
|
||||
form.setFieldsValue(initialValues)
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
@@ -91,6 +97,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
<FormItem
|
||||
name="auth"
|
||||
label={t('workflow.config.http-request.authType')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') }
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
@@ -103,6 +112,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
<FormItem
|
||||
name="auth_type"
|
||||
label={t('workflow.config.http-request.authType')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') }
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
|
||||
@@ -1,230 +1,175 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Select, Table } from 'antd';
|
||||
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import Editor from '../../Editor';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||
import Empty from '@/components/Empty';
|
||||
import VariableSelect from '../VariableSelect';
|
||||
|
||||
export interface TableRow {
|
||||
key: string;
|
||||
name: string;
|
||||
value: string;
|
||||
key?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface EditableTableProps {
|
||||
parentName: string | string[];
|
||||
title?: string;
|
||||
value?: Record<string, string> | TableRow[];
|
||||
onChange?: (value: TableRow[]) => void;
|
||||
options?: Suggestion[];
|
||||
typeOptions?: {value: string, label: string}[]
|
||||
typeOptions?: { value: string, label: string }[]
|
||||
filterBooleanType?: boolean;
|
||||
}
|
||||
|
||||
const EditableTable: React.FC<EditableTableProps> = ({
|
||||
parentName,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
options = [],
|
||||
typeOptions = []
|
||||
typeOptions = [],
|
||||
filterBooleanType = false
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [rows, setRows] = useState<TableRow[]>([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('EditableTable value', value)
|
||||
if (Array.isArray(value)) {
|
||||
setRows([...value])
|
||||
} else if (value && Object.keys(value).length > 0) {
|
||||
// Only update if rows are empty or significantly different
|
||||
const valueEntries = Object.entries(value)
|
||||
if (rows.length === 0 || rows.length !== valueEntries.length) {
|
||||
setRows(valueEntries.map(([key, val], index) => {
|
||||
console.log('val', val)
|
||||
return {
|
||||
key: index.toString(),
|
||||
name: key || '',
|
||||
value: val || '',
|
||||
type: typeOptions.length > 0 ? typeOptions[0].value : undefined
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
setRows([])
|
||||
}
|
||||
}, [JSON.stringify(value), typeOptions.length])
|
||||
const createNewRow = (): TableRow => ({
|
||||
name: undefined,
|
||||
value: undefined,
|
||||
...(typeOptions.length > 0 && { type: typeOptions[0].value })
|
||||
});
|
||||
|
||||
const handleChange = (key: string, field: 'name' | 'value' | 'type', val: string) => {
|
||||
const newRows = [...rows.map(row =>
|
||||
row.key === key ? { ...row, [field]: val } : row
|
||||
)];
|
||||
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||
const hasType = typeOptions.length > 0;
|
||||
const baseWidth = hasType ? '35%' : '45%';
|
||||
|
||||
setRows(newRows);
|
||||
onChange?.(newRows);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
const newKey = Date.now().toString();
|
||||
if (typeOptions.length) {
|
||||
setRows([...rows, { key: newKey, name: '', value: '', type: typeOptions[0].value }]);
|
||||
} else {
|
||||
setRows([...rows, { key: newKey, name: '', value: '' }]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (key: string, index: number) => {
|
||||
console.log('index', index)
|
||||
|
||||
if (rows.length === 1) {
|
||||
setRows([]);
|
||||
onChange?.([]);
|
||||
} else {
|
||||
const newRows = rows.filter(row => row.key !== key);
|
||||
setRows(newRows);
|
||||
onChange?.(newRows);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = typeOptions?.length > 0 ? [
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
dataIndex: 'name',
|
||||
width: '45%',
|
||||
render: (text: string, record: TableRow) => (
|
||||
<Editor
|
||||
options={options}
|
||||
value={text}
|
||||
height={32}
|
||||
variant="outlined"
|
||||
onChange={(value) => handleChange(record.key, 'name', value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('workflow.config.type'),
|
||||
dataIndex: 'type',
|
||||
width: '20%',
|
||||
render: (text: string, record: TableRow) => (
|
||||
<Select
|
||||
value={text}
|
||||
options={typeOptions}
|
||||
onChange={(value) => {
|
||||
console.log('value record', value)
|
||||
handleChange(record.key, 'type', value)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('workflow.config.value'),
|
||||
dataIndex: 'value',
|
||||
width: '45%',
|
||||
render: (text: string, record: TableRow) => {
|
||||
if (record.type === 'file') {
|
||||
|
||||
return (
|
||||
<VariableSelect
|
||||
return [
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
dataIndex: 'name',
|
||||
width: baseWidth,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item name={[index, 'name']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
options={options}
|
||||
value={text}
|
||||
onChange={(value) => {
|
||||
console.log('value record', value)
|
||||
handleChange(record.key, 'value', value)
|
||||
}}
|
||||
filterBooleanType={filterBooleanType}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Editor
|
||||
options={options}
|
||||
value={text}
|
||||
height={32}
|
||||
variant="outlined"
|
||||
onChange={(value) => {
|
||||
console.log('value record', value)
|
||||
handleChange(record.key, 'value', value)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
width: '10%',
|
||||
render: (_: any, record: TableRow, index: number) => (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.key, index)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
] : [
|
||||
{
|
||||
title: '键',
|
||||
dataIndex: 'name',
|
||||
width: '45%',
|
||||
render: (text: string, record: TableRow) => (
|
||||
<Editor
|
||||
options={options}
|
||||
value={text}
|
||||
height={32}
|
||||
variant="outlined"
|
||||
onChange={(value) => handleChange(record.key, 'name', value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
width: '45%',
|
||||
render: (text: string, record: TableRow) => (
|
||||
<Editor
|
||||
options={options}
|
||||
value={text}
|
||||
height={32}
|
||||
variant="outlined"
|
||||
onChange={(value) => handleChange(record.key, 'value', value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
width: '10%',
|
||||
render: (_: any, record: TableRow, index: number) => (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.key, index)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
...(hasType ? [{
|
||||
title: t('workflow.config.type'),
|
||||
dataIndex: 'type',
|
||||
width: '20%',
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => (
|
||||
<Form.Item name={[index, 'type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
options={typeOptions}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
}] : []),
|
||||
{
|
||||
title: t('workflow.config.value'),
|
||||
dataIndex: 'value',
|
||||
width: baseWidth,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
const prevType = prevValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type;
|
||||
const currentType = currentValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type;
|
||||
return prevType !== currentType;
|
||||
}}
|
||||
noStyle
|
||||
>
|
||||
{(form) => {
|
||||
const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']);
|
||||
const filteredOptions = currentType === 'file'
|
||||
? options.filter(option => option.dataType === 'file')
|
||||
: options;
|
||||
|
||||
return (
|
||||
<Form.Item name={[index, 'value']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
options={filteredOptions}
|
||||
filterBooleanType={filterBooleanType}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'actions',
|
||||
width: '10%',
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Button type="text" icon={<DeleteOutlined />} onClick={() => remove(index)} />
|
||||
)
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rb:mb-4">
|
||||
{title && <div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium">{title}</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
size="small"
|
||||
/>
|
||||
</div>}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: <Empty size={88} /> }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
{!title &&
|
||||
<Button type="dashed" onClick={handleAdd} block className='rb:mt-1'>
|
||||
+{t('common.add')}
|
||||
</Button>
|
||||
}
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => {
|
||||
const AddButton = ({ block = false }: { block?: boolean }) => (
|
||||
<Button
|
||||
type={block ? "dashed" : "text"}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1" : ""}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium">{title}</div>
|
||||
<AddButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Table<TableRow>
|
||||
bordered
|
||||
dataSource={fields.map((field) => ({
|
||||
key: String(field.key),
|
||||
name: undefined,
|
||||
value: undefined,
|
||||
type: undefined
|
||||
}))}
|
||||
columns={getColumns(remove)}
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: <Empty size={88} /> }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
|
||||
{!title && <AddButton block />}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { type FC, useEffect, useRef } from "react";
|
||||
import { type FC, useRef } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Slider } from 'antd'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd'
|
||||
import Editor from '../../Editor'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import AuthConfigModal from './AuthConfigModal'
|
||||
import type { AuthConfigModalRef, HttpRequestConfigForm } from './types'
|
||||
import VariableSelect from "../VariableSelect";
|
||||
import MessageEditor from '../MessageEditor'
|
||||
import EditableTable, { type TableRow } from './EditableTable'
|
||||
import EditableTable from './EditableTable'
|
||||
|
||||
const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({
|
||||
options,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
@@ -22,29 +24,45 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
}
|
||||
const handleRefresh = (auth: HttpRequestConfigForm['auth']) => {
|
||||
console.log('handleRefresh', auth)
|
||||
form.setFieldsValue({ auth: {...auth} })
|
||||
form.setFieldsValue({ auth })
|
||||
}
|
||||
|
||||
const handleChangeBodyContentType = (contentType: string) => {
|
||||
const currentValues = form.getFieldsValue()
|
||||
const handleChangeBodyContentType = () => {
|
||||
form.setFieldValue(['body', 'data'], undefined)
|
||||
}
|
||||
|
||||
const handleChangeErrorHandleMethod = (method: string) => {
|
||||
form.setFieldsValue({
|
||||
body: {
|
||||
...currentValues?.body,
|
||||
content_type: contentType,
|
||||
data: undefined
|
||||
error_handle: {
|
||||
method,
|
||||
body: undefined,
|
||||
status_code: undefined,
|
||||
headers: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateObjectList = (data: TableRow[], key: string) => {
|
||||
let obj: Record<string, string> = {}
|
||||
if (data.length) {
|
||||
data.forEach(vo => {
|
||||
obj[vo.name] = vo.value
|
||||
})
|
||||
|
||||
// 更新节点连接桩
|
||||
console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current)
|
||||
if (selectedNode && graphRef?.current) {
|
||||
const existingPorts = selectedNode.getPorts();
|
||||
const errorPort = existingPorts.find((port: any) => port.id === 'ERROR');
|
||||
|
||||
if (method === 'branch' && !errorPort) {
|
||||
// 添加异常节点连接桩
|
||||
selectedNode.addPort({
|
||||
id: 'ERROR',
|
||||
group: 'right',
|
||||
attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
} else if (method !== 'branch' && errorPort) {
|
||||
// 移除异常节点连接桩和相关连线
|
||||
const edges = graphRef.current.getEdges().filter((edge: any) =>
|
||||
edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR'
|
||||
);
|
||||
edges.forEach((edge: any) => graphRef.current.removeCell(edge));
|
||||
selectedNode.removePort('ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldValue(key, obj)
|
||||
}
|
||||
|
||||
console.log('HttpRequest', values)
|
||||
@@ -81,17 +99,19 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
|
||||
<Form.Item name="headers">
|
||||
<EditableTable
|
||||
parentName="headers"
|
||||
title="HEADERS"
|
||||
options={options}
|
||||
onChange={(headers) => updateObjectList(headers, 'headers')}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="params">
|
||||
<EditableTable
|
||||
parentName="params"
|
||||
title="PARAMS"
|
||||
options={options}
|
||||
onChange={(params) => updateObjectList(params, 'params')}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -113,15 +133,9 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
{values?.body?.content_type === 'form-data' &&
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
onChange={(data) => {
|
||||
form.setFieldsValue({
|
||||
body: {
|
||||
...form.getFieldValue('body'),
|
||||
data
|
||||
}
|
||||
})
|
||||
}}
|
||||
filterBooleanType={true}
|
||||
typeOptions={[
|
||||
{ label: 'text', value: 'text' },
|
||||
{ label: 'file', value: 'file' }
|
||||
@@ -132,19 +146,17 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
{values?.body?.content_type === 'x-www-form-urlencoded' &&
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
onChange={(data) => {
|
||||
const currentBody = form.getFieldValue('body') || {}
|
||||
form.setFieldsValue({
|
||||
body: { ...currentBody, data }
|
||||
})
|
||||
}}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'json' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<MessageEditor
|
||||
key="json"
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
isArray={false}
|
||||
title="JSON"
|
||||
@@ -154,6 +166,8 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
{values?.body?.content_type === 'raw' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<MessageEditor
|
||||
key="raw"
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
isArray={false}
|
||||
title="RAW TEXT"
|
||||
@@ -164,6 +178,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<VariableSelect
|
||||
options={options}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
@@ -179,19 +194,31 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
name={['timeouts', 'connect_timeout']}
|
||||
label={t('workflow.config.http-request.connect_timeout')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['timeouts', 'connect_timeout'], value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['timeouts', 'read_timeout']}
|
||||
label={t('workflow.config.http-request.read_timeout')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['timeouts', 'read_timeout'], value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['timeouts', 'write_timeout']}
|
||||
label={t('workflow.config.http-request.write_timeout')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['timeouts', 'write_timeout'], value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
@@ -204,13 +231,21 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
name={['retry', 'max_attempts']}
|
||||
label={t('workflow.config.http-request.max_attempts')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['retry', 'max_attempts'], value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['retry', 'retry_interval']}
|
||||
label={t('workflow.config.http-request.retry_interval')}
|
||||
label={<>{t('workflow.config.http-request.retry_interval')} <span className="rb:text-[#5B6167]">(ms)</span></>}
|
||||
>
|
||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['retry', 'retry_interval'], value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
@@ -219,6 +254,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<Form.Item layout="horizontal" name={['error_handle', 'method']} label={t('workflow.config.http-request.error_handle')}>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={handleChangeErrorHandleMethod}
|
||||
options={[
|
||||
{ value: 'none', label: t('workflow.config.http-request.none') },
|
||||
{ value: 'default', label: t('workflow.config.http-request.default') },
|
||||
@@ -230,32 +266,23 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<>
|
||||
<Form.Item
|
||||
name={['error_handle', 'body']}
|
||||
label="body"
|
||||
label={<>body <span className="rb:text-[#5B6167] rb:ml-1">string</span></>}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['error_handle', 'status_code']}
|
||||
label="status_code"
|
||||
label={<>status_code <span className="rb:text-[#5B6167] rb:ml-1">number</span></>}
|
||||
>
|
||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['error_handle', 'status_code'], value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['error_handle', 'headers']}
|
||||
label="headers"
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return Promise.resolve();
|
||||
} catch {
|
||||
return Promise.reject(new Error('Please enter valid JSON format'));
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
label={<>headers <span className="rb:text-[#5B6167] rb:ml-1">object</span></>}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -128,29 +128,32 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={knowledgeList}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.name}
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
renderItem={(item) => {
|
||||
if (!item.id) return null
|
||||
return (
|
||||
<List.Item>
|
||||
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.name}
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{/* 全局设置 */}
|
||||
|
||||
@@ -117,7 +117,12 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
extra={t('application.top_k_desc')}
|
||||
initialValue={5}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} min={1} max={20} />
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={20}
|
||||
onChange={(value) => form.setFieldValue('top_k', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* 语义相似度阈值 similarity_threshold */}
|
||||
{values?.retrieve_type === 'semantic' && (
|
||||
|
||||
@@ -110,7 +110,12 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
extra={t('application.reranker_top_k_desc')}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} min={1} max={20} />
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={20}
|
||||
onChange={(value) => form.setFieldValue('reranker_top_k', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
</>}
|
||||
</Form>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Space } from 'antd';
|
||||
import { Button, Form, Input, Space, Row, Col } from 'antd';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
interface MappingListProps {
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
}
|
||||
const MappingList: React.FC<MappingListProps> = ({ name }) => {
|
||||
const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
@@ -14,23 +17,33 @@ const MappingList: React.FC<MappingListProps> = ({ name }) => {
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Space>
|
||||
<Row key={key} gutter={12} className="rb:mb-2">
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block>
|
||||
|
||||
@@ -5,14 +5,15 @@ import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import Editor from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface TextareaProps {
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string
|
||||
isArray?: boolean;
|
||||
parentName?: string;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
onChange?: (value?: string) => void;
|
||||
}
|
||||
const roleOptions = [
|
||||
@@ -20,24 +21,32 @@ const roleOptions = [
|
||||
{ label: 'USER', value: 'USER' },
|
||||
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
||||
]
|
||||
const MessageEditor: FC<TextareaProps> = ({
|
||||
const MessageEditor: FC<MessageEditor> = ({
|
||||
title,
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
const values = form.getFieldsValue()
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 检查是否已经使用了context变量,将已使用的context设置为disabled
|
||||
const processedOptions = useMemo(() => {
|
||||
if (!isArray || !values[parentName]) return options;
|
||||
if (!isArray) return options;
|
||||
|
||||
// 获取表单中对应字段的值
|
||||
const fieldValue = Array.isArray(parentName)
|
||||
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||
: values?.[parentName];
|
||||
|
||||
if (!fieldValue) return options;
|
||||
|
||||
// 获取所有消息内容
|
||||
const allContents = values[parentName]
|
||||
.map((msg: any) => msg.content || '')
|
||||
const allContents = fieldValue
|
||||
.map((msg: any) => msg?.content || '')
|
||||
.join(' ');
|
||||
|
||||
// 将已使用的context变量标记为disabled
|
||||
@@ -50,83 +59,82 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
}, [options, values, parentName, isArray]);
|
||||
|
||||
const handleAdd = (add: FormListOperation['add']) => {
|
||||
const list = values[parentName];
|
||||
const lastRole = list[list.length - 1].role
|
||||
const fieldValue = Array.isArray(parentName)
|
||||
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||
: values?.[parentName];
|
||||
|
||||
const list = fieldValue || [];
|
||||
const lastRole = list.length > 0 ? list[list.length - 1]?.role : 'ASSISTANT';
|
||||
|
||||
add({
|
||||
role: lastRole === 'USER' ? 'ASSISTANT' : 'USER',
|
||||
content: undefined
|
||||
})
|
||||
content: ''
|
||||
});
|
||||
};
|
||||
|
||||
if (!isArray) {
|
||||
return (
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{title ?? t('workflow.answerDesc')}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isArray
|
||||
? <Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const currentRole = (values[parentName]?.[key].role || 'USER').toUpperCase()
|
||||
|
||||
return (
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'role']}
|
||||
noStyle
|
||||
>
|
||||
{currentRole === 'SYSTEM'
|
||||
? <Input disabled />
|
||||
:
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{currentRole !== 'SYSTEM' && <Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</div>
|
||||
</Col>}
|
||||
</Row>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'content']}
|
||||
noStyle
|
||||
>
|
||||
<Editor placeholder={placeholder} options={processedOptions} />
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const fieldValue = Array.isArray(parentName)
|
||||
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||
: values?.[parentName];
|
||||
|
||||
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
|
||||
|
||||
return (
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item {...restField} name={[name, 'role']} noStyle>
|
||||
{currentRole === 'SYSTEM' ? (
|
||||
<Input disabled />
|
||||
) : (
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block>
|
||||
+{t('workflow.addMessage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space >
|
||||
)}
|
||||
</Form.List>
|
||||
:
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{title ?? t('workflow.answerDesc')}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name={parentName}
|
||||
noStyle
|
||||
>
|
||||
<Editor placeholder={placeholder} options={processedOptions} />
|
||||
</Col>
|
||||
{currentRole !== 'SYSTEM' && (
|
||||
<Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block>
|
||||
+{t('workflow.addMessage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ const ParamEditModal = forwardRef<ParamEditModalRef, ParamEditModalProps>(({
|
||||
<FormItem
|
||||
name="desc"
|
||||
label={t('workflow.config.parameter-extractor.desc')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
@@ -197,7 +197,14 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
: parameter.type === 'boolean'
|
||||
? <Switch />
|
||||
: parameter.type === 'integer' || parameter.type === 'number'
|
||||
? <InputNumber min={parameter.minimum} max={parameter.maximum} step={parameter.type === 'integer' ? 1 : 0.01} placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
? <InputNumber
|
||||
min={parameter.minimum}
|
||||
max={parameter.maximum}
|
||||
step={parameter.type === 'integer' ? 1 : 0.01}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)}
|
||||
/>
|
||||
: <Editor
|
||||
height={32}
|
||||
variant="outlined"
|
||||
|
||||
@@ -141,7 +141,11 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
name="max_length"
|
||||
label={t('workflow.config.start.max_length')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
|
||||
<InputNumber
|
||||
placeholder={t('common.enter')}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => form.setFieldValue('max_length', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 默认值 */}
|
||||
@@ -151,7 +155,13 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
label={t('workflow.config.start.default')}
|
||||
>
|
||||
{['string'].includes(values.type) && <Input placeholder={t('common.enter')} />}
|
||||
{['number'].includes(values.type) && <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />}
|
||||
{['number'].includes(values.type) && (
|
||||
<InputNumber
|
||||
placeholder={t('common.enter')}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => form.setFieldValue('default', value)}
|
||||
/>
|
||||
)}
|
||||
{['boolean'].includes(values.type) && <Select placeholder={t('common.pleaseSelect')} options={[{ value: true, label: t('workflow.config.start.defaultChecked') }, { value: false, label: t('workflow.config.start.notDefaultChecked') }]} />}
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface VariableSelectProps extends SelectProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
allowClear?: boolean;
|
||||
filterBooleanType?: boolean;
|
||||
}
|
||||
|
||||
const VariableSelect: FC<VariableSelectProps> = ({
|
||||
@@ -18,6 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
allowClear = true,
|
||||
onChange,
|
||||
size,
|
||||
filterBooleanType = false,
|
||||
...resetPorps
|
||||
}) => {
|
||||
|
||||
@@ -26,7 +28,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
const labelRender: LabelRender = (props) => {
|
||||
const { value } = props
|
||||
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
|
||||
const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value)
|
||||
|
||||
if (filterOption) {
|
||||
return (
|
||||
@@ -54,7 +56,11 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
return null
|
||||
}
|
||||
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
const filteredOptions = filterBooleanType
|
||||
? options.filter(option => option.dataType !== 'boolean')
|
||||
: options;
|
||||
|
||||
const groupedSuggestions = filteredOptions.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
const { nodeData } = suggestion
|
||||
const nodeId = nodeData.id as string;
|
||||
if (!groups[nodeId]) {
|
||||
@@ -64,12 +70,13 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
|
||||
label: suggestions[0].nodeData.name,
|
||||
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
||||
options: suggestions.map(s => ({
|
||||
label: <div className="rb:flex rb:items-center rb:gap-1 rb:justify-between"> { s.label } <span>{s.dataType}</span></div>,
|
||||
value: `{{${s.value}}}`
|
||||
}))
|
||||
}));
|
||||
|
||||
console.log('groupedOptions', groupedOptions)
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -36,7 +36,7 @@ interface PropertiesProps {
|
||||
const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
graphRef,
|
||||
config,
|
||||
config: workflowConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
@@ -45,13 +45,134 @@ const Properties: FC<PropertiesProps> = ({
|
||||
const values = Form.useWatch([], form);
|
||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||
const prevMappingNamesRef = useRef<string[]>([])
|
||||
const prevTemplateVarsRef = useRef<string[]>([])
|
||||
const syncTimeoutRef = useRef<number | null>(null)
|
||||
const isSyncingRef = useRef(false)
|
||||
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode?.getData()?.id) {
|
||||
form.resetFields()
|
||||
prevMappingNamesRef.current = []
|
||||
prevTemplateVarsRef.current = []
|
||||
lastSyncSourceRef.current = null
|
||||
}
|
||||
}, [selectedNode?.getData()?.id])
|
||||
|
||||
// Sync template when mapping names change
|
||||
useEffect(() => {
|
||||
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
|
||||
|
||||
const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.map((item: any) => item.name).filter(Boolean) : []
|
||||
const prevNames = prevMappingNamesRef.current
|
||||
|
||||
if (prevNames.length === 0) {
|
||||
prevMappingNamesRef.current = currentMappingNames
|
||||
return
|
||||
}
|
||||
|
||||
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
|
||||
|
||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
|
||||
const activeElement = document.activeElement as HTMLElement
|
||||
|
||||
syncTimeoutRef.current = setTimeout(() => {
|
||||
let updatedTemplate = String(form.getFieldValue('template') || '')
|
||||
|
||||
prevNames.forEach((oldName, index) => {
|
||||
const newName = currentMappingNames[index]
|
||||
if (newName && oldName !== newName) {
|
||||
updatedTemplate = updatedTemplate.replace(new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), `{{${newName}}}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (updatedTemplate !== form.getFieldValue('template')) {
|
||||
isSyncingRef.current = true
|
||||
lastSyncSourceRef.current = 'mapping'
|
||||
const newTemplateVars = (updatedTemplate.match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
||||
prevTemplateVarsRef.current = newTemplateVars
|
||||
prevMappingNamesRef.current = currentMappingNames
|
||||
form.setFieldValue('template', updatedTemplate)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
activeElement?.focus?.()
|
||||
setTimeout(() => {
|
||||
isSyncingRef.current = false
|
||||
lastSyncSourceRef.current = null
|
||||
}, 50)
|
||||
})
|
||||
} else {
|
||||
prevMappingNamesRef.current = currentMappingNames
|
||||
}
|
||||
}, 0)
|
||||
}, [values?.mapping, selectedNode?.data?.type, form])
|
||||
|
||||
// Sync mapping when template variables change
|
||||
useEffect(() => {
|
||||
if (isSyncingRef.current || lastSyncSourceRef.current === 'template' || selectedNode?.data?.type !== 'jinja-render' || !values?.template || !values?.mapping) return
|
||||
|
||||
const templateVars = (String(values.template).match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
||||
if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return
|
||||
|
||||
const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]')
|
||||
if (!isTemplateEditor) {
|
||||
prevTemplateVarsRef.current = templateVars
|
||||
return
|
||||
}
|
||||
|
||||
const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping] : []
|
||||
const existingNames = updatedMapping.map(item => item.name)
|
||||
let updatedTemplate = String(values.template)
|
||||
|
||||
if (prevTemplateVarsRef.current.length > 0) {
|
||||
prevTemplateVarsRef.current.forEach((oldVar, index) => {
|
||||
const newVar = templateVars[index]
|
||||
if (newVar && oldVar !== newVar && updatedMapping[index]) {
|
||||
updatedMapping[index] = { ...updatedMapping[index], name: newVar }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
templateVars.forEach(varName => {
|
||||
const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`)
|
||||
const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g')
|
||||
|
||||
if (existingMapping) {
|
||||
updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`)
|
||||
} else if (!existingNames.includes(varName)) {
|
||||
const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName
|
||||
updatedMapping.push({ name: mappingName, value: `{{${varName}}}` })
|
||||
updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`)
|
||||
}
|
||||
})
|
||||
|
||||
const seenNames = new Set<string>()
|
||||
const finalMapping = updatedMapping.filter(item => {
|
||||
const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`)
|
||||
if (!isUsed || seenNames.has(item.name)) return false
|
||||
seenNames.add(item.name)
|
||||
return true
|
||||
})
|
||||
|
||||
isSyncingRef.current = true
|
||||
lastSyncSourceRef.current = 'template'
|
||||
prevMappingNamesRef.current = finalMapping.map((item: any) => item.name).filter(Boolean)
|
||||
prevTemplateVarsRef.current = templateVars
|
||||
|
||||
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
|
||||
form.setFieldValue('mapping', finalMapping)
|
||||
}
|
||||
if (updatedTemplate !== String(values.template)) {
|
||||
form.setFieldValue('template', updatedTemplate)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
isSyncingRef.current = false
|
||||
lastSyncSourceRef.current = null
|
||||
}, 50)
|
||||
}, [values?.template, selectedNode?.data?.type, form])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode && form) {
|
||||
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
||||
@@ -82,18 +203,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (values && selectedNode) {
|
||||
const { id, knowledge_retrieval, group, group_names, ...rest } = values
|
||||
const { id, knowledge_retrieval, group, group_variables, ...rest } = values
|
||||
const { knowledge_bases = [], ...restKnowledgeConfig } = (knowledge_retrieval as any) || {}
|
||||
|
||||
let groupNames: Record<string, string[]> | string[] = {}
|
||||
|
||||
if (group && group_names?.length) {
|
||||
group_names.forEach(vo => {
|
||||
(groupNames as Record<string, string[]>)[vo.key] = vo.value
|
||||
})
|
||||
} else if (!group) {
|
||||
groupNames = group_names?.[0]?.value || []
|
||||
}
|
||||
let allRest = {
|
||||
...rest,
|
||||
...restKnowledgeConfig,
|
||||
@@ -105,9 +217,18 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
Object.keys(values).forEach(key => {
|
||||
if (selectedNode.data?.config?.[key]) {
|
||||
selectedNode.data.config[key].defaultValue = values[key]
|
||||
// Create a deep copy to avoid reference sharing between nodes
|
||||
if (!selectedNode.data.config[key]) {
|
||||
selectedNode.data.config[key] = {};
|
||||
}
|
||||
selectedNode.data.config[key] = {
|
||||
...selectedNode.data.config[key],
|
||||
defaultValue: values[key]
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
@@ -116,7 +237,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
...allRest,
|
||||
})
|
||||
}
|
||||
}, [values, selectedNode])
|
||||
}, [values, selectedNode, form])
|
||||
|
||||
const handleAddVariable = () => {
|
||||
variableModalRef.current?.handleOpen()
|
||||
@@ -192,16 +313,95 @@ const Properties: FC<PropertiesProps> = ({
|
||||
.map(node => node.id);
|
||||
};
|
||||
|
||||
// Find parent loop/iteration node if current node is a child
|
||||
const getParentLoopNode = (nodeId: string): Node | null => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return null;
|
||||
|
||||
const nodeData = node.getData();
|
||||
const cycle = nodeData?.cycle;
|
||||
|
||||
if (cycle) {
|
||||
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||
if (parentNode) {
|
||||
const parentData = parentNode.getData();
|
||||
if (parentData?.type === 'loop' || parentData?.type === 'iteration') {
|
||||
return parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||
const childNodeIds = getChildNodes(selectedNode.id);
|
||||
console.log('childNodeIds', childNodeIds)
|
||||
const parentLoopNode = getParentLoopNode(selectedNode.id);
|
||||
|
||||
console.log('childNodeIds', selectedNode, childNodeIds)
|
||||
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||
|
||||
// Add parent loop/iteration node variables if current node is a child
|
||||
if (parentLoopNode) {
|
||||
const parentData = parentLoopNode.getData();
|
||||
const parentNodeId = parentLoopNode.getData().id;
|
||||
|
||||
if (parentData.type === 'loop') {
|
||||
const cycleVars = parentData.cycle_vars || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
const key = `${parentNodeId}_cycle_${cycleVar.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
value: `${parentNodeId}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (parentData.type === 'iteration') {
|
||||
// Add item and index variables for iteration parent
|
||||
const itemKey = `${parentNodeId}_item`;
|
||||
const indexKey = `${parentNodeId}_index`;
|
||||
|
||||
if (!addedKeys.has(itemKey)) {
|
||||
addedKeys.add(itemKey);
|
||||
variableList.push({
|
||||
key: itemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: 'Object',
|
||||
value: `${parentNodeId}.item`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!addedKeys.has(indexKey)) {
|
||||
addedKeys.add(indexKey);
|
||||
variableList.push({
|
||||
key: indexKey,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'Number',
|
||||
value: `${parentNodeId}.index`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add variables from nodes preceding the parent loop/iteration node
|
||||
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||
allRelevantNodeIds.push(...parentPreviousNodeIds);
|
||||
}
|
||||
|
||||
allRelevantNodeIds.forEach(nodeId => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const nodeData = node.getData();
|
||||
const dataNodeId = nodeData.id; // Use the data.id instead of node.id for consistency
|
||||
|
||||
switch(nodeData.type) {
|
||||
case 'start':
|
||||
@@ -211,7 +411,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
]
|
||||
list.forEach((variable: any) => {
|
||||
if (!variable || !variable?.name) return;
|
||||
const key = `${nodeId}_${variable.name}`;
|
||||
const key = `${dataNodeId}_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
@@ -219,14 +419,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `{{${nodeId}.${variable.name}}}`,
|
||||
value: `${dataNodeId}.${variable.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
nodeData.config?.variables?.sys?.forEach((variable: any) => {
|
||||
if (!variable || !variable?.name) return;
|
||||
const key = `${nodeId}_sys_${variable.name}`;
|
||||
const key = `${dataNodeId}_sys_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
@@ -241,7 +441,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
});
|
||||
break
|
||||
case 'llm':
|
||||
const llmKey = `${nodeId}_output`;
|
||||
const llmKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(llmKey)) {
|
||||
addedKeys.add(llmKey);
|
||||
variableList.push({
|
||||
@@ -249,13 +449,13 @@ const Properties: FC<PropertiesProps> = ({
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${nodeId}.output`,
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'knowledge-retrieval':
|
||||
const knowledgeKey = `${nodeId}_message`;
|
||||
const knowledgeKey = `${dataNodeId}_message`;
|
||||
if (!addedKeys.has(knowledgeKey)) {
|
||||
addedKeys.add(knowledgeKey);
|
||||
variableList.push({
|
||||
@@ -263,7 +463,219 @@ const Properties: FC<PropertiesProps> = ({
|
||||
label: 'message',
|
||||
type: 'variable',
|
||||
dataType: 'array[object]',
|
||||
value: `${nodeId}.message`,
|
||||
value: `${dataNodeId}.message`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'parameter-extractor':
|
||||
const successKey = `${dataNodeId}___is_success`;
|
||||
const reasonKey = `${dataNodeId}___reason`;
|
||||
if (!addedKeys.has(successKey)) {
|
||||
addedKeys.add(successKey);
|
||||
variableList.push({
|
||||
key: successKey,
|
||||
label: '__is_success',
|
||||
type: 'variable',
|
||||
dataType: 'number',
|
||||
value: `${dataNodeId}.__is_success`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(reasonKey)) {
|
||||
addedKeys.add(reasonKey);
|
||||
variableList.push({
|
||||
key: reasonKey,
|
||||
label: '__reason',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.__reason`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
// Add params variables
|
||||
const paramsList = nodeData.config?.params?.defaultValue || [];
|
||||
paramsList.forEach((param: any) => {
|
||||
if (!param || !param?.name) return;
|
||||
const paramKey = `${dataNodeId}_${param.name}`;
|
||||
if (!addedKeys.has(paramKey)) {
|
||||
addedKeys.add(paramKey);
|
||||
variableList.push({
|
||||
key: paramKey,
|
||||
label: param.name,
|
||||
type: 'variable',
|
||||
dataType: param.type || 'string',
|
||||
value: `${dataNodeId}.${param.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
break
|
||||
case 'var-aggregator':
|
||||
if (nodeData.config.group.defaultValue) {
|
||||
// If group=true, add variables from group_variables with key as variable name
|
||||
const groupVariables = nodeData.config.group_variables.defaultValue || [];
|
||||
groupVariables?.forEach((groupVar: any) => {
|
||||
if (!groupVar || !groupVar.key) return;
|
||||
const groupVarKey = `${dataNodeId}_${groupVar.key}`;
|
||||
if (!addedKeys.has(groupVarKey)) {
|
||||
addedKeys.add(groupVarKey);
|
||||
variableList.push({
|
||||
key: groupVarKey,
|
||||
label: groupVar.key,
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.${groupVar.key}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If group=false, add output variable
|
||||
const varAggregatorKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(varAggregatorKey)) {
|
||||
addedKeys.add(varAggregatorKey);
|
||||
variableList.push({
|
||||
key: varAggregatorKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'http-request':
|
||||
const httpBodyKey = `${dataNodeId}_body`;
|
||||
const httpStatusKey = `${dataNodeId}_status_code`;
|
||||
if (!addedKeys.has(httpBodyKey)) {
|
||||
addedKeys.add(httpBodyKey);
|
||||
variableList.push({
|
||||
key: httpBodyKey,
|
||||
label: 'body',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.body`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(httpStatusKey)) {
|
||||
addedKeys.add(httpStatusKey);
|
||||
variableList.push({
|
||||
key: httpStatusKey,
|
||||
label: 'status_code',
|
||||
type: 'variable',
|
||||
dataType: 'number',
|
||||
value: `${dataNodeId}.status_code`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'jinja-render':
|
||||
const jinjaOutputKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(jinjaOutputKey)) {
|
||||
addedKeys.add(jinjaOutputKey);
|
||||
variableList.push({
|
||||
key: jinjaOutputKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'question-classifier':
|
||||
const classNameKey = `${dataNodeId}_class_name`;
|
||||
const outputKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(classNameKey)) {
|
||||
addedKeys.add(classNameKey);
|
||||
variableList.push({
|
||||
key: classNameKey,
|
||||
label: 'class_name',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.class_name`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(outputKey)) {
|
||||
addedKeys.add(outputKey);
|
||||
variableList.push({
|
||||
key: outputKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'iteration':
|
||||
console.log('iteration addedKeys', addedKeys)
|
||||
const iterationOutputKey = `${dataNodeId}_output`;
|
||||
const iterationItemKey = `${dataNodeId}_item`;
|
||||
if (!addedKeys.has(iterationOutputKey)) {
|
||||
addedKeys.add(iterationOutputKey);
|
||||
// Get the data type from the output configuration, default to string
|
||||
const outputConfig = nodeData.output;
|
||||
let outputDataType = 'string';
|
||||
if (outputConfig) {
|
||||
// Find the selected variable from variableList to get its type
|
||||
const selectedVariable = variableList.find(v => v.value === outputConfig);
|
||||
if (selectedVariable) {
|
||||
outputDataType = selectedVariable.dataType;
|
||||
}
|
||||
}
|
||||
variableList.push({
|
||||
key: iterationOutputKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: outputDataType,
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(iterationItemKey)) {
|
||||
addedKeys.add(iterationItemKey);
|
||||
variableList.push({
|
||||
key: iterationItemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.item`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'loop':
|
||||
const cycleVars = nodeData.config.cycle_vars.defaultValue || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
const cycleVarKey = `${dataNodeId}_cycle_${cycleVar.name}`;
|
||||
if (!addedKeys.has(cycleVarKey)) {
|
||||
addedKeys.add(cycleVarKey);
|
||||
variableList.push({
|
||||
key: cycleVarKey,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'string',
|
||||
value: `${dataNodeId}.${cycleVar.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
break
|
||||
case 'tool':
|
||||
const toolDataKey = `${dataNodeId}_data`;
|
||||
if (!addedKeys.has(toolDataKey)) {
|
||||
addedKeys.add(toolDataKey);
|
||||
variableList.push({
|
||||
key: toolDataKey,
|
||||
label: 'data',
|
||||
type: 'variable',
|
||||
dataType: 'object',
|
||||
value: `${dataNodeId}.data`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
@@ -272,7 +684,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
});
|
||||
|
||||
// Add conversation variables from global config
|
||||
const conversationVariables = config?.variables || [];
|
||||
const conversationVariables = workflowConfig?.variables || [];
|
||||
|
||||
conversationVariables.forEach((variable: any) => {
|
||||
const key = `CONVERSATION_${variable.name}`;
|
||||
@@ -283,7 +695,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `conversation.${variable.name}`,
|
||||
value: `conv.${variable.name}`,
|
||||
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
|
||||
group: 'CONVERSATION'
|
||||
});
|
||||
@@ -291,7 +703,15 @@ const Properties: FC<PropertiesProps> = ({
|
||||
});
|
||||
|
||||
return variableList;
|
||||
}, [selectedNode, graphRef]);
|
||||
}, [selectedNode, graphRef, workflowConfig?.variables]);
|
||||
|
||||
// Filter out boolean type variables for loop and llm nodes
|
||||
const getFilteredVariableList = (nodeType?: string) => {
|
||||
if (nodeType === 'loop' || nodeType === 'llm') {
|
||||
return variableList.filter(variable => variable.dataType !== 'boolean');
|
||||
}
|
||||
return variableList;
|
||||
};
|
||||
|
||||
console.log('values', values)
|
||||
console.log('variableList', variableList, selectedNode?.data)
|
||||
@@ -317,6 +737,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
{selectedNode?.data?.type === 'http-request'
|
||||
? <HttpRequest
|
||||
options={variableList}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
/>
|
||||
: selectedNode?.data?.type === 'tool'
|
||||
? <ToolConfig options={variableList} />
|
||||
@@ -374,7 +796,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||
// 为llm节点且isArray=true时添加context变量支持
|
||||
let contextVariableList = [...variableList];
|
||||
let contextVariableList = [...getFilteredVariableList('llm')];
|
||||
const isArrayMode = config.isArray !== false; // 默认为true
|
||||
|
||||
if (isArrayMode) {
|
||||
@@ -387,7 +809,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
label: 'context',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `{{context}}`,
|
||||
value: `context`,
|
||||
nodeData: selectedNode.getData(),
|
||||
isContext: true,
|
||||
});
|
||||
@@ -396,14 +818,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor options={contextVariableList} parentName={key} />
|
||||
<MessageEditor key={key} options={contextVariableList} parentName={key} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor isArray={false} parentName={key} options={variableList} />
|
||||
<MessageEditor key={key} isArray={false} parentName={key} options={variableList} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
@@ -430,7 +852,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -451,7 +874,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<GroupVariableList
|
||||
name={key}
|
||||
options={variableList}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
isCanAdd={!!(values as any)?.group}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -463,7 +886,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<CaseList
|
||||
name={key}
|
||||
options={variableList}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
/>
|
||||
@@ -476,7 +899,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
>
|
||||
<MappingList name={key} />
|
||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type)} />
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
@@ -486,7 +909,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -498,71 +921,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
parentName={key}
|
||||
options={(() => {
|
||||
if (config.filterLoopIterationVars) {
|
||||
// Add loop cycle variables and iteration item/index variables
|
||||
const loopIterationVars: Suggestion[] = [];
|
||||
const graph = graphRef.current;
|
||||
if (graph && selectedNode) {
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find parent loop/iteration nodes
|
||||
const findParentLoopIteration = (nodeId: string): string[] => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return [];
|
||||
|
||||
const nodeData = node.getData();
|
||||
const cycle = nodeData?.cycle;
|
||||
|
||||
if (cycle) {
|
||||
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||
if (parentNode) {
|
||||
const parentData = parentNode.getData();
|
||||
if (parentData?.type === 'loop') {
|
||||
console.log('parentData', parentData)
|
||||
// Add cycle variables from loop node
|
||||
const cycleVars = parentData.cycle_vars || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
loopIterationVars.push({
|
||||
key: `${cycle}_cycle_${cycleVar.name}`,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${cycle}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
});
|
||||
} else if (parentData?.type === 'iteration') {
|
||||
// Add item and index variables from iteration node
|
||||
loopIterationVars.push(
|
||||
{
|
||||
key: `${cycle}_item`,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: 'Object',
|
||||
value: `${cycle}.item`,
|
||||
nodeData: parentData,
|
||||
},
|
||||
{
|
||||
key: `${cycle}_index`,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'Number',
|
||||
value: `${cycle}.index`,
|
||||
nodeData: parentData,
|
||||
}
|
||||
);
|
||||
}
|
||||
return [cycle, ...findParentLoopIteration(cycle)];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
findParentLoopIteration(selectedNode.id);
|
||||
}
|
||||
|
||||
return [...variableList, ...loopIterationVars];
|
||||
return [...getFilteredVariableList(selectedNode?.data?.type), ...loopIterationVars];
|
||||
}
|
||||
return variableList;
|
||||
return getFilteredVariableList(selectedNode?.data?.type);
|
||||
})()
|
||||
}
|
||||
/>
|
||||
@@ -583,11 +946,15 @@ const Properties: FC<PropertiesProps> = ({
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.needTranslation ? config.options?.map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
? <InputNumber />
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(key, value)}
|
||||
/>
|
||||
: config.type === 'slider'
|
||||
? <Slider min={config.min} max={config.max} step={config.step} />
|
||||
: config.type === 'customSelect'
|
||||
@@ -603,9 +970,10 @@ const Properties: FC<PropertiesProps> = ({
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return variableList.filter(variable => {
|
||||
return baseVariableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
@@ -626,22 +994,57 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return variableList.filter(variable =>
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||
);
|
||||
}
|
||||
return variableList;
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch />
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
options={(() => {
|
||||
// For loop nodes, add cycle_vars to condition options
|
||||
if (selectedNode?.data?.type === 'loop') {
|
||||
const cycleVars = values?.cycle_vars || [];
|
||||
const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({
|
||||
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
value: `${selectedNode.getData().id}.${cycleVar.name}`,
|
||||
nodeData: selectedNode.getData(),
|
||||
}));
|
||||
return [...getFilteredVariableList(selectedNode?.data?.type).filter(variable => {
|
||||
// Keep conversation variables
|
||||
if (variable.group === 'CONVERSATION') return true;
|
||||
// Keep sys variables from start nodes
|
||||
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
|
||||
// Keep variables from non-start nodes
|
||||
if (variable.nodeData?.type !== 'start') return true;
|
||||
// Filter out custom variables from start nodes
|
||||
return false;
|
||||
}), ...cycleVarSuggestions];
|
||||
}
|
||||
// Filter options for condition list: only sys variables from start nodes and conversation variables
|
||||
return getFilteredVariableList(selectedNode?.data?.type).filter(variable => {
|
||||
// Keep conversation variables
|
||||
if (variable.group === 'CONVERSATION') return true;
|
||||
// Keep sys variables from start nodes
|
||||
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
|
||||
// Keep variables from non-start nodes
|
||||
if (variable.nodeData?.type !== 'start') return true;
|
||||
// Filter out custom variables from start nodes
|
||||
return false;
|
||||
});
|
||||
})()
|
||||
}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
|
||||
@@ -39,6 +39,9 @@ import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png
|
||||
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png'
|
||||
import breakIcon from '@/assets/images/workflow/break.png'
|
||||
import assignerIcon from '@/assets/images/workflow/assigner.png'
|
||||
import memoryReadIcon from '@/assets/images/workflow/memory-read.png'
|
||||
import memoryWriteIcon from '@/assets/images/workflow/memory-write.png'
|
||||
|
||||
import { memoryConfigListUrl } from '@/api/memory'
|
||||
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
@@ -159,6 +162,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
text: {
|
||||
type: 'variableList',
|
||||
filterLoopIterationVars: true
|
||||
},
|
||||
params: {
|
||||
type: 'paramList',
|
||||
@@ -174,8 +178,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{
|
||||
category: "cognitiveUpgrading",
|
||||
nodes: [
|
||||
{
|
||||
type: "memory-read", icon: memoryEnhancementIcon,
|
||||
{ type: "memory-read", icon: memoryReadIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
@@ -198,7 +201,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: "memory-write", icon: memoryEnhancementIcon,
|
||||
{ type: "memory-write", icon: memoryWriteIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
@@ -272,6 +275,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
parallel: {
|
||||
type: 'switch',
|
||||
defaultValue: false
|
||||
},
|
||||
parallel_count: {
|
||||
type: 'slider',
|
||||
@@ -284,6 +288,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
flatten: { // 扁平化输出
|
||||
type: 'switch',
|
||||
defaultValue: false
|
||||
},
|
||||
output: {
|
||||
type: 'variableList',
|
||||
@@ -295,6 +300,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
cycle_vars: {
|
||||
type: 'cycleVarsList',
|
||||
defaultValue: []
|
||||
},
|
||||
condition: {
|
||||
type: 'conditionList',
|
||||
@@ -304,6 +310,13 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
expressions: []
|
||||
}
|
||||
},
|
||||
max_loop: {
|
||||
type: 'slider',
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
defaultValue: 10
|
||||
},
|
||||
}
|
||||
},
|
||||
{ type: "cycle-start", icon: loopIcon },
|
||||
@@ -315,9 +328,9 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: 'switch',
|
||||
defaultValue: false
|
||||
},
|
||||
group_names: {
|
||||
group_variables: {
|
||||
type: 'groupVariableList',
|
||||
defaultValue: [{ key: 'Group1', value: []}]
|
||||
defaultValue: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -361,11 +374,11 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
headers: {
|
||||
type: 'define',
|
||||
defaultValue: {}
|
||||
defaultValue: []
|
||||
},
|
||||
params: {
|
||||
type: 'define',
|
||||
defaultValue: {}
|
||||
defaultValue: []
|
||||
},
|
||||
body: {
|
||||
type: 'define',
|
||||
@@ -382,12 +395,15 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
defaultValue: {}
|
||||
},
|
||||
retry: {
|
||||
type: 'define',
|
||||
type: 'switch',
|
||||
defaultValue: {
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
error_handle: {
|
||||
type: 'define',
|
||||
defaultValue: {
|
||||
method: 'default'
|
||||
method: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,11 +423,13 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
mapping: {
|
||||
type: 'mappingList',
|
||||
defaultValue: []
|
||||
defaultValue: [{name: 'arg1'}]
|
||||
},
|
||||
template: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
enableJinja2: true,
|
||||
defaultValue: "{{arg1}}"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,17 @@ export const useWorkflowGraph = ({
|
||||
if (!id) return
|
||||
getWorkflowConfig(id)
|
||||
.then(res => {
|
||||
setConfig(res as WorkflowConfig)
|
||||
const { variables, ...rest } = res as WorkflowConfig
|
||||
setConfig({
|
||||
...rest,
|
||||
variables: variables.map(v => {
|
||||
const { default: _, ...cleanV } = v
|
||||
return {
|
||||
...cleanV,
|
||||
defaultValue: v.default ?? ''
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,13 +100,13 @@ export const useWorkflowGraph = ({
|
||||
nodeLibraryConfig.config[key].defaultValue = {
|
||||
...rest
|
||||
}
|
||||
} else if (key === 'group_names' && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
const { group_names, group } = config
|
||||
} else if (key === 'group_variables' && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
const { group_variables, group } = config
|
||||
nodeLibraryConfig.config[key].defaultValue = group
|
||||
? Object.entries(group_names as Record<string, any>).map(([key, value]) => ({ key, value }))
|
||||
: [{ key: 'Group1', value: group_names }]
|
||||
|
||||
console.log('group_names', nodeLibraryConfig.config)
|
||||
? Object.entries(group_variables as Record<string, any>).map(([key, value]) => ({ key, value }))
|
||||
: group_variables
|
||||
} else if (type === 'http-request' && (key === 'headers' || key === 'params') && config[key] && typeof config[key] === 'object' && !Array.isArray(config[key]) && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value }))
|
||||
} else if (nodeLibraryConfig.config && nodeLibraryConfig.config[key] && config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
@@ -193,6 +203,27 @@ export const useWorkflowGraph = ({
|
||||
nodeConfig.height = newHeight;
|
||||
}
|
||||
|
||||
// 如果是http-request节点,检查error_handle.method配置
|
||||
if (type === 'http-request' && (config as any).error_handle?.method === 'branch') {
|
||||
const portAttrs = {
|
||||
circle: {
|
||||
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||
},
|
||||
};
|
||||
|
||||
nodeConfig.ports = {
|
||||
groups: {
|
||||
right: { position: 'right', attrs: portAttrs },
|
||||
left: { position: 'left', attrs: portAttrs },
|
||||
},
|
||||
items: [
|
||||
{ group: 'left' },
|
||||
{ group: 'right', id: 'right' },
|
||||
{ group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return nodeConfig
|
||||
})
|
||||
|
||||
@@ -284,6 +315,14 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是http-request节点且有label,根据label匹配对应的端口
|
||||
if (sourceCell.getData()?.type === 'http-request' && label) {
|
||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||
if (matchingPort) {
|
||||
sourcePort = label;
|
||||
}
|
||||
}
|
||||
|
||||
const edgeConfig = {
|
||||
source: {
|
||||
cell: sourceCell.id,
|
||||
@@ -303,6 +342,7 @@ export const useWorkflowGraph = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
zIndex: 0
|
||||
}
|
||||
|
||||
return edgeConfig
|
||||
@@ -832,7 +872,7 @@ export const useWorkflowGraph = ({
|
||||
|
||||
// 创建干净的节点数据,只保留必要的字段
|
||||
const cleanNodeData = {
|
||||
id: `${dragData.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
id: `${dragData.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: t(`workflow.${dragData.type}`),
|
||||
...nodeLibraryConfig
|
||||
};
|
||||
@@ -842,6 +882,7 @@ export const useWorkflowGraph = ({
|
||||
...graphNodeLibrary[dragData.type],
|
||||
x: point.x - 150,
|
||||
y: point.y - 100,
|
||||
id: cleanNodeData.id,
|
||||
data: { ...cleanNodeData, isGroup: true },
|
||||
});
|
||||
} else if (dragData.type === 'if-else') {
|
||||
@@ -850,6 +891,7 @@ export const useWorkflowGraph = ({
|
||||
...graphNodeLibrary[dragData.type],
|
||||
x: point.x - 100,
|
||||
y: point.y - 60,
|
||||
id: cleanNodeData.id,
|
||||
data: { ...cleanNodeData },
|
||||
});
|
||||
} else {
|
||||
@@ -858,6 +900,7 @@ export const useWorkflowGraph = ({
|
||||
...(graphNodeLibrary[dragData.type] || graphNodeLibrary.default),
|
||||
x: point.x - 60,
|
||||
y: point.y - 20,
|
||||
id: cleanNodeData.id,
|
||||
data: { ...cleanNodeData },
|
||||
});
|
||||
}
|
||||
@@ -874,6 +917,13 @@ export const useWorkflowGraph = ({
|
||||
|
||||
const params = {
|
||||
...config,
|
||||
variables: config.variables.map(v => {
|
||||
const { defaultValue, ...cleanV } = v
|
||||
return {
|
||||
...cleanV,
|
||||
default: defaultValue ?? ''
|
||||
}
|
||||
}),
|
||||
nodes: nodes.map((node: Node) => {
|
||||
const data = node.getData();
|
||||
const position = node.getPosition();
|
||||
@@ -881,7 +931,23 @@ export const useWorkflowGraph = ({
|
||||
|
||||
if (data.config) {
|
||||
Object.keys(data.config).forEach(key => {
|
||||
if (data.config[key] && 'defaultValue' in data.config[key] && key !== 'knowledge_retrieval') {
|
||||
if (data.config[key] && 'defaultValue' in data.config[key] && key === 'group_variables') {
|
||||
let group_variables = data.config.group.defaultValue ? {} : data.config[key].defaultValue
|
||||
if (data.config.group.defaultValue) {
|
||||
data.config[key].defaultValue.map((vo: any) => {
|
||||
group_variables[vo.key] = vo.value
|
||||
})
|
||||
}
|
||||
itemConfig[key] = group_variables
|
||||
} else if (data.type === 'http-request' && (key === 'headers' || key === 'params') && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const value = data.config[key].defaultValue
|
||||
itemConfig[key] = {}
|
||||
if (value.length > 0) {
|
||||
value.forEach((vo: any) => {
|
||||
itemConfig[key][vo.name] = vo.value
|
||||
})
|
||||
}
|
||||
} else if (data.config[key] && 'defaultValue' in data.config[key] && key !== 'knowledge_retrieval') {
|
||||
itemConfig[key] = data.config[key].defaultValue
|
||||
} else if (key === 'knowledge_retrieval' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const { knowledge_bases } = data.config[key].defaultValue
|
||||
@@ -910,7 +976,7 @@ export const useWorkflowGraph = ({
|
||||
const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||
const sourcePortId = edge.getSourcePortId();
|
||||
|
||||
|
||||
// 过滤无效连线:源节点或目标节点不存在,或者是add-node类型
|
||||
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id ||
|
||||
sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') {
|
||||
@@ -935,6 +1001,23 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是http-request节点的右侧端口连线,添加label
|
||||
if (sourceCell?.getData()?.type === 'http-request') {
|
||||
if (sourcePortId === 'ERROR') {
|
||||
return {
|
||||
source: sourceCell.getData().id,
|
||||
target: targetCell?.getData().id,
|
||||
label: 'ERROR',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
source: sourceCell.getData().id,
|
||||
target: targetCell?.getData().id,
|
||||
label: 'SUCCESS',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
source: sourceCell?.getData().id,
|
||||
target: targetCell?.getData().id,
|
||||
|
||||
@@ -24,8 +24,9 @@ export interface NodeConfig {
|
||||
|
||||
knowledge_retrieval?: KnowledgeConfig;
|
||||
|
||||
group_names?: Array<{ key: string, value: string[] }>
|
||||
group_variables?: Array<{ key: string, value: string[] }>
|
||||
cycle?: string;
|
||||
cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }>
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,8 @@ export interface WorkflowConfig {
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
default: string;
|
||||
default?: string;
|
||||
defaultValue: string;
|
||||
}>,
|
||||
execution_config: {
|
||||
max_execution_time: number;
|
||||
@@ -113,7 +115,8 @@ export interface ChatVariable {
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
default: string;
|
||||
default?: string;
|
||||
defaultValue: string;
|
||||
}
|
||||
export interface AddChatVariableRef {
|
||||
handleOpen: (value?: ChatVariable) => void;
|
||||
|
||||