1
BIN
src/.DS_Store
vendored
Normal file
210
src/App.css
Normal file
@@ -0,0 +1,210 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
.custom-card-btn {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.custom-card:hover .custom-card-btn,
|
||||
.custom-card:hover .card-component-delete-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.l2Param .l2Param-edit {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.l2Param:hover .l2Param-edit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stroke-connection.selected path {
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 0;
|
||||
}
|
||||
|
||||
.animate-cursor {
|
||||
animation: pulse2 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown ol,
|
||||
.markdown ul {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
@keyframes pulse2 {
|
||||
|
||||
0%,
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(.6);
|
||||
}
|
||||
|
||||
/* 0%, 20%, 80%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
transform: scale(1.6);
|
||||
} */
|
||||
}
|
||||
|
||||
.anwser-souce span {
|
||||
color: #347ef9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: text-security-disc;
|
||||
src: url("assets/text-security-disc.woff") format("woff");
|
||||
}
|
||||
|
||||
/* pdf-text */
|
||||
.textLayer :is(span, br) {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
.textLayer span.markedContent {
|
||||
top: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.textLayer ::selection {
|
||||
background: #357ef9;
|
||||
}
|
||||
|
||||
.textLayer br::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.flag.active {
|
||||
background: linear-gradient(to right, rgba(53, 126, 249, .60), transparent);
|
||||
}
|
||||
|
||||
.flag:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 2px;
|
||||
right: -3px;
|
||||
top: 7px;
|
||||
background: rgba(53, 126, 249, .60);
|
||||
transform: rotate(-50deg) translate(0, 0);
|
||||
}
|
||||
|
||||
.flag::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 2px;
|
||||
right: -3px;
|
||||
top: 25px;
|
||||
background: rgba(53, 126, 249, .60);
|
||||
transform: rotate(50deg) translate(0, 0);
|
||||
}
|
||||
|
||||
.chat .chat-image {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.chat-bubble:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
path.react-flow__edge-interaction:hover {
|
||||
stroke: #eebbbb;
|
||||
stroke-opacity: 1;
|
||||
stroke-width: 12;
|
||||
}
|
||||
|
||||
.selection-tool-box {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #b4c3da;
|
||||
color: #b4c3da;
|
||||
border-radius: 3px;
|
||||
z-index: 99;
|
||||
cursor: pointer;
|
||||
}
|
||||
220
src/App.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import uniqueId from "lodash-es/uniqueId";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { RouterProvider, useSearchParams } from "react-router-dom";
|
||||
import "reactflow/dist/style.css";
|
||||
import "./App.css";
|
||||
|
||||
import ErrorAlert from "./alerts/error";
|
||||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import { userContext } from "./contexts/userContext";
|
||||
import { LoginPage } from "./pages/login";
|
||||
import router from "./routes";
|
||||
import routesM from "./routesM";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18next from "i18next";
|
||||
|
||||
// 是否为手机端
|
||||
const ISMOBILE = function () {
|
||||
let flag = navigator.userAgent.match(
|
||||
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
|
||||
);
|
||||
return flag;
|
||||
};
|
||||
export default function App() {
|
||||
let { setCurrent, setShowSideBar, setIsStackedOpen } = useContext(locationContext);
|
||||
// let location = useLocation();
|
||||
useEffect(() => {
|
||||
setCurrent(location.pathname.replace(/\/$/g, "").split("/"));
|
||||
setShowSideBar(true);
|
||||
setIsStackedOpen(true);
|
||||
}, [setCurrent, setIsStackedOpen, setShowSideBar]);
|
||||
const {
|
||||
errorData,
|
||||
errorOpen,
|
||||
setErrorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setNoticeOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
setErrorData,
|
||||
setSuccessOpen,
|
||||
} = useContext(alertContext);
|
||||
|
||||
// Initialize state variable for the list of alerts
|
||||
const [alertsList, setAlertsList] = useState<
|
||||
Array<{
|
||||
type: string;
|
||||
data: { title: string; list?: Array<string>; link?: string };
|
||||
id: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
if (errorOpen && errorData) {
|
||||
if (
|
||||
alertsList.length > 0 &&
|
||||
JSON.stringify(alertsList[alertsList.length - 1].data) ===
|
||||
JSON.stringify(errorData)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setErrorOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "error", data: cloneDeep(errorData), id: uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a notice alert open with data, add it to the alertsList
|
||||
else if (noticeOpen && noticeData) {
|
||||
if (
|
||||
alertsList.length > 0 &&
|
||||
JSON.stringify(alertsList[alertsList.length - 1].data) ===
|
||||
JSON.stringify(noticeData)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setNoticeOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "notice", data: cloneDeep(noticeData), id: uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a success alert open with data, add it to the alertsList
|
||||
else if (successOpen && successData) {
|
||||
if (
|
||||
alertsList.length > 0 &&
|
||||
JSON.stringify(alertsList[alertsList.length - 1].data) ===
|
||||
JSON.stringify(successData)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSuccessOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "success", data: cloneDeep(successData), id: uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
errorData,
|
||||
errorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setErrorOpen,
|
||||
setNoticeOpen,
|
||||
setSuccessOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* 暴露弹窗全局使用
|
||||
**/
|
||||
useEffect(() => {
|
||||
window.errorAlerts = (errorList: string[]) => {
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "error", data: { title: '', list: errorList }, id: uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const removeAlert = (id: string) => {
|
||||
setAlertsList((prevAlertsList) =>
|
||||
prevAlertsList.filter((alert) => alert.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const { user, setUser } = useContext(userContext);
|
||||
|
||||
// 退出
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.ctrlKey && event.keyCode === 81) {
|
||||
setUser(null)
|
||||
localStorage.setItem('UUR_INFO', '')
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// i18n title
|
||||
const { t } = useTranslation()
|
||||
useEffect(() => {
|
||||
document.title = t('title')
|
||||
}, [t])
|
||||
// init language
|
||||
useEffect(() => {
|
||||
const lang = user?.user_id ? localStorage.getItem('language-' + user.user_id) : null
|
||||
if (lang) {
|
||||
i18next.changeLanguage(lang)
|
||||
}
|
||||
}, [user])
|
||||
|
||||
// 免登录列表
|
||||
const noAuthPages = ['chat']
|
||||
const path = location.pathname.split('/')?.[1] || ''
|
||||
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<div className="flex h-full flex-col">
|
||||
{(user?.user_id || noAuthPages.includes(path)) ? <RouterProvider router={ISMOBILE()?routesM:router} />
|
||||
: user ? <div className="loading"></div>
|
||||
: <LoginPage></LoginPage>}
|
||||
<div></div>
|
||||
<div className="app-div" style={{ zIndex: 1000 }}>
|
||||
{alertsList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
list={alert.data.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
link={alert.data.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
src/CustomEdges/ClearableEdge/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getBezierPath,
|
||||
useReactFlow,
|
||||
} from 'reactflow';
|
||||
|
||||
import styles from './style.module.css';
|
||||
|
||||
const onEdgeClick = (evt, id) => {
|
||||
evt.stopPropagation();
|
||||
alert(`remove ${id}`);
|
||||
};
|
||||
|
||||
export default function CustomEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
}: EdgeProps) {
|
||||
const {setEdges} = useReactFlow();
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const onEdgeClick = () => {
|
||||
setEdges((edges) => edges.filter((edge) => edge.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style}/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
fontSize: 12,
|
||||
// everything inside EdgeLabelRenderer has no pointer events by default
|
||||
// if you have an interactive element, set pointer-events: all
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
<button className={styles.clearableButton} onClick={onEdgeClick}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
16
src/CustomEdges/ClearableEdge/style.module.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.clearableButton {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(238, 238, 238, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 16px;
|
||||
padding-top: 0;
|
||||
line-height: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clearableButton:hover {
|
||||
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { Info } from "lucide-react";
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import VariablesComponent from "../../../../components/VariablesComponent";
|
||||
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||
import DictComponent from "../../../../components/dictComponent";
|
||||
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 KeypairListComponent from "../../../../components/keypairListComponent";
|
||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { reloadCustom } from "../../../../controllers/API/flow";
|
||||
import { captureAndAlertRequestErrorHoc } from "../../../../controllers/request";
|
||||
import CollectionNameComponent from "../../../../pages/FlowPage/components/CollectionNameComponent";
|
||||
import { ParameterComponentType } from "../../../../types/components";
|
||||
import { cleanEdges, convertObjToArray, convertValuesToNumbers, hasDuplicateKeys } from "../../../../util/reactflowUtils";
|
||||
import {
|
||||
classNames,
|
||||
getNodeNames,
|
||||
groupByFamily,
|
||||
isValidConnection,
|
||||
nodeColors,
|
||||
nodeIconsLucide,
|
||||
nodeIMgsLucide
|
||||
} from "../../../../utils";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
|
||||
export default function ParameterComponent({
|
||||
left,
|
||||
id,
|
||||
data,
|
||||
tooltipTitle,
|
||||
title,
|
||||
color,
|
||||
type,
|
||||
name = "",
|
||||
required = false,
|
||||
optionalHandle = null,
|
||||
info = "",
|
||||
isGroup = false,
|
||||
onChange
|
||||
}: ParameterComponentType) {
|
||||
// console.log('data, id :>> ', name, optionalHandle);
|
||||
const { id: flowId } = useParams();
|
||||
|
||||
const ref = useRef(null);
|
||||
const refHtml = useRef(null);
|
||||
const refNumberComponents = useRef(0);
|
||||
const infoHtml = useRef(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const [position, setPosition] = useState(0);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const { setTabsState, flow, setFlow } = useContext(TabsContext);
|
||||
|
||||
const groupedEdge = useRef(null); // 用yu过滤菜单的数据
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
||||
updateNodeInternals(data.id);
|
||||
}
|
||||
}, [data.id, ref, ref.current, ref.current?.offsetTop, updateNodeInternals]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(data.id);
|
||||
}, [data.id, position, updateNodeInternals]);
|
||||
|
||||
useEffect(() => { }, [closePopUp, data.node.template]);
|
||||
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const disabled = useMemo(() => {
|
||||
let dis = reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||
// 特殊处理含有知识库组件的 disabled
|
||||
if (['index_name', 'collection_name'].includes(name)
|
||||
&& reactFlowInstance?.getEdges().some((e) => e.targetHandle.indexOf('documents') !== -1
|
||||
&& e.targetHandle.indexOf(data.id) !== -1)) {
|
||||
dis = true
|
||||
}
|
||||
return dis
|
||||
}, [id, data, reactFlowInstance])
|
||||
// milvus 组件,知识库不为空是 embbeding取消必填限制
|
||||
useEffect(() => {
|
||||
const {embedding, index_name, collection_name, connection_args} = data.node.template
|
||||
if ((index_name || collection_name) && embedding) {
|
||||
const hidden = disabled ? false : !!(collection_name || index_name).value
|
||||
data.node.template.embedding.required = !hidden
|
||||
data.node.template.embedding.show = !hidden
|
||||
if (hidden && connection_args) data.node.template.connection_args.value = ''
|
||||
onChange?.()
|
||||
}
|
||||
}, [data, disabled])
|
||||
const handleRemoveMilvusEmbeddingEdge = (nodeId) => {
|
||||
const edges = reactFlowInstance.getEdges().filter(edge => edge.targetHandle.indexOf('Embeddings|embedding|'+nodeId) === -1)
|
||||
reactFlowInstance.setEdges(edges)
|
||||
}
|
||||
const [myData, setMyData] = useState(useContext(typesContext).data);
|
||||
|
||||
const handleOnNewValue = useCallback((newValue: any) => {
|
||||
// TODO 使用setNodes 保存修改(onChange)
|
||||
data.node.template[name].value = ['float', 'int'].includes(type) ? Number(newValue) : newValue;
|
||||
// Set state to pending
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[flow.id]: {
|
||||
...prev[flow.id],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [data, flow.id]);
|
||||
|
||||
// 临时处理知识库保存方法, 类似方法多了需要抽象
|
||||
const handleOnNewLibValue = (newValue: string, collectionId: number | '') => {
|
||||
// TODO 使用setNodes 保存修改(onChange)
|
||||
data.node.template[name].value = newValue;
|
||||
data.node.template[name].collection_id = collectionId;
|
||||
// Set state to pending
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[flow.id]: {
|
||||
...prev[flow.id],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// custom 组件 reload
|
||||
const handleReloadCustom = (code) => {
|
||||
captureAndAlertRequestErrorHoc(reloadCustom(code)).then(res => {
|
||||
if (res) {
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((nd) => {
|
||||
if (nd.id === data.id) {
|
||||
let newNode = cloneDeep(nd);
|
||||
newNode.data.node = res
|
||||
return newNode;
|
||||
}
|
||||
return nd
|
||||
})
|
||||
)
|
||||
// 清理线
|
||||
setTimeout(() => {
|
||||
const edges = cleanEdges(
|
||||
reactFlowInstance.getNodes(),
|
||||
reactFlowInstance.getEdges()
|
||||
)
|
||||
reactFlowInstance.setEdges(edges)
|
||||
}, 60);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
infoHtml.current = (
|
||||
<div className="h-full w-full break-words">
|
||||
{info.split("\n").map((line, i) => (
|
||||
<p key={i} className="block">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [info]);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, flow.data?.nodes || []);
|
||||
groupedEdge.current = groupedObj;
|
||||
|
||||
if (groupedObj && groupedObj.length > 0) {
|
||||
//@ts-ignore
|
||||
refHtml.current = groupedObj.map((item, index) => {
|
||||
// const Icon: any =
|
||||
// nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
|
||||
const Icon: any =
|
||||
nodeIMgsLucide[item.family] ?? nodeIMgsLucide["unknown"];
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{index === 0 && (
|
||||
<span>
|
||||
{left
|
||||
? "Avaliable input components:"
|
||||
: "Avaliable output components:"}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
key={index}
|
||||
className={classNames(
|
||||
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="h-5 w-5"
|
||||
style={{
|
||||
color: nodeColors[item.family],
|
||||
}}
|
||||
>
|
||||
{/* <Icon
|
||||
className="h-5 w-5"
|
||||
strokeWidth={1.5}
|
||||
style={{
|
||||
color: nodeColors[item.family] ?? nodeColors.unknown,
|
||||
}}
|
||||
/> */}
|
||||
<img src={Icon} className="w-[14px]" alt="" />
|
||||
|
||||
</div>
|
||||
<span className="ps-2 text-xs text-foreground w-[400px]">
|
||||
{getNodeNames()[item.family] ?? "Other"}{" "}
|
||||
{item?.display_name && item?.display_name?.length > 0 ? (
|
||||
<span className="text-xs">
|
||||
{" "}
|
||||
{item.display_name === "" ? "" : " - "}
|
||||
{item.display_name.split(", ").length > 2
|
||||
? item.display_name.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + index}>
|
||||
<span>
|
||||
{index ===
|
||||
item.display_name.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.display_name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs">
|
||||
{" "}
|
||||
{item.type === "" ? "" : " - "}
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + index}>
|
||||
<span>
|
||||
{index === item.type.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.type}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
//@ts-ignore
|
||||
refHtml.current = <span>No compatible components found.</span>;
|
||||
}
|
||||
}, [tooltipTitle]);
|
||||
|
||||
|
||||
// 记录快照
|
||||
const {takeSnapshot} = useContext(undoRedoContext);
|
||||
const { types, deleteNode } = useContext(typesContext);
|
||||
// const onNodeDragStart: NodeDragHandler = useCallback(() => {
|
||||
// // 👇 make dragging a node undoable
|
||||
// takeSnapshot();
|
||||
// // 👉 you can place your event handlers here
|
||||
// }, [takeSnapshot]);
|
||||
const onMouseDownColor = useCallback(() => {
|
||||
// console.log(nodeColorsP)
|
||||
// console.log(data,color,nodeColorsP,nodeColorsP[types[data.type]]);
|
||||
// const type = types[data.type];
|
||||
// Object.keys(nodeColors).forEach(element => {
|
||||
// if(element != type){
|
||||
// nodeColorsP[element] = "#000000"
|
||||
// }
|
||||
// });
|
||||
// console.log(nodeColors);
|
||||
// takeSnapshot();
|
||||
|
||||
// data.node.display_name = "1";
|
||||
// console.log(flow)
|
||||
},[takeSnapshot]);
|
||||
// useEffect(() => {
|
||||
// takeSnapshot();
|
||||
// }, [nodeColors, nodeColorsP]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2"
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
"w-full truncate text-sm" +
|
||||
(left ? "" : " text-end") +
|
||||
(info !== "" ? " flex items-center" : "")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
<div className="">
|
||||
{info !== "" && (
|
||||
<ShadTooltip content={infoHtml.current}>
|
||||
<Info className="relative bottom-0.5 ml-2 h-3 w-3" />
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 触点 */}
|
||||
{left &&
|
||||
(type === "str" ||
|
||||
type === "bool" ||
|
||||
type === "float" ||
|
||||
type === "code" ||
|
||||
type === "prompt" ||
|
||||
type === "file" ||
|
||||
type === "int" ||
|
||||
type === "variable" ||
|
||||
type === "button" ||
|
||||
type === "NestedDict" ||
|
||||
type === "dict") &&
|
||||
!optionalHandle ? (
|
||||
<></>
|
||||
) : (
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance)
|
||||
}
|
||||
onConnect={(params) => console.log('handle onConnect', params)}
|
||||
className={classNames(
|
||||
left ? "-ml-0.5 " : "-mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
onMouseDown={onMouseDownColor}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
||||
{/* 左侧input输入项 */}
|
||||
{!data.node.template[name] ? null : left === true &&
|
||||
type === "str" &&
|
||||
!data.node.template[name].options ? (
|
||||
<div className="mt-2 w-full">
|
||||
{data.node.template[name].list ? (
|
||||
// input list
|
||||
<InputListComponent
|
||||
isGroup={isGroup}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
// 多行数如
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : ['index_name', 'collection_name'].includes(name) ? (
|
||||
// 知识库选择
|
||||
<CollectionNameComponent
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
id={data.node.template[name].collection_id ?? ""}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onSelect={(val, id) => { handleOnNewLibValue(val, id); val && handleRemoveMilvusEmbeddingEdge(data.id) }}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
) : (
|
||||
// 单行输入
|
||||
<InputComponent
|
||||
disabled={disabled}
|
||||
password={data.node.template[name].password ?? false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2 w-full">
|
||||
{/* switch */}
|
||||
<ToggleShadComponent
|
||||
disabled={disabled}
|
||||
enabled={data.node.template[name].value}
|
||||
setEnabled={(t) => {
|
||||
handleOnNewValue(t);
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "float" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true &&
|
||||
type === "str" &&
|
||||
data.node.template[name].options ? (
|
||||
// 下拉框
|
||||
<div className="mt-2 w-full">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : left === true && type === "code" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<CodeAreaComponent
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass; // 无用
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={data.type === 'Data' ? handleReloadCustom : handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "file" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<InputFileComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
fileTypes={data.node.template[name].fileTypes}
|
||||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[name].file_path = t;
|
||||
// save();
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : left === true && type === "int" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<PromptAreaComponent
|
||||
field_name={name}
|
||||
setNodeClass={(nodeClass, code) => {
|
||||
console.log(nodeClass)
|
||||
if (reactFlowInstance) {
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((nd) => {
|
||||
if (nd.id === data.id) {
|
||||
let newNode = cloneDeep(nd);
|
||||
newNode.data.node = nodeClass
|
||||
newNode.data.node.template[name].value = code;
|
||||
return newNode;
|
||||
}
|
||||
return nd
|
||||
})
|
||||
)
|
||||
// 清理线
|
||||
setTimeout(() => {
|
||||
const edges = cleanEdges(
|
||||
reactFlowInstance.getNodes(),
|
||||
reactFlowInstance.getEdges()
|
||||
)
|
||||
reactFlowInstance.setEdges(edges)
|
||||
}, 60);
|
||||
}
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "NestedDict" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
!data.node!.template[name].value ||
|
||||
data.node!.template[name].value?.toString() === "{}"
|
||||
? '{"yourkey": "value"}'
|
||||
: data.node!.template[name].value
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
data.node!.template[name].value = newValue;
|
||||
handleOnNewValue(newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "dict" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<KeypairListComponent
|
||||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
data.node!.template[name].value?.length === 0 ||
|
||||
!data.node!.template[name].value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(data.node!.template[name].value)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||
data.node!.template[name].value = valueToNumbers;
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
handleOnNewValue(valueToNumbers);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "variable" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<VariablesComponent nodeId={data.id} flowId={flowId} onChange={(newValue) => {
|
||||
data.node!.template[name].value = newValue;
|
||||
handleOnNewValue(newValue);
|
||||
}} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { useContext, useState } from "react";
|
||||
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||
import DictComponent from "../../../../components/dictComponent";
|
||||
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 KeypairListComponent from "../../../../components/keypairListComponent";
|
||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import CollectionNameComponent from "../../../../pages/FlowPage/components/CollectionNameComponent";
|
||||
import { cleanEdges, convertObjToArray, convertValuesToNumbers, hasDuplicateKeys } from "../../../../util/reactflowUtils";
|
||||
|
||||
export default function L2ParameterComponent({
|
||||
// id,
|
||||
data,
|
||||
type,
|
||||
name = "",
|
||||
}) {
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
// let disabled = reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||
let disabled = false
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
const { setTabsState, flow } = useContext(TabsContext);
|
||||
const handleOnNewValue = (newValue: any) => {
|
||||
data.node.template[name].value = ['float', 'int'].includes(type) ? Number(newValue) : newValue;
|
||||
// Set state to pending
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[flow.id]: {
|
||||
...prev[flow.id],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 临时处理知识库保存方法, 类似方法多了需要抽象
|
||||
const handleOnNewLibValue = (newValue: string, collectionId: number | '') => {
|
||||
data.node.template[name].value = newValue;
|
||||
data.node.template[name].collection_id = collectionId;
|
||||
// Set state to pending
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[flow.id]: {
|
||||
...prev[flow.id],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
return <div className="flex w-full flex-wrap items-center justify-between py-2 col-span-2" >
|
||||
{type === "str" &&
|
||||
!data.node.template[name].options ? (
|
||||
<div className="mt-2 w-full">
|
||||
{data.node.template[name].list ? (
|
||||
<InputListComponent
|
||||
disabled={disabled}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : ['index_name', 'collection_name'].some(key => name.indexOf(key) === 0) ? (
|
||||
// 知识库选择
|
||||
<CollectionNameComponent
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
id={data.node.template[name].collection_id ?? ""}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onSelect={handleOnNewLibValue}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
disabled={disabled}
|
||||
password={data.node.template[name].password ?? false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : type === "bool" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<ToggleShadComponent
|
||||
disabled={disabled}
|
||||
enabled={data.node.template[name].value}
|
||||
setEnabled={(t) => {
|
||||
handleOnNewValue(t);
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
) : type === "float" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : type === "str" &&
|
||||
data.node.template[name].options ? (
|
||||
<div className="mt-2 w-full">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "choose option"}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : type === "code" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<CodeAreaComponent
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : type === "file" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<InputFileComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
fileTypes={data.node.template[name].fileTypes}
|
||||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[name].file_path = t;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : type === "int" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : type === "prompt" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<PromptAreaComponent
|
||||
field_name={name}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
if (reactFlowInstance) {
|
||||
const edges = cleanEdges(
|
||||
reactFlowInstance.getNodes(),
|
||||
reactFlowInstance.getEdges()
|
||||
)
|
||||
reactFlowInstance.setEdges(edges)
|
||||
}
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : type === "NestedDict" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
!data.node!.template[name].value ||
|
||||
data.node!.template[name].value?.toString() === "{}"
|
||||
? '{"yourkey": "value"}'
|
||||
: data.node!.template[name].value
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
data.node!.template[name].value = newValue;
|
||||
handleOnNewValue(newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : type === "dict" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<KeypairListComponent
|
||||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
data.node!.template[name].value?.length === 0 ||
|
||||
!data.node!.template[name].value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(data.node!.template[name].value)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||
data.node!.template[name].value = valueToNumbers;
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
handleOnNewValue(valueToNumbers);
|
||||
}}
|
||||
/>
|
||||
</div>) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
271
src/CustomNodes/GenericNode/index.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { Zap } from "lucide-react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { useSSE } from "../../contexts/SSEContext";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import DescriptionModel from "../../modals/descriptionModel";
|
||||
import {
|
||||
classNames,
|
||||
nodeColors,
|
||||
nodeIconsLucide,
|
||||
toTitleCase,
|
||||
} from "../../utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import EditLabel from "../../components/ui/editLabel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function GenericNode({ data, xPos, yPos, selected }: {
|
||||
data: NodeDataType;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const { id: flowId } = useParams();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const flowList = [
|
||||
"chains", "agents", "prompts", "llms", "memories", "tools", "toolkits", "wrappers", "embeddings", "vectorstores", "documentloaders", "textsplitters", "utilities", "output_parsers", "retrievers", "input_output", "autogen_roles"];
|
||||
flowList.map(flowListItem => {
|
||||
const flowListItem_type = flowListItem +'.'+ data.type
|
||||
if(`${t(flowListItem_type)}`.indexOf("returned an object instead of string") >= 0){
|
||||
const _flowItem_description_url = flowListItem + '.' + data.type + '.description_url';
|
||||
data.node.description_url = `${t(_flowItem_description_url)}`;
|
||||
}
|
||||
})
|
||||
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const showError = useRef(true);
|
||||
const { types, deleteNode } = useContext(typesContext);
|
||||
|
||||
const { closePopUp, openPopUp } = useContext(PopUpContext);
|
||||
// any to avoid type conflict
|
||||
const Icon: any =
|
||||
nodeIconsLucide[data.type] || nodeIconsLucide[types[data.type]];
|
||||
const [validationStatus, setValidationStatus] = useState(null);
|
||||
// State for outline color
|
||||
const { sseData, isBuilding } = useSSE();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance) {
|
||||
// setParams(Object.values(reactFlowInstance.toObject()));
|
||||
// }
|
||||
// }, [save]);
|
||||
|
||||
// New useEffect to watch for changes in sseData and update validation status
|
||||
useEffect(() => {
|
||||
const relevantData = sseData[data.id];
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [sseData, data.id]);
|
||||
|
||||
if (!Icon) {
|
||||
if (showError.current) {
|
||||
setErrorData({
|
||||
title: data.type
|
||||
? `can be translated to "Unable to render the ${data.type} node. Please check your JSON file.`
|
||||
: `can be translated to "One node cannot be rendered. Please check your JSON file.`
|
||||
});
|
||||
showError.current = false;
|
||||
}
|
||||
deleteNode(data.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const [_, fouceUpdateNode] = useState(false)
|
||||
const isGroup = !!data.node?.flow;
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeToolbar align="end">
|
||||
<NodeToolbarComponent
|
||||
position={{ x: xPos, y: yPos }}
|
||||
data={data}
|
||||
openPopUp={openPopUp}
|
||||
deleteNode={deleteNode}
|
||||
></NodeToolbarComponent>
|
||||
</NodeToolbar>
|
||||
|
||||
<div className={classNames("border-4 generic-node-div relative", selected ? "border-ring" : "")} style={{ borderColor: nodeColors[types[data.type]] ?? nodeColors.unknown }}>
|
||||
{isGroup && <div className={`generic-node-div absolute border-2 w-full h-full left-3 top-3 z-[-1] ${selected ? "border-ring" : ""}`} style={{ borderColor: nodeColors[types[data.type]] ?? nodeColors.unknown }}>
|
||||
<div className={`generic-node-div absolute border-4 w-full h-full left-3 top-3 z-[-1] bg-transparent ${selected ? "border-ring" : ""}`} style={{ borderColor: nodeColors[types[data.type]] ?? nodeColors.unknown }}>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="generic-node-div-title">
|
||||
{/* title */}
|
||||
<div className="generic-node-title-arrangement">
|
||||
{/* <Icon
|
||||
strokeWidth={1.5}
|
||||
className="generic-node-icon"
|
||||
style={{
|
||||
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
|
||||
}}
|
||||
/> */}
|
||||
<div className="round-button-div">
|
||||
<div>
|
||||
<Tooltip
|
||||
title={
|
||||
isBuilding ? (<span>build...</span>) :
|
||||
!validationStatus ? (
|
||||
<span className="flex">
|
||||
Build{" "} <Zap className="mx-0.5 h-5 fill-build-trigger stroke-build-trigger stroke-1" strokeWidth={1.5} />{" "} flow to validate status.
|
||||
</span>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{validationStatus.params ? validationStatus.params.split("\n")
|
||||
.map((line, index) => <div key={index}>{line}</div>)
|
||||
: ""}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="generic-node-status-position">
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && validationStatus.valid ? "green-status" : "status-build-animation", "status-div"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && !validationStatus.valid ? "red-status" : "status-build-animation", "status-div"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
!validationStatus || isBuilding ? "yellow-status" : "status-build-animation", "status-div"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="generic-node-tooltip-div">
|
||||
<ShadTooltip content={data.node.display_name}>
|
||||
<div className="generic-node-tooltip-div text-primary">
|
||||
{isGroup ? <EditLabel
|
||||
rule={[
|
||||
{required: true}
|
||||
]}
|
||||
str={data.node.display_name}
|
||||
onChange={(val) => {(data.node.display_name = val);fouceUpdateNode(!_)}}>
|
||||
{(val) => <div className="max-w-[300px] overflow-hidden text-ellipsis">{val}</div>}
|
||||
</EditLabel> : data.node.display_name}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="round-button-div">
|
||||
<button className="relative" onClick={(event) => { event.preventDefault(); openPopUp(<NodeModal data={data} />)}} ></button>
|
||||
</div> */}
|
||||
</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*/}
|
||||
<>
|
||||
{Object.keys(data.node.template)
|
||||
.filter((t) => t.charAt(0) !== "_")
|
||||
.map((t: string, idx) => (
|
||||
<div key={idx}>
|
||||
{/* {idx === 0 ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"px-5 py-2 mt-2 text-center",
|
||||
Object.keys(data.node.template).filter(
|
||||
(key) =>
|
||||
!key.startsWith("_") &&
|
||||
data.node.template[key].show &&
|
||||
!data.node.template[key].advanced
|
||||
).length === 0
|
||||
? "hidden"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
Inputs
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)} */}
|
||||
{data.node.template[t].show &&
|
||||
!data.node.template[t].advanced ? (
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
isGroup={isGroup}
|
||||
color={
|
||||
nodeColors[types[data.node.template[t].type]] ??
|
||||
nodeColors[data.node.template[t].type] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node.template[t].display_name
|
||||
? data.node.template[t].display_name
|
||||
: data.node.template[t].name
|
||||
? toTitleCase(data.node.template[t].name)
|
||||
: toTitleCase(t)
|
||||
}
|
||||
info={data.node.template[t].info}
|
||||
name={t}
|
||||
tooltipTitle={
|
||||
data.node.template[t].input_types?.join("\n") ??
|
||||
data.node.template[t].type
|
||||
}
|
||||
required={data.node.template[t].required}
|
||||
id={(data.node.template[t].input_types?.join(";") ?? data.node.template[t].type) + "|" + t + "|" + data.id}
|
||||
left={true}
|
||||
type={data.node.template[t].type}
|
||||
optionalHandle={data.node.template[t].input_types}
|
||||
onChange={() => fouceUpdateNode(!_)}
|
||||
nodeColorsP={nodeColors}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={classNames(
|
||||
Object.keys(data.node.template).length < 1 ? "hidden" : "",
|
||||
"flex-max-width justify-center"
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
{/* 输出节点 */}
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={
|
||||
data.node.output_types && data.node.output_types.length > 0
|
||||
? data.node.output_types.join("|")
|
||||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node.base_classes.join("\n")}
|
||||
id={[data.type, data.id, ...data.node.base_classes].join("|")}
|
||||
type={data.node.base_classes.join("|")}
|
||||
left={false}
|
||||
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>
|
||||
</div>}
|
||||
</>
|
||||
</div>
|
||||
</div >
|
||||
</>
|
||||
);
|
||||
}
|
||||
73
src/CustomNodes/GroupNode/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import {memo, useContext, useEffect, useRef} from 'react';
|
||||
import {Handle, Position, NodeResizeControl, NodeToolbar} from 'reactflow';
|
||||
|
||||
import styles from './style.module.css';
|
||||
import {typesContext} from "../../contexts/typesContext";
|
||||
import NodeToolbarComponent4Group from "../../pages/FlowPage/components/NodeToolbarComponent4Group";
|
||||
import {NodeDataType} from "../../types/flow";
|
||||
|
||||
const controlStyle = {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
const CustomNode = ({data, id}) => {
|
||||
const {deleteGroupNode} = useContext(typesContext);
|
||||
function autoDeleteNode() {
|
||||
deleteGroupNode(id);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<NodeToolbar align="end">
|
||||
<NodeToolbarComponent4Group
|
||||
data={data}
|
||||
deleteNode={autoDeleteNode}
|
||||
></NodeToolbarComponent4Group>
|
||||
</NodeToolbar>
|
||||
<div className={styles.node}>
|
||||
{/*<div className={styles.titleBox}>*/}
|
||||
{/* <div className={styles.deleteIcon} title="Delete" onClick={() => {*/}
|
||||
{/* deleteGroupNode(id);*/}
|
||||
{/* }}></div>*/}
|
||||
{/*<svg className={styles.deleteIcon} viewBox="0 0 1024 1024" version="1.1" title="Delete"*/}
|
||||
{/* xmlns="http://www.w3.org/2000/svg" onClick={() => {*/}
|
||||
{/* deleteGroupNode(id);*/}
|
||||
{/*}}>*/}
|
||||
{/*<path*/}
|
||||
{/* d="M363.072 272.832H167.488c-21.76 0-39.488 11.84-39.488 32.256 0 20.352 17.664 31.744 39.488 31.744h48.512v418.24c0 77.568 51.328 140.928 120.896 140.928H691.2c69.376 0 120.64-63.04 120.64-140.928v-417.92h44.672c21.76 0 39.488-11.392 39.488-31.808 0-20.288-17.664-32.256-39.488-32.256H672.64C670.08 194.816 601.92 128 518.016 128s-152 66.816-154.88 144.832z m74.048 0C440 232.832 474.816 192 518.016 192s78.208 40.96 80.704 80.832H437.12z m-157.12 482.24v-417.92h467.84v418.304c0 42.24-32.128 76.544-56.768 76.544H336.768c-24.64-0.384-56.768-34.56-56.768-76.928z m109.824-16.832c18.048 0 32.96-16.896 32.96-38.08V483.2c0-21.12-14.72-38.016-32.96-38.016-18.048 0-32.896 16.832-32.896 38.016v216.96c0 21.12 14.464 38.08 32.896 38.08z m152.128-38.08V483.2c0-21.12-14.72-38.016-32.896-38.016-18.048 0-32.96 16.832-32.96 38.016v216.96c0 21.12 14.912 38.08 32.96 38.08 18.048 0 32.896-16.896 32.896-38.08z m92.48 38.08c18.048 0 32.896-16.896 32.896-38.08V483.2c0-21.12-14.72-38.016-32.896-38.016-18.048 0-32.96 16.832-32.96 38.016v216.96c0 21.12 14.4 38.08 32.96 38.08z"*/}
|
||||
{/*/>*/}
|
||||
{/*</svg>*/}
|
||||
{/*</div>*/}
|
||||
<NodeResizeControl style={controlStyle} minWidth={180} minHeight={150}>
|
||||
<ResizeIcon/>
|
||||
</NodeResizeControl>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function ResizeIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="#999999"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{position: 'absolute', right: 10, bottom: 10}}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<polyline points="16 20 20 20 20 16"/>
|
||||
<line x1="14" y1="14" x2="20" y2="20"/>
|
||||
<polyline points="8 4 4 4 4 8"/>
|
||||
<line x1="4" y1="4" x2="10" y2="10"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(CustomNode);
|
||||
39
src/CustomNodes/GroupNode/style.module.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.node {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*border: 3px solid rgba(0, 162, 250, 0.4);*/
|
||||
background-color: rgba(255,255,255,.1);
|
||||
border-radius: 20px;
|
||||
|
||||
box-sizing: border-box;
|
||||
z-index: -5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.node :global .react-flow__resize-control.handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.titleBox {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding-left: 10px;
|
||||
font-size: 26px;
|
||||
line-height: 50px;
|
||||
/*color: rgba(0, 162, 250, 0.6);*/
|
||||
/*background-color: rgba(0, 162, 250, 0.2);*/
|
||||
}
|
||||
|
||||
.deleteIcon {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
right: 10px;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
fill: #999999;
|
||||
cursor: pointer;
|
||||
background: url("../../assets/toolbar/ungroup.png") no-repeat 100% 100% /contain;
|
||||
}
|
||||
|
||||
BIN
src/alerts/.DS_Store
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { CheckCircle2, Info, X, XCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SingleAlertComponentType } from "../../../../types/alerts";
|
||||
|
||||
export default function SingleAlert({
|
||||
dropItem,
|
||||
removeAlert,
|
||||
}: SingleAlertComponentType) {
|
||||
const [show, setShow] = useState(true);
|
||||
const type = dropItem.type;
|
||||
|
||||
return (
|
||||
<Transition
|
||||
className="relative"
|
||||
show={show}
|
||||
appear={true}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
{type === "error" ? (
|
||||
<div
|
||||
className="mx-2 mb-2 flex rounded-md bg-error-background p-3"
|
||||
key={dropItem.id}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<XCircle className="h-5 w-5 text-status-red" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="break-words text-sm font-medium text-error-foreground">
|
||||
{dropItem.title}
|
||||
</h3>
|
||||
{dropItem.list ? (
|
||||
<div className="mt-2 text-sm text-error-foreground">
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
{dropItem.list.map((item, idx) => (
|
||||
<li className="break-words" key={idx}>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto pl-3">
|
||||
<div className="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(dropItem.id);
|
||||
}, 500);
|
||||
}}
|
||||
className="inline-flex rounded-md p-1.5 text-status-red"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<X
|
||||
className="h-4 w-4 text-error-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : type === "notice" ? (
|
||||
<div
|
||||
className="mx-2 mb-2 flex rounded-md bg-info-background p-3"
|
||||
key={dropItem.id}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<Info className="h-5 w-5 text-status-blue " aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm font-medium text-info-foreground">
|
||||
{dropItem.title}
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:ml-6 md:mt-0">
|
||||
{dropItem.link ? (
|
||||
<Link
|
||||
to={dropItem.link}
|
||||
className="whitespace-nowrap font-medium text-info-foreground hover:text-accent-foreground"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto pl-3">
|
||||
<div className="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(dropItem.id);
|
||||
}, 500);
|
||||
}}
|
||||
className="inline-flex rounded-md p-1.5 text-info-foreground"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<X
|
||||
className="h-4 w-4 text-info-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="mx-2 mb-2 flex rounded-md bg-success-background p-3"
|
||||
key={dropItem.id}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircle2
|
||||
className="h-5 w-5 text-status-green"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-success-foreground">
|
||||
{dropItem.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto pl-3">
|
||||
<div className="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(dropItem.id);
|
||||
}, 500);
|
||||
}}
|
||||
className="inline-flex rounded-md p-1.5 text-status-green"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<X
|
||||
className="h-4 w-4 text-success-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
66
src/alerts/alertDropDown/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Trash2, X } from "lucide-react";
|
||||
import { useContext, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { useOnClickOutside } from "../hooks/useOnClickOutside";
|
||||
import SingleAlert from "./components/singleAlertComponent";
|
||||
|
||||
export default function AlertDropdown() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Use the custom hook
|
||||
useOnClickOutside(componentRef, () => {
|
||||
closePopUp();
|
||||
});
|
||||
|
||||
const {
|
||||
notificationList,
|
||||
clearNotificationList,
|
||||
removeFromNotificationList,
|
||||
} = useContext(alertContext);
|
||||
|
||||
return <div
|
||||
ref={componentRef}
|
||||
className="z-10 flex h-[500px] w-[400px] flex-col overflow-hidden rounded-md bg-muted px-2 py-3 pb-4 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
|
||||
{t('flow.notification')}
|
||||
<div className="flex gap-3 pr-3">
|
||||
<button
|
||||
className="text-foreground hover:text-status-red"
|
||||
onClick={() => {
|
||||
closePopUp();
|
||||
setTimeout(clearNotificationList, 100);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-[1.1rem] w-[1.1rem]" />
|
||||
</button>
|
||||
<button
|
||||
className="text-foreground hover:text-status-red"
|
||||
onClick={closePopUp}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
|
||||
{notificationList.length !== 0 ? (
|
||||
notificationList.map((alertItem, index) => (
|
||||
<SingleAlert
|
||||
key={alertItem.id}
|
||||
dropItem={alertItem}
|
||||
removeAlert={removeFromNotificationList}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
|
||||
{t('flow.noNewNotifications')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
85
src/alerts/confirm/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import i18next from "i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { X } from "lucide-react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
interface ConfirmParams {
|
||||
title?: string
|
||||
desc: string | React.ReactNode
|
||||
canelTxt?: string
|
||||
okTxt?: string
|
||||
showClose?: boolean
|
||||
onClose?: () => void
|
||||
onCancel?: () => void
|
||||
onOk?: (next) => void
|
||||
}
|
||||
|
||||
let openFn = (_: ConfirmParams) => { }
|
||||
function ConfirmWrapper() {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const paramRef = useRef(null)
|
||||
|
||||
openFn = (params: ConfirmParams) => {
|
||||
paramRef.current = params
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
paramRef.current?.onClose?.()
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleCancelClick = () => {
|
||||
paramRef.current?.onCancel?.()
|
||||
close()
|
||||
}
|
||||
|
||||
const handleOkClick = () => {
|
||||
paramRef.current?.onOk
|
||||
? paramRef.current?.onOk?.(close)
|
||||
: close()
|
||||
}
|
||||
|
||||
if (!paramRef.current) return null
|
||||
const { title, desc, okTxt, canelTxt, showClose = false } = paramRef.current
|
||||
|
||||
return <dialog className={`modal ${open && 'modal-open'}`}>
|
||||
<form method="dialog" className="modal-box w-[400px] bg-[#262626] shadow-lg relative">
|
||||
{showClose && <X size={20} onClick={close} className="absolute right-4 cursor-pointer text-gray-400 hover:text-gray-600"></X>}
|
||||
<h3 className="text-[16px] font-bold text-center" style={{color:"#FFFFFF"}}>{title}</h3>
|
||||
<p className="text-[12px] text-center mt-[18px]" style={{color:"#FFFFFF"}}>{desc}</p>
|
||||
<div className="flex justify-center mt-[27px]">
|
||||
<Button className="baogao-btn" variant="outline" onClick={handleCancelClick}>{canelTxt}</Button>
|
||||
<Button className="baogao-btn ml-[27px]" variant="destructive" onClick={handleOkClick}>{okTxt}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
}
|
||||
|
||||
(function () {
|
||||
let el = document.getElementById('#message-wrap');
|
||||
if (!el) {
|
||||
el = document.createElement('div')
|
||||
el.className = 'message-wrap'
|
||||
el.id = 'message-wrap'
|
||||
document.body.append(el)
|
||||
}
|
||||
// ReactDOM.render(<ConfirmWrapper />, el);
|
||||
const root = createRoot(el);
|
||||
root.render(<ConfirmWrapper />);
|
||||
})();
|
||||
|
||||
|
||||
export const bsconfirm = (params: ConfirmParams) => {
|
||||
const resource = i18next.getResourceBundle(i18next.language, 'bs')
|
||||
|
||||
openFn({
|
||||
title: resource.prompt,
|
||||
canelTxt: resource.cancel,
|
||||
okTxt: resource.confirmButton,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
71
src/alerts/error/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ErrorAlertType } from "../../types/alerts";
|
||||
|
||||
export default function ErrorAlert({
|
||||
title,
|
||||
list = [],
|
||||
id,
|
||||
removeAlert,
|
||||
}: ErrorAlertType) {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}, [id, removeAlert, show]);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
className="relative"
|
||||
show={show}
|
||||
appear={true}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}}
|
||||
className="error-build-message"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<XCircle
|
||||
className="error-build-message-circle"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="error-build-foreground">{title}</h3>
|
||||
{list?.length !== 0 &&
|
||||
list?.some((item) => item !== null && item !== undefined) ? (
|
||||
<div className="error-build-message-div">
|
||||
<ul className="error-build-message-list">
|
||||
{list.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
33
src/alerts/hooks/useOnClickOutside/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useOnClickOutside(ref, handler) {
|
||||
useEffect(() => {
|
||||
const listener = (event) => {
|
||||
// Do nothing if clicking ref's element or its children
|
||||
if (!ref.current || ref.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(event);
|
||||
};
|
||||
|
||||
// Attach the listener to the document
|
||||
document.addEventListener("mousedown", listener, { passive: true });
|
||||
|
||||
// Attach the listener to the react-flow instance
|
||||
const reactFlowContainer = document.querySelector(".react-flow");
|
||||
if (reactFlowContainer) {
|
||||
reactFlowContainer.addEventListener("mousedown", listener, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up the listener when the component is unmounted
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
if (reactFlowContainer) {
|
||||
reactFlowContainer.removeEventListener("mousedown", listener);
|
||||
}
|
||||
};
|
||||
}, [ref, handler]); // Rerun only if ref or handler changes
|
||||
}
|
||||
64
src/alerts/notice/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { Info } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { NoticeAlertType } from "../../types/alerts";
|
||||
|
||||
export default function NoticeAlert({
|
||||
title,
|
||||
link = "",
|
||||
id,
|
||||
removeAlert,
|
||||
}: NoticeAlertType) {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}, [id, removeAlert, show]);
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
removeAlert(id);
|
||||
}}
|
||||
className="mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Info className="h-5 w-5 text-status-blue " aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-info-foreground">{title}</p>
|
||||
<p className="mt-3 text-sm md:ml-6 md:mt-0">
|
||||
{link !== "" ? (
|
||||
<Link
|
||||
to={link}
|
||||
className="whitespace-nowrap font-medium text-info-foreground hover:text-accent-foreground"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
50
src/alerts/success/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SuccessAlertType } from "../../types/alerts";
|
||||
|
||||
export default function SuccessAlert({
|
||||
title,
|
||||
id,
|
||||
removeAlert,
|
||||
}: SuccessAlertType) {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}, [id, removeAlert, show]);
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
removeAlert(id);
|
||||
}}
|
||||
className="success-alert"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircle2 className="success-alert-icon" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="success-alert-message">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
BIN
src/assets/.DS_Store
vendored
Normal file
BIN
src/assets/Login/.DS_Store
vendored
Normal file
BIN
src/assets/Login/bg.png
Normal file
|
After Width: | Height: | Size: 7.7 MiB |
BIN
src/assets/Login/denglu.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/Login/login-icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/Login/login.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
src/assets/Login/password.png
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
src/assets/Login/phone.png
Normal file
|
After Width: | Height: | Size: 756 B |
BIN
src/assets/Login/tuijian.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/Login/zhuce.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/chat/.DS_Store
vendored
Normal file
BIN
src/assets/chat/bianji.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/chat/btn-del.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/chat/btn-edit.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/chat/btn-reSend.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/chat/cai.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/chat/caiActive.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/chat/down.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/chat/duihua-bg.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/assets/chat/duihua-del.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/chat/duihua-gengduo.png
Normal file
|
After Width: | Height: | Size: 488 B |
BIN
src/assets/chat/duihua-item-+.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/chat/duihua-item-top.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/chat/duihua-item-x.png
Normal file
|
After Width: | Height: | Size: 510 B |
BIN
src/assets/chat/duihua-send.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/chat/yuyin.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/chat/zan.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/chat/zanActive.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/chatM/.DS_Store
vendored
Normal file
BIN
src/assets/chatM/duihua-guan.png
Normal file
|
After Width: | Height: | Size: 900 B |
BIN
src/assets/chatM/duihua-icon-active.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/chatM/duihua-icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/chatM/duihua-item-bianji.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/chatM/duihua-item-del.png
Normal file
|
After Width: | Height: | Size: 1019 B |
BIN
src/assets/chatM/duihua-item-shoucang.png
Normal file
|
After Width: | Height: | Size: 838 B |
BIN
src/assets/chatM/tit-icon-l.png
Normal file
|
After Width: | Height: | Size: 856 B |
BIN
src/assets/chatM/tit-icon-r.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/.DS_Store
vendored
Normal file
BIN
src/assets/externalUse/111.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/externalUse/api-fangwen.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/bianji.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/externalUse/bianji1.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/externalUse/buquan-active.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/externalUse/buquan.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/externalUse/cai.png
Normal file
|
After Width: | Height: | Size: 890 B |
BIN
src/assets/externalUse/chuangjian.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/externalUse/copy.png
Normal file
|
After Width: | Height: | Size: 917 B |
BIN
src/assets/externalUse/del.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/externalUse/duihua-active.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/externalUse/duihua.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/externalUse/duihuajilu.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/fanhui.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/fankui.png
Normal file
|
After Width: | Height: | Size: 969 B |
BIN
src/assets/externalUse/fenxiang-active.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/externalUse/fenxiang-icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/fenxiang.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/guan.png
Normal file
|
After Width: | Height: | Size: 575 B |
BIN
src/assets/externalUse/neirong-active.png
Normal file
|
After Width: | Height: | Size: 925 B |
BIN
src/assets/externalUse/neirong.png
Normal file
|
After Width: | Height: | Size: 931 B |
BIN
src/assets/externalUse/shanchushuju.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/externalUse/sousuo-active.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/externalUse/sousuo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/externalUse/suoyin-active.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/externalUse/suoyin.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/externalUse/tishi.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/externalUse/wendang.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/externalUse/xiangqing.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/externalUse/zan.png
Normal file
|
After Width: | Height: | Size: 983 B |
BIN
src/assets/externalUse/zhanwei.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/externalUse/zhishiku.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/externalUse/图层 1275@2x.png
Normal file
|
After Width: | Height: | Size: 681 B |
BIN
src/assets/externalUse/图层 1280@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/knowledge/add-icon.png
Normal file
|
After Width: | Height: | Size: 853 B |
BIN
src/assets/knowledge/back-icon.png
Normal file
|
After Width: | Height: | Size: 601 B |
BIN
src/assets/knowledge/change-icon.png
Normal file
|
After Width: | Height: | Size: 572 B |
BIN
src/assets/knowledge/delete-icon.png
Normal file
|
After Width: | Height: | Size: 608 B |
BIN
src/assets/knowledge/export-icon.png
Normal file
|
After Width: | Height: | Size: 393 B |
BIN
src/assets/knowledge/folder-icon.png
Normal file
|
After Width: | Height: | Size: 438 B |
BIN
src/assets/knowledge/knowledge-active-icon.png
Normal file
|
After Width: | Height: | Size: 461 B |
BIN
src/assets/knowledge/knowledge-icon.png
Normal file
|
After Width: | Height: | Size: 842 B |
BIN
src/assets/knowledge/knowledge-no-icon.png
Normal file
|
After Width: | Height: | Size: 383 B |
BIN
src/assets/knowledge/move-icon.png
Normal file
|
After Width: | Height: | Size: 651 B |