Add backend and frontend

This commit is contained in:
Gk0Wk
2024-04-07 15:04:00 +08:00
parent 49fdd9cc43
commit 84a7cb1b7e
233 changed files with 29927 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
import React, { useState } from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import { IconButton, SxProps } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import Divider from '@mui/material/Divider';
import RemoveIcon from '@mui/icons-material/Remove';
import AdjustIcon from '@mui/icons-material/Adjust';
import { ObjectProps, ProcessProps } from './interface';
import AgentIcon from '@/components/AgentIcon';
import { globalStorage } from '@/storage';
export interface IEditObjectProps {
finishEdit: (objectName: string) => void;
}
export const EditObjectCard: React.FC<IEditObjectProps> = React.memo(
({ finishEdit }) => {
const handleKeyPress = (event: any) => {
if (event.key === 'Enter') {
finishEdit(event.target.value);
}
};
return (
<TextField
onKeyPress={handleKeyPress}
sx={{
backgroundColor: '#D9D9D9',
borderRadius: '6px',
padding: '8px',
userSelect: 'none',
margin: '6px 0px',
}}
/>
);
},
);
interface IHoverIconButtonProps {
onAddClick: () => void;
isActive: boolean;
style: SxProps;
responseToHover?: boolean;
addOrRemove: boolean | undefined; // true for add, false for remove,undefined for adjust
}
const HoverIconButton: React.FC<IHoverIconButtonProps> = ({
onAddClick,
isActive,
style,
addOrRemove,
responseToHover = true,
}) => {
const [addIconHover, setAddIconHover] = useState(false);
return (
<Box
onMouseOver={() => {
setAddIconHover(true);
}}
onMouseOut={() => {
setAddIconHover(false);
}}
onClick={() => {
onAddClick();
}}
sx={{ ...style, justifySelf: 'start' }}
>
<IconButton
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
padding: '0px',
borderRadius: 10,
border: '1px dotted #333',
visibility:
(responseToHover && addIconHover) || isActive
? 'visible'
: 'hidden',
'& .MuiSvgIcon-root': {
fontSize: '1.25rem',
},
}}
>
{addOrRemove === undefined ? <AdjustIcon /> : <></>}
{addOrRemove === true ? <AddIcon /> : <></>}
{addOrRemove === false ? <RemoveIcon /> : <></>}
</IconButton>
</Box>
);
};
interface IEditableBoxProps {
text: string;
inputCallback: (text: string) => void;
}
const EditableBox: React.FC<IEditableBoxProps> = ({ text, inputCallback }) => {
const [isEditable, setIsEditable] = useState(false);
const handleDoubleClick = () => {
setIsEditable(true);
};
const handleKeyPress = (event: any) => {
if (event.key === 'Enter') {
inputCallback(event.target.value);
setIsEditable(false);
}
};
return (
<Box>
{isEditable ? (
<TextField
defaultValue={text}
multiline
onKeyPress={handleKeyPress}
onBlur={() => setIsEditable(false)} // 失去焦点时也关闭编辑状态
autoFocus
sx={{
'& .MuiInputBase-root': {
// 目标 MUI 的输入基础根元素
padding: '0px 0px', // 你可以设置为你希望的内边距值
},
width: '100%',
}}
/>
) : (
<span
onDoubleClick={handleDoubleClick}
style={{
color: '#707070',
// textDecoration: 'underline',
// textDecorationColor: '#C1C1C1',
borderBottom: '1.5px solid #C1C1C1',
fontSize: '15px',
}}
>
{text}
</span>
)}
</Box>
);
};
export interface IObjectCardProps {
object: ObjectProps;
isAddActive?: boolean;
handleAddActive?: (objectName: string) => void;
addOrRemove?: boolean;
}
export const ObjectCard = React.memo<IObjectCardProps>(
({
object,
isAddActive = false,
handleAddActive = (objectName: string) => {
console.log(objectName);
},
addOrRemove = true,
}) => {
const onAddClick = () => {
handleAddActive(object.name);
};
return (
<Box
sx={{
position: 'relative',
// maxWidth: '100%',
}}
>
<HoverIconButton
style={{
position: 'absolute',
left: '100%',
top: '50%',
transform: 'translateY(-50%)translateX(-50%)',
}}
onAddClick={onAddClick}
isActive={isAddActive}
responseToHover={false}
addOrRemove={addOrRemove}
/>
<Box
ref={object.cardRef}
sx={{
backgroundColor: '#F6F6F6',
borderRadius: '15px',
border: '2px solid #E5E5E5',
padding: '10px 4px',
userSelect: 'none',
margin: '12px 0px',
maxWidth: '100%',
wordWrap: 'break-word',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontSize: '16px',
fontWeight: 800,
textAlign: 'center',
color: '#222',
}}
>
{object.name}
</Box>
</Box>
);
},
);
export interface IProcessCardProps {
process: ProcessProps;
handleProcessClick: (stepId: string) => void;
isFocusing: boolean;
isAddActive?: boolean;
handleAddActive?: (objectName: string) => void;
handleEditContent: (stepTaskId: string, newContent: string) => void;
// handleSizeChange: () => void;
}
export const ProcessCard: React.FC<IProcessCardProps> = React.memo(
({
process,
handleProcessClick,
isFocusing,
isAddActive = false,
handleAddActive = (objectName: string) => {
console.log(objectName);
},
// handleSizeChange,
handleEditContent,
}) => {
const onAddClick = () => {
handleAddActive(process.id);
};
return (
<Box
sx={{
position: 'relative',
// width: '100%',
}}
>
<HoverIconButton
style={{
position: 'absolute',
left: '0',
top: '50%',
transform: 'translateY(-50%)translateX(-50%)',
}}
onAddClick={onAddClick}
isActive={isAddActive}
addOrRemove={undefined}
/>
<Box
ref={process.cardRef}
sx={{
backgroundColor: '#F6F6F6',
borderRadius: '15px',
padding: '8px',
margin: '18px 0px',
userSelect: 'none',
cursor: 'pointer',
border: isFocusing ? '2px solid #43b2aa' : '2px solid #E5E5E5',
transition: 'all 80ms ease-in-out',
'&:hover': {
border: isFocusing ? '2px solid #03a89d' : '2px solid #b3b3b3',
backgroundImage: 'linear-gradient(0, #0001, #0001)',
},
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
onClick={() => handleProcessClick(process.id)}
>
<Box
sx={{
fontSize: '16px',
fontWeight: 800,
textAlign: 'center',
color: '#222',
marginTop: '4px',
marginBottom: '4px',
}}
>
{process.name}
</Box>
{/* Assuming AgentIcon is another component */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: 'center',
flexWrap: 'wrap',
margin: '8px 0',
}}
>
{process.agents.map(agentName => (
<AgentIcon
key={`outline.${process.name}.${agentName}`}
name={globalStorage.agentMap.get(agentName)?.icon ?? 'unknown'}
style={{
width: '40px',
height: 'auto',
marginRight: '3px',
userSelect: 'none',
}}
tooltipInfo={globalStorage.agentMap.get(agentName)}
/>
))}
</Box>
{isFocusing && (
<Box onClick={e => e.stopPropagation()}>
<Divider
sx={{
margin: '5px 0px',
borderBottom: '2px dashed', // 设置为虚线
borderColor: '#d4d4d4',
}}
/>
<EditableBox
text={process.content}
inputCallback={(text: string) => {
handleEditContent(process.id, text);
// handleEditStep(step.name, { ...step, task: text });
}}
/>
</Box>
)}
</Box>
</Box>
);
},
);
const Card: React.FC = React.memo(() => {
return <></>; // Replace with your component JSX
});
export default Card;

View File

@@ -0,0 +1,241 @@
// D3Graph.tsx
import React, { useState } from 'react';
export interface SvgLineProp {
x1: number;
y1: number;
x2: number;
y2: number;
type: string;
key: string;
stepTaskId: string;
stepName: string;
objectName: string;
}
const getRefOffset = (
child: React.RefObject<HTMLElement>,
grandParent: React.RefObject<HTMLElement>,
) => {
const offset = { top: 0, left: 0, width: 0, height: 0 };
if (!child.current || !grandParent.current) {
return offset;
}
let node = child.current;
// Traverse up the DOM tree until we reach the grandparent or run out of elements
while (node && node !== grandParent.current) {
offset.top += node.offsetTop;
offset.left += node.offsetLeft;
// Move to the offset parent (the nearest positioned ancestor)
node = node.offsetParent as HTMLElement;
}
// If we didn't reach the grandparent, return null
if (node !== grandParent.current) {
return offset;
}
offset.width = child.current.offsetWidth;
offset.height = child.current.offsetHeight;
return offset;
};
// 辅助函数来计算均值和最大值
const calculateLineMetrics = (
cardRect: Map<
string,
{
top: number;
left: number;
width: number;
height: number;
}
>,
prefix: string,
) => {
const filteredRects = Array.from(cardRect.entries())
.filter(([key]) => key.startsWith(prefix))
.map(([, rect]) => rect);
return {
x:
filteredRects.reduce(
(acc, rect) => acc + (rect.left + 0.5 * rect.width),
0,
) / filteredRects.length,
y2: Math.max(...filteredRects.map(rect => rect.top + rect.height), 0),
};
};
interface D3GraphProps {
// objProCards_: ObjectProcessCardProps[];
cardRefMap: Map<string, React.RefObject<HTMLElement>>;
relations: {
type: string;
stepTaskId: string;
stepCardName: string;
objectCardName: string;
}[];
focusingStepId: string;
forceRender: number;
}
const D3Graph: React.FC<D3GraphProps> = ({
cardRefMap,
relations,
forceRender,
focusingStepId,
}) => {
const [svgLineProps, setSvgLineProps] = useState<SvgLineProp[]>([]);
const [objectLine, setObjectLine] = useState({
x: 0,
y2: 0,
});
const [processLine, setProcessLine] = useState({
x: 0,
y2: 0,
});
const cardRect = new Map<
string,
{ top: number; left: number; width: number; height: number }
>();
React.useEffect(() => {
const svgLines_ = relations
.filter(({ stepCardName, objectCardName }) => {
return cardRefMap.has(stepCardName) && cardRefMap.has(objectCardName);
})
.map(({ type, stepTaskId, stepCardName, objectCardName }) => {
const stepRect = getRefOffset(
cardRefMap.get(stepCardName)!,
cardRefMap.get('root')!,
);
cardRect.set(stepCardName, stepRect);
const objectRect = getRefOffset(
cardRefMap.get(objectCardName)!,
cardRefMap.get('root')!,
);
cardRect.set(objectCardName, objectRect);
return {
key: `${type}.${stepCardName}.${objectCardName}`,
stepTaskId,
stepName: stepCardName,
objectName: objectCardName,
type,
x1: objectRect.left + objectRect.width,
y1: objectRect.top + 0.5 * objectRect.height,
x2: stepRect.left,
y2: stepRect.top + 0.5 * stepRect.height,
};
});
const objectMetrics = calculateLineMetrics(cardRect, 'object');
const processMetrics = calculateLineMetrics(cardRect, 'process');
const maxY2 = Math.max(objectMetrics.y2, processMetrics.y2);
setObjectLine({ ...objectMetrics, y2: maxY2 });
setProcessLine({ ...processMetrics, y2: maxY2 });
setSvgLineProps(svgLines_);
}, [forceRender, focusingStepId, relations]);
return (
// <Box
// sx={{
// width: '100%',
// height: '100%',
// position: 'absolute',
// zIndex: 1,
// }}
// >
<svg
style={{
position: 'absolute',
width: '100%',
height: objectLine.y2 + 50,
zIndex: 1,
userSelect: 'none',
}}
>
<marker
id="arrowhead"
markerWidth="4"
markerHeight="4"
refX="2"
refY="2"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L4,2 L0,4 z" fill="#E5E5E5" />
</marker>
<marker
id="starter"
markerWidth="4"
markerHeight="4"
refX="0"
refY="2"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L1,0 L1,4 L0,4 z" fill="#E5E5E5" />
</marker>
<g>
<text
x={objectLine.x}
y="15"
textAnchor="middle"
dominantBaseline="middle"
fill="#898989"
fontWeight="800"
>
Key Object
</text>
<line
x1={objectLine.x}
y1={30}
x2={objectLine.x}
y2={objectLine.y2 + 30}
stroke="#E5E5E5"
strokeWidth="8"
markerEnd="url(#arrowhead)"
markerStart="url(#starter)"
></line>
<text
x={processLine.x}
y="15"
textAnchor="middle"
dominantBaseline="middle"
fill="#898989"
fontWeight="800"
>
Process
</text>
<line
x1={processLine.x}
y1={30}
x2={processLine.x}
y2={processLine.y2 + 30}
stroke="#E5E5E5"
strokeWidth="8"
markerEnd="url(#arrowhead)"
markerStart="url(#starter)"
></line>
</g>
<g>
{svgLineProps.map(edgeValue => (
<line
key={edgeValue.key}
x1={edgeValue.x1}
y1={edgeValue.y1}
x2={edgeValue.x2}
y2={edgeValue.y2}
strokeWidth="5"
stroke={edgeValue.type === 'output' ? '#FFCA8C' : '#B9DCB0'}
strokeOpacity={
focusingStepId === edgeValue.stepTaskId ? '100%' : '20%'
}
></line>
))}
</g>
</svg>
// </Box>
);
};
export default D3Graph;

View File

@@ -0,0 +1,281 @@
// 已移除对d3的引用
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import D3Graph from './D3Graph';
import { ObjectCard, ProcessCard, EditObjectCard } from './Cards';
import { RectWatcher } from './RectWatcher';
import { globalStorage } from '@/storage';
export default observer(() => {
const { outlineRenderingStepTaskCards, focusingStepTaskId } = globalStorage;
const [renderCount, setRenderCount] = useState(0);
const [addObjectHover, setAddObjectHover] = useState(false);
const [isAddingObject, setIsAddingObject] = useState(false);
const [activeObjectAdd, setActiveObjectAdd] = useState('');
const [activeProcessIdAdd, setactiveProcessIdAdd] = useState('');
const handleProcessClick = (processName: string) => {
if (processName === focusingStepTaskId) {
globalStorage.setFocusingStepTaskId(undefined);
} else {
globalStorage.setFocusingStepTaskId(processName);
}
};
const finishAddInitialObject = (objectName: string) => {
setIsAddingObject(false);
globalStorage.addUserInput(objectName);
};
const addInitialObject = () => setIsAddingObject(true);
const handleObjectAdd = (objectName: string) =>
setActiveObjectAdd(activeObjectAdd === objectName ? '' : objectName);
const handleProcessAdd = (processName: string) =>
setactiveProcessIdAdd(
activeProcessIdAdd === processName ? '' : processName,
);
const cardRefMap = new Map<string, React.RefObject<HTMLElement>>();
const getCardRef = (cardId: string) => {
if (cardRefMap.has(cardId)) {
return cardRefMap.get(cardId);
} else {
cardRefMap.set(cardId, React.createRef<HTMLElement>());
return cardRefMap.get(cardId);
}
};
const handleEditContent = (stepTaskId: string, newContent: string) => {
globalStorage.setStepTaskContent(stepTaskId, newContent);
};
const WidthRatio = ['30%', '15%', '52.5%'];
const [cardRefMapReady, setCardRefMapReady] = React.useState(false);
React.useEffect(() => {
setCardRefMapReady(true);
setRenderCount(old => (old + 1) % 10);
}, []);
React.useEffect(() => {
if (activeObjectAdd !== '' && activeProcessIdAdd !== '') {
if (
outlineRenderingStepTaskCards
.filter(({ id }) => id === activeProcessIdAdd)[0]
.inputs.includes(activeObjectAdd)
) {
globalStorage.removeStepTaskInput(activeProcessIdAdd, activeObjectAdd);
} else {
globalStorage.addStepTaskInput(activeProcessIdAdd, activeObjectAdd);
}
// globalStorage.addStepTaskInput(activeProcessIdAdd, activeObjectAdd);
setActiveObjectAdd('');
setactiveProcessIdAdd('');
}
}, [activeObjectAdd, activeProcessIdAdd]);
return (
<Box
sx={{
position: 'relative',
height: '100%',
overflow: 'auto',
}}
ref={getCardRef('root')}
onScroll={() => {
globalStorage.renderLines({ delay: 0, repeat: 2 });
}}
>
<RectWatcher onRectChange={() => setRenderCount(old => (old + 1) % 10)}>
<Stack
sx={{
position: 'absolute',
zIndex: 2,
paddingTop: '30px',
width: '100%',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: WidthRatio[0],
flexDirection: 'column',
justifyContent: 'center',
}}
>
{isAddingObject ? (
<EditObjectCard finishEdit={finishAddInitialObject} />
) : (
<Box
onMouseOver={() => setAddObjectHover(true)}
onMouseOut={() => setAddObjectHover(false)}
onClick={() => addInitialObject()}
sx={{ display: 'inline-flex', paddingTop: '6px' }}
>
<IconButton
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
padding: '0px',
borderRadius: 0,
border: '1px dotted #333',
visibility: addObjectHover ? 'visible' : 'hidden',
}}
>
<AddIcon />
</IconButton>
</Box>
)}
</Box>
{globalStorage.userInputs.map(initialInput => (
<Box key={initialInput} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: `0 0 ${WidthRatio[0]}`,
}}
>
<ObjectCard
key={initialInput}
object={{
name: initialInput,
cardRef: getCardRef(`object.${initialInput}`),
}}
// isAddActive={initialInput === activeObjectAdd}
isAddActive={activeProcessIdAdd !== ''}
{...(activeProcessIdAdd !== ''
? {
addOrRemove: !outlineRenderingStepTaskCards
.filter(({ id }) => id === activeProcessIdAdd)[0]
.inputs.includes(initialInput),
}
: {})}
handleAddActive={handleObjectAdd}
/>
</Box>
</Box>
))}
{outlineRenderingStepTaskCards.map(
({ id, name, output, agentIcons, agents, content, ref }, index) => (
<Box
key={`stepTaskCard.${id}`}
sx={{ display: 'flex' }}
ref={ref}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: WidthRatio[0],
justifyContent: 'center',
flex: `0 0 ${WidthRatio[0]}`,
}}
>
{output && (
<ObjectCard
key={`objectCard.${output}`}
object={{
name: output,
cardRef: getCardRef(`object.${output}`),
}}
// isAddActive={output === activeObjectAdd}
isAddActive={
activeProcessIdAdd !== '' &&
outlineRenderingStepTaskCards
.map(({ id }) => id)
.indexOf(activeProcessIdAdd) > index
}
{...(activeProcessIdAdd !== ''
? {
addOrRemove: !outlineRenderingStepTaskCards
.filter(({ id }) => id === activeProcessIdAdd)[0]
.inputs.includes(output),
}
: {})}
handleAddActive={handleObjectAdd}
/>
)}
</Box>
<Box sx={{ flex: `0 0 ${WidthRatio[1]}` }} />
<Box
sx={{
// display: 'flex',
alignItems: 'center',
// width: WidthRatio[2],
justifyContent: 'center',
flex: `0 0 ${WidthRatio[2]}`,
}}
>
{name && (
<ProcessCard
process={{
id,
name,
icons: agentIcons,
agents,
content,
cardRef: getCardRef(`process.${name}`),
}}
handleProcessClick={handleProcessClick}
isFocusing={focusingStepTaskId === id}
isAddActive={id === activeProcessIdAdd}
handleAddActive={handleProcessAdd}
handleEditContent={handleEditContent}
/>
)}
</Box>
</Box>
),
)}
</Stack>
</RectWatcher>
{cardRefMapReady && (
<D3Graph
cardRefMap={cardRefMap}
focusingStepId={focusingStepTaskId || ''}
relations={outlineRenderingStepTaskCards
.map(({ id, name, inputs, output }) => {
const relations: {
type: string;
stepTaskId: string;
stepCardName: string;
objectCardName: string;
}[] = [];
inputs.forEach(input => {
relations.push({
type: 'input',
stepTaskId: id,
stepCardName: `process.${name}`,
objectCardName: `object.${input}`,
});
});
if (output) {
relations.push({
type: 'output',
stepTaskId: id,
stepCardName: `process.${name}`,
objectCardName: `object.${output}`,
});
}
return relations;
})
.flat()}
forceRender={renderCount}
/>
)}
</Box>
);
});

