diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 3c0fe6fa..0ac14451 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -117,26 +117,26 @@ export const getRagContent = (end_user_id: string) => { } // 情感分布分析 export const getWordCloud = (group_id: string) => { - return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 }) + return request.post(`/memory/emotion-memory/wordcloud`, { group_id, limit: 20 }) } // 高频情绪关键词 export const getEmotionTags = (group_id: string) => { - return request.post(`/memory/emotion/tags`, { group_id, limit: 20 }) + return request.post(`/memory/emotion-memory/tags`, { group_id, limit: 20 }) } // 情绪健康指数 export const getEmotionHealth = (group_id: string) => { - return request.post(`/memory/emotion/health`, { group_id, limit: 20 }) + return request.post(`/memory/emotion-memory/health`, { group_id, limit: 20 }) } // 个性化建议 export const getEmotionSuggestions = (group_id: string) => { - return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 }) + return request.post(`/memory/emotion-memory/suggestions`, { group_id, limit: 20 }) } 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 }) + return request.get(`/memory/forget-memory/stats`, { group_id }) } // 隐性记忆-偏好 export const getImplicitPreferences = (end_user_id: string) => { @@ -176,10 +176,10 @@ export const getPerceptualTimeline = (end_user: string) => { } // 情景记忆-总览 export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => { - return request.post(`/memory-storage/classifications/episodic-memory`, data) + return request.post(`/memory/episodic-memory/overview`, data) } export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => { - return request.post(`/memory-storage/classifications/episodic-memory-details`, data) + return request.post(`/memory/episodic-memory/details`, data) } // 关系演化 export const getRelationshipEvolution = (data: { id: string; label: string; } ) => { @@ -190,10 +190,10 @@ 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 }) + return request.post(`/memory/explicit-memory/overview`, { end_user_id }) } export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => { - return request.post(`/memory-storage/classifications/explicit-memory-details`, data) + return request.post(`/memory/explicit-memory/details`, data) } export const getConversations = (end_user: string) => { return request.get(`/memory/work/${end_user}/conversations`) @@ -205,7 +205,7 @@ export const getConversationDetail = (end_user: string, conversation_id: string) return request.get(`/memory/work/${end_user}/detail`, { conversation_id }) } export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => { - return request.post(`/memory/forget/trigger`, data) + return request.post(`/memory/forget-memory/trigger`, data) } /*************** end 用户记忆 相关接口 ******************************/ @@ -229,11 +229,11 @@ export const deleteMemoryConfig = (config_id: number) => { } // 遗忘引擎-获取配置 export const getMemoryForgetConfig = (config_id: number | string) => { - return request.get('/memory/forget/read_config', { config_id }) + return request.get('/memory/forget-memory/read_config', { config_id }) } // 遗忘引擎-更新配置 export const updateMemoryForgetConfig = (values: ForgetConfigForm) => { - return request.post('/memory/forget/update_config', values) + return request.post('/memory/forget-memory/update_config', values) } // 记忆萃取引擎-获取配置 export const getMemoryExtractionConfig = (config_id: number | string) => { diff --git a/web/src/assets/images/menu/spaceConfig.svg b/web/src/assets/images/menu/spaceConfig.svg new file mode 100644 index 00000000..bcfeae12 --- /dev/null +++ b/web/src/assets/images/menu/spaceConfig.svg @@ -0,0 +1,17 @@ + + + 模型 (1) + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menu/spaceConfig_active.svg b/web/src/assets/images/menu/spaceConfig_active.svg new file mode 100644 index 00000000..41b25689 --- /dev/null +++ b/web/src/assets/images/menu/spaceConfig_active.svg @@ -0,0 +1,17 @@ + + + 模型 (1) + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/goto.svg b/web/src/assets/images/userMemory/goto.svg new file mode 100644 index 00000000..a66e2011 --- /dev/null +++ b/web/src/assets/images/userMemory/goto.svg @@ -0,0 +1,19 @@ + + + 编组 13备份 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/CustomSelect/index.tsx b/web/src/components/CustomSelect/index.tsx index 97ca4e4b..e9ccce74 100644 --- a/web/src/components/CustomSelect/index.tsx +++ b/web/src/components/CustomSelect/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useRef, type FC, type Key } from 'react'; +import { useEffect, useState, type FC, type Key } from 'react'; import { Select } from 'antd' import type { SelectProps, DefaultOptionType } from 'antd/es/select' import { useTranslation } from 'react-i18next'; @@ -26,7 +26,7 @@ interface CustomSelectProps extends Omit { disabled?: boolean; style?: React.CSSProperties; className?: string; - filterOption?: (inputValue: string, option: DefaultOptionType) => boolean; + filterOption?: (inputValue: string, option?: DefaultOptionType) => boolean; } interface OptionType { [key: string]: Key | string | number; @@ -48,44 +48,27 @@ const CustomSelect: FC = ({ }) => { const { t } = useTranslation(); const [options, setOptions] = useState([]); - // 创建防抖定时器引用 - const debounceRef = useRef(); - - // 防抖搜索函数 - const handleSearch = useCallback((value?: string) => { - // 清除之前的定时器 - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - - // 设置新的定时器 - debounceRef.current = window.setTimeout(() => { - request.get>(url, {...params, [optionFilterProp]: value}).then((res) => { - const data = res; - setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []); - }); - }, 300); // 300毫秒防抖延迟 - }, [url, params, optionFilterProp]); + // 默认模糊搜索函数 + const defaultFilterOption = (inputValue: string, option?: DefaultOptionType) => { + if (!option || !inputValue) return true; + const label = String(option.children || option.label || ''); + return label.toLowerCase().includes(inputValue.toLowerCase()); + }; // 组件挂载时获取初始数据 useEffect(() => { - handleSearch(); - - // 组件卸载时清除定时器 - return () => { - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - }; - }, [url, handleSearch]); + request.get>(url, params).then((res) => { + const data = res; + setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []); + }); + }, []); return ( + - - + ((_props, ref) => { const { t } = useTranslation(); @@ -88,8 +89,8 @@ const TimeToolModal = forwardRef((_props, ref) => { } }) .then(res => { - const response = res as { data: CurrentTimeObj } - setTimestampFormat(response.data.datetime) + const response = res as { data: string } + setTimestampFormat(response.data) }) } const handleChangeFormatType = () => { @@ -149,7 +150,7 @@ const TimeToolModal = forwardRef((_props, ref) => { - + diff --git a/web/src/views/ToolManagement/constant.ts b/web/src/views/ToolManagement/constant.ts index 1e30bafa..6763a140 100644 --- a/web/src/views/ToolManagement/constant.ts +++ b/web/src/views/ToolManagement/constant.ts @@ -10,10 +10,10 @@ export const InnerConfigData: Record = { }, JsonTool: { features: [ - 'jsonFormat', - 'jsonGzip', - 'jsonCheck', - 'jsonConversion' + 'jsonParse', + 'jsonInsert', + 'jsonReplace', + 'jsonDelete' ], eg: '{"name":"工具","tool_class":"内置"}' }, diff --git a/web/src/views/ToolManagement/types.ts b/web/src/views/ToolManagement/types.ts index 6fd4e439..aa97db66 100644 --- a/web/src/views/ToolManagement/types.ts +++ b/web/src/views/ToolManagement/types.ts @@ -130,6 +130,7 @@ export interface ExecuteData { ensure_ascii?: boolean; sort_keys?: boolean; input_data?: string; + json_path?: string; } } export interface CustomToolModalRef { diff --git a/web/src/views/UserMemory/components/ConfigModal.tsx b/web/src/views/UserMemory/components/ConfigModal.tsx deleted file mode 100644 index 86ea8f19..00000000 --- a/web/src/views/UserMemory/components/ConfigModal.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Form, App } from 'antd'; -import { useTranslation } from 'react-i18next'; - -import type { ConfigModalData, ConfigModalRef } from '../types' -import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces' -import { getModelListUrl } from '@/api/models' -import CustomSelect from '@/components/CustomSelect' -import RbModal from '@/components/RbModal' - -const ConfigModal = forwardRef((_props, ref) => { - const { t } = useTranslation(); - const { message } = App.useApp(); - const [visible, setVisible] = useState(false); - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false) - - const values = Form.useWatch([], form); - - // 封装取消方法,添加关闭弹窗逻辑 - const handleClose = () => { - setVisible(false); - form.resetFields(); - setLoading(false) - }; - - const handleOpen = () => { - getWorkspaceModels().then((res) => { - const { llm, embedding, rerank } = res as ConfigModalData - form.setFieldsValue({ - llm, - embedding, - rerank - }) - }) - setVisible(true); - }; - // 封装保存方法,添加提交逻辑 - const handleSave = () => { - form - .validateFields() - .then(() => { - setLoading(true) - updateWorkspaceModels(values) - .then(() => { - setLoading(false) - handleClose() - message.success(t('common.updateSuccess')) - }) - .catch(() => { - setLoading(false) - }); - - handleClose() - }) - .catch((err) => { - console.log('err', err) - }); - } - - // 暴露给父组件的方法 - useImperativeHandle(ref, () => ({ - handleOpen, - handleClose - })); - - return ( - -
- - - - - - - - - -
-
- ); -}); - -export default ConfigModal; \ No newline at end of file diff --git a/web/src/views/UserMemory/index.tsx b/web/src/views/UserMemory/index.tsx index 7065f036..064b55be 100644 --- a/web/src/views/UserMemory/index.tsx +++ b/web/src/views/UserMemory/index.tsx @@ -1,56 +1,28 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom' -import { Row, Col, Radio, Button, List, Skeleton, Space } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import type { RadioChangeEvent } from 'antd'; -import { AppstoreOutlined, MenuOutlined } from '@ant-design/icons'; +import { Row, Col, List, Skeleton } from 'antd'; import Empty from '@/components/Empty' -import type { Data, ConfigModalRef } from './types' -import totalNum from '@/assets/images/memory/totalNum.svg' -import onlineNum from '@/assets/images/memory/onlineNum.svg' -import Table from '@/components/Table' -import { getTotalEndUsers, userMemoryListUrl, getUserMemoryList } from '@/api/memory'; -import ConfigModal from './components/ConfigModal'; +import type { Data } from './types' +import { getUserMemoryList } from '@/api/memory'; import { useUser } from '@/store/user' +import RbCard from '@/components/RbCard/Card' +import SearchInput from '@/components/SearchInput'; -const bgList = [ - 'linear-gradient( 180deg, #F1F6FE 0%, #FBFDFF 100%)', - 'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)', - 'linear-gradient( 180deg, #FEFBF7 0%, #FBFDFF 100%)', - 'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)', -] - -const countList = [ - 'total_num', 'online_num', -] -const IconList: Record = { - total_num: totalNum, - online_num: onlineNum, -} export default function UserMemory() { const { t } = useTranslation(); const navigate = useNavigate() const { storageType } = useUser() - const configModalRef = useRef(null) const [loading, setLoading] = useState(false); const [data, setData] = useState([]); - const [countData, setCountData] = useState>({}); - const [layout, setLayout] = useState<'card' | 'list'>('card'); + const [search, setSearch] = useState(undefined); // 获取数据 useEffect(() => { - getCountData() getData() }, []); - // 用户记忆统计 - const getCountData = () => { - getTotalEndUsers().then((res) => { - setCountData(res as Record || {}) - }) - } const getData = () => { setLoading(true) getUserMemoryList().then((res) => { @@ -60,7 +32,6 @@ export default function UserMemory() { setLoading(false) }) } - console.log('storageType', storageType) const handleViewDetail = (id: string | number) => { switch (storageType) { case 'neo4j': @@ -70,112 +41,77 @@ export default function UserMemory() { navigate(`/user-memory/${id}`) } } - const handleChangeLayout = (e: RadioChangeEvent) => { - const type = e.target.value - setLayout(type) + const handleViewMemoryConfig = () => { + navigate(`/memory`) } - // 表格列配置 - const columns: ColumnsType = [ - { - title: t('userMemory.user'), - dataIndex: 'end_user', - key: 'end_user', - render: (value) => value?.other_name && value?.other_name !== '' ? value?.other_name : value?.id || '-' - }, - { - title: t('userMemory.knowledgeEntryCount'), - dataIndex: 'memory_num', - key: 'memory_num', - render: (value) => value?.total || 0 - }, - { - title: t('common.operation'), - key: 'action', - render: (_, record) => ( - - ), - }, - ]; + + const filterData = useMemo(() => { + if (search && search.trim() !== '') { + return data.filter((item) => { + const { end_user } = item as Data; + const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id + return name?.includes(search) + }) + } + + return data + }, [search, data]) return (
- {countList.map(key => ( - -
-
- {countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''} - -
-
{t(`userMemory.${key}`)}
-
- - ))} - - - - - - - - + + setSearch(value)} + style={{ width: '100%' }} + />
- {layout === 'card' && - <> - {loading ? - - : data.length > 0 ? ( - { - const { end_user, memory_num } = item as Data; - const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id - return ( - -
+ : filterData.length > 0 ? ( + { + const { end_user, memory_num, memory_config } = item as Data; + const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id + return ( + + {name[0]}
} + title={name || '-'} + extra={
handleViewDetail(end_user.id)} - > -
-
{name[0]}
-
- {name || '-'}
-
-
-
-
-
{memory_num.total || 0}
-
{t(`userMemory.knowledgeEntryCount`)}
-
-
+ >
} + > +
+
{t('userMemory.capacity')}
+
{memory_num?.total || 0} {t('userMemory.memoryNum')}
+
+
+
{t('userMemory.type')}
+
{t(`userMemory.${item.type || 'person'}`)}
-
- ) - }} - /> - ) : } - - } - {layout === 'list' && - +
+
+ {t('userMemory.memory_config_name')} +
+
+
{memory_config?.memory_config_name || '-'}
+
+ + + ) + }} + /> + ) : } - ); } \ No newline at end of file diff --git a/web/src/views/UserMemory/types.ts b/web/src/views/UserMemory/types.ts index 696b1694..927cf778 100644 --- a/web/src/views/UserMemory/types.ts +++ b/web/src/views/UserMemory/types.ts @@ -17,13 +17,10 @@ export interface Data { entity: number; } }, + memory_config: { + memory_config_id: string; + memory_config_name: string; + }, + type: string; name?: string; -} -export interface ConfigModalData { - llm: string; - embedding: string; - rerank: string; -} -export interface ConfigModalRef { - handleOpen: () => void; } \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/EmotionLine.tsx b/web/src/views/UserMemoryDetail/components/EmotionLine.tsx index c62fbfb9..68664d39 100644 --- a/web/src/views/UserMemoryDetail/components/EmotionLine.tsx +++ b/web/src/views/UserMemoryDetail/components/EmotionLine.tsx @@ -3,8 +3,7 @@ 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' -import { format } from 'echarts'; +import type { Emotion } from '../pages/GraphDetail' interface EmotionLineProps { chartData: Emotion[]; diff --git a/web/src/views/UserMemoryDetail/components/InteractionBar.tsx b/web/src/views/UserMemoryDetail/components/InteractionBar.tsx index 0db33b6f..60c977fd 100644 --- a/web/src/views/UserMemoryDetail/components/InteractionBar.tsx +++ b/web/src/views/UserMemoryDetail/components/InteractionBar.tsx @@ -3,7 +3,7 @@ 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' +import type { Interaction } from '../pages/GraphDetail' interface InteractionBarProps { chartData: Interaction[]; diff --git a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx index 07095fe4..d12c3e57 100644 --- a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx +++ b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx @@ -1,19 +1,18 @@ import React, { type FC, useEffect, useState, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { useParams } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' 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, StatementNodeProperties, ExtractedEntityNodeProperties, GraphDetailRef } from '../types' +import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties } 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 = () => { @@ -26,7 +25,7 @@ const RelationshipNetwork:FC = () => { const [categories, setCategories] = useState<{ name: string }[]>([]) const [selectedNode, setSelectedNode] = useState(null) // const [fullScreen, setFullScreen] = useState(false) - const graphDetailRef = useRef(null) + const navigate = useNavigate() console.log('categories', categories) // 关系网络 @@ -133,15 +132,14 @@ const RelationshipNetwork:FC = () => { } }, [nodes]) - // const handleFullScreen = () => { - // setFullScreen(prev => !prev) - // } - - console.log('selectedNode', selectedNode) - const handleViewAll = () => { if (!selectedNode) return - graphDetailRef.current?.handleOpen(selectedNode) + const params = new URLSearchParams({ + nodeId: selectedNode.id, + nodeLabel: selectedNode.label, + nodeName: selectedNode.name || '' + }) + navigate(`/user-memory/detail/${id}/GRAPH?${params.toString()}`) } return ( @@ -336,8 +334,6 @@ const RelationshipNetwork:FC = () => { - - ) } diff --git a/web/src/views/UserMemoryDetail/components/GraphDetail.tsx b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx similarity index 50% rename from web/src/views/UserMemoryDetail/components/GraphDetail.tsx rename to web/src/views/UserMemoryDetail/pages/GraphDetail.tsx index aed795f5..47efce76 100644 --- a/web/src/views/UserMemoryDetail/components/GraphDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx @@ -1,16 +1,17 @@ -import { useState, forwardRef, useImperativeHandle, useMemo } from 'react' +import { useState, forwardRef, useImperativeHandle, useMemo, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { useSearchParams } from 'react-router-dom' 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 EmotionLine from '../components/EmotionLine' import { formatDateTime } from '@/utils/format' import Tag from '@/components/Tag' -import InteractionBar from './InteractionBar' +import InteractionBar from '../components/InteractionBar' import Empty from '@/components/Empty' +import PageHeader from '../components/PageHeader' export interface Emotion { emotion_intensity: number; @@ -35,7 +36,7 @@ interface Timeline { const GraphDetail = forwardRef((_props, ref) => { const { t } = useTranslation() - const [open, setOpen] = useState(false); + const [searchParams] = useSearchParams() const [vo, setVo] = useState(null) const [loading, setLoading] = useState(false) const [emotionData, setEmotionData] = useState([]) @@ -43,14 +44,23 @@ const GraphDetail = forwardRef((_props, ref) => { const [activeTab, setActiveTab] = useState('timelines_memory') const [timelineLoading, setTimelineLoading] = useState(false) const [timelineMemories, setTimelineMemories] = useState({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []}) + useEffect(() => { + const nodeId = searchParams.get('nodeId') + const nodeLabel = searchParams.get('nodeLabel') + const nodeName = searchParams.get('nodeName') + + if (nodeId && nodeLabel) { + const nodeFromUrl = { + id: nodeId, + label: nodeLabel, + name: nodeName || nodeLabel + } + handleOpen(nodeFromUrl as Node) + } + }, [searchParams]) - const handleCancel = () => { - setVo(null) - setOpen(false) - } const handleOpen = (vo: Node) => { setActiveTab('timelines_memory') - setOpen(true) setVo(vo) getRelationshipEvolutionData(vo) getTimelineMemoriesData(vo) @@ -85,56 +95,57 @@ const GraphDetail = forwardRef((_props, ref) => { }, [activeTab, timelineMemories]) return ( - -
{t('userMemory.relationshipEvolution')}
- - -
- - - - - - - + <> + +
+
{t('userMemory.relationshipEvolution')}
+ + +
+ + + + + + + -
{t('userMemory.timelineMemories')}
- - ({ - label: t(`userMemory.${key}`), - key - }))} - onChange={(key: string) => setActiveTab(key)} - /> - {timelineLoading - ? - : !activeContent || activeContent.length === 0 - ? - : - {activeContent.map((vo, index) => ( - -
{formatDateTime(vo.created_at)}
- {vo.type} -
- ))} -
- } +
{t('userMemory.timelineMemories')}
+ + ({ + label: t(`userMemory.${key}`), + key + }))} + onChange={(key: string) => setActiveTab(key)} + /> + {timelineLoading + ? + : !activeContent || activeContent.length === 0 + ? + : + {activeContent.map((vo, index) => ( + +
{formatDateTime(vo.created_at)}
+ {vo.type} +
+ ))} +
+ } - -
- + +
+ + ) }) export default GraphDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/index.tsx b/web/src/views/UserMemoryDetail/pages/index.tsx index 8f5ee146..f5b1a937 100644 --- a/web/src/views/UserMemoryDetail/pages/index.tsx +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -1,7 +1,7 @@ import { type FC, useEffect, useState, useMemo, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Dropdown, Space, Button } from 'antd' +import { Dropdown, Button } from 'antd' import PageHeader from '../components/PageHeader' import StatementDetail from './StatementDetail' @@ -16,6 +16,7 @@ import { getEndUserProfile, } from '@/api/memory' import refreshIcon from '@/assets/images/refresh_hover.svg' +import GraphDetail from './GraphDetail' const Detail: FC = () => { const { t } = useTranslation() @@ -47,6 +48,10 @@ const Detail: FC = () => { forgetDetailRef.current?.handleRefresh() } + if (type === 'GRAPH') { + return + } + return (
{
{nodeLibrary.map((category, categoryIndex) => { const filteredNodes = category.nodes.filter(nodeType => - nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' + nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'iteration' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' ); if (filteredNodes.length === 0) return null; diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index dac91b68..40b4b8ec 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -33,7 +33,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { y: cycleStartBBox.y, data: { type: 'add-node', - label: '添加节点', + label: t('workflow.addNode'), icon: '+', parentId: node.id, cycle: data.id, @@ -61,7 +61,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { }, }, }, - zIndex: 3 + zIndex: 10 }); } } @@ -97,7 +97,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { y: centerY, data: { type: 'add-node', - label: '添加节点', + label: t('workflow.addNode'), icon: '+', parentId: node.id, cycle: data.id, @@ -128,7 +128,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { }, }, }, - zIndex: 3 + zIndex: 10 } graph.addEdge(edgeConfig) diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index 9a644438..9d9225e8 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -151,11 +151,11 @@ const PortClickHandler: React.FC = ({ graph }) => { let filteredNodes; if (isChildOfLoop) { - // Use same filtering as AddNode for child nodes of loop + // Use same filtering as AddNode for child nodes of loop, but allow break filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); } else if (isChildOfIteration) { - // Filter out loop and iteration nodes for children of iteration nodes - filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type)); + // Filter out loop and iteration nodes for children of iteration nodes, but allow break + filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); } else { // Original filtering for non-loop child nodes filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type)); diff --git a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx index 97f28668..494e4342 100644 --- a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx +++ b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx @@ -60,7 +60,7 @@ const AssignmentList: FC = ({ > vo.nodeData.type === 'loop' || vo.value.includes('conv.'))} popupMatchSelectWidth={false} onChange={() => { form.setFieldValue([parentName, name, 'operation'], undefined); diff --git a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx index 69ed2030..6fa47421 100644 --- a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx @@ -1,17 +1,19 @@ import { type FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { Input, Button, Form, Space } from 'antd'; -import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons'; +import { Button, Form, Space } from 'antd'; +import { DeleteOutlined } from '@ant-design/icons'; import { Graph, Node } from '@antv/x6'; -import type { PortMetadata } from '@antv/x6/lib/model/port'; +import Editor from '../../Editor'; +import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' interface CategoryListProps { parentName: string; + options: Suggestion[]; selectedNode?: Node | null; graphRef?: React.MutableRefObject; } -const CategoryList: FC = ({ parentName, selectedNode, graphRef }) => { +const CategoryList: FC = ({ parentName, selectedNode, graphRef, options }) => { const { t } = useTranslation(); const form = Form.useFormInstance(); const formValues = Form.useWatch([parentName], form); @@ -167,9 +169,9 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe name={[name, 'class_name']} noStyle > -
diff --git a/web/src/views/Workflow/components/Properties/ConditionList/index.tsx b/web/src/views/Workflow/components/Properties/ConditionList/index.tsx index 8fbebeda..d809fec5 100644 --- a/web/src/views/Workflow/components/Properties/ConditionList/index.tsx +++ b/web/src/views/Workflow/components/Properties/ConditionList/index.tsx @@ -1,6 +1,6 @@ import { type FC } from 'react' import { useTranslation } from 'react-i18next'; -import { Form, Button, Select, Row, Col, InputNumber, Radio, type SelectProps } from 'antd' +import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd' import { DeleteOutlined } from '@ant-design/icons'; import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' @@ -114,7 +114,7 @@ const ConditionList: FC = ({
vo.value.includes('sys.') || vo.value.includes('conv.') || vo.nodeData.type === 'loop')} size="small" allowClear={false} popupMatchSelectWidth={false} @@ -186,7 +186,7 @@ const ConditionList: FC = ({ True False - : + : } diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index c05cce25..4d436af0 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -1,6 +1,6 @@ import { type FC } from 'react' import { useTranslation } from 'react-i18next'; -import { Form, Button, Select, Row, Col, Input } from 'antd' +import { Form, Select, Row, Col, Input } from 'antd' import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import VariableSelect from '../VariableSelect' @@ -36,7 +36,6 @@ const CycleVarsList: FC = ({ value = [], options, parentName, - onChange, selectedNode, graphRef }) => { @@ -139,12 +138,17 @@ const CycleVarsList: FC = ({ {currentInputType === 'variable' ? ( { + const currentType = value?.[index]?.type; + if (!currentType) return true; + + return option.dataType === currentType + })} /> ) : ( diff --git a/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx b/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx index 2b2db0f7..61cdd7b0 100644 --- a/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx +++ b/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx @@ -18,8 +18,22 @@ const GroupVariableList: FC = ({ isCanAdd = false }) => { const { t } = useTranslation(); + const form = Form.useFormInstance(); + const value = form.getFieldValue(name) || []; + + console.log('GroupVariableList', value) if (!isCanAdd) { + // Filter options based on first variable's dataType if value exists + let filteredOptions = options; + if (value.length > 0) { + const firstVariableValue = value[0]; + const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue); + if (firstVariable) { + filteredOptions = options.filter(opt => opt.dataType === firstVariable.dataType); + } + } + return (
@@ -38,7 +52,7 @@ const GroupVariableList: FC = ({ > @@ -77,7 +91,18 @@ const GroupVariableList: FC = ({ > { + const currentGroupValue = value[name]?.value || []; + if (currentGroupValue.length > 0) { + const firstVariableValue = currentGroupValue[0]; + const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue); + if (firstVariable) { + return options.filter(opt => opt.dataType === firstVariable.dataType); + } + } + return options; + })() + } mode="multiple" /> diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index bbb3238d..5823c1d8 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -90,7 +90,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
- + vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" /> @@ -144,7 +144,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} filterBooleanType={true} /> @@ -154,7 +154,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} isArray={false} title="JSON" /> diff --git a/web/src/views/Workflow/components/Properties/VariableSelect.tsx b/web/src/views/Workflow/components/Properties/VariableSelect.tsx index b92475d7..5f0f1f0b 100644 --- a/web/src/views/Workflow/components/Properties/VariableSelect.tsx +++ b/web/src/views/Workflow/components/Properties/VariableSelect.tsx @@ -91,6 +91,7 @@ const VariableSelect: FC = ({ showSearch allowClear={allowClear} filterOption={(input, option) => { + if (input === '/') return true; if (option?.options) { return option.label?.toLowerCase().includes(input.toLowerCase()) || option.options.some((opt: any) => diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index 765fd207..9fcc8821 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -22,6 +22,7 @@ import ConditionList from './ConditionList' import CycleVarsList from './CycleVarsList' import AssignmentList from './AssignmentList' import ToolConfig from './ToolConfig' +import { calculateVariableList } from './utils/variableListCalculator' interface PropertiesProps { selectedNode?: Node | null; @@ -338,112 +339,35 @@ const Properties: FC = ({ const parentLoopNode = getParentLoopNode(selectedNode.id); console.log('childNodeIds', selectedNode, childNodeIds) - const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds]; + let allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds]; - // Add parent loop/iteration node variables if current node is a child + // Add variables from nodes preceding the parent loop/iteration node 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, - }); - } - } - - // Check if parent loop/iteration is connected to http-request via ERROR connection - if (parentData.type === 'loop' || parentData.type === 'iteration') { - const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); - parentPreviousNodeIds.forEach(prevNodeId => { - const prevNode = nodes.find(n => n.id === prevNodeId); - if (!prevNode) return; - - const prevNodeData = prevNode.getData(); - if (prevNodeData.type === 'http-request') { - // Check if connected via ERROR connection point - const errorEdges = edges.filter(edge => { - return edge.getTargetCellId() === parentLoopNode.id && - edge.getSourceCellId() === prevNodeId && - edge.getSourcePortId() === 'ERROR' - }); - - if (errorEdges.length > 0) { - const errorMessageKey = `${prevNodeData.id}_error_message`; - const errorTypeKey = `${prevNodeData.id}_error_type`; - - if (!addedKeys.has(errorMessageKey)) { - addedKeys.add(errorMessageKey); - variableList.push({ - key: errorMessageKey, - label: 'error_message', - type: 'variable', - dataType: 'string', - value: `${prevNodeData.id}.error_message`, - nodeData: prevNodeData, - }); - } - - if (!addedKeys.has(errorTypeKey)) { - addedKeys.add(errorTypeKey); - variableList.push({ - key: errorTypeKey, - label: 'error_type', - type: 'variable', - dataType: 'string', - value: `${prevNodeData.id}.error_type`, - nodeData: prevNodeData, - }); - } - } - } - }); - } - - // Add variables from nodes preceding the parent loop/iteration node const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); allRelevantNodeIds.push(...parentPreviousNodeIds); } + + + // Add conversation variables from global config + const conversationVariables = workflowConfig?.variables || []; + + conversationVariables.forEach((variable: any) => { + const key = `CONVERSATION_${variable.name}`; + if (!addedKeys.has(key)) { + addedKeys.add(key); + variableList.push({ + key, + label: variable.name, + type: 'variable', + dataType: variable.type, + value: `conv.${variable.name}`, + nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, + group: 'CONVERSATION' + }); + } + }); + allRelevantNodeIds.forEach(nodeId => { const node = nodes.find(n => n.id === nodeId); if (!node) return; @@ -496,7 +420,7 @@ const Properties: FC = ({ key: llmKey, label: 'output', type: 'variable', - dataType: 'String', + dataType: 'string', value: `${dataNodeId}.output`, nodeData: nodeData, }); @@ -565,6 +489,17 @@ const Properties: FC = ({ const groupVariables = nodeData.config.group_variables.defaultValue || []; groupVariables?.forEach((groupVar: any) => { if (!groupVar || !groupVar.key) return; + + // Determine dataType from first variable in the group + let groupDataType = 'string'; + if (groupVar.value && Array.isArray(groupVar.value) && groupVar.value.length > 0) { + const firstVariableValue = groupVar.value[0]; + const firstVariable = variableList.find(v => `{{${v.value}}}` === firstVariableValue); + if (firstVariable) { + groupDataType = firstVariable.dataType; + } + } + const groupVarKey = `${dataNodeId}_${groupVar.key}`; if (!addedKeys.has(groupVarKey)) { addedKeys.add(groupVarKey); @@ -572,14 +507,26 @@ const Properties: FC = ({ key: groupVarKey, label: groupVar.key, type: 'variable', - dataType: 'string', + dataType: groupDataType, value: `${dataNodeId}.${groupVar.key}`, nodeData: nodeData, }); } }); } else { - // If group=false, add output variable + // If group=false, add output variable with type from first group_variable + const groupVariables = nodeData.config.group_variables.defaultValue || []; + const firstVariable = groupVariables[0]; + let outputDataType: string = 'any'; + if (firstVariable) { + const filterVo = [...variableList].find(v => { + return `{{${v.value}}}` === firstVariable + }) + if (filterVo) { + outputDataType = filterVo?.dataType + } + } + const varAggregatorKey = `${dataNodeId}_output`; if (!addedKeys.has(varAggregatorKey)) { addedKeys.add(varAggregatorKey); @@ -587,7 +534,7 @@ const Properties: FC = ({ key: varAggregatorKey, label: 'output', type: 'variable', - dataType: 'string', + dataType: outputDataType, value: `${dataNodeId}.output`, nodeData: nodeData, }); @@ -684,21 +631,20 @@ const Properties: FC = ({ nodeData: nodeData, }); } - if (!addedKeys.has(outputKey)) { - addedKeys.add(outputKey); - variableList.push({ - key: outputKey, - label: 'output', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.output`, - 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': 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 @@ -715,22 +661,11 @@ const Properties: FC = ({ key: iterationOutputKey, label: 'output', type: 'variable', - dataType: outputDataType, + dataType: `array[${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 || []; @@ -760,47 +695,337 @@ const Properties: FC = ({ key: toolDataKey, label: 'data', type: 'variable', - dataType: 'object', + dataType: 'string', value: `${dataNodeId}.data`, nodeData: nodeData, }); } break + case 'memory-read': + const memoryReadAnswerKey = `${dataNodeId}_answer`; + const memoryReadIntermediateOutputs = `${dataNodeId}_intermediate_outputs`; + if (!addedKeys.has(memoryReadAnswerKey)) { + addedKeys.add(memoryReadAnswerKey); + variableList.push({ + key: memoryReadAnswerKey, + label: 'answer', + type: 'variable', + dataType: 'string', + value: `${dataNodeId}.answer`, + nodeData: nodeData, + }); + } + if (!addedKeys.has(memoryReadIntermediateOutputs)) { + addedKeys.add(memoryReadIntermediateOutputs); + variableList.push({ + key: memoryReadIntermediateOutputs, + label: 'intermediate_outputs', + type: 'variable', + dataType: 'array[object]', + value: `${dataNodeId}.intermediate_outputs`, + nodeData: nodeData, + }); + } + break } }); - // Add conversation variables from global config - const conversationVariables = workflowConfig?.variables || []; - - conversationVariables.forEach((variable: any) => { - const key = `CONVERSATION_${variable.name}`; - if (!addedKeys.has(key)) { - addedKeys.add(key); - variableList.push({ - key, - label: variable.name, - type: 'variable', - dataType: variable.type, - value: `conv.${variable.name}`, - nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, - group: 'CONVERSATION' + + // 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 only if input has value + if (parentData.config.input.defaultValue) { + const itemKey = `${parentNodeId}_item`; + const indexKey = `${parentNodeId}_index`; + + // Determine item dataType from input variable + let itemDataType = 'object'; + const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue); + console.log('itemDataType defaultValue', parentData.config.input.defaultValue, variableList, inputVariable) + if (inputVariable && inputVariable.dataType.startsWith('array[')) { + itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1'); + console.log('itemDataType', itemDataType) + } + + + if (!addedKeys.has(itemKey)) { + addedKeys.add(itemKey); + variableList.push({ + key: itemKey, + label: 'item', + type: 'variable', + dataType: itemDataType, + 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, + }); + } + } + } + + // Check if parent loop/iteration is connected to http-request via ERROR connection + if (parentData.type === 'loop' || parentData.type === 'iteration') { + const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); + parentPreviousNodeIds.forEach(prevNodeId => { + const prevNode = nodes.find(n => n.id === prevNodeId); + if (!prevNode) return; + + const prevNodeData = prevNode.getData(); + if (prevNodeData.type === 'http-request') { + // Check if connected via ERROR connection point + const errorEdges = edges.filter(edge => { + return edge.getTargetCellId() === parentLoopNode.id && + edge.getSourceCellId() === prevNodeId && + edge.getSourcePortId() === 'ERROR' + }); + + if (errorEdges.length > 0) { + const errorMessageKey = `${prevNodeData.id}_error_message`; + const errorTypeKey = `${prevNodeData.id}_error_type`; + + if (!addedKeys.has(errorMessageKey)) { + addedKeys.add(errorMessageKey); + variableList.push({ + key: errorMessageKey, + label: 'error_message', + type: 'variable', + dataType: 'string', + value: `${prevNodeData.id}.error_message`, + nodeData: prevNodeData, + }); + } + + if (!addedKeys.has(errorTypeKey)) { + addedKeys.add(errorTypeKey); + variableList.push({ + key: errorTypeKey, + label: 'error_type', + type: 'variable', + dataType: 'string', + value: `${prevNodeData.id}.error_type`, + nodeData: prevNodeData, + }); + } + } + } }); } - }); + } return variableList; }, [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'); + const getFilteredVariableList = (nodeType?: string, key?: string) => { + // Check if current node is a child of iteration node + const parentIterationNode = selectedNode ? (() => { + const nodes = graphRef.current?.getNodes() || []; + const nodeData = selectedNode.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 === 'iteration') { + return parentNode; + } + } + } + return null; + })() : null; + + // Helper function to add parent iteration variables + const addParentIterationVars = (filteredList: any[]) => { + if (parentIterationNode) { + const parentData = parentIterationNode.getData(); + const parentNodeId = parentData.id; + + if (parentData.config?.input?.defaultValue) { + const itemKey = `${parentNodeId}_item`; + const indexKey = `${parentNodeId}_index`; + + const existingItemVar = filteredList.find(v => v.key === itemKey); + const existingIndexVar = filteredList.find(v => v.key === indexKey); + + if (!existingItemVar) { + // Determine item dataType from input variable + let itemDataType = 'object'; + const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue); + if (inputVariable && inputVariable.dataType.startsWith('array[')) { + itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1'); + } + + filteredList.push({ + key: itemKey, + label: 'item', + type: 'variable', + dataType: itemDataType, + value: `${parentNodeId}.item`, + nodeData: parentData, + }); + } + + if (!existingIndexVar) { + filteredList.push({ + key: indexKey, + label: 'index', + type: 'variable', + dataType: 'number', + value: `${parentNodeId}.index`, + nodeData: parentData, + }); + } + } + } + return filteredList; + }; + + if (nodeType === 'llm') { + // For LLM nodes that are children of iteration or loop nodes, include parent variables + const parentLoopNode = selectedNode ? (() => { + const nodes = graphRef.current?.getNodes() || []; + const nodeData = selectedNode.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; + })() : null; + + let filteredList = variableList.filter(variable => variable.dataType !== 'boolean'); + + // If this LLM node is a child of iteration/loop, ensure parent variables are included + if (parentLoopNode) { + const parentData = parentLoopNode.getData(); + const parentNodeId = parentData.id; + + // Ensure parent loop/iteration variables are included + if (parentData.type === 'loop') { + const cycleVars = parentData.cycle_vars || []; + cycleVars.forEach((cycleVar: any) => { + const key = `${parentNodeId}_cycle_${cycleVar.name}`; + const existingVar = filteredList.find(v => v.key === key); + if (!existingVar && cycleVar.name && cycleVar.type !== 'boolean') { + filteredList.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 + if (parentData.config?.input?.defaultValue) { + const itemKey = `${parentNodeId}_item`; + const indexKey = `${parentNodeId}_index`; + + const existingItemVar = filteredList.find(v => v.key === itemKey); + const existingIndexVar = filteredList.find(v => v.key === indexKey); + + if (!existingItemVar) { + // Determine item dataType from input variable + let itemDataType = 'object'; + const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue); + if (inputVariable && inputVariable.dataType.startsWith('array[')) { + itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1'); + } + + filteredList.push({ + key: itemKey, + label: 'item', + type: 'variable', + dataType: itemDataType, + value: `${parentNodeId}.item`, + nodeData: parentData, + }); + } + + if (!existingIndexVar) { + filteredList.push({ + key: indexKey, + label: 'index', + type: 'variable', + dataType: 'Number', + value: `${parentNodeId}.index`, + nodeData: parentData, + }); + } + } + } + } + + return filteredList; } - return variableList; + if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'memory-write' || nodeType === 'question-classifier') { + let filteredList = variableList.filter(variable => variable.dataType === 'string'); + return addParentIterationVars(filteredList); + } + if (nodeType === 'parameter-extractor' && key === 'prompt') { + let filteredList = variableList.filter(variable => variable.dataType === 'string' || variable.dataType === 'number'); + return addParentIterationVars(filteredList); + } + if (nodeType === 'iteration' && key === 'output') { + return variableList.filter(variable => variable.value.includes('sys.')); + } + if (nodeType === 'iteration') { + return variableList.filter(variable => variable.dataType.includes('array')); + } + if (nodeType === 'loop' && key === 'condition') { + let filteredList = variableList.filter(variable => variable.nodeData.type !== 'loop'); + return addParentIterationVars(filteredList); + } + + // For all other node types, add parent iteration variables if applicable + let baseList = variableList; + return addParentIterationVars(baseList); }; + const defaultVariableList = calculateVariableList(selectedNode as Node, graphRef, workflowConfig ) + console.log('values', values) - console.log('variableList', variableList, selectedNode?.data) + console.log('variableList', variableList, defaultVariableList) return (
@@ -901,11 +1126,10 @@ const Properties: FC = ({ }); } } - return ( variable.nodeData?.type !== 'knowledge-retrieval')} parentName={key} /> @@ -915,7 +1139,12 @@ const Properties: FC = ({ if (selectedNode?.data?.type === 'end' && key === 'output') { return ( - + variable.nodeData?.type !== 'knowledge-retrieval')} + /> ) } @@ -943,7 +1172,7 @@ const Properties: FC = ({ isArray={!!config.isArray} parentName={key} enableJinja2={config.enableJinja2 as boolean} - options={getFilteredVariableList(selectedNode?.data?.type)} + options={getFilteredVariableList(selectedNode?.data?.type, key)} /> ) @@ -964,7 +1193,7 @@ const Properties: FC = ({ @@ -976,7 +1205,7 @@ const Properties: FC = ({ @@ -989,7 +1218,7 @@ const Properties: FC = ({ - + ) @@ -999,7 +1228,7 @@ const Properties: FC = ({ ) @@ -1013,9 +1242,9 @@ const Properties: FC = ({ if (config.filterLoopIterationVars) { const loopIterationVars: Suggestion[] = []; - return [...getFilteredVariableList(selectedNode?.data?.type), ...loopIterationVars]; + return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars]; } - return getFilteredVariableList(selectedNode?.data?.type); + return getFilteredVariableList(selectedNode?.data?.type, key); })() } /> @@ -1060,7 +1289,7 @@ const Properties: FC = ({ ? { - const baseVariableList = getFilteredVariableList(selectedNode?.data?.type); + const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key); // Apply filtering if specified in config if (config.filterNodeTypes || config.filterVariableNames) { return baseVariableList.filter(variable => { @@ -1068,7 +1297,7 @@ const Properties: FC = ({ (Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type)); const variableNameMatch = !config.filterVariableNames || (Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label)); - return nodeTypeMatch && variableNameMatch; + return nodeTypeMatch || variableNameMatch; }); } // Filter child nodes for iteration output @@ -1085,7 +1314,7 @@ const Properties: FC = ({ }); return baseVariableList.filter(variable => - childNodes.some(node => node.id === variable.nodeData?.id) + childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.') ); } return baseVariableList; @@ -1095,7 +1324,12 @@ const Properties: FC = ({ : config.type === 'switch' ? { form.setFieldValue('group_variables', []) } : undefined} /> : config.type === 'categoryList' - ? + ? : config.type === 'conditionList' ? = ({ value: `${selectedNode.getData().id}.${cycleVar.name}`, nodeData: selectedNode.getData(), })); - return [...variableList.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' && variable.nodeData?.type !== 'http-request' && variable.dataType !== 'boolean') return true; - // Filter out custom variables from start nodes - return false; - }), ...cycleVarSuggestions]; - })() - } + + return [...getFilteredVariableList(selectedNode?.data?.type, key), ...cycleVarSuggestions]; + })()} selectedNode={selectedNode} graphRef={graphRef} addBtnText={t('workflow.config.addCase')} diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 692339da..593639ce 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -270,7 +270,7 @@ export const nodeLibrary: NodeLibrary[] = [ config: { input: { type: 'variableList', - filterNodeTypes: ['knowledge-retrieval'], + filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop'], filterVariableNames: ['message'] }, parallel: { @@ -334,8 +334,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { - type: "assigner", icon: assignerIcon, + { type: "assigner", icon: assignerIcon, config: { assignments: { type: 'assignmentList', @@ -628,4 +627,114 @@ export const graphNodeLibrary: Record = { items: [{ group: 'left' }], }, } +} + + +export interface OutputVariable { + default?: Array<{ + name: string; + type: string; + }>; + define?: string[]; + sys?: Array<{ + name: string; + type: string; + }>; + error?: Array<{ + name: string; + type: string; + }>; +} +export const outputVariable: { [key: string]: OutputVariable } = { + start: { + sys: [ + { name: "message", type: "string" }, + { name: "conversation_id", type: "string" }, + { name: "execution_id", type: "string", }, + { name: "workspace_id", type: "string" }, + { name: "user_id", type: "string" }, + ], + define: ['variables'] + }, + end: { + }, + llm: { + default: [ + { name: "output", type: "string" }, + ] + }, + 'knowledge-retrieval': { + default: [ + { name: "output", type: "array[object]" }, + ] + }, + 'parameter-extractor': { + default: [ + { name: "__is_success", type: "number" }, + { name: "__reason", type: "string" }, + ], + define: ['params'] + }, + 'memory-read': { + default: [ + { name: "answer", type: "string" }, + { name: "intermediate_outputs", type: "array[object]" }, + ], + }, + 'memory-write': { + + }, + 'if-else': { + + }, + 'question-classifier': { + default: [ + { name: "class_name", type: "string" }, + // { name: "output", type: "string" }, + ], + }, + 'iteration': { + default: [ + // { name: "item", type: "string" }, // 仅内部使用 + { name: "output", type: "array[string]" }, + ], + }, + 'loop': { + define: ['cycle_vars'] + }, + 'cycle-start': { + + }, + 'break': { + + }, + 'var-aggregator': { + // default: [ + // { name: "output", type: "string" }, + // ], + define: ['group_variables'] + }, + 'assigner': { + + }, + 'http-request': { + default: [ + { name: "body", type: "string" }, + { name: "status_code", type: "number" }, + ], + error: [ + { name: "error_message", type: "string" }, + { name: "error_type", type: "string" }, + ] + }, + 'tool': { + default: [ + { name: "data", type: "string" }, + ], + }, + 'jinja-render': { + default: [ + { name: "output", type: "string" }, + ], + }, } \ No newline at end of file