setup(frontend): rename frontend-react

This commit is contained in:
Nex Zhu
2025-11-20 09:41:20 +08:00
parent e40cdd1dee
commit 4fa5504697
195 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import React from 'react';
import _ from 'lodash';
import { observer } from 'mobx-react-lite';
import { useTaskModificationContext } from './context';
import { globalStorage } from '@/storage';
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 TaskModificationSvg = observer(() => {
const {
forestPaths,
whoIsAddingBranch,
nodeRefMap,
svgForceRenderCounter,
containerRef,
} = useTaskModificationContext();
const { currentActionNodeSet } = globalStorage;
const [nodeRectMap, setNodeRectMap] = React.useState(
new Map<
string,
{
top: number;
left: number;
width: number;
height: number;
}
>(),
);
React.useEffect(() => {
if (containerRef) {
const nodeRectMap_ = new Map(
[...nodeRefMap].map(kv => {
return [kv[0], getRefOffset(kv[1], containerRef)];
}),
);
setNodeRectMap(nodeRectMap_);
}
}, [svgForceRenderCounter, whoIsAddingBranch]);
const renderLine = (startid: string, endid: string) => {
const startRect = nodeRectMap.get(startid);
const endRect = nodeRectMap.get(endid);
if (!startRect || !endRect) {
return <></>;
}
let isCurrent = false;
if (currentActionNodeSet.has(startid) && currentActionNodeSet.has(endid)) {
isCurrent = true;
}
if (startid === 'root' && currentActionNodeSet.has(endid)) {
isCurrent = true;
}
// console.log(`line.${startid}${startRect.left}.${endid}${endRect.left}`);
return (
<path
key={`line.${startid}${startRect.left}.${endid}${endRect.left}`}
d={`M ${startRect.left + 0.5 * startRect.width} ${
startRect.top + 0.5 * startRect.height
}
C ${startRect.left + startRect.width * 0.5} ${
endRect.top + 0.5 * endRect.height
},
${endRect.left} ${endRect.top + 0.5 * endRect.height},
${endRect.left} ${endRect.top + 0.5 * endRect.height}`}
fill="none"
stroke={isCurrent ? '#4a9c9e' : '#D9D9D9'}
strokeWidth="6"
></path>
);
};
const renderRoot = () => {
const rootRect = nodeRectMap.get('root');
if (rootRect && forestPaths.length > 0) {
return (
<circle
key={`root${rootRect.left}`}
cx={rootRect.left + 0.5 * rootRect.width}
cy={rootRect.top + 0.5 * rootRect.height}
r="10"
fill="#4a9c9e"
/>
);
}
return <></>;
};
return (
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
// backgroundColor: 'red',
width: _.max(
[...nodeRectMap.values()].map(rect => rect.left + rect.width),
),
height: _.max(
[...nodeRectMap.values()].map(rect => rect.top + rect.height),
),
}}
>
{forestPaths.map(pair => renderLine(pair[0], pair[1]))}
{whoIsAddingBranch && renderLine(whoIsAddingBranch, 'requirement')}
{renderRoot()}
</svg>
);
});
export default TaskModificationSvg;

View File

