### What problem does this PR solve? feat: Automatically save agent page data #3301 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.14.0
| @@ -1036,6 +1036,7 @@ The above is the content you need to summarize.`, | |||
| howUseId: 'How to use agent ID?', | |||
| content: 'Content', | |||
| operationResults: 'Operation Results', | |||
| autosave: 'Automatically saved', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -984,6 +984,7 @@ export default { | |||
| howUseId: '如何使用Agent ID?', | |||
| content: '內容', | |||
| operationResults: '運行結果', | |||
| autosave: '已自動儲存', | |||
| }, | |||
| footer: { | |||
| profile: '“保留所有權利 @ react”', | |||
| @@ -1004,6 +1004,7 @@ export default { | |||
| howUseId: '如何使用Agent ID?', | |||
| content: '内容', | |||
| operationResults: '运行结果', | |||
| autosave: '已自动保存', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -128,6 +128,9 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) { | |||
| onSelectionChange={onSelectionChange} | |||
| nodeOrigin={[0.5, 0]} | |||
| isValidConnection={isValidConnection} | |||
| onChangeCapture={(...params) => { | |||
| console.info('onChangeCapture:', ...params); | |||
| }} | |||
| onChange={(...params) => { | |||
| console.info('params:', ...params); | |||
| }} | |||
| @@ -140,18 +143,6 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) { | |||
| }, | |||
| }} | |||
| deleteKeyCode={['Delete', 'Backspace']} | |||
| onPaste={(...params) => { | |||
| console.info('onPaste:', ...params); | |||
| }} | |||
| onPasteCapture={(...params) => { | |||
| console.info('onPasteCapture:', ...params); | |||
| }} | |||
| onCopy={(...params) => { | |||
| console.info('onCopy:', ...params); | |||
| }} | |||
| onCopyCapture={(...params) => { | |||
| console.info('onCopyCapture:', ...params); | |||
| }} | |||
| > | |||
| <Background /> | |||
| <Controls /> | |||
| @@ -5,12 +5,15 @@ import { NodeData } from '../../interface'; | |||
| import { RightHandleStyle } from './handle-icon'; | |||
| import { get } from 'lodash'; | |||
| import { useReplaceIdWithName } from '../../hooks'; | |||
| import styles from './index.less'; | |||
| import NodeHeader from './node-header'; | |||
| export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { | |||
| const yes = get(data, 'form.yes'); | |||
| const no = get(data, 'form.no'); | |||
| const replaceIdWithName = useReplaceIdWithName(); | |||
| return ( | |||
| <section | |||
| className={classNames(styles.logicNode, { | |||
| @@ -50,11 +53,11 @@ export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { | |||
| <Flex vertical gap={10}> | |||
| <Flex vertical> | |||
| <div className={styles.relevantLabel}>Yes</div> | |||
| <div className={styles.nodeText}>{yes}</div> | |||
| <div className={styles.nodeText}>{replaceIdWithName(yes)}</div> | |||
| </Flex> | |||
| <Flex vertical> | |||
| <div className={styles.relevantLabel}>No</div> | |||
| <div className={styles.nodeText}>{no}</div> | |||
| <div className={styles.nodeText}>{replaceIdWithName(no)}</div> | |||
| </Flex> | |||
| </Flex> | |||
| </section> | |||
| @@ -1,3 +1,3 @@ | |||
| .flowHeader { | |||
| padding: 0 20px; | |||
| padding: 10px 20px; | |||
| } | |||
| @@ -5,7 +5,11 @@ import { ArrowLeftOutlined } from '@ant-design/icons'; | |||
| import { Button, Flex, Space } from 'antd'; | |||
| import { Link, useParams } from 'umi'; | |||
| import FlowIdModal from '../flow-id-modal'; | |||
| import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer } from '../hooks'; | |||
| import { | |||
| useSaveGraph, | |||
| useSaveGraphBeforeOpeningDebugDrawer, | |||
| useWatchAgentChange, | |||
| } from '../hooks'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| @@ -20,10 +24,11 @@ const FlowHeader = ({ showChatDrawer }: IProps) => { | |||
| const { | |||
| visible: overviewVisible, | |||
| hideModal: hideOverviewModal, | |||
| showModal: showOverviewModal, | |||
| // showModal: showOverviewModal, | |||
| } = useSetModalState(); | |||
| const { visible, hideModal, showModal } = useSetModalState(); | |||
| const { id } = useParams(); | |||
| const time = useWatchAgentChange(); | |||
| return ( | |||
| <> | |||
| @@ -37,7 +42,10 @@ const FlowHeader = ({ showChatDrawer }: IProps) => { | |||
| <Link to={`/flow`}> | |||
| <ArrowLeftOutlined /> | |||
| </Link> | |||
| <h3>{data.title}</h3> | |||
| <div className="flex flex-col"> | |||
| <span className="font-semibold text-[18px]">{data.title}</span> | |||
| <span className="font-normal text-sm">已自动保存 {time}</span> | |||
| </div> | |||
| </Space> | |||
| <Space size={'large'}> | |||
| <Button onClick={handleRun}> | |||
| @@ -19,7 +19,9 @@ import { | |||
| import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks'; | |||
| import { Variable } from '@/interfaces/database/chat'; | |||
| import api from '@/utils/api'; | |||
| import { useDebounceEffect } from 'ahooks'; | |||
| import { FormInstance, message } from 'antd'; | |||
| import dayjs from 'dayjs'; | |||
| import { humanId } from 'human-id'; | |||
| import { lowerFirst } from 'lodash'; | |||
| import trim from 'lodash/trim'; | |||
| @@ -446,12 +448,21 @@ export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { | |||
| return handleRun; | |||
| }; | |||
| export const useReplaceIdWithText = (output: unknown) => { | |||
| export const useReplaceIdWithName = () => { | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| const getNameById = (id?: string) => { | |||
| return getNode(id)?.data.name; | |||
| }; | |||
| const replaceIdWithName = useCallback( | |||
| (id?: string) => { | |||
| return getNode(id)?.data.name; | |||
| }, | |||
| [getNode], | |||
| ); | |||
| return replaceIdWithName; | |||
| }; | |||
| export const useReplaceIdWithText = (output: unknown) => { | |||
| const getNameById = useReplaceIdWithName(); | |||
| return { | |||
| replacedOutput: replaceIdWithText(output, getNameById), | |||
| @@ -547,6 +558,7 @@ export const useWatchNodeFormDataChange = () => { | |||
| ); | |||
| useEffect(() => { | |||
| console.info('xxx'); | |||
| nodes.forEach((node) => { | |||
| const currentNode = getNode(node.id); | |||
| const form = currentNode?.data.form ?? {}; | |||
| @@ -668,3 +680,36 @@ export const useCopyPaste = () => { | |||
| }; | |||
| }, [onPasteCapture]); | |||
| }; | |||
| export const useWatchAgentChange = () => { | |||
| const [time, setTime] = useState<string>(); | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const edges = useGraphStore((state) => state.edges); | |||
| const { saveGraph } = useSaveGraph(); | |||
| const { data: flowDetail } = useFetchFlow(); | |||
| const setSaveTime = useCallback((updateTime: number) => { | |||
| setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')); | |||
| }, []); | |||
| useEffect(() => { | |||
| setSaveTime(flowDetail?.update_time); | |||
| }, [flowDetail, setSaveTime]); | |||
| const saveAgent = useCallback(async () => { | |||
| const ret = await saveGraph(); | |||
| setSaveTime(ret.data.update_time); | |||
| }, [saveGraph, setSaveTime]); | |||
| useDebounceEffect( | |||
| () => { | |||
| saveAgent(); | |||
| }, | |||
| [nodes, edges], | |||
| { | |||
| wait: 1000 * 20, | |||
| }, | |||
| ); | |||
| return time; | |||
| }; | |||
| @@ -24,6 +24,7 @@ import { immer } from 'zustand/middleware/immer'; | |||
| import { Operator, SwitchElseTo } from './constant'; | |||
| import { NodeData } from './interface'; | |||
| import { | |||
| duplicateNodeForm, | |||
| generateNodeNamesWithIncreasingIndex, | |||
| getNodeDragHandle, | |||
| getOperatorIndex, | |||
| @@ -242,7 +243,10 @@ const useGraphStore = create<RFState>()( | |||
| addNode({ | |||
| ...(node || {}), | |||
| data: { ...(node?.data ?? {}), name: generateNodeName(name) }, | |||
| data: { | |||
| ...duplicateNodeForm(node?.data), | |||
| name: generateNodeName(name), | |||
| }, | |||
| selected: false, | |||
| dragging: false, | |||
| id: `${node?.data?.label}:${humanId()}`, | |||
| @@ -289,3 +289,31 @@ export const generateNodeNamesWithIncreasingIndex = ( | |||
| return `${name}_${index}`; | |||
| }; | |||
| export const duplicateNodeForm = (nodeData?: NodeData) => { | |||
| const form: Record<string, any> = { ...(nodeData?.form ?? {}) }; | |||
| // Delete the downstream node corresponding to the to field of the Categorize operator | |||
| if (nodeData?.label === Operator.Categorize) { | |||
| form.category_description = Object.keys(form.category_description).reduce< | |||
| Record<string, Record<string, any>> | |||
| >((pre, cur) => { | |||
| pre[cur] = { | |||
| ...form.category_description[cur], | |||
| to: undefined, | |||
| }; | |||
| return pre; | |||
| }, {}); | |||
| } | |||
| // Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator | |||
| if (nodeData?.label === Operator.Relevant) { | |||
| form.yes = undefined; | |||
| form.no = undefined; | |||
| } | |||
| return { | |||
| ...(nodeData ?? {}), | |||
| form, | |||
| }; | |||
| }; | |||