View File

@@ -0,0 +1,73 @@
import React from 'react';
import debounce from 'lodash/debounce';
// Define the props for the RectWatcher component
interface RectWatcherProps {
children: React.ReactNode;
onRectChange: (size: { height: number; width: number }) => void;
debounceDelay?: number; // Optional debounce delay with a default value
}
// Rewrite the RectWatcher component with TypeScript
export const RectWatcher = React.memo<RectWatcherProps>(
({
children,
onRectChange,
debounceDelay = 10, // Assuming the delay is meant to be in milliseconds
}) => {
const [lastSize, setLastSize] = React.useState<{
height: number;
width: number;
}>({
height: -1,
width: -1,
});
const ref = React.createRef<HTMLElement>(); // Assuming the ref is attached to a div element
const debouncedHeightChange = React.useMemo(
() =>
debounce((newSize: { height: number; width: number }) => {
if (
newSize.height !== lastSize.height ||
newSize.width !== lastSize.width
) {
onRectChange(newSize);
setLastSize(newSize);
}
}, debounceDelay),
[onRectChange, debounceDelay, lastSize],
);
React.useEffect(() => {
if (ref.current) {
const resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[]) => {
if (!entries.length) {
return;
}
const entry = entries[0];
debouncedHeightChange({
height: entry.contentRect.height,
width: entry.contentRect.width,
});
},
);
resizeObserver.observe(ref.current);
return () => resizeObserver.disconnect();
}
return () => undefined;
}, [debouncedHeightChange]);
// Ensure children is a single React element
if (
React.Children.count(children) !== 1 ||
!React.isValidElement(children)
) {
console.error('RectWatcher expects a single React element as children.');
return <></>;
}
// Clone the child element with the ref attached
return React.cloneElement(children, { ref } as any);
},
);