@@ -0,0 +1,210 @@
// TaskModificationContext.tsx
import React, {
ReactNode,
RefObject,
createContext,
useContext,
useState,
useEffect,
} from 'react';
import { IAgentActionTreeNode, globalStorage } from '@/storage';
interface TaskModificationContextProps {
forest: IAgentActionTreeNode | undefined;
setForest: (forest: IAgentActionTreeNode) => void;
forestPaths: [string, string][];
setForestPaths: (paths: [string, string][]) => void;
whoIsAddingBranch: string | undefined;
setWhoIsAddingBranch: (whoIsAddingBranch: string | undefined) => void;
updateWhoIsAddingBranch: (whoIsAddingBranch: string | undefined) => void;
containerRef: React.RefObject<HTMLElement> | undefined;
setContainerRef: (containerRef: React.RefObject<HTMLElement>) => void;
nodeRefMap: Map<string, RefObject<HTMLElement>>;
updateNodeRefMap: (key: string, value: RefObject<HTMLElement>) => void;
baseNodeSet: Set<string>;
setBaseNodeSet: (baseNodeSet: Set<string>) => void;
baseLeafNodeId: string | undefined;
setBaseLeafNodeId: (baseLeafNodeId: string | undefined) => void;
handleRequirementSubmit: (requirement: string, number: number) => void;
handleNodeClick: (nodeId: string) => void;
handleNodeHover: (nodeId: string | undefined) => void;
svgForceRenderCounter: number;
setSVGForceRenderCounter: (n: number) => void;
svgForceRender: () => void;
}
const TaskModificationContext = createContext<TaskModificationContextProps>(
{} as TaskModificationContextProps,
);
export const useTaskModificationContext = () =>
useContext(TaskModificationContext);
export const TaskModificationProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [forest, setForest] = useState<IAgentActionTreeNode>();
const [forestPaths, setForestPaths] = useState<[string, string][]>([]);
useEffect(() => {
if (forest) {
setForestPaths(_getFatherChildrenIdPairs(forest));
}
}, [forest]);
const [whoIsAddingBranch, setWhoIsAddingBranch] = useState<
string | undefined
>(undefined);
const updateWhoIsAddingBranch = (whoId: string | undefined) => {
if (whoId === whoIsAddingBranch) {
setWhoIsAddingBranch(undefined);
} else {
setWhoIsAddingBranch(whoId);
}
};
const [containerRef, setContainerRef] = React.useState<
React.RefObject<HTMLElement> | undefined
>(undefined);
const [baseNodeSet, setBaseNodeSet] = React.useState<Set<string>>(
new Set<string>(),
);
const [baseLeafNodeId, setBaseLeafNodeId] = React.useState<
string | undefined
>(undefined);
const [nodeRefMap] = React.useState(
new Map<string, RefObject<HTMLElement>>(),
);
const updateNodeRefMap = (key: string, value: RefObject<HTMLElement>) => {
nodeRefMap.set(key, value);
};
const handleRequirementSubmit = (requirement: string, number: number) => {
if (whoIsAddingBranch) {
const start =
whoIsAddingBranch === 'root' ? undefined : whoIsAddingBranch;
// globalStorage.newPlanBranch(start, requirement, number, baseLeafNodeId);
globalStorage.newActionBranch(
globalStorage.currentFocusingAgentSelection?.id,
start,
requirement,
number,
baseLeafNodeId,
);
setWhoIsAddingBranch(undefined);
setBaseNodeSet(new Set());
setBaseLeafNodeId(undefined);
}
};
const handleNodeClick = (nodeId: string) => {
const leafId = globalStorage.getFirstLeafAction(nodeId)?.node.id;
console.log(leafId);
if (leafId) {
if (whoIsAddingBranch) {
if (baseLeafNodeId === leafId) {
setBaseNodeSet(new Set());
setBaseLeafNodeId(undefined);
} else {
const pathNodeSet = new Set(globalStorage.getActionLeafPath(leafId));
if (
pathNodeSet.has(whoIsAddingBranch) ||
whoIsAddingBranch === 'root'
) {
setBaseLeafNodeId(leafId);
setBaseNodeSet(pathNodeSet);
}
}
} else {
globalStorage.setCurrentAgentActionBranch(leafId, undefined);
}
}
};
const [svgForceRenderCounter, setSVGForceRenderCounter] = useState(0);
const svgForceRender = () => {
setSVGForceRenderCounter((svgForceRenderCounter + 1) % 100);
};
const handleNodeHover = (nodeId: string | undefined) => {
if (!whoIsAddingBranch) {
if (nodeId) {
const leafNode = globalStorage.getFirstLeafAction(nodeId);
if (leafNode?.branchInfo) {
// const branchInfo =
// globalStorage.focusingStepTask.agentSelection.branches[leafNode.id];
// console.log(leafNode);
if (leafNode.branchInfo.base) {
const pathNodeSet = new Set(
globalStorage.getActionLeafPath(leafNode.branchInfo.base),
);
setBaseNodeSet(pathNodeSet);
}
}
} else {
setBaseNodeSet(new Set());
}
}
};
useEffect(() => {
setBaseNodeSet(new Set());
setBaseLeafNodeId(undefined);
}, [whoIsAddingBranch]);
useEffect(() => {
svgForceRender();
}, [forest, whoIsAddingBranch]);
return (
<TaskModificationContext.Provider
value={{
forest,
setForest,
forestPaths,
setForestPaths,
whoIsAddingBranch,
setWhoIsAddingBranch,
updateWhoIsAddingBranch,
containerRef,
setContainerRef,
nodeRefMap,
updateNodeRefMap,
baseNodeSet,
setBaseNodeSet,
baseLeafNodeId,
setBaseLeafNodeId,
handleRequirementSubmit,
handleNodeClick,
handleNodeHover,
svgForceRenderCounter,
setSVGForceRenderCounter,
svgForceRender,
}}
>
{children}
</TaskModificationContext.Provider>
);
};
// ----------------------------------------------------------------
const _getFatherChildrenIdPairs = (
node: IAgentActionTreeNode,
): [string, string][] => {
let pairs: [string, string][] = [];
// 对于每个子节点,添加 (父ID, 子ID) 对,并递归调用函数
node.children.forEach(child => {
pairs.push([node.id, child.id]);
pairs = pairs.concat(_getFatherChildrenIdPairs(child));
});
return pairs;
};

