This commit is contained in:
zhangkai
2024-06-05 14:27:06 +08:00
commit b825dcd4d5
730 changed files with 100244 additions and 0 deletions

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

210
src/App.css Normal file
View 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
View 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>
);
}

View 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>
</>
);
}

View 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);
}

View File

@@ -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>
);
}

View File

@@ -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>
}

View 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 >
</>
);
}

View 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);

View 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

Binary file not shown.

View 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>
);
}

View 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>
}

View 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,
})
}

View 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>
);
}

View 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
}

View 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>
);
}

View 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

Binary file not shown.

BIN
src/assets/Login/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/assets/Login/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

BIN
src/assets/Login/denglu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/Login/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

BIN
src/assets/Login/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/Login/zhuce.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
src/assets/chat/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/assets/chat/bianji.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/chat/btn-del.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
src/assets/chat/cai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/chat/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/chat/yuyin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
src/assets/chat/zan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/chatM/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/externalUse/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Some files were not shown because too many files have changed in this diff Show More