diff --git a/src/CustomNodes/GenericNode/index.tsx b/src/CustomNodes/GenericNode/index.tsx index bd8a02b..276b765 100644 --- a/src/CustomNodes/GenericNode/index.tsx +++ b/src/CustomNodes/GenericNode/index.tsx @@ -171,12 +171,14 @@ export default function GenericNode({ data, xPos, yPos, selected }: { -
e.stopPropagation()}> -
data.node.description_url && openPopUp()}>{data.node.description}
+ {/*
e.stopPropagation()}> +
data.node.description_url && openPopUp()}>{data.node.description}
*/} {/*=======*/} {/*
*/} {/*
{data.node.description}
*/} {/*>>>>>>> bisheng_github*/} +
+
{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) => ( +
+ {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 */} {/*