View File

@@ -0,0 +1,75 @@
export const fakeTaskTree = {
id: 'root', // 根节点
children: [
{
id: 'child1', // 子节点
children: [
// 子节点的子节点数组
{
id: 'child1_1', // 子节点的子节点
children: [], // 叶节点,没有子节点
},
{
id: 'child1_2', // 另一个子节点的子节点
children: [], // 叶节点,没有子节点
},
],
},
{
id: 'child2', // 另一个子节点
children: [
{
id: 'child2_1',
children: [],
},
],
},
],
};
export const fakeNodeData = new Map([
[
'child1',
{
agentIcon: 'John_Lin',
style: { backgroundColor: '#b4d0d9', borderColor: '#b4d099' },
isLeaf: true,
requirement: 'hhhh',
},
],
[
'child2',
{
agentIcon: 'Mei_Lin',
style: { backgroundColor: '#b4d0d9', borderColor: '#b4d099' },
isLeaf: false,
},
],
[
'child1_1',
{
agentIcon: 'Tamara_Taylor',
style: { backgroundColor: '#b4d0d9', borderColor: '#b4d099' },
isLeaf: true,
requirement: 'requirement',
},
],
[
'child1_2',
{
agentIcon: 'Sam_Moore',
style: { backgroundColor: '#b4d0d9', borderColor: '#b4d099' },
isLeaf: true,
requirement: 'requirement',
},
],
[
'child2_1',
{
agentIcon: 'Sam_Moore',
style: { backgroundColor: '#b4d0d9', borderColor: '#b4d099' },
isLeaf: true,
requirement: 'requirement',
},
],
]);

View File

