+
{data.node.description}
<>
{Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_")
@@ -230,7 +232,7 @@ export default function GenericNode({ data, xPos, yPos, selected }: {
type={data.node.template[t].type}
optionalHandle={data.node.template[t].input_types}
onChange={() => fouceUpdateNode(!_)}
- nodeColorsP={nodeColors}
+ // nodeColorsP={nodeColors}
/>
) : (
<>>
@@ -258,7 +260,7 @@ export default function GenericNode({ data, xPos, yPos, selected }: {
id={[data.type, data.id, ...data.node.base_classes].join("|")}
type={data.node.base_classes.join("|")}
left={false}
- nodeColorsP={nodeColors}
+ // nodeColorsP={nodeColors}
/>
{data.type === 'Report' &&
diff --git a/src/assets/toolbar/version.png b/src/assets/toolbar/version.png
new file mode 100644
index 0000000..4fb6f4d
Binary files /dev/null and b/src/assets/toolbar/version.png differ
diff --git a/src/components/chatComponent/buildTrigger/index.tsx b/src/components/chatComponent/buildTrigger/index.tsx
index bf638b3..1fcdafd 100644
--- a/src/components/chatComponent/buildTrigger/index.tsx
+++ b/src/components/chatComponent/buildTrigger/index.tsx
@@ -52,13 +52,13 @@ export default function BuildTrigger({
/**
* 拦截flow,过滤node数据,去除groupNode
*/
- try {
- flow.data.nodes = flow?.data?.nodes?.filter(node => {
- return node.id.indexOf('groupNode') < 0;
- })
- } catch (e) {
- console.log(e)
- }
+ // try {
+ // flow.data.nodes = flow?.data?.nodes?.filter(node => {
+ // return node.id.indexOf('groupNode') < 0;
+ // })
+ // } catch (e) {
+ // console.log(e)
+ // }
const allNodesValid = await streamNodeData(flow);
await enforceMinimumLoadingTime(startTime, minimumLoadingTime); // 200内完成streamNodeData,阻塞剩余时间;否则不阻塞(最大等待200)
@@ -79,7 +79,7 @@ export default function BuildTrigger({
}
async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID
- const { flowId } = await postBuildInit(flow);
+ const { flowId } = await postBuildInit({ flow });
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults = [];
let finished = false;
diff --git a/src/contexts/tabsContext.tsx b/src/contexts/tabsContext.tsx
index 632ef30..5cc9c71 100644
--- a/src/contexts/tabsContext.tsx
+++ b/src/contexts/tabsContext.tsx
@@ -3,7 +3,7 @@ import { ReactNode, createContext, useContext, useState } from "react";
import { addEdge } from "reactflow";
import { updateFlowApi } from "../controllers/API/flow";
import { APIClassType, APITemplateType } from "../types/api";
-import { FlowType, NodeType } from "../types/flow";
+import { FlowType, FlowVersionItem, NodeType } from "../types/flow";
import { TabsContextType, TabsState } from "../types/tabs";
import { generateUUID, updateTemplate } from "../utils";
import { alertContext } from "./alertContext";
@@ -28,6 +28,8 @@ const TabsContextInitialValue: TabsContextType = {
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number }
) => { },
+ version: null,
+ setVersion: (version: FlowVersionItem | null) => ""
};
export const TabsContext = createContext
(
@@ -36,6 +38,7 @@ export const TabsContext = createContext(
export function TabsProvider({ children }: { children: ReactNode }) {
const [flow, setFlow] = useState(null);
+ const [version, setVersion] = useState(null);
// flowid: formKeysData
const [tabsState, setTabsState] = useState({});
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
@@ -289,6 +292,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
}
+ // 上线版本的版本 id
+ const [onlineVid, setOnlineVid] = useState(0);
+ const updateOnlineVid = (vid: number) => {
+ setOnlineVid(flow.status === 2 ? vid : 0);
+ }
+
return (
version.id === onlineVid,
+ updateOnlineVid
}}
>
{children}
diff --git a/src/pages/DiffFlowPage/components/Cell.tsx b/src/pages/DiffFlowPage/components/Cell.tsx
new file mode 100644
index 0000000..eda8530
--- /dev/null
+++ b/src/pages/DiffFlowPage/components/Cell.tsx
@@ -0,0 +1,93 @@
+import Skeleton from "@/components/bs-ui/skeleton";
+import { CodeBlock } from "@/modals/formModal/chatMessage/codeBlock";
+import { useDiffFlowStore } from "@/store/diffFlowStore";
+import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
+import ReactMarkdown from "react-markdown";
+import rehypeMathjax from "rehype-mathjax";
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+
+const Cell = forwardRef((props, ref) => {
+
+ const [value, setValue] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ useImperativeHandle(ref, () => ({
+ loading: () => {
+ setLoading(true)
+ },
+ loaded: () => {
+ setLoading(false)
+ },
+ setData: (val) => {
+ setLoading(false)
+
+ let i = 0
+ const print = () => {
+ const value = val.substring(0, i++)
+ setValue(value)
+ i < val.length && setTimeout(print, Math.floor(Math.random() * 10) + 20)
+ }
+ print()
+ },
+ getData() {
+ return value
+ }
+ }));
+
+ if (loading) return
+
+ return
+ {
+ if (children.length) {
+ if (children[0] === "▍") {
+ return ( ▍ );
+ }
+
+ children[0] = (children[0] as string).replace("`▍`", "▍");
+ }
+
+ const match = /language-(\w+)/.exec(className || "");
+
+ return !inline ? (
+
+ ) : (
+ {children}
+ );
+ },
+ }}
+ >
+ {value.toString()}
+
+
+})
+
+
+export default function CellWarp({ qIndex, versionId }) {
+ const ref = useRef(null);
+ const addCellRef = useDiffFlowStore(state => state.addCellRef);
+ const removeCellRef = useDiffFlowStore(state => state.removeCellRef);
+
+ useEffect(() => {
+ const key = `${qIndex}-${versionId}`
+ addCellRef(key, ref);
+
+ // 组件卸载时删除 ref
+ return () => {
+ removeCellRef(key);
+ };
+ }, [qIndex, versionId, addCellRef, removeCellRef]);
+
+ return |
+};
diff --git a/src/pages/DiffFlowPage/components/Component.tsx b/src/pages/DiffFlowPage/components/Component.tsx
new file mode 100644
index 0000000..ac8fbc5
--- /dev/null
+++ b/src/pages/DiffFlowPage/components/Component.tsx
@@ -0,0 +1,122 @@
+import { DelIcon } from "@/components/bs-icons/del";
+import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/bs-ui/select";
+import { useMemo } from "react";
+import ComponentParameter from "./ComponentParameter";
+import del from "../../../assets/npc/del.png";
+
+export default function Component({ compId, options, disables, version, className, onChangeVersion, onClose }) {
+
+ // 保留当前compId和上游组件
+ const nodes = useMemo(() => {
+ if (!version?.data) return [];
+ const showNodes = {}
+ const edges = version.data.edges
+
+ const deep = (_compId) => {
+ edges.forEach(edge => {
+ if (edge.target === _compId) {
+ showNodes[edge.source] = true
+ showNodes[edge.target] = true
+ deep(edge.source)
+ }
+ })
+ }
+ deep(compId)
+
+ return version.data.nodes.filter(node => showNodes[node.id])
+ }, [version, compId])
+
+ // empty
+ if (!version) return
+
+
+ {/*
*/}
+

+
+
+
+
+ // 版本信息
+ return
+
+
+
+ {version.update_time.replace('T', ' ')}
+ {/* */}
+
+
+
+
+
+
+ 组件
+ 参数名
+ 参数值
+
+ {
+ nodes.map(node => (
+
+
{node.data.type}
+
+ {
+
+ {
+ (key, name, formItem) => (
+
+ )
+ }
+
+ }
+
+
+ ))
+ }
+
+
+};
diff --git a/src/pages/DiffFlowPage/components/ComponentParameter.tsx b/src/pages/DiffFlowPage/components/ComponentParameter.tsx
new file mode 100644
index 0000000..2842127
--- /dev/null
+++ b/src/pages/DiffFlowPage/components/ComponentParameter.tsx
@@ -0,0 +1,176 @@
+import CodeAreaComponent from "@/components/codeAreaComponent";
+import Dropdown from "@/components/dropdownComponent";
+import FloatComponent from "@/components/floatComponent";
+import InputComponent from "@/components/inputComponent";
+import InputFileComponent from "@/components/inputFileComponent";
+import InputListComponent from "@/components/inputListComponent";
+import IntComponent from "@/components/intComponent";
+import PromptAreaComponent from "@/components/promptComponent";
+import TextAreaComponent from "@/components/textAreaComponent";
+import ToggleShadComponent from "@/components/toggleShadComponent";
+import { useMemo } from "react";
+
+/**
+ * 组件中的填写参数罗列
+ * 参数模板 template
+ */
+export default function ComponentParameter({ disabled = false, flow, node, template, children, onChange = () => { } }) {
+ const _disabled = false // disabled || (flow.data.edges.some((e) => e.targetHandle === node.id) ?? false);
+
+ const keys = useMemo(() => {
+ return Object.keys(template).filter(
+ (t) =>
+ t.charAt(0) !== "_" &&
+ template[t].show &&
+ (template[t].type === "str" ||
+ template[t].type === "bool" ||
+ template[t].type === "float" ||
+ template[t].type === "code" ||
+ template[t].type === "prompt" ||
+ template[t].type === "file" ||
+ template[t].type === "int" ||
+ template[t].type === "dict")
+ )
+ }, [template])
+
+ const handleOnNewValue = (newValue: any, name) => {
+ // console.log('object :>> ', object);
+ // 引用更新
+ node.data.node.template[name].value = newValue;
+ // 手动修改知识库,collection_id 清空
+ if (['index_name', 'collection_name'].includes(name)) delete node.data.node.template[name].collection_id
+ onChange() // 更新通知
+ }
+
+ const getStrComp = (template, n) => {
+ return template[n].list ? (
+ {
+ handleOnNewValue(t, n);
+ }}
+ />
+ ) : template[n].multiline ? (
+ {
+ handleOnNewValue(t, n);
+ }}
+ />
+ ) : (
+ {
+ handleOnNewValue(t, n);
+ }}
+ />
+ )
+ }
+
+ return <>
+ {keys.map((n, i) => {
+ const name = template[n].name || template[n].display_name
+
+ if (template[n].type === "str") {
+ if (template[n].options) {
+ return children(n, name, handleOnNewValue(t, n)}
+ value={
+ template[n].value ??
+ "Choose an option"
+ }
+ >)
+ } else {
+ return children(n, name, getStrComp(template, n))
+ }
+ }
+
+ switch (template[n].type) {
+ case "bool":
+ return children(n, name, {
+ handleOnNewValue(t, n);
+ }}
+ size="small"
+ />)
+ case "float":
+ return children(n, name, {
+ template[n].value = t;
+ }}
+ />)
+ case "int":
+ return children(n, name, {
+ handleOnNewValue(t, n);
+ }}
+ />)
+ case "file":
+ return children(n, name, {
+ handleOnNewValue(t, n);
+ }}
+ fileTypes={template[n].fileTypes}
+ suffixes={template[n].suffixes}
+ onFileChange={(t: string) => {
+ handleOnNewValue(t, n);
+ }}
+ >)
+ case "prompt":
+ return children(n, name, {
+ node.data.node = nodeClass;
+ }}
+ value={template[n].value ?? ""}
+ onChange={(t: string) => {
+ handleOnNewValue(t, n);
+ }}
+ />)
+ case "code":
+ return children(n, name, {
+ handleOnNewValue(t, n);
+ }}
+ />)
+ case "Any": return children(n, name, "-")
+ default: return children(n, name, )
+ }
+ })
+ }
+ >
+};
diff --git a/src/pages/DiffFlowPage/components/RunForm.tsx b/src/pages/DiffFlowPage/components/RunForm.tsx
new file mode 100644
index 0000000..47b97e9
--- /dev/null
+++ b/src/pages/DiffFlowPage/components/RunForm.tsx
@@ -0,0 +1,28 @@
+import { Button } from "@/components/bs-ui/button";
+import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/bs-ui/dialog";
+import ChatReportForm from "@/pages/ChatAppPage/components/ChatReportForm";
+import { useRef } from "react";
+
+export default function RunForm({ show, flow, onChangeShow, onSubmit }) {
+
+ const formRef = useRef(null);
+ const handleSubmit = () => {
+ formRef.current.submit()
+ }
+
+ return
+
+ 测试运行
+ 请输入上游依赖参数
+
+ {
+ show &&
+ }
+
+
+
+
+
+
+
+};
diff --git a/src/pages/DiffFlowPage/components/RunTest.tsx b/src/pages/DiffFlowPage/components/RunTest.tsx
new file mode 100644
index 0000000..e47418e
--- /dev/null
+++ b/src/pages/DiffFlowPage/components/RunTest.tsx
@@ -0,0 +1,345 @@
+import { Button } from "@/components/bs-ui/button";
+import { Dialog, DialogTrigger } from "@/components/bs-ui/dialog";
+import { Input } from "@/components/bs-ui/input";
+import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/bs-ui/table";
+import { useToast } from "@/components/bs-ui/toast/use-toast";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/bs-ui/tooltip";
+import { useDiffFlowStore } from "@/store/diffFlowStore";
+import { DownloadIcon, PlayIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
+import { useMemo, useRef, useState } from "react";
+import CellWarp from "./Cell";
+import RunForm from "./RunForm";
+import { DelIcon } from "@/components/bs-icons/del";
+import * as XLSX from 'xlsx';
+import { useTranslation } from "react-i18next";
+import { FlowStyleType, FlowType } from "@/types/flow";
+import { postBuildInit } from "@/controllers/API";
+import { generateUUID } from "@/components/bs-ui/utils";
+import del from "../../../assets/npc/del.png";
+
+export default function RunTest({ nodeId }) {
+
+ const { t } = useTranslation()
+ const [formShow, setFormShow] = useState(false)
+ const { running, runningType, mulitVersionFlow, readyVersions, questions, removeQuestion, cellRefs,
+ allRunStart, rowRunStart, colRunStart, overQuestions, addQuestion, updateQuestion } = useDiffFlowStore()
+
+ // 是否展示表单
+ const isForm = useMemo(() => {
+ const flowData = mulitVersionFlow?.[0]?.data
+ if (!flowData) return false
+
+ return flowData.nodes.some(node => ["VariableNode", "InputFileNode"].includes(node.data.type))
+ }, [mulitVersionFlow])
+
+ // 选中的测试版本数
+ const versionColWidth = useMemo(() => {
+ const count = mulitVersionFlow.reduce((count, cur) => {
+ return cur ? count + 1 : count
+ }, 0) + 1 // +1 测试用例列
+
+ return 100 / (count === 2 ? 2 : count + 1) // hack 两个 按 45% 分
+ }, [mulitVersionFlow])
+
+ const handleUploadTxt = () => {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = ".txt";
+ input.onchange = (e: Event) => {
+ if (
+ (e.target as HTMLInputElement).files[0].type === "text/plain"
+ ) {
+ const currentfile = (e.target as HTMLInputElement).files[0];
+ currentfile.text().then((text) => {
+ console.log(text, "text");
+ overQuestions(text.split('\n'))
+ });
+ }
+ };
+ input.click();
+ }
+
+ const { message } = useToast()
+ const inputsRef = useRef(null)
+ const build = useBuild()
+ const handleRunTest = async (inputs = null, query = '') => {
+ setFormShow(false)
+ const res = await build(mulitVersionFlow[0])
+ // console.log('res :>> ', res);
+ const input = res.input_keys.find((el: any) => !el.type)
+ const inputKey = input ? Object.keys(input)[0] : '';
+ inputsRef.current = { ...input, id: nodeId, [inputKey]: query, data: inputs }
+ //
+ if (questions.length === 0) return message({
+ title: t('prompt'),
+ description: t('test.addTest'),
+ variant: 'warning'
+ })
+ allRunStart(nodeId, inputsRef.current)
+ }
+
+ const handleColRunTest = (versionId) => {
+ colRunStart(versionId, nodeId, inputsRef.current)
+ }
+
+ const handleRowRunTest = (qIndex) => {
+ rowRunStart(qIndex, nodeId, inputsRef.current)
+ }
+
+ // 导出结果(excle)
+ const handleDownExcle = () => {
+ const data = [['测试用例', ...mulitVersionFlow.map(version => version.name)]];
+
+ questions.forEach((_, index) => {
+ const rowData = [_.q]
+ mulitVersionFlow.forEach(version => {
+ rowData.push(cellRefs[`${index}-${version.id}`].current.getData())
+ })
+ data.push(rowData)
+ })
+ mulitVersionFlow
+
+ // 创建Workbook对象
+ const wb = XLSX.utils.book_new();
+ // 添加Worksheet到Workbook中
+ const ws = XLSX.utils.aoa_to_sheet(data);
+ XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
+
+ // 生成Excel文件
+ const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
+ const blob = new Blob([wbout], { type: 'application/octet-stream' });
+ // 创建下载链接
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "test_result.xlsx";
+
+ // 模拟点击下载链接
+ document.body.appendChild(a);
+ a.click();
+
+ // 清理URL对象
+ setTimeout(function () {
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+ }, 0);
+ }
+
+ const notDiffVersion = useMemo(() => !mulitVersionFlow.some((version) => version), [mulitVersionFlow])
+
+ return
+
+
+
+
+
+
+
+
+
+
+ {t('test.explain')}
+
+
+
+
+ {
+ isForm ?
:
+
+ }
+
+ {/* table */}
+
+
+
+ {t('test.testCase')}
+ {
+ mulitVersionFlow.map(version =>
+ version &&
+
+
{version.name}
+ {readyVersions[version.id] &&
}
+
+
+ )
+ }
+
+
+
+
+
+
+ {
+ questions.map((question, index) => (
+
+
+
+
updateQuestion(e.target.value, index)}
+ >
+ {question.ready &&
}
+
+
+ {/* 版本 */}
+ {mulitVersionFlow.map(flow =>
+ flow &&
+
+
+ )}
+
+
+
+
+ ))
+ }
+
+
+
+ {questions.length < 20 &&
+
+ {
+ if (e.key === 'Enter') {
+ if (!e.target.value) return
+ addQuestion(e.target.value)
+ e.target.value = ''
+ }
+ }}
+ onBlur={(e) => {
+ if (!e.target.value) return
+ addQuestion(e.target.value)
+ e.target.value = ''
+ }} />
+
+
+ }
+
+
+
+
+
+
+};
+
+
+const useBuild = () => {
+ const { toast } = useToast()
+
+ // SSE 服务端推送
+ async function streamNodeData(flow: FlowType, chatId: string) {
+ let res = null
+ // Step 1: Make a POST request to send the flow data and receive a unique session ID
+ const _flow = { ...flow, id: flow.flow_id }
+ const { flowId } = await postBuildInit({ flow: _flow, versionId: flow.id });
+ // Step 2: Use the session ID to establish an SSE connection using EventSource
+ let validationResults = [];
+ let finished = false;
+ let buildEnd = false
+ const qstr = flow.id ? `?version_id=${flow.id}` : ''
+ const apiUrl = `/api/v1/build/stream/${flowId}${qstr}`;
+ const eventSource = new EventSource(apiUrl);
+
+ eventSource.onmessage = (event) => {
+ // If the event is parseable, return
+ if (!event.data) {
+ return;
+ }
+ const parsedData = JSON.parse(event.data);
+ // if the event is the end of the stream, close the connection
+ if (parsedData.end_of_stream) {
+ eventSource.close(); // 结束关闭链接
+ buildEnd = true
+ return;
+ } else if (parsedData.log) {
+ // If the event is a log, log it
+ // setSuccessData({ title: parsedData.log });
+ } else if (parsedData.input_keys) {
+ res = parsedData
+ } else {
+ // setProgress(parsedData.progress);
+ validationResults.push(parsedData.valid);
+ }
+ };
+
+ eventSource.onerror = (error: any) => {
+ console.error("EventSource failed:", error);
+ eventSource.close();
+ if (error.data) {
+ const parsedData = JSON.parse(error.data);
+ toast({
+ title: parsedData.error,
+ variant: 'error',
+ description: ''
+ });
+ }
+ };
+ // Step 3: Wait for the stream to finish
+ while (!finished) {
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ finished = buildEnd // validationResults.length === flow.data.nodes.length;
+ }
+ // Step 4: Return true if all nodes are valid, false otherwise
+ if (validationResults.every((result) => result)) {
+ return res
+ }
+ }
+
+ // 延时器
+ async function enforceMinimumLoadingTime(
+ startTime: number,
+ minimumLoadingTime: number
+ ) {
+ const elapsedTime = Date.now() - startTime;
+ const remainingTime = minimumLoadingTime - elapsedTime;
+
+ if (remainingTime > 0) {
+ return new Promise((resolve) => setTimeout(resolve, remainingTime));
+ }
+ }
+
+ async function handleBuild(flow: FlowStyleType) {
+ try {
+ const minimumLoadingTime = 200; // in milliseconds
+ const startTime = Date.now();
+
+ const res = await streamNodeData(flow, generateUUID(32));
+ await enforceMinimumLoadingTime(startTime, minimumLoadingTime); // 至少等200ms, 再继续(强制最小load时间)
+ return res
+ } catch (error) {
+ console.error("Error:", error);
+ } finally {
+ }
+ }
+
+ return handleBuild
+}
\ No newline at end of file
diff --git a/src/pages/DiffFlowPage/index.tsx b/src/pages/DiffFlowPage/index.tsx
new file mode 100644
index 0000000..ddbca35
--- /dev/null
+++ b/src/pages/DiffFlowPage/index.tsx
@@ -0,0 +1,86 @@
+import { PlusIcon } from "@/components/bs-icons/plus"
+import { Button } from "@/components/bs-ui/button"
+import { ChevronLeftIcon } from "@radix-ui/react-icons"
+import { useNavigate, useParams } from "react-router-dom"
+import Component from "./components/Component"
+import RunTest from "./components/RunTest"
+import { useDiffFlowStore } from "@/store/diffFlowStore"
+import { useEffect, useState } from "react"
+import { useToast } from "@/components/bs-ui/toast/use-toast"
+import { getFlowVersions } from "@/controllers/API/flow"
+import { FlowVersionItem } from "@/types/flow"
+import gongjuAdd from "../../assets/npc/gongjuAdd.png";
+
+export default function index(params) {
+ // 技能 id, 版本id, 组件id
+ const { id, vid, cid } = useParams()
+ const navigate = useNavigate()
+ const { message } = useToast()
+
+ const versions = useVersions(id)
+
+ const { mulitVersionFlow, removeVersionFlow, initFristVersionFlow, addEmptyVersionFlow, addVersionFlow } = useDiffFlowStore()
+ useEffect(() => {
+ initFristVersionFlow(vid)
+ }, [])
+
+ const handleAddVersion = () => {
+ if (mulitVersionFlow.length >= 4) return message({
+ title: '',
+ description: '最多添加4个版本',
+ variant: 'error',
+ })
+ addEmptyVersionFlow()
+ }
+
+ console.log('mulitVersionFlow', mulitVersionFlow);
+
+
+ return
+ {/* header */}
+
+
+
版本评估
+
+
+
+ {/* content */}
+
+ {/* comps */}
+
+ {
+ mulitVersionFlow.map((version, index) => (
+ v?.id)}
+ version={version}
+ className={''}
+ onChangeVersion={(vid) => addVersionFlow(vid, index)}
+ onClose={() => removeVersionFlow(index)}
+ />
+ ))
+ }
+
+ {/* run test */}
+
+
+
+};
+
+
+const useVersions = (flowId) => {
+ const [versions, setVersions] = useState([])
+ useEffect(() => {
+ getFlowVersions(flowId).then(({ data }) => {
+ setVersions(data)
+ })
+ }, [])
+
+ return versions
+}
\ No newline at end of file
diff --git a/src/pages/FlowPage/components/Header.tsx b/src/pages/FlowPage/components/Header.tsx
new file mode 100644
index 0000000..4e4db08
--- /dev/null
+++ b/src/pages/FlowPage/components/Header.tsx
@@ -0,0 +1,288 @@
+import AlertDropdown from "@/alerts/alertDropDown";
+import { DelIcon } from "@/components/bs-icons/del";
+import { LoadIcon } from "@/components/bs-icons/loading";
+import { SaveIcon } from "@/components/bs-icons/save";
+import { bsConfirm } from "@/components/bs-ui/alertDialog/useConfirm";
+import { Button } from "@/components/bs-ui/button";
+import ActionButton from "@/components/bs-ui/button/actionButton";
+import TextInput from "@/components/bs-ui/input/textInput";
+import { RadioGroup, RadioGroupItem } from "@/components/bs-ui/radio";
+import { useToast } from "@/components/bs-ui/toast/use-toast";
+import { alertContext } from "@/contexts/alertContext";
+import { PopUpContext } from "@/contexts/popUpContext";
+import { TabsContext } from "@/contexts/tabsContext";
+import { typesContext } from "@/contexts/typesContext";
+import { undoRedoContext } from "@/contexts/undoRedoContext";
+import { createFlowVersion, deleteVersion, getFlowVersions, getVersionDetails, updateVersion } from "@/controllers/API/flow";
+import { captureAndAlertRequestErrorHoc } from "@/controllers/request";
+import ApiModal from "@/modals/ApiModal";
+import L2ParamsModal from "@/modals/L2ParamsModal";
+import ExportModal from "@/modals/exportModal";
+import { FlowVersionItem } from "@/types/flow";
+import { ArrowDownIcon, ArrowUpIcon, BellIcon, CodeIcon, ExitIcon, LayersIcon, StackIcon } from "@radix-ui/react-icons";
+import { t } from "i18next";
+import { useContext, useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+// import TipPng from "../../../assets/tip.jpg";
+import jianhua from "../../../assets/npc/jianhua.png";
+import xiaoxi from "../../../assets/npc/xiaoxi.png";
+import tuichu from "../../../assets/npc/tuichu.png";
+import lingcun from "../../../assets/npc/lingcun.png";
+import rb_1 from "../../../assets/npc/rb-1.png";
+import rb_2 from "../../../assets/npc/rb-2.png";
+import rb_3 from "../../../assets/npc/rb-3.png";
+import rb_4 from "../../../assets/npc/rb-4.png";
+import rb_4_active from "../../../assets/npc/rb-4-active.png";
+
+export default function Header({ flow }) {
+ const navgate = useNavigate()
+ const { t } = useTranslation()
+ const { message } = useToast()
+ const [open, setOpen] = useState(false)
+ const AlertWidth = 384;
+ const { notificationCenter, setNotificationCenter, setSuccessData } = useContext(alertContext);
+ const { uploadFlow, setFlow, tabsState, saveFlow } = useContext(TabsContext);
+ const { reactFlowInstance } = useContext(typesContext);
+
+ const isPending = tabsState[flow.id]?.isPending;
+ console.log(isPending)
+ const { openPopUp } = useContext(PopUpContext);
+ // 记录快照
+ const { takeSnapshot } = useContext(undoRedoContext);
+
+ const handleSaveNewVersion = async () => {
+ // 累加版本 vx ++
+ const maxNo = lastVersionIndexRef.current + 1
+ // versions.forEach(v => {
+ // const match = v.name.match(/[vV](\d+)/)
+ // maxNo = match ? Math.max(Number(match[1]), maxNo) : maxNo
+ // })
+ // maxNo++
+ // save
+ const res = await captureAndAlertRequestErrorHoc(
+ createFlowVersion(flow.id, { name: `v${maxNo}`, description: '', data: flow.data, original_version_id: version.id })
+ )
+ message({
+ variant: "success",
+ title: `${t('skills.version')} v${maxNo} ${t('skills.saveSuccessful')}`,
+ description: ""
+ })
+ // 更新版本列表
+ await refrenshVersions()
+ // 切换到最新版本
+
+ setVersionId(res.id)
+ }
+ //
+ const [saveVersionId, setVersionId] = useState('')
+ useEffect(() => {
+ saveVersionId && handleChangeVersion(saveVersionId)
+ }, [saveVersionId])
+
+ // 版本管理
+ const [loading, setLoading] = useState(false)
+ const { versions, version, lastVersionIndexRef, changeName, deleteVersion, refrenshVersions, setCurrentVersion } = useVersion(flow)
+ // 切换版本
+ const handleChangeVersion = async (versionId) => {
+ setLoading(true)
+ reactFlowInstance.setNodes([]) // 便于重新渲染节点
+ // 保存当前版本
+ // updateVersion(version.id, { name: version.name, description: '', data: flow.data })
+ // 切换版本UI
+ setCurrentVersion(Number(versionId))
+ // 加载选中版本data
+ const res = await getVersionDetails(versionId)
+ // 自动触发 page的 clone flow
+ setFlow('versionChange', { ...flow, data: res.data })
+ message({
+ variant: "success",
+ title: `切换到 ${res.name}`,
+ description: ""
+ })
+ setLoading(false)
+ }
+ // 保存版本
+ const handleSaveVersion = async () => {
+ // 保存当前版本
+ captureAndAlertRequestErrorHoc(updateVersion(version.id, { name: version.name, description: '', data: flow.data }).then(_ => {
+ setFlow('versionChange', { ...flow }) // 更新clone flow,避免触发diff不同
+
+ _ && message({
+ variant: "success",
+ title: t('success'),
+ description: ""
+ })
+ }))
+ }
+
+ return
+ {
+ loading &&
+
+ 切换到 {version.name}
+
+ }
+ {/*
+
+
+
+
+
+
*/}
+ {
+ version &&
+ {/*
*/}
+

setOpen(true)} alt="" />
+ {isPending ?

:

}
+
+ {/*
*/}
+ {t('skills.supportVersions')}
+
+ )}
+ dropDown={(
+
+
{
+ updateVersion(version.id, { name: version.name, description: '', data: flow.data })
+ handleChangeVersion(vid)
+ }} className="gap-0">
+ {versions.map((vers, index) => (
+
+
+
+
changeName(vers.id, val)}
+ >
+
{vers.update_time.replace('T', ' ').substring(0, 16)}
+
+ {
+ // 最后一个 V0 版本和当前选中版本不允许删除
+ !(version.id === vers.id)
+ &&
+ }
+
+
+ ))}
+
+
+ )}
+ >
{t('skills.saveVersion')}
+
) => {
+ setNotificationCenter(false);
+ const { top, left } = (event.target as Element).getBoundingClientRect();
+ openPopUp(
+ <>
+
+
+ >
+ );
+ }}>
+