View File

@@ -0,0 +1,97 @@
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import OutlineView from './OutlineView';
import Title from '@/components/Title';
import LoadingMask from '@/components/LoadingMask';
import { globalStorage } from '@/storage';
import BranchIcon from '@/icons/BranchIcon';
export default observer(({ style = {} }: { style?: SxProps }) => {
const {
api: { planReady },
} = globalStorage;
return (
<Box
sx={{
position: 'relative',
background: '#FFF',
border: '3px solid #E1E1E1',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
...style,
}}
>
<Title title="Plan Outline" />
<Box
sx={{
position: 'relative',
height: 0,
flexGrow: 1,
overflowY: 'auto',
overflowX: 'hidden',
padding: '6px 12px',
}}
>
{planReady ? <OutlineView /> : <></>}
{globalStorage.api.planGenerating ? (
<LoadingMask
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
) : (
<></>
)}
</Box>
{planReady ? (
<Box
sx={{
cursor: 'pointer',
userSelect: 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '36px',
height: '32px',
bgcolor: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '10px 0 0 0',
zIndex: 100,
'&:hover': {
filter: 'brightness(0.9)',
},
}}
onClick={() => (globalStorage.planModificationWindow = true)}
>
<BranchIcon />
<Box
component="span"
sx={{
fontSize: '12px',
position: 'absolute',
right: '4px',
bottom: '2px',
color: 'white',
fontWeight: 800,
textAlign: 'right',
}}
>
{globalStorage.planManager.leaves.length}
</Box>
</Box>
) : (
<></>
)}
</Box>
);
});

View File

@@ -0,0 +1,20 @@
export interface ObjectProps {
name: string;
cardRef: any;
}
export interface ProcessProps {
id: string;
name: string;
icons: string[];
agents: string[];
cardRef: any;
content: string;
}
export interface ObjectProcessCardProps {
process: ProcessProps;
inputs: ObjectProps[];
outputs: ObjectProps[];
cardRef: any;
focusStep?: (stepId: string) => void;
focusing: boolean;
}