feat: Automatically save agent page data #3301 (#3302)

### 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)
This commit is contained in:
balibabu 2024-11-08 17:28:11 +08:00 committed by GitHub
parent 464a4d6ead
commit 74d1eeb4d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 105 additions and 23 deletions

View File

@ -1036,6 +1036,7 @@ The above is the content you need to summarize.`,
howUseId: 'How to use agent ID?', howUseId: 'How to use agent ID?',
content: 'Content', content: 'Content',
operationResults: 'Operation Results', operationResults: 'Operation Results',
autosave: 'Automatically saved',
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

View File

@ -984,6 +984,7 @@ export default {
howUseId: '如何使用Agent ID', howUseId: '如何使用Agent ID',
content: '內容', content: '內容',
operationResults: '運行結果', operationResults: '運行結果',
autosave: '已自動儲存',
}, },
footer: { footer: {
profile: '“保留所有權利 @ react”', profile: '“保留所有權利 @ react”',

View File

@ -1004,6 +1004,7 @@ export default {
howUseId: '如何使用Agent ID', howUseId: '如何使用Agent ID',
content: '内容', content: '内容',
operationResults: '运行结果', operationResults: '运行结果',
autosave: '已自动保存',
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

View File

@ -128,6 +128,9 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
nodeOrigin={[0.5, 0]} nodeOrigin={[0.5, 0]}
isValidConnection={isValidConnection} isValidConnection={isValidConnection}
onChangeCapture={(...params) => {
console.info('onChangeCapture:', ...params);
}}
onChange={(...params) => { onChange={(...params) => {
console.info('params:', ...params); console.info('params:', ...params);
}} }}
@ -140,18 +143,6 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
}, },
}} }}
deleteKeyCode={['Delete', 'Backspace']} 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 /> <Background />
<Controls /> <Controls />

View File

@ -5,12 +5,15 @@ import { NodeData } from '../../interface';
import { RightHandleStyle } from './handle-icon'; import { RightHandleStyle } from './handle-icon';
import { get } from 'lodash'; import { get } from 'lodash';
import { useReplaceIdWithName } from '../../hooks';
import styles from './index.less'; import styles from './index.less';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
const yes = get(data, 'form.yes'); const yes = get(data, 'form.yes');
const no = get(data, 'form.no'); const no = get(data, 'form.no');
const replaceIdWithName = useReplaceIdWithName();
return ( return (
<section <section
className={classNames(styles.logicNode, { className={classNames(styles.logicNode, {
@ -50,11 +53,11 @@ export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
<Flex vertical gap={10}> <Flex vertical gap={10}>
<Flex vertical> <Flex vertical>
<div className={styles.relevantLabel}>Yes</div> <div className={styles.relevantLabel}>Yes</div>
<div className={styles.nodeText}>{yes}</div> <div className={styles.nodeText}>{replaceIdWithName(yes)}</div>
</Flex> </Flex>
<Flex vertical> <Flex vertical>
<div className={styles.relevantLabel}>No</div> <div className={styles.relevantLabel}>No</div>
<div className={styles.nodeText}>{no}</div> <div className={styles.nodeText}>{replaceIdWithName(no)}</div>
</Flex> </Flex>
</Flex> </Flex>
</section> </section>

View File

@ -1,3 +1,3 @@
.flowHeader { .flowHeader {
padding: 0 20px; padding: 10px 20px;
} }

View File

@ -5,7 +5,11 @@ import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd'; import { Button, Flex, Space } from 'antd';
import { Link, useParams } from 'umi'; import { Link, useParams } from 'umi';
import FlowIdModal from '../flow-id-modal'; import FlowIdModal from '../flow-id-modal';
import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer } from '../hooks'; import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from '../hooks';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
@ -20,10 +24,11 @@ const FlowHeader = ({ showChatDrawer }: IProps) => {
const { const {
visible: overviewVisible, visible: overviewVisible,
hideModal: hideOverviewModal, hideModal: hideOverviewModal,
showModal: showOverviewModal, // showModal: showOverviewModal,
} = useSetModalState(); } = useSetModalState();
const { visible, hideModal, showModal } = useSetModalState(); const { visible, hideModal, showModal } = useSetModalState();
const { id } = useParams(); const { id } = useParams();
const time = useWatchAgentChange();
return ( return (
<> <>
@ -37,7 +42,10 @@ const FlowHeader = ({ showChatDrawer }: IProps) => {
<Link to={`/flow`}> <Link to={`/flow`}>
<ArrowLeftOutlined /> <ArrowLeftOutlined />
</Link> </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>
<Space size={'large'}> <Space size={'large'}>
<Button onClick={handleRun}> <Button onClick={handleRun}>

View File

@ -19,7 +19,9 @@ import {
import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks'; import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks';
import { Variable } from '@/interfaces/database/chat'; import { Variable } from '@/interfaces/database/chat';
import api from '@/utils/api'; import api from '@/utils/api';
import { useDebounceEffect } from 'ahooks';
import { FormInstance, message } from 'antd'; import { FormInstance, message } from 'antd';
import dayjs from 'dayjs';
import { humanId } from 'human-id'; import { humanId } from 'human-id';
import { lowerFirst } from 'lodash'; import { lowerFirst } from 'lodash';
import trim from 'lodash/trim'; import trim from 'lodash/trim';
@ -446,13 +448,22 @@ export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
return handleRun; return handleRun;
}; };
export const useReplaceIdWithText = (output: unknown) => { export const useReplaceIdWithName = () => {
const getNode = useGraphStore((state) => state.getNode); const getNode = useGraphStore((state) => state.getNode);
const getNameById = (id?: string) => { const replaceIdWithName = useCallback(
(id?: string) => {
return getNode(id)?.data.name; return getNode(id)?.data.name;
},
[getNode],
);
return replaceIdWithName;
}; };
export const useReplaceIdWithText = (output: unknown) => {
const getNameById = useReplaceIdWithName();
return { return {
replacedOutput: replaceIdWithText(output, getNameById), replacedOutput: replaceIdWithText(output, getNameById),
getNameById, getNameById,
@ -547,6 +558,7 @@ export const useWatchNodeFormDataChange = () => {
); );
useEffect(() => { useEffect(() => {
console.info('xxx');
nodes.forEach((node) => { nodes.forEach((node) => {
const currentNode = getNode(node.id); const currentNode = getNode(node.id);
const form = currentNode?.data.form ?? {}; const form = currentNode?.data.form ?? {};
@ -668,3 +680,36 @@ export const useCopyPaste = () => {
}; };
}, [onPasteCapture]); }, [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;
};

View File

@ -24,6 +24,7 @@ import { immer } from 'zustand/middleware/immer';
import { Operator, SwitchElseTo } from './constant'; import { Operator, SwitchElseTo } from './constant';
import { NodeData } from './interface'; import { NodeData } from './interface';
import { import {
duplicateNodeForm,
generateNodeNamesWithIncreasingIndex, generateNodeNamesWithIncreasingIndex,
getNodeDragHandle, getNodeDragHandle,
getOperatorIndex, getOperatorIndex,
@ -242,7 +243,10 @@ const useGraphStore = create<RFState>()(
addNode({ addNode({
...(node || {}), ...(node || {}),
data: { ...(node?.data ?? {}), name: generateNodeName(name) }, data: {
...duplicateNodeForm(node?.data),
name: generateNodeName(name),
},
selected: false, selected: false,
dragging: false, dragging: false,
id: `${node?.data?.label}:${humanId()}`, id: `${node?.data?.label}:${humanId()}`,

View File

@ -289,3 +289,31 @@ export const generateNodeNamesWithIncreasingIndex = (
return `${name}_${index}`; 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,
};
};