Add backend and frontend
This commit is contained in:
344
frontend/src/components/Outline/Cards.tsx
Normal file
344
frontend/src/components/Outline/Cards.tsx
Normal 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;
|
||||
241
frontend/src/components/Outline/D3Graph.tsx
Normal file
241
frontend/src/components/Outline/D3Graph.tsx
Normal 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;
|
||||
281
frontend/src/components/Outline/OutlineView.tsx
Normal file
281
frontend/src/components/Outline/OutlineView.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
73
frontend/src/components/Outline/RectWatcher.tsx
Normal file
73
frontend/src/components/Outline/RectWatcher.tsx
Normal 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);
|
||||
},
|
||||
);
|
||||
97
frontend/src/components/Outline/index.tsx
Normal file
97
frontend/src/components/Outline/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
20
frontend/src/components/Outline/interface.tsx
Normal file
20
frontend/src/components/Outline/interface.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user