@@ -0,0 +1,457 @@
/* eslint-disable max-lines */
import React from 'react';
import { SxProps, CircularProgress } from '@mui/material';
import Box from '@mui/material/Box';
import AddIcon from '@mui/icons-material/Add';
import RemoveIcon from '@mui/icons-material/Remove';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import InputBase from '@mui/material/InputBase';
// import SendIcon from '@mui/icons-material/Send';
import { observer } from 'mobx-react-lite';
import AgentIcon from '../AgentIcon';
import TaskModificationSvg from './TaskModificationSvg';
import {
TaskModificationProvider,
useTaskModificationContext,
} from './context';
import { IAgentActionTreeNode, globalStorage } from '@/storage';
import SendIcon from '@/icons/SendIcon';
import { ActionType } from '@/storage/plan';
const RequirementNoteNode: React.FC<{
data: {
text: string;
};
style?: SxProps;
}> = ({ data, style }) => {
return (
<Box sx={{ ...style }}>
<Box sx={{ color: '#ACACAC', userSelect: 'none', minWidth: '250px' }}>
{data.text}
</Box>
</Box>
);
};
const RequirementInputNode: React.FC<{
style?: SxProps;
}> = ({ style }) => {
const { handleRequirementSubmit, updateNodeRefMap } =
useTaskModificationContext();
const [number, setNumber] = React.useState(1);
const myRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
updateNodeRefMap('requirement', myRef);
}, []);
// const handleWheel = (event: any) => {
// // 向上滚动时减少数字,向下滚动时增加数字
// if (event.deltaY < 0) {
// setNumber(prevNumber => prevNumber + 1);
// } else {
// setNumber(prevNumber => Math.max(1, prevNumber - 1));
// }
// };
const handleSubmit = () => {
handleRequirementSubmit(textValue, number);
};
const [textValue, setTextValue] = React.useState('');
return (
<Box
sx={{
...style,
}}
ref={myRef}
>
<Paper
sx={{
p: '0px',
display: 'flex',
alignItems: 'center',
width: 250,
backgroundColor: 'white',
boxShadow: 'none',
border: '2px solid #b0b0b0',
borderRadius: '8px',
}}
>
<InputBase
sx={{ marginLeft: 1, flex: 1, backgroundColor: 'white' }}
placeholder="Add Branch"
onChange={e => {
setTextValue(e.target.value);
}}
onKeyDown={e => {
if (e.key === 'ArrowUp') {
setNumber(prevNumber => prevNumber + 1);
} else if (e.key === 'ArrowDown') {
setNumber(prevNumber => Math.max(1, prevNumber - 1));
}
}}
/>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
height: '100%',
}}
// onWheel={handleWheel}
>
<IconButton
type="submit"
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
padding: '0px',
}}
onClick={handleSubmit}
>
<SendIcon color="#b6b6b6" />
</IconButton>
<Box
sx={{
height: 'min-content',
paddingLeft: '4px',
paddingRight: '4px',
cursor: 'pointer', // 提示用户可以与之互动
}}
>
<Box component="span" sx={{ fontSize: '0.5em' }}>
X
</Box>
<Box component="span" sx={{ fontSize: '1em' }}>
{number}
</Box>
</Box>
</Box>
</Paper>
</Box>
);
};
const RootNode: React.FC<{
style?: SxProps;
}> = ({ style }) => {
const { updateNodeRefMap, updateWhoIsAddingBranch, whoIsAddingBranch } =
useTaskModificationContext();
const [onHover, setOnHover] = React.useState(false);
const myRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
updateNodeRefMap('root', myRef);
}, []);
// React.useEffect(() => {
// console.log(onHover, whoIsAddingBranch);
// }, [onHover, whoIsAddingBranch]);
return (
<Box
onMouseOver={() => setOnHover(true)}
onMouseOut={() => setOnHover(false)}
sx={{
...style,
flexDirection: 'column',
position: 'relative',
backgroundColor: 'red',
}}
ref={myRef}
>
<IconButton
sx={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translateX(-50%) ',
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
opacity: onHover || whoIsAddingBranch === 'root' ? '1' : '0',
// visibility: 'visible',
// visibility:
// onHover || whoIsAddingBranch === 'root' ? 'visible' : 'hidden',
padding: '0px',
borderRadius: '50%',
border: '1px dotted #333',
height: '16px',
width: '16px',
marginTop: '-6px',
'& .MuiSvgIcon-root': {
fontSize: '14px',
},
}}
onClick={() => updateWhoIsAddingBranch('root')}
>
{whoIsAddingBranch !== 'root' ? <AddIcon /> : <RemoveIcon />}
</IconButton>
</Box>
);
};
const Node: React.FC<{
data: {
id: string;
agentIcon: string;
agent: string;
action: { type: ActionType; description: string; style: SxProps };
focusing: boolean;
};
style?: SxProps;
}> = ({ data, style }) => {
const {
updateNodeRefMap,
whoIsAddingBranch,
updateWhoIsAddingBranch,
handleNodeClick,
handleNodeHover,
baseNodeSet,
} = useTaskModificationContext();
const myRef = React.useRef<HTMLElement>(null);
const [onHover, setOnHover] = React.useState(false);
React.useEffect(() => {
updateNodeRefMap(data.id, myRef);
}, []);
return (
// <RectWatcher onRectChange={onRectChange}>
<Box
onMouseOver={() => {
setOnHover(true);
handleNodeHover(data.id);
}}
onMouseOut={() => {
setOnHover(false);
handleNodeHover(undefined);
}}
sx={{
...style,
flexDirection: 'column',
// backgroundColor: '#d9d9d9',
borderStyle: 'solid',
borderRadius: '50%',
height: data.focusing ? '45px' : '30px',
width: data.focusing ? '45px' : '30px',
position: 'relative',
borderWidth: data.focusing ? '3px' : '2px',
boxShadow: baseNodeSet.has(data.id) ? '0 0 10px 5px #43b2aa' : '',
}}
ref={myRef}
onClick={() => handleNodeClick(data.id)}
>
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<AgentIcon
name={data.agentIcon}
style={{
width: data.focusing ? '36px' : '28px',
height: 'auto',
margin: '0px',
userSelect: 'none',
}}
tooltipInfo={{
...globalStorage.agentMap.get(data.agent)!,
action: data.action,
}}
/>
</Box>
<Box
sx={{
position: 'absolute',
left: '50%',
top: '100%',
transform: 'translateX(-50%) translateY(-50%)',
}}
>
<IconButton
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
visibility:
onHover || whoIsAddingBranch === data.id ? 'visible' : 'hidden',
padding: '0px',
borderRadius: '50%',
border: '1px dotted #333',
height: '16px',
width: '16px',
marginTop: '-6px',
'& .MuiSvgIcon-root': {
fontSize: '14px',
},
}}
onClick={() => {
updateWhoIsAddingBranch(data.id);
}}
>
{whoIsAddingBranch !== data.id ? <AddIcon /> : <RemoveIcon />}
</IconButton>
</Box>
</Box>
// </RectWatcher>
);
};
const Tree: React.FC<{
tree: IAgentActionTreeNode;
}> = ({ tree }) => {
const { whoIsAddingBranch } = useTaskModificationContext();
const generalNodeStyle = {
height: '30px',
// padding: '4px 8px',
display: 'flex',
alignItems: 'center',
margin: '8px 20px 8px 8px',
};
const focusNodeStyle = {
height: '45px',
// padding: '4px 8px',
display: 'flex',
alignItems: 'center',
margin: '8px 20px 8px 8px',
};
return (
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<>
{tree.id !== 'root' && (
<Node
data={{
id: tree.id,
agentIcon: tree.icon,
agent: tree.agent,
action: { ...tree.action, style: tree.style },
focusing: tree.focusing,
}}
style={{
justifyContent: 'center',
alignSelf: 'flex-start',
cursor: 'pointer',
aspectRatio: '1 / 1',
...(tree.focusing ? focusNodeStyle : generalNodeStyle),
...tree.style,
}}
/>
)}
{tree.id === 'root' && (
<RootNode
style={{
justifyContent: 'center',
...(tree.children[0] && tree.children[0].focusing
? focusNodeStyle
: generalNodeStyle),
}}
/>
)}
</>
<>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
{tree.id !== 'root' && tree.leaf && (
<RequirementNoteNode
data={{ text: tree.requirement || '' }}
style={{
...(tree.focusing ? focusNodeStyle : generalNodeStyle),
}}
/>
)}
{tree.children.map(childTree => (
<Tree key={`taskTree-${childTree.id}`} tree={childTree} />
))}
{tree.id === whoIsAddingBranch && (
<RequirementInputNode
style={{ height: '50px', display: 'flex', alignItems: 'center' }}
/>
)}
</Box>
</>
</Box>
);
};
interface ITaskModificationProps {
style?: SxProps;
resizeSignal?: number;
}
const TheViewContent = observer(
({ style, resizeSignal }: ITaskModificationProps) => {
const { renderingActionForest } = globalStorage;
const { forest, setForest, setContainerRef, svgForceRender } =
useTaskModificationContext();
const myRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
setForest({
agent: '',
icon: '',
children: renderingActionForest,
id: 'root',
leaf: false,
style: {},
action: { type: ActionType.Propose, description: '' },
focusing: true,
});
}, [renderingActionForest]);
React.useEffect(() => {
setContainerRef(myRef);
}, []);
React.useEffect(() => {
svgForceRender();
}, [resizeSignal]);
return (
<Box
sx={{
backgroundColor: 'white',
position: 'relative',
overflowY: 'auto',
overflowX: 'auto',
padding: '4px 6px',
userSelect: 'none',
...style,
}}
ref={myRef}
>
{myRef.current && <TaskModificationSvg />}
{forest && <Tree tree={forest} />}
</Box>
);
},
);
/* eslint-enable max-lines */
const TaskModification: React.FC<ITaskModificationProps> = observer(
({ style, resizeSignal }) => {
return (
<TaskModificationProvider>
<TheViewContent style={style} resizeSignal={resizeSignal} />
{globalStorage.api.agentActionTreeGenerating && (
<Box
sx={{
position: 'absolute',
bottom: '10px',
right: '20px',
zIndex: 999,
}}
>
<CircularProgress size={40} />
</Box>
)}
</TaskModificationProvider>
);
},
);
export default TaskModification;