高级编排分组
This commit is contained in:
@@ -171,12 +171,14 @@ export default function GenericNode({ data, xPos, yPos, selected }: {
|
||||
</div >
|
||||
|
||||
|
||||
<div className="generic-node-desc nodrag" onKeyDown={e => e.stopPropagation()}>
|
||||
<div className="generic-node-desc-text" onClick={() => data.node.description_url && openPopUp(<DescriptionModel data={data.node.description_url} />)}>{data.node.description}</div>
|
||||
{/* <div className="generic-node-desc nodrag" onKeyDown={e => e.stopPropagation()}>
|
||||
<div className="generic-node-desc-text" onClick={() => data.node.description_url && openPopUp(<DescriptionModel data={data.node.description_url} />)}>{data.node.description}</div> */}
|
||||
{/*=======*/}
|
||||
{/* <div className="generic-node-desc nodrag">*/}
|
||||
{/* <div className="generic-node-desc-text">{data.node.description}</div>*/}
|
||||
{/*>>>>>>> bisheng_github*/}
|
||||
<div className="generic-node-desc nodrag">
|
||||
<div className="generic-node-desc-text">{data.node.description}</div>
|
||||
<>
|
||||
{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' && <div className="w-full bg-muted px-5 py-2">
|
||||
<Link to={`/report/${flowId}`}><Button variant="outline" className="px-10">Edit</Button></Link>
|
||||
|
||||
BIN
src/assets/toolbar/version.png
Normal file
BIN
src/assets/toolbar/version.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -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;
|
||||
|
||||
@@ -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<TabsContextType>(
|
||||
@@ -36,6 +38,7 @@ export const TabsContext = createContext<TabsContextType>(
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
const [flow, setFlow] = useState<FlowType>(null);
|
||||
const [version, setVersion] = useState<FlowVersionItem | null>(null);
|
||||
// flowid: formKeysData
|
||||
const [tabsState, setTabsState] = useState<TabsState>({});
|
||||
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 (
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
@@ -313,7 +322,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
getNodeId,
|
||||
tabsState,
|
||||
setTabsState,
|
||||
paste
|
||||
paste,
|
||||
version,
|
||||
setVersion,
|
||||
isOnlineVersion: () => version.id === onlineVid,
|
||||
updateOnlineVid
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
93
src/pages/DiffFlowPage/components/Cell.tsx
Normal file
93
src/pages/DiffFlowPage/components/Cell.tsx
Normal file
@@ -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 <Skeleton className="h-4 w-[200px]" />
|
||||
|
||||
return <div>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
linkTarget="_blank"
|
||||
className="bs-mkdown inline-block break-all max-w-full text-sm text-[#111]"
|
||||
components={{
|
||||
code: ({ node, inline, className, children, ...props }) => {
|
||||
if (children.length) {
|
||||
if (children[0] === "▍") {
|
||||
return (<span className="form-modal-markdown-span"> ▍ </span>);
|
||||
}
|
||||
|
||||
children[0] = (children[0] as string).replace("`▍`", "▍");
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
language={(match && match[1]) || ""}
|
||||
value={String(children).replace(/\n$/, "")}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}> {children} </code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{value.toString()}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
})
|
||||
|
||||
|
||||
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 <Cell ref={ref} />
|
||||
};
|
||||
122
src/pages/DiffFlowPage/components/Component.tsx
Normal file
122
src/pages/DiffFlowPage/components/Component.tsx
Normal file
@@ -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 <div className="bg-[#000000] rounded-md p-2 shadow-sm">
|
||||
<div className="group flex justify-center items-center pb-2 border-b relative">
|
||||
<Select onValueChange={onChangeVersion}>
|
||||
<SelectTrigger className="w-[120px] h-6">
|
||||
<SelectValue placeholder="选择版本" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{
|
||||
options.map(vs => (
|
||||
<SelectItem key={vs.id} value={vs.id} textValue={'vs.name'} disabled={disables.includes(vs.id)}>
|
||||
<div className="flex justify-between w-64">
|
||||
<span className="w-36 overflow-hidden text-ellipsis whitespace-nowrap">{vs.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{vs.update_time.replace('T', ' ').substring(0, 16)}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* <DelIcon
|
||||
className="absolute right-0 -top-1 cursor-pointer text-muted-foreground hidden group-hover:block"
|
||||
onClick={onClose}
|
||||
/> */}
|
||||
<img src={del} alt="" className="absolute w-[14px] right-[2px] top-[2px] cursor-pointer text-muted-foreground hidden group-hover:block" onClick={onClose}/>
|
||||
</div>
|
||||
<div className="min-h-[100px]"></div>
|
||||
</div>
|
||||
|
||||
// 版本信息
|
||||
return <div className={'bg-[#000000] rounded-md p-2 shadow-sm ' + className}>
|
||||
<div className="group flex justify-between items-center pb-2 border-b">
|
||||
<Select value={version.id} onValueChange={onChangeVersion}>
|
||||
<SelectTrigger className="w-[120px] h-6">
|
||||
<SelectValue placeholder="选择版本" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{
|
||||
options.map(vs => (
|
||||
<SelectItem key={vs.id} value={vs.id} textValue={'vs.name'} disabled={disables.includes(vs.id)}>
|
||||
<div className="flex justify-between w-64">
|
||||
<span className="w-36 overflow-hidden text-ellipsis whitespace-nowrap text-left">{vs.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{vs.update_time.replace('T', ' ').substring(0, 16)}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-[#999999] relative pr-8">
|
||||
{version.update_time.replace('T', ' ')}
|
||||
{/* <DelIcon
|
||||
className="absolute right-0 -top-1 cursor-pointer text-muted-foreground hidden group-hover:block"
|
||||
onClick={onClose}
|
||||
/> */}
|
||||
<img src={del} alt="" className="absolute w-[14px] right-[2px] top-[2px] cursor-pointer text-muted-foreground hidden group-hover:block" onClick={onClose}/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="max-h-52 overflow-y-auto pb-10">
|
||||
<div className="flex gap-1 px-2 py-1 text-sm text-muted-foreground">
|
||||
<span className="min-w-12 w-28 text-[#999999]">组件</span>
|
||||
<span className="min-w-12 w-28 text-[#999999]">参数名</span>
|
||||
<span className="flex-1 text-[#999999]">参数值</span>
|
||||
</div>
|
||||
{
|
||||
nodes.map(node => (
|
||||
<div className="flex odd:bg-[#2B2B2B] bg-[#1A1A1A] gap-1 mt-1 px-2 py-1 text-sm rounded-sm">
|
||||
<span className="min-w-12 w-28 break-all self-center text-[#FFFFFF]">{node.data.type}</span>
|
||||
<div className="flex-1 min-w-0 pointer-events-none opacity-60">
|
||||
{
|
||||
<ComponentParameter
|
||||
disabled
|
||||
flow={version}
|
||||
node={node}
|
||||
template={node.data.node.template}
|
||||
>
|
||||
{
|
||||
(key, name, formItem) => (
|
||||
<div key={key} className="flex mb-1">
|
||||
<span className="min-w-12 w-28 break-all text-[#999999]">{name}</span>
|
||||
<div className="flex-1 min-w-0 text-[#999999]">{formItem}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</ComponentParameter>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
176
src/pages/DiffFlowPage/components/ComponentParameter.tsx
Normal file
176
src/pages/DiffFlowPage/components/ComponentParameter.tsx
Normal file
@@ -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 ? (
|
||||
<InputListComponent
|
||||
editNode={true}
|
||||
disabled={_disabled}
|
||||
value={
|
||||
!template[n].value ||
|
||||
template[n].value === ""
|
||||
? [""]
|
||||
: template[n].value
|
||||
}
|
||||
onChange={(t: string[]) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
/>
|
||||
) : template[n].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={_disabled}
|
||||
editNode={true}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
editNode={true}
|
||||
disabled={_disabled}
|
||||
password={
|
||||
template[n].password ?? false
|
||||
}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
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, <Dropdown
|
||||
numberOfOptions={keys.length}
|
||||
editNode={true}
|
||||
options={template[n].options}
|
||||
onSelect={(t) => handleOnNewValue(t, n)}
|
||||
value={
|
||||
template[n].value ??
|
||||
"Choose an option"
|
||||
}
|
||||
></Dropdown>)
|
||||
} else {
|
||||
return children(n, name, getStrComp(template, n))
|
||||
}
|
||||
}
|
||||
|
||||
switch (template[n].type) {
|
||||
case "bool":
|
||||
return children(n, name, <ToggleShadComponent
|
||||
disabled={_disabled}
|
||||
enabled={template[n].value}
|
||||
setEnabled={(t) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
size="small"
|
||||
/>)
|
||||
case "float":
|
||||
return children(n, name, <FloatComponent
|
||||
disabled={_disabled}
|
||||
editNode={true}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
template[n].value = t;
|
||||
}}
|
||||
/>)
|
||||
case "int":
|
||||
return children(n, name, <IntComponent
|
||||
disabled={_disabled}
|
||||
editNode={true}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
/>)
|
||||
case "file":
|
||||
return children(n, name, <InputFileComponent
|
||||
editNode={true}
|
||||
disabled={_disabled}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
fileTypes={template[n].fileTypes}
|
||||
suffixes={template[n].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
></InputFileComponent>)
|
||||
case "prompt":
|
||||
return children(n, name, <PromptAreaComponent
|
||||
field_name={n}
|
||||
editNode={true}
|
||||
disabled={_disabled}
|
||||
nodeClass={node.data.node}
|
||||
setNodeClass={(nodeClass) => {
|
||||
node.data.node = nodeClass;
|
||||
}}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
/>)
|
||||
case "code":
|
||||
return children(n, name, <CodeAreaComponent
|
||||
disabled={_disabled}
|
||||
editNode={true}
|
||||
value={template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
handleOnNewValue(t, n);
|
||||
}}
|
||||
/>)
|
||||
case "Any": return children(n, name, "-")
|
||||
default: return children(n, name, <div className="hidden"></div>)
|
||||
}
|
||||
})
|
||||
}
|
||||
</>
|
||||
};
|
||||
28
src/pages/DiffFlowPage/components/RunForm.tsx
Normal file
28
src/pages/DiffFlowPage/components/RunForm.tsx
Normal file
@@ -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 <DialogContent className="sm:max-w-[625px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>测试运行</DialogTitle>
|
||||
<DialogDescription>请输入上游依赖参数</DialogDescription>
|
||||
</DialogHeader>
|
||||
{
|
||||
show && <ChatReportForm ref={formRef} type='diff' vid={flow.id} flow={flow} onStart={onSubmit} />
|
||||
}
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
<Button variant="outline" className="px-11" type="button" onClick={onChangeShow}>取消</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit" className="px-11" onClick={handleSubmit}>开始运行</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
};
|
||||
345
src/pages/DiffFlowPage/components/RunTest.tsx
Normal file
345
src/pages/DiffFlowPage/components/RunTest.tsx
Normal file
@@ -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 <div className="mt-4 px-4">
|
||||
<div className="bg-[#000000] p-2">
|
||||
<div className="flex items-center justify-between ">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button size="sm" disabled={['all', 'row', 'col'].includes(runningType)} className="baogao-btn2 border-radius-14 ml-0" onClick={handleUploadTxt}>{t('test.uploadTest')}</Button>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<QuestionMarkCircledIcon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('test.explain')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{
|
||||
isForm ? <Dialog open={formShow} onOpenChange={setFormShow}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" className="baogao-btn2 border-radius-14 ml-0" disabled={runningType === 'all' || notDiffVersion}><PlayIcon />{t('test.testRun')}</Button>
|
||||
</DialogTrigger>
|
||||
<RunForm show={formShow} flow={mulitVersionFlow[0]} onChangeShow={setFormShow} onSubmit={handleRunTest} />
|
||||
</Dialog> :
|
||||
<Button size="sm" className="baogao-btn2 border-radius-14 ml-0" disabled={runningType === 'all' || notDiffVersion} onClick={() => handleRunTest()}><PlayIcon />{t('test.testRun')}</Button>
|
||||
}
|
||||
</div>
|
||||
{/* table */}
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-[#999999]" style={{ width: `${versionColWidth}%` }}>{t('test.testCase')}</TableHead>
|
||||
{
|
||||
mulitVersionFlow.map(version =>
|
||||
version && <TableHead className="text-[#999999]" key={version.id} style={{ width: `${versionColWidth + 10}%` }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{version.name}</span>
|
||||
{readyVersions[version.id] && <Button
|
||||
disabled={['all'].includes(runningType)}
|
||||
size='icon'
|
||||
className="w-6 h-6"
|
||||
title={t('test.run')}
|
||||
onClick={() => handleColRunTest(version.id)}
|
||||
><PlayIcon /></Button>}
|
||||
</div>
|
||||
</TableHead>
|
||||
)
|
||||
}
|
||||
<TableHead className="text-right min-w-[135px] text-[#FFD025]" style={{ width: 135 }}>
|
||||
<Button variant="link" className="text-[#FFD025] disabled:opacity-1" disabled={runningType !== '' || !running} onClick={handleDownExcle}><DownloadIcon className="mr-1" />{t('test.downloadResults')}</Button>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{
|
||||
questions.map((question, index) => (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Input
|
||||
className="npcInput1"
|
||||
disabled={['all', 'row'].includes(runningType)}
|
||||
placeholder={t('test.testCases')}
|
||||
value={question.q}
|
||||
onChange={(e) => updateQuestion(e.target.value, index)}
|
||||
></Input>
|
||||
{question.ready && <Button
|
||||
disabled={['all'].includes(runningType) || notDiffVersion}
|
||||
size='icon'
|
||||
className="min-w-6 h-6 bg-[#FFD025]"
|
||||
title="运行"
|
||||
onClick={() => handleRowRunTest(index)}
|
||||
><PlayIcon /></Button>}
|
||||
</div>
|
||||
</TableCell>
|
||||
{/* 版本 */}
|
||||
{mulitVersionFlow.map(flow =>
|
||||
flow && <TableCell key={index + '-' + flow.id} className=''>
|
||||
<CellWarp qIndex={index} versionId={flow.id} />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="link"
|
||||
disabled={['all', 'row'].includes(runningType)}
|
||||
onClick={() => removeQuestion(index)}>
|
||||
{/* <DelIcon /> */}
|
||||
<img src={del} alt="" className="w-[14px]" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
{questions.length < 20 && <TableCell>
|
||||
<div className="flex items-center gap-2 font-medium min-w-52">
|
||||
<Input
|
||||
className="npcInput1"
|
||||
placeholder={t('test.testCases')}
|
||||
onKeyDown={(e) => {
|
||||
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 = ''
|
||||
}} />
|
||||
</div>
|
||||
</TableCell>
|
||||
}
|
||||
<TableCell colSpan={5} className="text-right"></TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
86
src/pages/DiffFlowPage/index.tsx
Normal file
86
src/pages/DiffFlowPage/index.tsx
Normal file
@@ -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 <div className="bg-[#1A1A1A] h-full relative">
|
||||
{/* header */}
|
||||
<div className="absolute top-0 w-full h-14 flex justify-between items-center border-b px-4 bg-[#000000]">
|
||||
<Button variant="outline" size="icon" onClick={() => navigate(-1)}><ChevronLeftIcon className="h-4 w-4" /></Button>
|
||||
<span className="text-[#FFFFFF]">版本评估</span>
|
||||
<Button type="button" className="baogao-btn2 border-radius-14" onClick={handleAddVersion}>
|
||||
{/* <PlusIcon className="text-primary" /> */}
|
||||
<img src={gongjuAdd} alt="" className="w-[14px] mr-[7px]" />
|
||||
添加版本({mulitVersionFlow.length}/4)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* content */}
|
||||
<div className="h-full pt-14 overflow-y-auto">
|
||||
{/* comps */}
|
||||
<div className={`grid gap-4 mt-4 px-4 box-border ${mulitVersionFlow.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}`}>
|
||||
{
|
||||
mulitVersionFlow.map((version, index) => (
|
||||
<Component
|
||||
key={index}
|
||||
compId={cid}
|
||||
options={versions}
|
||||
disables={mulitVersionFlow.map((v) => v?.id)}
|
||||
version={version}
|
||||
className={''}
|
||||
onChangeVersion={(vid) => addVersionFlow(vid, index)}
|
||||
onClose={() => removeVersionFlow(index)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{/* run test */}
|
||||
<RunTest nodeId={cid}></RunTest>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
|
||||
const useVersions = (flowId) => {
|
||||
const [versions, setVersions] = useState<FlowVersionItem[]>([])
|
||||
useEffect(() => {
|
||||
getFlowVersions(flowId).then(({ data }) => {
|
||||
setVersions(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return versions
|
||||
}
|
||||
288
src/pages/FlowPage/components/Header.tsx
Normal file
288
src/pages/FlowPage/components/Header.tsx
Normal file
@@ -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 <div className="">
|
||||
{
|
||||
loading && <div className=" fixed left-0 top-0 w-full h-screen bg-gray-50/60 z-50 flex items-center justify-center">
|
||||
<LoadIcon className="mr-2 text-gray-600" />
|
||||
<span>切换到 {version.name}</span>
|
||||
</div>
|
||||
}
|
||||
{/* <div className="flex items-center gap-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => navgate('/build/skills', { replace: true })}
|
||||
><ExitIcon className="h-4 w-4 rotate-180" /></Button>
|
||||
<Button variant="outline" onClick={() => { takeSnapshot(); uploadFlow() }} >
|
||||
<ArrowUpIcon className="h-4 w-4 mr-1" />{t('skills.import')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => { openPopUp(<ExportModal />) }}>
|
||||
<ArrowDownIcon className="h-4 w-4 mr-1" />{t('skills.export')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => { openPopUp(<ApiModal flow={flow} />) }} >
|
||||
<CodeIcon className="h-4 w-4 mr-1" />{t('skills.code')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setOpen(true)} >
|
||||
<StackIcon className="h-4 w-4 mr-1" />{t('skills.simplify')}
|
||||
</Button>
|
||||
</div> */}
|
||||
{
|
||||
version && <div className="flex fixed right-[14px] top-[14px] z-10">
|
||||
{/* <Button className="px-6 flex gap-2" type="button" onClick={handleSaveVersion}
|
||||
disabled={!isPending}><SaveIcon />{t('skills.save')}</Button> */}
|
||||
<img src={jianhua} className="w-[27px] cursor-pointer" onClick={() => setOpen(true)} alt="" />
|
||||
{isPending ? <img src={rb_4_active} onClick={handleSaveVersion} className="w-[27px] ml-[1px] mr-[1px] cursor-pointer" alt="" /> : <img src={rb_4} className="w-[27px] ml-[1px] mr-[1px] cursor-pointer" alt="" />}
|
||||
<ActionButton
|
||||
className="px-6 flex gap-2 bg-[#1A1A1A] hover:bg-[#1A1A1A] border-[#1A1A1A]"
|
||||
align="end"
|
||||
variant="outline"
|
||||
onClick={handleSaveNewVersion}
|
||||
delayDuration={200}
|
||||
buttonTipContent={(
|
||||
<div>
|
||||
{/* <img src={TipPng} alt="" className="w-80" /> */}
|
||||
<p className="text-sm">{t('skills.supportVersions')}</p>
|
||||
</div>
|
||||
)}
|
||||
dropDown={(
|
||||
<div className=" overflow-y-auto max-h-96 max-h">
|
||||
<RadioGroup value={version.id + ''} onValueChange={(vid) => {
|
||||
updateVersion(version.id, { name: version.name, description: '', data: flow.data })
|
||||
handleChangeVersion(vid)
|
||||
}} className="gap-0">
|
||||
{versions.map((vers, index) => (
|
||||
<div key={vers.id} className="group flex items-center gap-4 px-4 py-2 cursor-pointer hover:bg-gray-100 border-b">
|
||||
<RadioGroupItem value={vers.id + ''} />
|
||||
<div className="w-[198px]">
|
||||
<TextInput
|
||||
className="h-[30px]"
|
||||
type="hover"
|
||||
value={vers.name}
|
||||
maxLength={30}
|
||||
onSave={val => changeName(vers.id, val)}
|
||||
></TextInput>
|
||||
<p className="text-sm text-muted-foreground mt-2">{vers.update_time.replace('T', ' ').substring(0, 16)}</p>
|
||||
</div>
|
||||
{
|
||||
// 最后一个 V0 版本和当前选中版本不允许删除
|
||||
!(version.id === vers.id)
|
||||
&& <Button
|
||||
className="group-hover:flex hidden"
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => deleteVersion(vers, index)}
|
||||
><DelIcon /></Button>
|
||||
}
|
||||
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
><img src={lingcun} className="w-[11px]" alt="" /><span className="text-[#999999]">{t('skills.saveVersion')}</span></ActionButton>
|
||||
<div className="relative" onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
setNotificationCenter(false);
|
||||
const { top, left } = (event.target as Element).getBoundingClientRect();
|
||||
openPopUp(
|
||||
<>
|
||||
<div className="absolute z-10" style={{ top: top + 40, left: left - AlertWidth }} ><AlertDropdown /></div>
|
||||
<div className="header-notifications-box"></div>
|
||||
</>
|
||||
);
|
||||
}}>
|
||||
<img src={xiaoxi} className="w-[27px] ml-[1px] cursor-pointer" alt="" />
|
||||
{notificationCenter && <div className="header-notifications top-[6px]"></div>}
|
||||
</div>
|
||||
<img src={tuichu} className="w-[27px] ml-[1px] cursor-pointer" onClick={(event) => navgate('/build/skills', { replace: true })} alt="" />
|
||||
</div>
|
||||
}
|
||||
<div className="flex fixed right-[70px] bottom-[14px] z-10">
|
||||
<img src={rb_1} className="w-[27px] cursor-pointer" onClick={() => { openPopUp(<ApiModal flow={flow} />); }} alt="" />
|
||||
<img src={rb_2} className="w-[27px] ml-[1px] cursor-pointer" onClick={(event) => { openPopUp(<ExportModal />); }} alt="" />
|
||||
<img src={rb_3} className="w-[27px] ml-[1px] cursor-pointer" onClick={(event) => { takeSnapshot(); uploadFlow() }} alt="" />
|
||||
</div>
|
||||
{/* 高级配置l2配置 */}
|
||||
<L2ParamsModal data={flow} open={open} setOpen={setOpen} onSave={handleSaveVersion}></L2ParamsModal>
|
||||
</div>
|
||||
};
|
||||
|
||||
// 技能版本管理
|
||||
const useVersion = (flow) => {
|
||||
const [versions, setVersions] = useState<FlowVersionItem[]>([])
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean>(false);
|
||||
const [selectionNode, setSelectionNode] = useState([]);
|
||||
// const [isSelecting, setIsSelecting] = useState<Boolean>(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 <>
|
||||
<Header flow={flow}></Header>
|
||||
<div className="flex h-full overflow-hidden">
|
||||
{Object.keys(data).length ? <ExtraSidebar flow={flow} /> : <></>}
|
||||
{/* 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
|
||||
/>
|
||||
</ReactFlow>
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
|
||||
<p className="absolute top-0 left-[25%] w-[50%] text-[16px] mt-[14px]" style={{textAlign:"center",fontWeight:"600",color:"#fff"}}>{flow.name}</p>
|
||||
{/* <p className="absolute top-0 left-[25%] w-[50%] text-[16px] mt-[14px]" style={{textAlign:"center",fontWeight:"600",color:"#fff"}}>{flow.name}</p> */}
|
||||
<div className="absolute top-0 left-[25%] w-[50%] mt-[14px] flex justify-center items-center">
|
||||
<p className="text-[#FFFFFF] text-[16px]" style={{fontWeight:"600"}}>{flow.name}</p>
|
||||
<div className="w-[95px] h-[20px] bg-[#1A1A1A] text-[#999999] text-[11px] ml-[20px] flex items-center justify-center" style={{borderRadius:"10px"}}>{t('skills.currentVersion')}{version?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
||||
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
|
||||
}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
{/* <div className="overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
"duration-400 rounded-full border border-gray-200 bg-background px-2.5 text-primary shadow-inner transition-all ease-in-out" +
|
||||
@@ -42,7 +42,7 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="flex gap-2 leading-8 items-center justify-between text-sm hover:scale-110 transition-all ease-in-out"
|
||||
className="group-icon flex gap-2 leading-8 items-center justify-between text-sm hover:scale-110 transition-all ease-in-out"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Combine
|
||||
@@ -53,6 +53,9 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
||||
Group
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="selected-toolbar">
|
||||
<button id="frame-button" className="group-icon frame-button" onClick={onClick}/>
|
||||
</div>
|
||||
</NodeToolbar>
|
||||
);
|
||||
|
||||
@@ -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 }) {
|
||||
<img src={launch ? dakai : shouqi} className="w-[39px]" alt=""/>
|
||||
</div>
|
||||
{/* 简化 */}
|
||||
<div className="flex fixed right-[80px] top-4 z-10 config-bell-exit-box">
|
||||
{/* <div className="flex fixed right-[80px] top-4 z-10 config-bell-exit-box">
|
||||
<ShadTooltip content={t('flow.simplifyConfig')} side="bottom">
|
||||
<button className="extra-side-bar-buttons whitespace-pre bg-gray-0 rounded-l-full rounded-r-none" onClick={() => setOpen(true)}>
|
||||
<Combine strokeWidth={1.5} className="side-bar-button-size pr-[2px]" color="#999999"></Combine>
|
||||
{/*{t('flow.simplify')}*/}
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
{isPending ? <img src={rb_4_active} onClick={(event) =>
|
||||
saveFlow(flow).then(_ =>
|
||||
_ && setSuccessData({ title: t('success') }))
|
||||
} className="w-[27px] ml-[1px] cursor-pointer" alt="" /> : <img src={rb_4} className="w-[27px] ml-[1px] cursor-pointer" alt="" />}
|
||||
<ShadTooltip content={t('flow.notifications')} side="bottom">
|
||||
<button
|
||||
className="extra-side-bar-buttons whitespace-pre bg-gray-0 rounded-none"
|
||||
@@ -125,25 +128,16 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
|
||||
>
|
||||
{notificationCenter && <div className="header-notifications"></div>}
|
||||
<Bell className="side-bar-button-size" aria-hidden="true" color="#999999"/>
|
||||
{/*{t('flow.notifications')}*/}
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
<ShadTooltip content={t('flow.exit')} side="bottom">
|
||||
<button className="extra-side-bar-buttons whitespace-pre bg-gray-0 rounded-r-full rounded-l-none" onClick={async () => {
|
||||
// await saveFlow(flow).then(() => {
|
||||
// setTimeout(() => {
|
||||
// navigate('/skill/' + flow.id, { replace: true })
|
||||
// }, 100)
|
||||
// })
|
||||
// setTimeout(() => {
|
||||
navigate('/skill/' + flow.id, { replace: true })
|
||||
// }, 100)
|
||||
navigate('/build/skills' + flow.id, { replace: true })
|
||||
}} >
|
||||
<LogOut strokeWidth={1.5} className="side-bar-button-size pr-[2px]" color="#999999"></LogOut>
|
||||
{/*{t('flow.exit')}*/}
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</div> */}
|
||||
{/* 顶部按钮组 */}
|
||||
{/* <div className="side-bar-buttons-arrangement">
|
||||
<ShadTooltip content={t('flow.import')} side="bottom">
|
||||
@@ -173,14 +167,14 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
</div> */}
|
||||
<div className="flex fixed right-[70px] bottom-[14px] z-10">
|
||||
{/* <div className="flex fixed right-[70px] bottom-[14px] z-10">
|
||||
<img src={rb_1} className="w-[27px] cursor-pointer" onClick={() => { openPopUp(<ApiModal flow={flow} />); }} alt="" />
|
||||
<img src={rb_2} className="w-[27px] ml-[1px] cursor-pointer" onClick={(event) => { openPopUp(<ExportModal />); }} alt="" />
|
||||
<img src={rb_3} className="w-[27px] ml-[1px] cursor-pointer" onClick={(event) => { takeSnapshot(); uploadFlow() }} alt="" />
|
||||
{isPending ? <img src={rb_4_active} onClick={(event) =>
|
||||
saveFlow(flow).then(_ =>
|
||||
_ && setSuccessData({ title: t('success') }))
|
||||
} className="w-[27px] ml-[1px] cursor-pointer" alt="" /> : <img src={rb_4} className="w-[27px] ml-[1px] cursor-pointer" alt="" />}
|
||||
} className="w-[27px] ml-[1px] cursor-pointer" alt="" /> : <img src={rb_4} className="w-[27px] ml-[1px] cursor-pointer" alt="" />} */}
|
||||
{/* <ShadTooltip content={t('flow.import')} side="bottom">
|
||||
<button className="extra-side-bar-buttons" onClick={() => { takeSnapshot(); uploadFlow() }} >
|
||||
<FileUp strokeWidth={1.5} className="side-bar-button-size " ></FileUp>
|
||||
@@ -207,7 +201,7 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
|
||||
<Save strokeWidth={1.5} className={"side-bar-button-size" + (isPending ? " " : " extra-side-bar-save-disable")} ></Save>
|
||||
</button>
|
||||
</ShadTooltip> */}
|
||||
</div>
|
||||
{/* </div> */}
|
||||
{/* <Separator /> */}
|
||||
{/* <div className="side-bar-search-div-placement">
|
||||
<input type="text" name="search" id="search" placeholder={t('flow.searchComponent')} className="input-search rounded-full"
|
||||
@@ -335,7 +329,7 @@ export default function ExtraSidebar({ flow }: { flow: FlowType }) {
|
||||
</div>
|
||||
{/* 高级配置l2配置 */}
|
||||
<L2ParamsModal data={flow} open={open} setOpen={setOpen} onSave={() => {
|
||||
saveFlow(flow, true);
|
||||
saveFlow(flow);
|
||||
setSuccessData({ title: t('success') });
|
||||
}}></L2ParamsModal>
|
||||
</div >
|
||||
|
||||
@@ -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 }) => {
|
||||
{/*<Copy className="h-4 w-4"></Copy>*/}
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
{/* 版本 */}
|
||||
{
|
||||
hasVersion && !isGroup && <ShadTooltip content="version" side="top">
|
||||
<button
|
||||
className="version-icon -ml-px bg-background px-2 py-2 shadow-md ring-inset transition-all hover:bg-muted"
|
||||
onClick={() => handleSelectChange('version')}
|
||||
>
|
||||
{/* <CounterClockwiseClockIcon className="h-4 w-4"></CounterClockwiseClockIcon> */}
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
}
|
||||
|
||||
{nodeLength > 0 && (
|
||||
<ShadTooltip content="edit" side="top">
|
||||
@@ -169,6 +198,17 @@ const NodeToolbarComponent = ({ data, deleteNode, openPopUp, position }) => {
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
{isGroup && (
|
||||
<ShadTooltip content="ungroup" side="top">
|
||||
<button
|
||||
className="ungroup-icon -ml-px bg-background px-2 py-2 shadow-md ring-inset transition-all"
|
||||
onClick={() => { handleSelectChange('ungroup'); }}
|
||||
>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
||||
|
||||
{/* more */}
|
||||
{/*<Select onValueChange={handleSelectChange} value="">*/}
|
||||
{/* <ShadTooltip content="More" side="top">*/}
|
||||
|
||||
@@ -20,6 +20,9 @@ import L2Edit from "./pages/SkillPage/l2Edit";
|
||||
import SystemPage from "./pages/SystemPage";
|
||||
import BuildLayout from "./layout/BuildLayout";
|
||||
import Templates from "./pages/SkillPage/temps";
|
||||
import DiffFlowPage from "./pages/DiffFlowPage";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||
|
||||
// react 与 react router dom版本不匹配
|
||||
// const FileLibPage = lazy(() => import(/* webpackChunkName: "FileLibPage" */ "./pages/FileLibPage"));
|
||||
@@ -28,6 +31,17 @@ import Templates from "./pages/SkillPage/temps";
|
||||
// const SkillChatPage = lazy(() => import(/* webpackChunkName: "SkillChatPage" */ "./pages/SkillChatPage"));
|
||||
// const FileViewPage = lazy(() => import(/* webpackChunkName: "FileViewPage" */ "./pages/FileViewPage"));
|
||||
|
||||
const ErrorHoc = ({ Comp }) => {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
onReset={() => window.location.href = window.location.href}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<Comp />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
@@ -73,6 +87,7 @@ const router = createBrowserRouter([
|
||||
{ path: "/chat", element: <SkillChatPage /> },
|
||||
{ path: "/chat/:id/", element: <ChatShare /> },
|
||||
{ path: "/report/:id/", element: <Report /> },
|
||||
{ path: "/diff/:id/:vid/:cid", element: <ErrorHoc Comp={DiffFlowPage} /> },
|
||||
// { path: "/test", element: <Test /> },
|
||||
{ path: "*", element: <Navigate to="/" replace /> }
|
||||
]);
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
background-image: url('../assets/npc/border-r.png');
|
||||
background-size: 30px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
opacity: 90%;
|
||||
background-color: #000;
|
||||
opacity: 70%;
|
||||
}
|
||||
.xinDuiHua{
|
||||
display: flex;
|
||||
@@ -2472,7 +2472,7 @@
|
||||
}
|
||||
}
|
||||
.border-radius-14{
|
||||
border-radius: 14px;
|
||||
border-radius: 14px!important;
|
||||
}
|
||||
.border-radius-7{
|
||||
border-radius: 7px;
|
||||
@@ -4152,3 +4152,6 @@
|
||||
overflow: hidden; /* 隐藏溢出的内容 */
|
||||
text-overflow: ellipsis; /* 使用省略号表示文本溢出 */
|
||||
}
|
||||
.box-shadow{
|
||||
box-shadow: 0px 0px 4px 0px rgba(255,255,255,0.5)!important;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { FlowType, TweaksType } from "../flow";
|
||||
import { FlowType, FlowVersionItem, TweaksType } from "../flow";
|
||||
|
||||
export type TabsContextType = {
|
||||
flow: FlowType | null;
|
||||
@@ -22,6 +22,8 @@ export type TabsContextType = {
|
||||
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
|
||||
setTweak: (tweak: TweaksType) => void;
|
||||
getTweak: TweaksType[];
|
||||
setVersion: (version: FlowVersionItem | null) => {},
|
||||
version: FlowVersionItem
|
||||
};
|
||||
|
||||
export type TabsState = {
|
||||
|
||||
Reference in New Issue
Block a user