+ {notificationCenter &&
}
+
+

navgate('/build/skills', { replace: true })} alt="" />
+
+ }
+
+

{ openPopUp(
); }} alt="" />
+

{ openPopUp(
); }} alt="" />
+

{ takeSnapshot(); uploadFlow() }} alt="" />
+
+ {/* 高级配置l2配置 */}
+
+
+};
+
+// 技能版本管理
+const useVersion = (flow) => {
+ const [versions, setVersions] = useState
([])
+ const { version, setVersion, updateOnlineVid } = useContext(TabsContext)
+ const lastVersionIndexRef = useRef(0)
+
+ const refrenshVersions = () => {
+ return getFlowVersions(flow.id).then(({ data, total }) => {
+ setVersions(data)
+ lastVersionIndexRef.current = total - 1
+ const currentV = data.find(el => el.is_current === 1)
+ setVersion(currentV)
+ // 记录上线的版本
+ updateOnlineVid(currentV?.id)
+ })
+ }
+
+ useEffect(() => {
+ refrenshVersions()
+ }, [])
+
+ // 修改名字
+ const handleChangName = (id, name) => {
+ captureAndAlertRequestErrorHoc(updateVersion(id, { name, description: '', data: null }))
+ // 乐观更新
+ setVersions(versions.map(version => {
+ if (version.id === id) {
+ version.name = name;
+ }
+ return version;
+ }))
+ }
+
+ const handleDeleteVersion = (version, index) => {
+ bsConfirm({
+ title: t('prompt'),
+ desc: `${t('skills.deleteOrNot')} ${version.name} ${t('skills.version')}?`,
+ onOk: (next) => {
+ captureAndAlertRequestErrorHoc(deleteVersion(version.id)).then(res => {
+ if (res === null) {
+ // 乐观更新
+ setVersions(versions.filter((_, i) => i !== index))
+ }
+ })
+ next()
+ }
+ })
+ }
+
+ return {
+ versions,
+ version,
+ lastVersionIndexRef,
+ setCurrentVersion(versionId) {
+ const currentV = versions.find(el => el.id === versionId)
+ setVersion(currentV)
+ return currentV
+ },
+ refrenshVersions,
+ deleteVersion: handleDeleteVersion,
+ changeName: handleChangName,
+ }
+}
diff --git a/src/pages/FlowPage/components/PageComponent/index.tsx b/src/pages/FlowPage/components/PageComponent/index.tsx
index 9882c26..45da5e4 100644
--- a/src/pages/FlowPage/components/PageComponent/index.tsx
+++ b/src/pages/FlowPage/components/PageComponent/index.tsx
@@ -22,11 +22,11 @@ import ReactFlow, {
useReactFlow,
} from "reactflow";
import GenericNode from "../../../../CustomNodes/GenericNode";
-import FrameSelectToolbar from "../FrameSelectToolbarComponent";
-import GroupNode from "../../../../CustomNodes/GroupNode";
-import ClearableEdge from "../../../../CustomEdges/ClearableEdge";
+// import FrameSelectToolbar from "../FrameSelectToolbarComponent";
+// import GroupNode from "../../../../CustomNodes/GroupNode";
+// import ClearableEdge from "../../../../CustomEdges/ClearableEdge";
import Chat from "../../../../components/chatComponent";
-import { Button } from "../../../../components/ui/button";
+// import { Button } from "../../../../components/ui/button";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
@@ -39,19 +39,26 @@ import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent";
import { alertContext } from "../../../../contexts/alertContext";
+import Header from "../Header";
+import { Badge } from "@/components/bs-ui/badge";
+import { LayersIcon } from "@radix-ui/react-icons";
+import { Button } from "@/components/bs-ui/button";
+import { updateVersion } from "@/controllers/API/flow";
+import { captureAndAlertRequestErrorHoc } from "@/controllers/request";
const nodeTypes = {
genericNode: GenericNode,
- frameSelectToolbar: FrameSelectToolbar,
- groupNode: GroupNode
+ // frameSelectToolbar: FrameSelectToolbar,
+ // groupNode: GroupNode
};
const edgeTypes = {
- clearableEdge: ClearableEdge
+ // clearableEdge: ClearableEdge
};
export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string }) {
let {
+ version,
setFlow,
setTabsState,
saveFlow,
@@ -72,34 +79,37 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
const { takeSnapshot } = useContext(undoRedoContext);
// 快捷键
const { keyBoardPanneRef, lastSelection, setLastSelection } = useKeyBoard(reactFlowWrapper)
- const [isSelecting, setIsSelecting] = useState(false);
- const [selectionNode, setSelectionNode] = useState([]);
+ // const [isSelecting, setIsSelecting] = useState(false);
+ // const [selectionNode, setSelectionNode] = useState([]);
// 选区左上角坐标(x1,y1) 右下角坐标(x2,y2)
- let selectPosition = {x1: 0, y1: 0, x2: 0, y2: 0};
+ // let selectPosition = {x1: 0, y1: 0, x2: 0, y2: 0};
const onSelectionChange = useCallback((flowItem) => {
- if (flowItem.nodes.length > 1) {
- flowItem.nodes.forEach(node => {
- selectPosition.x1 = selectPosition.x1 ? Math.min(node.position.x, selectPosition.x1) : node.position.x;
- selectPosition.y1 = selectPosition.y1 ? Math.min(node.position.y, selectPosition.y1) : node.position.y;
+ // if (flowItem.nodes.length > 1) {
+ // flowItem.nodes.forEach(node => {
+ // selectPosition.x1 = selectPosition.x1 ? Math.min(node.position.x, selectPosition.x1) : node.position.x;
+ // selectPosition.y1 = selectPosition.y1 ? Math.min(node.position.y, selectPosition.y1) : node.position.y;
- selectPosition.x2 = Math.max(node.position.x + node.width, selectPosition.x2);
- selectPosition.y2 = Math.max(node.position.y + node.height, selectPosition.y2);
- });
- setIsSelecting(() => true);
- setSelectionNode(flowItem.nodes);
- } else {
- setIsSelecting(() => false);
- setSelectionNode([]);
- }
- setLastSelection(flow);
- }, [isSelecting, setSelectionNode]);
+ // selectPosition.x2 = Math.max(node.position.x + node.width, selectPosition.x2);
+ // selectPosition.y2 = Math.max(node.position.y + node.height, selectPosition.y2);
+ // });
+ // setIsSelecting(() => true);
+ // setSelectionNode(flowItem.nodes);
+ // } else {
+ // setIsSelecting(() => false);
+ // setSelectionNode([]);
+ // }
+ setLastSelection(flowItem);
+ }, []);
+ // }, [isSelecting, setSelectionNode]);
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
const [selectionEnded, setSelectionEnded] = useState(true);
// Workaround to show the menu only after the selection has ended.
useEffect(() => {
- if (selectionEnded && lastSelection && lastSelection.nodes && lastSelection.nodes.length > 1) {
+ console.log(lastSelection)
+ // if (selectionEnded && lastSelection && lastSelection.nodes && lastSelection.nodes.length > 1) {
+ if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) {
setSelectionMenuVisible(true);
} else {
setSelectionMenuVisible(false);
@@ -171,39 +181,39 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
);
// 分组节点内边距
- const groupPadding = 50;
+ // const groupPadding = 50;
// 分组节点标题高度
- const groupTitleHeight = 0;
+ // const groupTitleHeight = 0;
// 创建分组
- const createGroup = useCallback(() => {
- let selectionIds = selectionNode.map(node => node.id);
- let newId = getNodeId("groupNode");
- let newNode = {
- data: {},
- l2_name: 'groupNode',
- id: newId,
- type: "groupNode",
- style: {
- width: selectPosition.x2 - selectPosition.x1 + groupPadding * 2,
- height: selectPosition.y2 - selectPosition.y1 + groupTitleHeight + groupPadding * 2,
- zIndex: -1
- },
- position: {
- x: selectPosition.x1 - groupPadding,
- y: selectPosition.y1 - groupPadding - groupTitleHeight
- }
- };
- setNodes((nds) => {
- nds.forEach(nd => {
- if (selectionIds.indexOf(nd.id) >= 0) {
- nd.parentNode = newId;
- nd.position.x -= (selectPosition.x1 - groupPadding);
- nd.position.y -= (selectPosition.y1 - groupPadding - groupTitleHeight);
- }
- });
- return nds.concat(newNode);
- });
- }, [selectionNode, setNodes]);
+ // const createGroup = useCallback(() => {
+ // let selectionIds = selectionNode.map(node => node.id);
+ // let newId = getNodeId("groupNode");
+ // let newNode = {
+ // data: {},
+ // l2_name: 'groupNode',
+ // id: newId,
+ // type: "groupNode",
+ // style: {
+ // width: selectPosition.x2 - selectPosition.x1 + groupPadding * 2,
+ // height: selectPosition.y2 - selectPosition.y1 + groupTitleHeight + groupPadding * 2,
+ // zIndex: -1
+ // },
+ // position: {
+ // x: selectPosition.x1 - groupPadding,
+ // y: selectPosition.y1 - groupPadding - groupTitleHeight
+ // }
+ // };
+ // setNodes((nds) => {
+ // nds.forEach(nd => {
+ // if (selectionIds.indexOf(nd.id) >= 0) {
+ // nd.parentNode = newId;
+ // nd.position.x -= (selectPosition.x1 - groupPadding);
+ // nd.position.y -= (selectPosition.y1 - groupPadding - groupTitleHeight);
+ // }
+ // });
+ // return nds.concat(newNode);
+ // });
+ // }, [selectionNode, setNodes]);
// const deleteGroup = useCallback((groupId) => {
// setNodes((nds) => {
@@ -218,28 +228,28 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
// }, [setNodes]);
// 进入多选模式后添加一个可点击的按钮节点
- useEffect(() => {
- if (isSelecting) {// 多选模式
- let newNode = {
- selectable: false,
- data: {
- createGroup: createGroup
- },
- id: "multipartNode",
- type: 'frameSelectToolbar',
- position: {
- x: selectPosition.x1,
- y: selectPosition.y1 - 50
- }
- };
- setNodes((nds) => nds.concat(newNode));
- } else {
- // 延时进程队列,防止还未触发点击事件,按钮就消失
- setTimeout(() => {
- setNodes((nds) => nds.filter((nd) => nd.id !== 'multipartNode'));
- }, 100);
- }
- }, [isSelecting, setNodes]);
+ // useEffect(() => {
+ // if (isSelecting) {// 多选模式
+ // let newNode = {
+ // selectable: false,
+ // data: {
+ // createGroup: createGroup
+ // },
+ // id: "multipartNode",
+ // type: 'frameSelectToolbar',
+ // position: {
+ // x: selectPosition.x1,
+ // y: selectPosition.y1 - 50
+ // }
+ // };
+ // setNodes((nds) => nds.concat(newNode));
+ // } else {
+ // // 延时进程队列,防止还未触发点击事件,按钮就消失
+ // setTimeout(() => {
+ // setNodes((nds) => nds.filter((nd) => nd.id !== 'multipartNode'));
+ // }, 100);
+ // }
+ // }, [isSelecting, setNodes]);
// const deleteGroup = useCallback((groupId) => {
// setNodes((nds) => {
@@ -261,7 +271,7 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
{
...params,
style: {stroke: "#555"},
- type: 'clearableEdge',
+ // type: 'clearableEdge',
className:
(params.targetHandle.split("|")[0] === "Text"
? "stroke-foreground "
@@ -339,8 +349,6 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
y: event.clientY - reactflowBounds.top,
});
- console.log(position);
-
// Generate a unique node ID
let {type} = data;
let newId = getNodeId(type);
@@ -414,7 +422,11 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
// 离开并保存
const handleSaveAndClose = async () => {
- await saveFlow(flow, true)
+ // await saveFlow(flow, true)
+ // blocker.proceed?.()
+ setFlow('leave and save', { ...flow })
+
+ await captureAndAlertRequestErrorHoc(updateVersion(version.id, { name: version.name, description: '', data: flow.data }))
blocker.proceed?.()
}
@@ -441,6 +453,7 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
}, [flow.data]); // 修改 id后, 需要监听 data这一层
return <>
+
{Object.keys(data).length ?
: <>>}
{/* Main area */}
@@ -464,7 +477,7 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
disableKeyboardA11y={true}
onInit={setReactFlowInstance}
nodeTypes={nodeTypes}
- edgeTypes={edgeTypes}
+ // edgeTypes={edgeTypes}
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
@@ -540,7 +553,7 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
]);
} else {
setErrorData({
- title: "Invalid selection",
+ title: "无效的选择",
list: valiDateRes,
});
}
@@ -548,7 +561,11 @@ export default function Page({flow, preFlow}: { flow: FlowType, preFlow: string
/>
-
{flow.name}
+ {/*
{flow.name}
*/}
+
+
{flow.name}
+
{t('skills.currentVersion')}{version?.name}
+
) : (
<>>
diff --git a/src/pages/FlowPage/components/SelectionMenuComponent/index.tsx b/src/pages/FlowPage/components/SelectionMenuComponent/index.tsx
index 13e2897..f7f75f9 100644
--- a/src/pages/FlowPage/components/SelectionMenuComponent/index.tsx
+++ b/src/pages/FlowPage/components/SelectionMenuComponent/index.tsx
@@ -34,7 +34,7 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
}
>
-
+ {/*
*/}
+
+
);
diff --git a/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/pages/FlowPage/components/extraSidebarComponent/index.tsx
index ad2369f..9a20cba 100644
--- a/src/pages/FlowPage/components/extraSidebarComponent/index.tsx
+++ b/src/pages/FlowPage/components/extraSidebarComponent/index.tsx
@@ -42,7 +42,7 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
const { notificationCenter, setNotificationCenter, setSuccessData, setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
- const isPending = tabsState[flow.id]?.isPending;
+ // const isPending = tabsState[flow.id]?.isPending;
const [launch, setLaunch] = useState(true);
const [npc_zujianGengduo, setNpc_zujianGengduo] = useState(true);
const [npc_zujianScrollTop, setNpc_zujianScrollTop] = useState(0);
@@ -102,13 +102,16 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
{/* 简化 */}
-
+ {/*
+ {isPending ?

+ saveFlow(flow).then(_ =>
+ _ && setSuccessData({ title: t('success') }))
+ } className="w-[27px] ml-[1px] cursor-pointer" alt="" /> :

}
-
+
*/}
{/* 顶部按钮组 */}
{/*
@@ -173,14 +167,14 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
*/}
-
+ {/*

{ openPopUp(
); }} alt="" />

{ openPopUp(
); }} alt="" />

{ takeSnapshot(); uploadFlow() }} alt="" />
{isPending ?

saveFlow(flow).then(_ =>
_ && setSuccessData({ title: t('success') }))
- } className="w-[27px] ml-[1px] cursor-pointer" alt="" /> :

}
+ } className="w-[27px] ml-[1px] cursor-pointer" alt="" /> :

} */}
{/*
*/}
-
+ {/*
*/}
{/* */}
{/*
{/* 高级配置l2配置 */}
{
- saveFlow(flow, true);
+ saveFlow(flow);
setSuccessData({ title: t('success') });
}}>
diff --git a/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx
index 990efca..381c7b3 100644
--- a/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx
+++ b/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx
@@ -1,6 +1,6 @@
import { Combine, Copy, Download, MoreHorizontal, SaveAll, Settings2, Trash2 } from "lucide-react";
import cloneDeep from "lodash-es/cloneDeep";
-import { useContext, useState } from "react";
+import { useContext, useMemo, useState } from "react";
import { useReactFlow } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import { TabsContext } from "../../../../contexts/tabsContext";
@@ -13,6 +13,9 @@ import { userContext } from "../../../../contexts/userContext";
import { bsconfirm } from "../../../../alerts/confirm";
import { alertContext } from "../../../../contexts/alertContext";
import React from "react";
+import { typesContext } from "@/contexts/typesContext";
+import { useNavigate, useParams } from "react-router-dom";
+import { CounterClockwiseClockIcon } from "@radix-ui/react-icons";
const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
const [nodeLength, setNodeLength] = useState(
@@ -31,7 +34,7 @@ const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
).length
);
- const { paste } = useContext(TabsContext);
+ const { version, paste } = useContext(TabsContext);
const reactFlowInstance = useReactFlow();
const isGroup = !!data.node?.flow;
@@ -44,6 +47,14 @@ const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
});
}
+ const { types } = useContext(typesContext);
+ const hasVersion = useMemo(() => {
+ // 部分组件开放“历史/history”入口:agent、chains、retrievers 、vector store 4类组件。
+ return ["chains", "agents", "vectorstores", "retrievers"].includes(types[data.type])
+ }, [data, types])
+
+ const navigate = useNavigate()
+ const { id: flowId } = useParams()
const { addSavedComponent, checkComponentsName } = useContext(userContext)
const handleSelectChange = (event) => {
switch (event) {
@@ -80,6 +91,13 @@ const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
break;
case "disabled":
break;
+ case "version":
+ navigate(`/diff/${flowId}/${version.id}/${data.id}`)
+ break;
+ case "export":
+ const cleanFlow = removeApiKeys({ data: { nodes: [{ data }] } } as any)
+ downloadNode(cleanFlow.data.nodes[0].data);
+ break;
case "ungroup":
takeSnapshot();
expandGroupNode(
@@ -130,6 +148,17 @@ const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
{/**/}
+ {/* 版本 */}
+ {
+ hasVersion && !isGroup &&
+
+
+ }
{nodeLength > 0 && (
@@ -169,6 +198,17 @@ const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
+ {isGroup && (
+
+
+
+ )}
+
+
{/* more */}
{/*