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,54 @@
const agentList = [
'Alice',
'Morgan',
'Riley',
'Charlie',
'Bob',
'Jordan',
'Sam',
'Quinn',
'Parker',
'Skyler',
'Reagan',
'Pat',
'Leslie',
'Dana',
'Casey',
'Terry',
'Shawn',
'Kim',
'Alexis',
'Taylor',
'Bailey',
'Drew',
'Cameron',
'Sage',
'Peyton',
];
const aspectList = [
'Creative Thinking',
'Emotional Intelligence',
'Philosophical Reasoning',
];
export const fakeAgentScoreMap = new Map(
aspectList.map(aspect => [
aspect,
new Map(
agentList.map(agent => [
agent,
{
Reason: `reason for ${agent} in ${aspect}`,
Score: Math.floor(Math.random() * 5) + 1,
},
]),
),
]),
);
export const fakeAgentSelections = new Map([
['1', { agents: ['Alice', 'Morgan'] }],
['2', { agents: ['Alice', 'Morgan', 'Riley'] }],
['3', { agents: ['Alice', 'Bob', 'Riley'] }],
]);
export const fakeCurrentAgentSelection = '1';

View File

@@ -0,0 +1,561 @@
/* eslint-disable max-lines */
import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { CircularProgress, SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import { styled } from '@mui/material/styles';
import Paper from '@mui/material/Paper';
import InputBase from '@mui/material/InputBase';
import IconButton from '@mui/material/IconButton';
// import SendIcon from '@mui/icons-material/Send';
import _ from 'lodash';
// import { autorun } from 'mobx';
// import {
// fakeAgentScoreMap,
// fakeAgentSelections,
// fakeCurrentAgentSelection,
// } from './data/fakeAgentAssignment';
import CheckIcon from '@/icons/CheckIcon';
import AgentIcon from '@/components/AgentIcon';
import { globalStorage } from '@/storage';
import SendIcon from '@/icons/SendIcon';
const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: '#e7e7e7',
color: 'rgba(0, 0, 0, 0.87)',
width: 'fit-content',
fontSize: theme.typography.pxToRem(12),
border: '1px solid #dadde9',
},
}));
const getHeatColor = (value: number) => {
return `rgba(74, 156, 158,${value / 5})`;
};
const AgentScoreCell: React.FC<{
data: { score: number; reason: string };
}> = ({ data }) => {
return (
<HtmlTooltip
title={
<>
<Box>Score Reason:</Box>
<Box>{data.reason}</Box>
</>
}
followCursor
placement="right-start"
>
<Box
sx={{
width: '35px',
height: '35px',
backgroundColor: getHeatColor(data.score),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
fontWeight: '200',
fontStyle: 'italic',
}}
>
{data.score}
</Box>
</HtmlTooltip>
);
};
const AspectCell: React.FC<{
key?: string;
aspect?: string;
style?: SxProps;
isSelected?: boolean;
handleSelected?: (aspect: string) => void;
}> = ({ key, aspect, style, isSelected, handleSelected }) => {
const mystyle: SxProps = {
width: '150px',
height: '35px',
position: 'sticky', // 使得这个Box成为sticky元素
right: -1, // 0距离左侧这将确保它卡在左侧
backgroundColor: '#ffffff', // 防止滚动时格子被内容覆盖
zIndex: 1, // 确保它在其他内容上方
display: 'flex',
alignItems: 'center',
paddingLeft: '8px',
fontSize: '14px',
lineHeight: '1',
borderBottom: '2px solid #ffffff',
...style,
};
if (!aspect) {
return <Box sx={mystyle} />;
}
return (
<Box
key={key}
sx={{
...mystyle,
cursor: 'pointer',
color: isSelected ? 'black' : '#ACACAC',
textDecoration: isSelected ? 'underline' : 'none',
textDecorationColor: '#43b2aa',
textDecorationThickness: '1.5px',
fontWeight: '300',
fontSize: '14px',
fontStyle: 'italic',
}}
onClick={() => {
if (handleSelected) {
handleSelected(aspect);
}
}}
>
{aspect || ''}
</Box>
);
};
const EmotionInput: React.FC<{
inputCallback: (arg0: string) => void;
}> = ({ inputCallback }) => {
const [inputValue, setInputValue] = useState('');
const inputRef = React.useRef();
const handleInputChange = (event: any) => {
setInputValue(event.target.value);
};
const handleButtonClick = () => {
inputCallback(inputValue);
setInputValue('');
};
return (
<Paper
sx={{
p: '0px',
display: 'flex',
alignItems: 'center',
width: 400,
backgroundColor: 'rgb(0,0,0,0)',
boxShadow: 'none',
border: '2px solid #b0b0b0',
borderRadius: '8px',
}}
>
<InputBase
sx={{ marginLeft: 1, flex: 1 }}
placeholder="Aspect"
value={inputValue}
onChange={handleInputChange}
ref={inputRef}
/>
<IconButton
type="submit"
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
padding: '0px 6px',
height: 'min-content',
aspectRatio: '1 / 1',
}}
onClick={handleButtonClick}
>
<SendIcon color="#b6b6b6" />
</IconButton>
</Paper>
);
};
const findSameSelectionId = (
a: Record<
string,
{
id: string;
agents: string[];
}
>,
b: Set<string>,
) => {
const sortedB = Array.from(b).slice().sort(); // 对 b 进行排序
const bString = sortedB.join(',');
const akeys = Object.keys(a);
for (const akey of akeys) {
const sortedA = a[akey].agents.slice().sort(); // 对 a 中的每个数组进行排序
if (sortedA.join(',') === bString) {
return akey; // 找到相同数组则返回索引
}
}
return undefined; // 未找到相同数组
};
interface IPlanModification {
style?: SxProps;
}
export default observer(({ style = {} }: IPlanModification) => {
// console.log(prop);
const {
agentMap,
renderingAgentSelections,
api: { agentsReady },
} = globalStorage;
// autorun(() => {
// console.log(renderingAgentSelections);
// });
const [agentSelections, setAgentSelections] = React.useState<
Record<
string,
{
id: string;
agents: string[];
}
>
>({});
const [currentAgentSelection, setCurrentSelection] = React.useState<
string | undefined
>();
const [heatdata, setHeatdata] = React.useState<
Record<
string,
Record<
string,
{
score: number;
reason: string;
}
>
>
>({});
const [aspectSelectedSet, setAspectSelectedSet] = React.useState(
new Set<string>(),
);
useEffect(() => {
if (renderingAgentSelections.current) {
setAgentSelections(renderingAgentSelections.selections);
setHeatdata(renderingAgentSelections.heatdata);
setCurrentSelection(renderingAgentSelections.current);
setAgentSelectedSet(
new Set(
renderingAgentSelections.selections[
renderingAgentSelections.current
].agents,
),
);
}
}, [renderingAgentSelections]);
const handleAspectSelected = (aspect: string) => {
const newSet = new Set(aspectSelectedSet);
if (newSet.has(aspect)) {
newSet.delete(aspect);
} else {
newSet.add(aspect);
}
setAspectSelectedSet(newSet);
};
const [agentSelectedSet, setAgentSelectedSet] = React.useState(
new Set<string>(),
);
const handleAgentSelected = (agent: string) => {
const newSet = new Set(agentSelectedSet);
if (newSet.has(agent)) {
newSet.delete(agent);
} else {
newSet.add(agent);
}
setAgentSelectedSet(newSet);
};
const [agentKeyList, setAgentKeyList] = useState<string[]>([]);
useEffect(() => {
// 计算平均分的函数
function calculateAverageScore(agent: string) {
const aspects = aspectSelectedSet.size
? Array.from(aspectSelectedSet)
: Object.keys(heatdata);
const meanScore = _.mean(
aspects.map(aspect => heatdata[aspect]?.[agent]?.score ?? 0),
);
return meanScore;
}
// 对agentMap.keys()进行排序
const newAgentKeyList = Array.from(agentMap.keys()).sort((a, b) => {
const isSelectedA = agentSelectedSet.has(a);
const isSelectedB = agentSelectedSet.has(b);
if (isSelectedA && !isSelectedB) {
return -1;
} else if (!isSelectedA && isSelectedB) {
return 1;
} else {
const averageScoreA = calculateAverageScore(a);
const averageScoreB = calculateAverageScore(b);
// 降序排序(平均分高的在前)
return averageScoreB - averageScoreA;
}
});
setAgentKeyList(newAgentKeyList);
}, [agentMap, heatdata, aspectSelectedSet, agentSelectedSet]);
if (!agentsReady) {
return <></>;
}
return (
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
overflowY: 'hidden',
}}
>
{globalStorage.api.agentAspectScoresGenerating && (
<Box
sx={{
position: 'absolute',
bottom: '10px',
right: '20px',
zIndex: 999,
}}
>
<CircularProgress size={40} />
</Box>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'row',
...style,
}}
>
{/* assignments */}
<Box
sx={{
width: '20%',
backgroundColor: 'white',
padding: '8px 6px',
overflowY: 'auto',
}}
>
<Box sx={{ marginBottom: '6px', fontWeight: '600' }}>Assignment</Box>
<Box>
{Object.keys(agentSelections).map(selectionId => (
<Box
key={`agentSelectionSet.${selectionId}`}
sx={{
border: (() => {
if (selectionId === currentAgentSelection) {
return '2px solid #508a87';
}
return '2px solid #afafaf';
})(),
borderRadius: '10px',
margin: '4px 0px',
padding: '4px 0px 4px 0px',
backgroundColor: '#f6f6f6',
cursor: 'pointer',
display: 'flex',
justifyContent: 'center', // 添加这一行
alignItems: 'center', // 添加这一行
flexWrap: 'wrap',
}}
onClick={() => {
globalStorage.setCurrentAgentSelection(selectionId);
}}
>
{agentSelections[selectionId].agents.map(agentName => (
<AgentIcon
key={`${selectionId}.${agentName}`}
name={agentMap.get(agentName)?.icon}
style={{
width: 'auto',
height: '30px',
marginRight: '-5px',
userSelect: 'none',
margin: '0px 0px',
}}
tooltipInfo={agentMap.get(agentName)}
/>
))}
</Box>
))}
</Box>
</Box>
{/* comparison */}
<Box
sx={{
width: '80%',
backgroundColor: 'white',
marginLeft: '4px',
padding: '8px 6px',
overflowY: 'auto',
}}
>
<Box sx={{ marginBottom: '4px', fontWeight: '600' }}>Comparison</Box>
<Box
sx={{
overflowX: 'auto',
width: '-webkit-fill-available',
}}
>
<Box
sx={{
display: 'grid',
gridTemplateColumns: `repeat(${agentMap.size}, 35px) max-content`,
alignItems: 'center',
gridAutoFlow: 'column',
gridTemplateRows: `35px repeat(${
Object.keys(heatdata).length
}, 35px)`, // 第一列设置为max-content其余列为1fr
}}
>
{agentSelectedSet.size > 0 && (
<Box
sx={{
gridColumn: `1 / ${1 + agentSelectedSet.size}`,
gridRow: `1`,
border: '2px dashed #508a87',
borderRadius: '10px 10px 0px 10px',
boxSizing: 'border-box',
width: '100%',
height: '100%',
position: 'relative',
pointerEvents: 'none',
}}
>
<div
style={{
position: 'absolute',
right: '-3px',
bottom: '-3px',
height: '50%',
// width: '1.35px',
aspectRatio: '1 / 1',
backgroundColor: '#508a87',
borderRadius: '10px 0px 0px 0px',
pointerEvents: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => {
const findSelectionId = findSameSelectionId(
agentSelections,
agentSelectedSet,
);
if (findSelectionId) {
globalStorage.setCurrentAgentSelection(findSelectionId);
} else {
globalStorage.addAgentSelection(
Array.from(agentSelectedSet),
);
}
}}
>
<CheckIcon color="white" size="80%" />
</div>
</Box>
)}
{agentKeyList.map((agentKey, agentIndex) => (
<Box
key={agentKey}
sx={{
gridColumn: `${agentIndex + 1}`,
gridRow: `1 / ${2 + Object.keys(heatdata).length}`,
}}
>
<Box
key={agentKey}
onClick={() => {
handleAgentSelected(agentKey);
}}
style={{
display: 'grid',
placeItems: 'center',
gridColumn: `${agentIndex + 1} / ${agentIndex + 2}`,
gridRow: '1 / 2',
height: '100%',
width: '100%',
padding: '0px 0px',
cursor: 'pointer',
}}
>
<AgentIcon
key={`agentIcon.${agentKey}`}
name={agentMap.get(agentKey)!.icon}
style={{
width: 'auto',
height: '80%',
userSelect: 'none',
margin: '0px',
}}
tooltipInfo={agentMap.get(agentKey)}
/>
</Box>
{Object.keys(heatdata).map(aspect => {
return (
<AgentScoreCell
key={`${aspect}.${agentKey}`}
data={
heatdata[aspect][agentKey] || {
reason: '',
score: 0,
}
}
/>
);
})}
</Box>
))}
<AspectCell style={{ height: '100%' }} />
{Object.keys(heatdata).map(aspect => (
<AspectCell
key={`aspect.${aspect}`}
aspect={aspect}
isSelected={aspectSelectedSet.has(aspect)}
handleSelected={handleAspectSelected}
style={{ height: '100%', fontSize: '12px' }}
/>
))}
</Box>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
height: '35px',
alignItems: 'center',
marginTop: '8px',
}}
>
<EmotionInput
inputCallback={(arg0: string) => {
globalStorage.addAgentSelectionAspects(arg0);
}}
/>
</Box>
</Box>
</Box>
</Box>
);
// return <Box>hhh</Box>;
});
/* eslint-enable max-lines */

View File

@@ -0,0 +1,165 @@
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import DutyItem from './DutyItem';
import AgentIcon from '@/components/AgentIcon';
import { IAgentCard, globalStorage } from '@/storage';
import ChangeAgentIcon from '@/icons/ChangeAgentIcon';
export interface IAgentCardProps {
agent: IAgentCard;
style?: SxProps;
}
export default observer(({ agent, style = {} }: IAgentCardProps) => (
<Box sx={{ position: 'relative' }}>
<Box
sx={{
width: 'calc(100% - 16px)',
borderRadius: '8px',
display: 'flex',
backgroundColor: '#F6F6F6',
border: '2px solid #E5E5E5',
padding: '6px',
flexDirection: 'column',
flexShrink: 0,
...style,
}}
ref={agent.ref}
>
<Box
sx={{
display: 'flex',
alignItems: 'start',
justifyContent: 'space-between',
padding: '0px 10px 4px 12px',
userSelect: 'none',
marginBottom: agent.actions.length > 0 ? '4px' : undefined,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'start',
width: 'calc(100% - 55px)',
paddingTop: '8px',
}}
>
<Box
sx={{
fontSize: '18px',
fontWeight: 800,
lineHeight: '20px',
}}
>
{agent.name}
</Box>
<Box
sx={{
margin: '4px 0',
fontSize: '14px',
lineHeight: '16px',
fontWeight: 600,
color: '#707070',
userSelect: 'none',
}}
>
{agent.profile}
</Box>
</Box>
<Box
sx={{
flexShrink: 0,
width: '55px',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<AgentIcon
name={agent.icon}
style={{
width: '80%',
height: 'auto',
margin: '0px',
}}
/>
</Box>
</Box>
{agent.actions.length > 0 ? (
<Box
sx={{
padding: '4px 8px',
background: '#E4F0F0',
borderRadius: '12px',
border: '3px solid #A9C6C5',
}}
>
<Box
sx={{
fontSize: '16px',
fontWeight: 800,
color: '#4F8A87',
marginLeft: '2px',
}}
>
Current Duty:
</Box>
<Box>
{agent.actions.map((action, index) => (
<DutyItem
// eslint-disable-next-line react/no-array-index-key
key={`${agent.name}.${action.type}.${index}`}
action={action}
/>
))}
</Box>
</Box>
) : (
<></>
)}
</Box>
{agent.lastOfUsed ? (
<Box
sx={{
cursor: 'pointer',
userSelect: 'none',
position: 'absolute',
right: '-4px',
bottom: 0,
width: '32px',
height: '32px',
bgcolor: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '10px 0 0 0',
'&:hover': {
filter: 'brightness(0.9)',
},
}}
onClick={() => (globalStorage.agentAssigmentWindow = true)}
>
<ChangeAgentIcon />
<Box
component="span"
sx={{
fontSize: '12px',
position: 'absolute',
right: '1px',
bottom: 0,
color: 'white',
fontWeight: 800,
textAlign: 'right',
}}
>
{globalStorage.focusingStepTask?.agentSelectionIds?.length ?? 0}
</Box>
</Box>
) : (
<></>
)}
</Box>
));

View File

@@ -0,0 +1,123 @@
import React from 'react';
import { Divider, SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import { IAgentAction, globalStorage } from '@/storage';
export interface IDutyItem {
action: IAgentAction;
style?: SxProps;
}
export default React.memo<IDutyItem>(({ action, style = {} }) => {
const [expand, setExpand] = React.useState(false);
const _style = {
...action.style,
...style,
};
React.useEffect(() => {
globalStorage.renderLines({
repeat: 3,
delay: 0,
interval: 20,
});
}, [expand]);
if (!expand) {
return (
<Box
sx={{
display: 'inline-flex',
fontSize: '14px',
alignItems: 'center',
borderRadius: '10px',
padding: '0 4px 0 8px',
marginRight: '4px',
fontWeight: 600,
border: '2px solid #0003',
..._style,
}}
>
{action.type}
<Box
onClick={() => setExpand(true)}
sx={{
cursor: 'pointer',
userSelect: 'none',
height: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#0002',
borderRadius: '8px',
marginLeft: '8px',
padding: '0 4px',
'&:hover': {
background: '#0003',
},
}}
>
<MoreHorizIcon sx={{ fontSize: '16px' }} />
</Box>
</Box>
);
}
return (
<Box
sx={{
position: 'relative',
display: 'flex',
fontSize: '14px',
flexDirection: 'column',
borderRadius: '10px',
border: '2px solid #0003',
padding: '4px 4px 4px 8px',
margin: '4px 0',
..._style,
}}
>
<Box
sx={{
fontWeight: 600,
paddingBottom: '4px',
marginLeft: '2px',
}}
>
{action.type}
</Box>
<Divider
sx={{
margin: '1px 0px',
borderBottom: '2px dashed', // 设置为虚线
borderColor: (action.style as any).borderColor ?? '#888888',
}}
/>
<Box sx={{ margin: '4px 2px', color: '#0009', fontWeight: 400 }}>
{action.description}
</Box>
<Box
onClick={() => setExpand(false)}
sx={{
position: 'absolute',
right: '6px',
bottom: '6px',
cursor: 'pointer',
userSelect: 'none',
height: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#0002',
borderRadius: '12px',
marginLeft: '4px',
padding: '0 4px',
'&:hover': {
background: '#0003',
},
}}
>
<UnfoldLessIcon sx={{ fontSize: '16px', transform: 'rotate(90deg)' }} />
</Box>
</Box>
);
});

View File

@@ -0,0 +1,163 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton';
import PersonAddAlt1Icon from '@mui/icons-material/PersonAddAlt1';
import AgentCard from './AgentCard';
import { globalStorage } from '@/storage';
import { IconMap } from '@/components/AgentIcon';
import LoadingMask from '@/components/LoadingMask';
import Title from '@/components/Title';
export interface IAgentBoardProps {
style?: SxProps;
onAddAgent?: () => void;
}
export default observer(({ style = {} }: IAgentBoardProps) => {
const {
agentCards,
api: { fetchingAgents },
} = globalStorage;
const onFileChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = e => {
if (!e.target?.result) {
return;
}
try {
const json = JSON.parse(e.target.result?.toString?.() ?? '{}');
// 检查json是否满足{Name:string,Icon:string,Profile:string}[]
if (Array.isArray(json)) {
const isValid = json.every(
item =>
typeof item.Name === 'string' &&
typeof item.Icon === 'string' &&
typeof item.Profile === 'string',
);
if (isValid) {
globalStorage.setAgents(
json.map(agent => ({
name: agent.Name,
icon: IconMap[agent.Icon.replace(/\.png$/, '')],
profile: agent.Profile,
})),
);
} else {
console.error('Invalid JSON format');
}
} else {
console.error('JSON is not an array');
}
} catch (e) {
console.error(e);
}
};
reader.readAsText(file);
event.target.value = '';
event.target.files = null;
}
},
[],
);
return (
<Box
sx={{
background: '#FFF',
border: '3px solid #E1E1E1',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
...style,
}}
>
<Box
sx={{
fontWeight: 600,
fontSize: '18px',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
position: 'relative',
'& > input': { display: 'none' },
}}
>
<Title title="Agent Board" />
<Box
sx={{
flexGrow: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
}}
>
<IconButton
size="small"
component="label"
disabled={fetchingAgents}
sx={{
color: 'primary.main',
'&:hover': {
color: 'primary.dark',
},
}}
>
<PersonAddAlt1Icon />
<input
type="file"
accept=".json"
onChange={onFileChange}
style={{
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
}}
/>
</IconButton>
</Box>
</Box>
<Stack
spacing={1}
sx={{
position: 'relative',
padding: '6px 12px',
// paddingBottom: '44px',
borderRadius: '10px',
height: 0,
flexGrow: 1,
overflowY: 'auto',
}}
onScroll={() => globalStorage.renderLines({ delay: 0, repeat: 2 })}
>
{agentCards.map(agent => {
return <AgentCard key={agent.name} agent={agent} />;
})}
{fetchingAgents ? (
<LoadingMask
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
) : (
<></>
)}
</Stack>
</Box>
);
});

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import AgentIcon, { IAgentIconProps } from '@/components/AgentIcon';
export interface IHiringCardProps {
icon: IAgentIconProps['name'];
name: string;
profile: string;
style?: SxProps;
}
export default React.memo<IHiringCardProps>(
({ icon, name, profile, style = {} }) => (
<Box
sx={{
borderRadius: '6px',
display: 'flex',
backgroundColor: '#BBB',
padding: '8px 4px',
...style,
}}
>
<AgentIcon
name={icon}
style={{
flexGrow: 0,
width: '40px',
height: 'auto',
marginRight: '6px',
}}
/>
<Box sx={{ flexGrow: 1, width: 0, height: '100%', fontSize: '14px' }}>
<strong>{name}</strong>: {profile}
</Box>
</Box>
),
);

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton';
import RefreshIcon from '@mui/icons-material/Refresh';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import HiringCard from './HiringCard';
import { globalStorage } from '@/storage';
import LoadingMask from '@/components/LoadingMask';
export interface IAgentCartProps {
style?: SxProps;
}
export default React.memo<IAgentCartProps>(() => {
// const { agents, refreshAgents, refresingAgents } =
// React.useContext(GlobalContext);
// return (
// <>
// <Box
// sx={{
// width: '100%',
// opacity: 0.5,
// fontWeight: 600,
// fontSize: '18px',
// userSelect: 'none',
// padding: '2px 6px',
// }}
// >
// Agent Cart
// </Box>
// {refresingAgents ? (
// <LoadingMask
// style={{
// position: 'absolute',
// top: 0,
// left: 0,
// right: 0,
// bottom: 0,
// }}
// />
// ) : (
// <Stack
// spacing={1}
// sx={{
// position: 'relative',
// padding: '6px',
// paddingBottom: '44px',
// background: '#CCC',
// borderRadius: '10px',
// }}
// >
// {agents.map(agent => (
// <HiringCard
// key={agent.name}
// icon={agent.icon}
// name={agent.name}
// profile={agent.profile}
// />
// ))}
// <IconButton
// aria-label="刷新"
// onClick={refreshAgents}
// disabled={refresingAgents}
// sx={{ position: 'absolute', right: '36px', bottom: '2px' }}
// >
// <RefreshIcon />
// </IconButton>
// <IconButton
// aria-label="提交"
// disabled={refresingAgents}
// sx={{ position: 'absolute', right: '6px', bottom: '2px' }}
// >
// <CheckCircleOutlineIcon />
// </IconButton>
// </Stack>
// )}
// </>
// );
return <></>;
});

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import SwapVertIcon from '@mui/icons-material/SwapVert';
export interface IAgentRepoProps {
style?: SxProps;
}
const REPO_COLORS: string[] = [
'rgb(172,172,172)',
'rgb(165,184,182)',
'rgb(159,195,192)',
'rgb(153,206,202)',
'rgb(107,204,198)',
];
export default React.memo<IAgentRepoProps>(() => {
const repos = React.useMemo(
() =>
Array(30)
.fill(0)
.map((_, index) => (
<Box
key={index}
sx={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box
sx={{
width: '36px',
height: '36px',
borderRadius: '50%',
background:
REPO_COLORS[Math.floor(Math.random() * REPO_COLORS.length)],
cursor: 'pointer',
transition: 'all 0.3s',
filter: 'contrast(1.0)',
'&:hover': {
filter: 'contrast(1.3)',
},
}}
/>
</Box>
)),
[],
);
return (
<>
<Box
sx={{
width: '100%',
opacity: 0.5,
fontWeight: 600,
fontSize: '18px',
userSelect: 'none',
padding: '2px 6px',
}}
>
Agent Repo
</Box>
<Box
sx={{
position: 'relative',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(40px, 1fr))',
gap: '6px',
padding: '16px 10px',
background: '#CCC',
borderRadius: '10px',
}}
>
{repos}
<IconButton
aria-label="提交"
sx={{ position: 'absolute', right: '6px', bottom: '2px' }}
>
<SwapVertIcon />
</IconButton>
</Box>
</>
);
});

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import FormControl from '@mui/material/FormControl';
import FilledInput from '@mui/material/FilledInput';
import CampaignIcon from '@mui/icons-material/Campaign';
export interface IHireRequirementProps {
style?: SxProps;
valueRef?: React.MutableRefObject<string>;
onSubmit?: (value: string) => void;
onChange?: (value: string) => void;
}
export default React.memo<IHireRequirementProps>(
({ style = {}, valueRef, onSubmit, onChange }) => {
const [value, setValue] = React.useState('');
const handleChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
onChange?.(event.target.value);
},
[onChange],
);
const handleSubmit = React.useCallback(() => {
onSubmit?.(value);
}, [onSubmit, value]);
React.useEffect(() => {
if (valueRef) {
valueRef.current = value;
}
}, [valueRef, value]);
return (
<FormControl
sx={{
width: '100%',
position: 'relative',
...style,
}}
>
<Box
sx={{
width: '100%',
opacity: 0.5,
fontWeight: 600,
fontSize: '18px',
userSelect: 'none',
padding: '2px 6px',
}}
>
Hire Requirement
</Box>
<FilledInput
placeholder="请输入……"
fullWidth
multiline
rows={4}
value={value}
onChange={handleChange}
size="small"
sx={{
fontSize: '14px',
paddingTop: '10px',
paddingBottom: '10px',
borderRadius: '10px',
borderBottom: 'none !important',
'&::before': {
borderBottom: 'none !important',
},
}}
/>
<IconButton
disabled={!value}
aria-label="提交"
sx={{ position: 'absolute', right: '6px', bottom: '2px' }}
onClick={handleSubmit}
>
<CampaignIcon />
</IconButton>
</FormControl>
);
},
);

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import HireRequirement from './HireRequirement';
import AgentRepo from './AgentRepo';
import AgentCart from './AgentCart';
export default React.memo<{ style?: SxProps }>(({ style = {} }) => {
return (
<Box
sx={{
background: '#F3F3F3',
// borderRadius: '10px',
padding: '10px',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
overflowY: 'auto',
overflowX: 'hidden',
...style,
}}
>
<HireRequirement />
<AgentRepo />
<AgentCart />
</Box>
);
});

View File

@@ -0,0 +1,119 @@
import AbigailChen from '@/static/AgentIcons/Abigail_Chen.png';
import AdamSmith from '@/static/AgentIcons/Adam_Smith.png';
import ArthurBurton from '@/static/AgentIcons/Arthur_Burton.png';
import AyeshaKhan from '@/static/AgentIcons/Ayesha_Khan.png';
import CarlosGomez from '@/static/AgentIcons/Carlos_Gomez.png';
import CarmenOrtiz from '@/static/AgentIcons/Carmen_Ortiz.png';
import EddyLin from '@/static/AgentIcons/Eddy_Lin.png';
import FranciscoLopez from '@/static/AgentIcons/Francisco_Lopez.png';
import GiorgioRossi from '@/static/AgentIcons/Giorgio_Rossi.png';
import HaileyJohnson from '@/static/AgentIcons/Hailey_Johnson.png';
import IsabellaRodriguez from '@/static/AgentIcons/Isabella_Rodriguez.png';
import JaneMoreno from '@/static/AgentIcons/Jane_Moreno.png';
import JenniferMoore from '@/static/AgentIcons/Jennifer_Moore.png';
import JohnLin from '@/static/AgentIcons/John_Lin.png';
import KlausMueller from '@/static/AgentIcons/Klaus_Mueller.png';
import LatoyaWilliams from '@/static/AgentIcons/Latoya_Williams.png';
import MariaLopez from '@/static/AgentIcons/Maria_Lopez.png';
import MeiLin from '@/static/AgentIcons/Mei_Lin.png';
import RajivPatel from '@/static/AgentIcons/Rajiv_Patel.png';
import RyanPark from '@/static/AgentIcons/Ryan_Park.png';
import SamMoore from '@/static/AgentIcons/Sam_Moore.png';
import TamaraTaylor from '@/static/AgentIcons/Tamara_Taylor.png';
import TomMoreno from '@/static/AgentIcons/Tom_Moreno.png';
import WolfgangSchulz from '@/static/AgentIcons/Wolfgang_Schulz.png';
import YurikoYamamoto from '@/static/AgentIcons/Yuriko_Yamamoto.png';
import Unknown from '@/static/AgentIcons/Unknow.png';
export enum IconName {
AbigailChen = 'Abigail_Chen',
AdamSmith = 'Adam_Smith',
ArthurBurton = 'Arthur_Burton',
AyeshaKhan = 'Ayesha_Khan',
CarlosGomez = 'Carlos_Gomez',
CarmenOrtiz = 'Carmen_Ortiz',
EddyLin = 'Eddy_Lin',
FranciscoLopez = 'Francisco_Lopez',
CassandraSmith = 'Cassandra_Smith',
ChristopherCarter = 'Christopher_Carter',
DaveJones = 'Dave_Jones',
DerekSmith = 'Derek_Smith',
ElisaSmith = 'Elisa_Smith',
EricJones = 'Eric_Jones',
FayeSmith = 'Faye_Smith',
FrankSmith = 'Frank_Smith',
GabeSmith = 'Gabe_Smith',
GiorgioRossi = 'Giorgio_Rossi',
HaileyJohnson = 'Hailey_Johnson',
IsabellaRodriguez = 'Isabella_Rodriguez',
JaneMoreno = 'Jane_Moreno',
JenniferMoore = 'Jennifer_Moore',
JohnLin = 'John_Lin',
KlausMueller = 'Klaus_Mueller',
LatoyaWilliams = 'Latoya_Williams',
MariaLopez = 'Maria_Lopez',
MeiLin = 'Mei_Lin',
RajivPatel = 'Rajiv_Patel',
RyanPark = 'Ryan_Park',
SamMoore = 'Sam_Moore',
TamaraTaylor = 'Tamara_Taylor',
TomMoreno = 'Tom_Moreno',
WolfgangSchulz = 'Wolfgang_Schulz',
YurikoYamamoto = 'Yuriko_Yamamoto',
Unknown = 'Unknown',
}
const LowercaseNameMap: { [key: string]: IconName } = Object.fromEntries(
Object.entries(IconName).map(([name, value]) => [name.toLowerCase(), value]),
);
export const IconMap = new Proxy<{ [key: string]: IconName }>(
{},
{
get: (target, name) => {
const lowerCaseName = name
.toString()
.toLowerCase()
.replace(/[\s_]+/g, '');
return LowercaseNameMap[lowerCaseName] || IconName.Unknown;
},
},
);
export const IconUrl: { [key in IconName]: string } = {
[IconName.Unknown]: Unknown,
[IconName.AbigailChen]: AbigailChen,
[IconName.AdamSmith]: AdamSmith,
[IconName.ArthurBurton]: ArthurBurton,
[IconName.AyeshaKhan]: AyeshaKhan,
[IconName.CarlosGomez]: CarlosGomez,
[IconName.CarmenOrtiz]: CarmenOrtiz,
[IconName.EddyLin]: EddyLin,
[IconName.FranciscoLopez]: FranciscoLopez,
[IconName.CassandraSmith]: AbigailChen,
[IconName.ChristopherCarter]: AbigailChen,
[IconName.DaveJones]: AbigailChen,
[IconName.DerekSmith]: AbigailChen,
[IconName.ElisaSmith]: AbigailChen,
[IconName.EricJones]: AbigailChen,
[IconName.FayeSmith]: AbigailChen,
[IconName.FrankSmith]: AbigailChen,
[IconName.GabeSmith]: AbigailChen,
[IconName.GiorgioRossi]: GiorgioRossi,
[IconName.HaileyJohnson]: HaileyJohnson,
[IconName.IsabellaRodriguez]: IsabellaRodriguez,
[IconName.JaneMoreno]: JaneMoreno,
[IconName.JenniferMoore]: JenniferMoore,
[IconName.JohnLin]: JohnLin,
[IconName.KlausMueller]: KlausMueller,
[IconName.LatoyaWilliams]: LatoyaWilliams,
[IconName.MariaLopez]: MariaLopez,
[IconName.MeiLin]: MeiLin,
[IconName.RajivPatel]: RajivPatel,
[IconName.RyanPark]: RyanPark,
[IconName.SamMoore]: SamMoore,
[IconName.TamaraTaylor]: TamaraTaylor,
[IconName.TomMoreno]: TomMoreno,
[IconName.WolfgangSchulz]: WolfgangSchulz,
[IconName.YurikoYamamoto]: YurikoYamamoto,
};

View File

@@ -0,0 +1,119 @@
import React from 'react';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import { styled, SxProps } from '@mui/material/styles';
// import Box from '@mui/material/Box';
import { Divider, Box } from '@mui/material';
import { IconName, IconUrl, IconMap } from './agents';
import { IAgent } from '@/apis/get-agents';
import { ActionType } from '@/storage/plan';
interface ITooltipInfo extends IAgent {
action?: { type: ActionType; description: string; style: SxProps };
}
export interface IAgentIconProps {
name?: IconName | string;
style?: React.CSSProperties;
tooltipInfo?: ITooltipInfo;
}
const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: '#f2f2f2',
color: 'rgba(0, 0, 0, 0.87)',
width: 'fit-content',
fontSize: theme.typography.pxToRem(12),
border: '1px solid #0003',
boxShadow: '1px 1px 4px #0003',
},
}));
const generateTooltip = (info: ITooltipInfo) => {
return (
<Box sx={{ maxWidth: '20vh', padding: '4px 0px' }}>
<Box
sx={{
fontSize: '16px',
fontWeight: 600,
userSelect: 'none',
padding: '0 4px',
}}
>
{info.name}
</Box>
<Box
sx={{
margin: '4px 0',
fontSize: '14px',
padding: '0 4px',
borderRadius: '6px',
fontWeight: 400,
userSelect: 'none',
}}
>
{info.profile}
</Box>
{info.action && (
<Box
sx={{
borderRadius: '6px',
padding: '4px 4px',
border: '1px solid #333',
...info.action.style,
}}
>
<Box sx={{ fontWeight: 600 }}>{info.action.type}</Box>
<Divider
sx={{
margin: '1px 0px',
borderBottom: '1px dashed', // 设置为虚线
borderColor: '#888888',
}}
/>
<Box>{info.action.description}</Box>
</Box>
)}
</Box>
);
};
export default React.memo<IAgentIconProps>(
({ style = {}, name = 'Unknown', tooltipInfo }) => {
const _name = React.useMemo(() => IconMap[name], [name]);
return tooltipInfo ? (
<HtmlTooltip
title={generateTooltip(tooltipInfo)}
followCursor
placement="right-start"
>
<img
// title={_name}
alt={_name}
src={IconUrl[_name]}
style={{
width: '100%',
height: '100%',
imageRendering: 'pixelated',
...style,
}}
/>
</HtmlTooltip>
) : (
<img
// title={_name}
alt={_name}
src={IconUrl[_name]}
style={{
width: '100%',
height: '100%',
imageRendering: 'pixelated',
...style,
}}
/>
);
},
);
export { IconName, IconMap, IconUrl } from './agents';

View File

@@ -0,0 +1,120 @@
import React from 'react';
import { Rnd } from 'react-rnd';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
import { debounce } from 'lodash';
export interface IFloatingWindowProps {
title: React.ReactNode;
children: React.ReactNode;
onClose?: () => void;
onResize?: () => void;
}
let windowsArrange: HTMLElement[] = [];
const focusWindow = (element: HTMLElement) => {
windowsArrange = windowsArrange.filter(
ele => ele.ownerDocument.contains(ele) && element !== ele,
);
windowsArrange.push(element);
windowsArrange.forEach(
(ele, index) => (ele.style.zIndex = `${index + 1000}`),
);
};
export default React.memo<IFloatingWindowProps>(
({ title, children, onClose, onResize }) => {
const [resizeable, setResizeable] = React.useState(true);
const containerRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
if (containerRef.current) {
focusWindow(containerRef.current.parentElement!);
}
}, []);
const defaultSize = React.useMemo(() => {
const width = Math.min(800, window.innerWidth - 20);
const height = Math.min(600, window.innerHeight - 20);
const x = (window.innerWidth - width) / 2;
const y = (window.innerHeight - height) / 2;
return { x, y, width, height };
}, []);
return (
<Rnd
/* optional props */
style={{
borderColor: '#43A8AA',
borderWidth: '3px',
borderStyle: 'solid',
boxShadow: '3px 3px 20px #0005',
display: 'flex',
flexDirection: 'column',
}}
minHeight={60}
minWidth={150}
default={defaultSize}
onMouseDown={() => {
if (containerRef.current) {
focusWindow(containerRef.current.parentElement!);
}
}}
bounds={document.body}
disableDragging={!resizeable}
onResize={debounce(() => {
onResize?.();
}, 50)}
>
<Box
sx={{
bgcolor: 'primary.main',
color: 'white',
height: '36px',
display: 'flex',
alignItems: 'center',
padding: '0 5px',
userSelect: 'none',
cursor: 'move',
}}
>
{typeof title === 'string' ? (
<Box
sx={{
fontSize: '18px',
fontWeight: 800,
flexGrow: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{title}
</Box>
) : (
title
)}
<Box sx={{ display: 'flex' }}>
<IconButton
disabled={onClose === undefined}
onClick={onClose}
sx={{ color: 'white' }}
>
<CloseIcon />
</IconButton>
</Box>
</Box>
<Box
ref={containerRef}
sx={{
flexGrow: 1,
background: '#F3F3F3',
overflow: 'auto',
}}
onPointerEnter={() => setResizeable(false)}
onPointerLeave={() => setResizeable(true)}
>
{children}
</Box>
</Rnd>
);
},
);

View File

@@ -0,0 +1,231 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { IconButton, SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import AddIcon from '@mui/icons-material/Add';
import CloseIcon from '@mui/icons-material/Close';
import UploadIcon from '@mui/icons-material/Upload';
import DownloadIcon from '@mui/icons-material/Download';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import LogoIcon from '@/icons/LogoIcon';
import { globalStorage } from '@/storage';
export default observer(({ style = {} }: { style?: SxProps }) => {
const pageTags = React.useMemo(
() => (
<Tabs
value={globalStorage.currentPlanId ?? ''}
onChange={(_event, newId) =>
globalStorage.focusPlan(newId || undefined)
}
aria-label="plan tabs"
variant="scrollable"
scrollButtons="auto"
TabIndicatorProps={{
children: <span className="MuiTabs-indicatorSpan" />,
}}
sx={{
minHeight: '40px',
height: '40px',
'& .MuiTabs-indicator': {
display: 'flex',
justifyContent: 'center',
backgroundColor: 'transparent',
},
'& .MuiTabs-indicatorSpan': {
maxWidth: 60,
width: '100%',
backgroundColor: 'rgb(168, 247, 227)',
},
}}
>
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<AddIcon />
</Box>
}
value=""
sx={{
color: '#fffb',
minHeight: '40px',
height: '40px',
background: '#fff1',
width: '40px',
minWidth: '0',
'&.Mui-selected': {
color: '#fff',
fontWeight: 900,
},
}}
/>
{globalStorage.planTabArrange.map(id => (
<Tab
value={id}
key={id}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton
size="small"
onClick={() => globalStorage.removePlan(id)}
sx={{
opacity: 0.6,
'&:hover': { opacity: 1 },
}}
>
<CloseIcon
sx={{
color: '#fff',
fontSize: '16px',
}}
/>
</IconButton>
Plan
<IconButton
size="small"
sx={{
opacity: 0.6,
'&:hover': { opacity: 1 },
}}
onClick={() => {
const jsonString = JSON.stringify(
globalStorage.dumpPlan(id),
);
// download file
const blob = new Blob([jsonString], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `plan-${id}.json`;
a.click();
a.remove();
}}
>
<DownloadIcon
sx={{
color: '#fff',
fontSize: '16px',
}}
/>
</IconButton>
</Box>
}
sx={{
color: '#fffb',
minHeight: '40px',
height: '40px',
borderLeft: '1px solid #3333',
background: '#fff1',
'&.Mui-selected': {
color: '#fff',
fontWeight: 900,
},
padding: '0 4px',
}}
/>
))}
</Tabs>
),
[globalStorage.planTabArrange, globalStorage.currentPlanId],
);
return (
<Box
sx={{
bgcolor: 'primary.main',
color: '#fff',
width: '100%',
display: 'flex',
userSelect: 'none',
...style,
}}
>
<Box sx={{ flexGrow: 1, display: 'flex' }}>
{globalStorage.devMode ? (
<Box
sx={{ display: 'flex', alignItems: 'center', marginLeft: '10px' }}
>
<LogoIcon />
<Box sx={{ marginLeft: '6px', fontWeight: 800, fontSize: '20px' }}>
AGENTCOORD
</Box>
</Box>
) : (
pageTags
)}
</Box>
<IconButton component="label">
<input
type="file"
accept=".json"
style={{
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
}}
onChange={e => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = event => {
const content = event.target?.result as string;
globalStorage.loadPlan(JSON.parse(content));
};
reader.readAsText(file);
}
}}
/>
<UploadIcon sx={{ color: '#fff' }} />
</IconButton>
{globalStorage.devMode && globalStorage.currentPlanId && (
<IconButton
disabled={globalStorage.currentPlanId === undefined}
onClick={() => {
const jsonString = JSON.stringify(
globalStorage.dumpPlan(globalStorage.currentPlanId!),
);
// download file
const blob = new Blob([jsonString], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `plan-${globalStorage.currentPlanId!}.json`;
a.click();
a.remove();
}}
>
<DownloadIcon sx={{ color: '#fff' }} />
</IconButton>
)}
{!globalStorage.devMode && (
<Box
sx={{
display: 'flex',
height: '100%',
marginRight: '10px',
alignItems: 'center',
fontSize: '13px',
textShadow: '0 0 2px #000',
}}
>
AgentCoord
</Box>
)}
<IconButton>
<HelpOutlineIcon sx={{ color: '#fff' }} />
</IconButton>
</Box>
);
});

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
export default React.memo<{ style?: SxProps }>(({ style = {} }) => (
<Box
sx={{
position: 'absolute',
height: '100%',
width: '100%',
backdropFilter: 'blur(5px)',
backgroundColor: '#FFF3',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
...style,
}}
>
<CircularProgress />
</Box>
));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import React from 'react';
import remarkGfm from 'remark-gfm';
import Box from '@mui/material/Box';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import { SxProps } from '@mui/material';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
// import './markdown-style.css';
import './github-mardown.css';
// import './hljs-atom-one-dark.min.css';
// import './prism-one-dark.css';
export interface IMarkdownViewArguments {
text?: string;
style?: SxProps;
}
export default React.memo<IMarkdownViewArguments>(
({ text = '', style = {} }) => {
return (
<Box className="markdown-body" sx={{ minHeight: '20px', ...style }}>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeHighlight, rehypeKatex]}
>
{text}
</ReactMarkdown>
</Box>
);
},
);

View File

@@ -0,0 +1,702 @@
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
line-height: 1.5;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI',
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body * {
margin-top: 0px !important;
margin-bottom: 0px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
margin-block-start: 0px !important;
margin-block-end: 0px !important;
white-space: normal !important;
}
.markdown-body .pl-c {
color: #6a737d;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: #005cc5;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: #6f42c1;
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: #24292e;
}
.markdown-body .pl-ent {
color: #22863a;
}
.markdown-body .pl-k {
color: #d73a49;
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: #032f62;
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: #e36209;
}
.markdown-body .pl-bu {
color: #b31d28;
}
.markdown-body .pl-ii {
color: #fafbfc;
background-color: #b31d28;
}
.markdown-body .pl-c2 {
color: #fafbfc;
background-color: #d73a49;
}
.markdown-body .pl-c2::before {
content: '^M';
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: #22863a;
}
.markdown-body .pl-ml {
color: #735c0f;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: #005cc5;
}
.markdown-body .pl-mi {
font-style: italic;
color: #24292e;
}
.markdown-body .pl-mb {
font-weight: bold;
color: #24292e;
}
.markdown-body .pl-md {
color: #b31d28;
background-color: #ffeef0;
}
.markdown-body .pl-mi1 {
color: #22863a;
background-color: #f0fff4;
}
.markdown-body .pl-mc {
color: #e36209;
background-color: #ffebda;
}
.markdown-body .pl-mi2 {
color: #f6f8fa;
background-color: #005cc5;
}
.markdown-body .pl-mdr {
font-weight: bold;
color: #6f42c1;
}
.markdown-body .pl-ba {
color: #586069;
}
.markdown-body .pl-sg {
color: #959da5;
}
.markdown-body .pl-corl {
text-decoration: underline;
color: #032f62;
}
.markdown-body .octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
.markdown-body a {
background-color: transparent;
}
.markdown-body a:active,
.markdown-body a:hover {
outline-width: 0;
}
.markdown-body strong {
font-weight: inherit;
}
.markdown-body strong {
font-weight: bolder;
}
.markdown-body h1 {
font-size: 2em;
margin: 0.67em 0;
}
.markdown-body img {
border-style: none;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre {
font-family: monospace, monospace;
font-size: 1em;
}
.markdown-body hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
.markdown-body input {
font: inherit;
margin: 0;
}
.markdown-body input {
overflow: visible;
}
.markdown-body [type='checkbox'] {
box-sizing: border-box;
padding: 0;
}
.markdown-body * {
box-sizing: border-box;
}
.markdown-body input {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body a {
color: #0366d6;
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body strong {
font-weight: 600;
}
.markdown-body hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 0.05em solid #dfe2e5;
}
.markdown-body hr::before {
display: table;
content: '';
}
.markdown-body hr::after {
display: table;
clear: both;
content: '';
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body h1 {
font-size: 32px;
font-weight: 600;
}
.markdown-body h2 {
font-size: 24px;
font-weight: 600;
}
.markdown-body h3 {
font-size: 20px;
font-weight: 600;
}
.markdown-body h4 {
font-size: 16px;
font-weight: 600;
}
.markdown-body h5 {
font-size: 14px;
font-weight: 600;
}
.markdown-body h6 {
font-size: 12px;
font-weight: 600;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
font-size: 12px;
}
.markdown-body .octicon {
vertical-align: text-bottom;
}
.markdown-body .pl-0 {
padding-left: 0 !important;
}
.markdown-body .pl-1 {
padding-left: 4px !important;
}
.markdown-body .pl-2 {
padding-left: 8px !important;
}
.markdown-body .pl-3 {
padding-left: 16px !important;
}
.markdown-body .pl-4 {
padding-left: 24px !important;
}
.markdown-body .pl-5 {
padding-left: 32px !important;
}
.markdown-body .pl-6 {
padding-left: 40px !important;
}
.markdown-body::before {
display: table;
content: '';
}
.markdown-body::after {
display: table;
clear: both;
content: '';
}
.markdown-body > *:first-child {
margin-top: 0 !important;
}
.markdown-body > *:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body blockquote > :first-child {
margin-top: 0;
}
.markdown-body blockquote > :last-child {
margin-bottom: 0;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #444d56;
vertical-align: middle;
background-color: #fafbfc;
border: solid 1px #c6cbd1;
border-bottom-color: #959da5;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #959da5;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #1b1f23;
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 {
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid #888a;
}
.markdown-body h2 {
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid #888a;
}
.markdown-body h3 {
font-size: 1.25em;
}
.markdown-body h4 {
font-size: 1em;
}
.markdown-body h5 {
font-size: 0.875em;
}
.markdown-body h6 {
font-size: 0.85em;
color: #6a737d;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li {
word-wrap: break-all;
}
.markdown-body li > p {
margin-top: 16px;
}
.markdown-body li + li {
margin-top: 0.25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table {
display: block;
width: 100%;
overflow: auto;
}
.markdown-body table th {
font-weight: 600;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
background-color: #fff;
}
.markdown-body hr {
height: 0.05em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.markdown-body blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
.markdown-body img[align='right'] {
padding-left: 20px;
}
.markdown-body img[align='left'] {
padding-right: 20px;
}
.markdown-body code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(197, 212, 228, 0.115);
border-radius: 3px;
}
.markdown-body pre {
word-wrap: normal;
}
.markdown-body pre > code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
border-radius: 3px;
background: #0003;
}
.markdown-body pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body :checked + .radio-label {
position: relative;
z-index: 1;
border-color: #0366d6;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item + .task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item input {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.markdown-body hr {
border-bottom-color: #555;
}
.markdown-body .full-commit .btn-outline:not(:disabled):hover {
color: #005cc5;
border-color: #005cc5;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
line-height: 10px;
color: #444d56;
vertical-align: middle;
background-color: #fafbfc;
border: solid 1px #d1d5da;
border-bottom-color: #c6cbd1;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #c6cbd1;
}

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

View File

@@ -0,0 +1,134 @@
import React from 'react';
import _ from 'lodash';
import { observer } from 'mobx-react-lite';
import { usePlanModificationContext } 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 PlanModificationSvg = observer(() => {
const {
forestPaths,
whoIsAddingBranch,
nodeRefMap,
svgForceRenderCounter,
containerRef,
} = usePlanModificationContext();
const { currentStepTaskNodeSet } = 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 (
currentStepTaskNodeSet.has(startid) &&
currentStepTaskNodeSet.has(endid)
) {
isCurrent = true;
}
if (startid === 'root' && currentStepTaskNodeSet.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]))}
{renderRoot()}
{whoIsAddingBranch && renderLine(whoIsAddingBranch, 'requirement')}
</svg>
);
});
export default PlanModificationSvg;

View File

@@ -0,0 +1,195 @@
// PlanModificationContext.tsx
import React, {
ReactNode,
RefObject,
createContext,
useContext,
useState,
useEffect,
} from 'react';
import { IPlanTreeNode, globalStorage } from '@/storage';
interface PlanModificationContextProps {
forest: IPlanTreeNode | undefined;
setForest: (forest: IPlanTreeNode) => 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 PlanModificationContext = createContext<PlanModificationContextProps>(
{} as PlanModificationContextProps,
);
export const usePlanModificationContext = () =>
useContext(PlanModificationContext);
export const PlanModificationProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [forest, setForest] = useState<IPlanTreeNode>();
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);
setWhoIsAddingBranch(undefined);
setBaseNodeSet(new Set());
setBaseLeafNodeId(undefined);
}
};
const handleNodeClick = (nodeId: string) => {
const leafId = globalStorage.getFirstLeafStepTask(nodeId).id;
if (whoIsAddingBranch) {
if (baseLeafNodeId === leafId) {
setBaseNodeSet(new Set());
setBaseLeafNodeId(undefined);
} else {
const pathNodeSet = new Set(globalStorage.getStepTaskLeafPath(leafId));
if (
pathNodeSet.has(whoIsAddingBranch) ||
whoIsAddingBranch === 'root'
) {
setBaseLeafNodeId(leafId);
setBaseNodeSet(pathNodeSet);
}
}
} else {
globalStorage.setCurrentPlanBranch(leafId);
globalStorage.setFocusingStepTaskId(nodeId);
}
};
const [svgForceRenderCounter, setSVGForceRenderCounter] = useState(0);
const svgForceRender = () => {
setSVGForceRenderCounter((svgForceRenderCounter + 1) % 100);
};
const handleNodeHover = (nodeId: string | undefined) => {
if (!whoIsAddingBranch) {
if (nodeId) {
const leafNode = globalStorage.getFirstLeafStepTask(nodeId);
const branchInfo = globalStorage.planManager.branches[leafNode.id];
if (branchInfo.base) {
const pathNodeSet = new Set(
globalStorage.getStepTaskLeafPath(branchInfo.base),
);
setBaseNodeSet(pathNodeSet);
}
} else {
setBaseNodeSet(new Set());
}
}
};
useEffect(() => {
setBaseNodeSet(new Set());
setBaseLeafNodeId(undefined);
}, [whoIsAddingBranch]);
useEffect(() => {
svgForceRender();
}, [forest, whoIsAddingBranch]);
return (
<PlanModificationContext.Provider
value={{
forest,
setForest,
forestPaths,
setForestPaths,
whoIsAddingBranch,
setWhoIsAddingBranch,
updateWhoIsAddingBranch,
containerRef,
setContainerRef,
nodeRefMap,
updateNodeRefMap,
baseNodeSet,
setBaseNodeSet,
baseLeafNodeId,
setBaseLeafNodeId,
handleRequirementSubmit,
handleNodeClick,
handleNodeHover,
svgForceRenderCounter,
setSVGForceRenderCounter,
svgForceRender,
}}
>
{children}
</PlanModificationContext.Provider>
);
};
// ----------------------------------------------------------------
const _getFatherChildrenIdPairs = (node: IPlanTreeNode): [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,445 @@
/* eslint-disable max-lines */
import React from 'react';
import { SxProps } 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 CircularProgress from '@mui/material/CircularProgress';
import AgentIcon from '../AgentIcon';
import PlanModificationSvg from './PlanModificationSvg';
import {
PlanModificationProvider,
usePlanModificationContext,
} from './context';
import { IPlanTreeNode, globalStorage } from '@/storage';
import SendIcon from '@/icons/SendIcon';
const RequirementNoteNode: React.FC<{
data: {
text: string;
};
style?: SxProps;
}> = ({ data, style }) => {
return (
<Box sx={{ ...style, flexShrink: 0 }}>
<Box
sx={{
color: '#ACACAC',
userSelect: 'none',
// width: 'max-content',
minWidth: '250px',
}}
>
{data.text}
</Box>
</Box>
);
};
const RequirementInputNode: React.FC<{
style?: SxProps;
}> = ({ style }) => {
const { handleRequirementSubmit, updateNodeRefMap } =
usePlanModificationContext();
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%',
}}
>
<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 } =
usePlanModificationContext();
const [onHover, setOnHover] = React.useState(false);
const myRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
updateNodeRefMap('root', myRef);
}, []);
return (
<Box
onMouseOver={() => setOnHover(true)}
onMouseOut={() => setOnHover(false)}
sx={{ ...style, flexDirection: 'column', position: 'relative' }}
ref={myRef}
>
<IconButton
sx={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translateX(-50%) ',
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
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<{
node: IPlanTreeNode;
style?: SxProps;
}> = ({ node, style = {} }) => {
const {
updateNodeRefMap,
whoIsAddingBranch,
updateWhoIsAddingBranch,
handleNodeClick,
handleNodeHover,
baseNodeSet,
} = usePlanModificationContext();
const [onHover, setOnHover] = React.useState(false);
const myRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
updateNodeRefMap(node.id, myRef);
}, []);
return (
// <RectWatcher onRectChange={onRectChange}>
<Box sx={{ ...style }}>
<Box
onMouseOver={() => {
setOnHover(true);
handleNodeHover(node.id);
}}
onMouseOut={() => {
setOnHover(false);
handleNodeHover(undefined);
}}
sx={{
flexDirection: 'column',
backgroundColor: '#F6F6F6',
border: node.focusing ? '2px solid #4A9C9E' : '2px solid #E5E5E5',
borderRadius: '12px',
maxWidth: '140px',
minWidth: '100px',
position: 'relative',
padding: '8px 6px',
boxShadow: baseNodeSet.has(node.id) ? '0 0 10px 5px #43b2aa' : '',
}}
ref={myRef}
onClick={() => handleNodeClick(node.id)}
>
<Box sx={{ textAlign: 'center', fontWeight: 600, marginBottom: '4px' }}>
{node.name}
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
{node.focusing &&
node.agents.map(agentName => (
<AgentIcon
key={`planModification.${node.id}.${agentName}`}
name={globalStorage.agentMap.get(agentName)?.icon ?? 'unknown'}
style={{
width: '36px',
height: 'auto',
margin: '0px',
userSelect: 'none',
}}
tooltipInfo={globalStorage.agentMap.get(agentName)}
/>
))}
</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 === node.id ? 'visible' : 'hidden',
padding: '0px',
borderRadius: '50%',
border: '1px dotted #333',
height: '16px',
width: '16px',
marginTop: '-6px',
'& .MuiSvgIcon-root': {
fontSize: '14px',
},
}}
onClick={() => {
updateWhoIsAddingBranch(node.id);
}}
>
{whoIsAddingBranch !== node.id ? <AddIcon /> : <RemoveIcon />}
</IconButton>
</Box>
</Box>
</Box>
);
};
const Tree: React.FC<{
tree: IPlanTreeNode;
}> = ({ tree }) => {
const { whoIsAddingBranch } = usePlanModificationContext();
const generalNodeStyle = {
height: '60px',
padding: '8px',
display: 'flex',
alignItems: 'center',
margin: '8px 32px 8px 0px',
};
const focusedNodeStyle = {
height: '80px',
padding: '8px',
display: 'flex',
alignItems: 'center',
margin: '8px 32px 8px 0px',
};
return (
<Box sx={{ display: 'flex', flexDirection: 'row', zIndex: 999 }}>
<>
{tree.id !== 'root' && (
<Node
node={tree}
style={{
justifyContent: 'center',
alignSelf: 'flex-start',
cursor: 'pointer',
...(tree.focusing ? focusedNodeStyle : generalNodeStyle),
}}
/>
)}
{tree.id === 'root' && (
<RootNode
style={{
justifyContent: 'center',
...(tree.children[0] && tree.children[0].focusing
? focusedNodeStyle
: generalNodeStyle),
}}
/>
)}
</>
<>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
{tree.id !== 'root' && tree.leaf && (
<RequirementNoteNode
data={{ text: tree.requirement || '' }}
style={{ ...generalNodeStyle }}
/>
)}
{tree.children.map(childTree => (
<Tree key={`taskTree-${childTree.id}`} tree={childTree} />
))}
{tree.id === whoIsAddingBranch && (
<RequirementInputNode
style={{ height: '80px', display: 'flex', alignItems: 'center' }}
/>
)}
</Box>
</>
</Box>
);
};
interface IPlanModificationProps {
style?: SxProps;
resizeSignal?: number;
}
const TheViewContent = observer(
({ style, resizeSignal }: IPlanModificationProps) => {
const { renderingPlanForest } = globalStorage;
const { forest, setForest, setContainerRef, svgForceRender } =
usePlanModificationContext();
const myRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
setForest({
agentIcons: [],
agents: [],
children: renderingPlanForest,
id: 'root',
leaf: false,
name: 'root',
focusing: true,
});
}, [renderingPlanForest]);
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 && <PlanModificationSvg />}
{forest && <Tree tree={forest} />}
</Box>
);
},
);
/* eslint-enable max-lines */
const PlanModification: React.FC<IPlanModificationProps> = observer(
({ style, resizeSignal }) => {
return (
<PlanModificationProvider>
<TheViewContent style={style} resizeSignal={resizeSignal} />
{globalStorage.api.stepTaskTreeGenerating && (
<Box
sx={{
position: 'absolute',
bottom: '10px',
right: '20px',
zIndex: 999,
}}
>
<CircularProgress size={40} />
</Box>
)}
</PlanModificationProvider>
);
},
);
export default PlanModification;

View File

@@ -0,0 +1,178 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import Box from '@mui/material/Box';
// import EditIcon from '@mui/icons-material/Edit';
import TextField from '@mui/material/TextField';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import { IRichSentence } from '@/storage/plan';
import { globalStorage } from '@/storage';
interface IAgentDetailCardProps {
id: string;
node: IRichSentence;
render: () => void;
}
export default observer(({ id, node, render }: IAgentDetailCardProps) => {
const editContentRef = React.useRef('');
const [edit, setEdit] = React.useState(false);
React.useEffect(() => {
render();
}, [edit]);
return (
<Box
component="span"
sx={{
cursor: 'pointer',
marginRight: '4px',
userSelect: 'none',
transition: 'all 200ms ease-out',
borderRadius: '0',
display: edit ? 'flex' : 'inline',
flexDirection: 'column',
border: edit ? '2px solid' : undefined,
'& .edit-button': {
display: 'none',
},
'&:hover': {
filter: edit ? undefined : 'brightness(1.1)',
backgroundColor: edit
? undefined
: ((node?.style ?? { borderColor: '#0003' }) as any)!.borderColor,
'& .edit-button': {
display: edit ? 'none' : 'inline-flex',
},
},
'&:active': {
filter: edit ? undefined : 'brightness(1)',
backgroundColor: edit
? undefined
: ((node?.style ?? { borderColor: '#0003' }) as any)!.borderColor,
},
lineHeight: '1.4rem',
padding: edit ? '6px' : undefined,
marginBottom: edit ? '6px' : undefined,
marginTop: edit ? '6px' : undefined,
borderBottom: `2px solid ${(node.style as any).borderColor}`,
backgroundImage: edit
? 'linear-gradient(0deg, #FFF7, #FFF7)'
: undefined,
...(edit ? node.whoStyle ?? {} : node.style),
}}
onDoubleClick={() => {
editContentRef.current = node.content;
setEdit(true);
}}
title="Double click to edit"
>
<Box
component="span"
sx={{
userSelect: 'none',
padding: edit ? '0 6px' : '0 2px',
borderRadius: '14px',
border: '2px solid #0005',
transition: 'filter 100ms',
fontWeight: edit ? 800 : undefined,
...node.whoStyle,
}}
>
{node.who}
</Box>
{edit ? (
<>
<TextField
variant="outlined"
multiline
inputRef={ref => {
if (ref) {
ref.value = editContentRef.current;
}
}}
sx={{
'& > .MuiInputBase-root': {
padding: '6px',
'& > fieldset': {
border: 'none',
},
background: '#FFF8',
borderRadius: '12px',
},
borderRadius: '12px',
border: '2px solid #0002',
marginTop: '4px',
'*': {
fontSize: '14px',
},
}}
onChange={event => (editContentRef.current = event.target.value)}
/>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
paddingTop: '4px',
}}
>
<Box
onClick={() => {
if (!editContentRef.current) {
return;
}
setEdit(false);
const t = editContentRef.current;
globalStorage.updateAgentActionNodeContent(
id,
t[0].toUpperCase() + t.slice(1),
);
}}
title="Save change"
sx={{
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#3ec807',
borderRadius: '6px',
marginLeft: '4px',
padding: '0 4px',
'&:hover': {
filter: 'contrast(1.3)',
},
}}
>
<CheckIcon sx={{ fontSize: '18px', color: 'white' }} />
</Box>
<Box
onClick={() => setEdit(false)}
title="Cancel change"
sx={{
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#d56464',
borderRadius: '6px',
marginLeft: '4px',
padding: '0 4px',
'&:hover': {
filter: 'contrast(1.3)',
},
}}
>
<CloseIcon sx={{ fontSize: '18px', color: 'white' }} />
</Box>
</Box>
</>
) : (
// <>&nbsp;{node.content}</>
<span>&nbsp;{node.content}</span>
)}
</Box>
);
});

View File

@@ -0,0 +1,184 @@
import React from 'react';
import throttle from 'lodash/throttle';
import { observer } from 'mobx-react-lite';
import Box from '@mui/material/Box';
import AgentDetailCard from './AgentDetailCard';
import { globalStorage } from '@/storage';
import { StepTaskNode } from '@/storage/plan';
import { useResize } from '@/utils/resize-hook';
import BranchIcon from '@/icons/BranchIcon';
export default observer(({ step }: { step: StepTaskNode }) => {
const card = step.descriptionCard;
const [expand, setExpand] = React.useState(false);
const expandRef = React.useRef(expand);
React.useEffect(() => {
globalStorage.renderLines({ delay: 1, repeat: 20 });
}, [expand]);
const render = React.useMemo(
() =>
throttle(
() =>
requestAnimationFrame(() => {
if (refDetail.current) {
refDetail.current.style.height = expandRef.current
? `${
refDetail.current.querySelector('.description-detail')!
.scrollHeight + 12
}px`
: '0px';
}
}),
5,
{
leading: false,
trailing: true,
},
),
[],
);
React.useEffect(() => {
expandRef.current = expand;
render();
}, [expand]);
React.useEffect(() => {
setExpand(globalStorage.focusingStepTaskId === step.id);
}, [globalStorage.focusingStepTaskId]);
const refDetail = useResize<HTMLDivElement>(render);
return (
<Box sx={{ position: 'relative' }}>
<Box
sx={{
position: 'relative',
fontSize: '14px',
flexDirection: 'column',
background: '#F6F6F6',
borderRadius: '8px',
padding: '8px 4px',
margin: '2px 0',
cursor: 'pointer',
border: '2px solid #E5E5E5',
transition: 'all 80ms ease-in-out',
'&:hover': {
border: '2px solid #cdcdcd',
backgroundImage: 'linear-gradient(0, #00000008, #00000008)',
},
display: step.brief.template ? 'flex' : 'none',
}}
ref={card.ref}
onClick={() => globalStorage.setFocusingStepTaskId(step.id)}
>
<Box sx={{ marginLeft: '4px' }}>
{card.brief.map(({ text, style }, index) =>
style ? (
<Box
component="span"
// eslint-disable-next-line react/no-array-index-key
key={index}
sx={{
userSelect: 'none',
padding: '0 4px',
fontWeight: 600,
transition: 'filter 100ms',
...style,
}}
>
{text}
</Box>
) : (
<Box
component="span"
key={index}
sx={{ userSelect: 'none', lineHeight: '1.43rem' }}
>
{text}
</Box>
),
)}
</Box>
<Box
ref={refDetail}
sx={{
overflow: 'hidden',
transition: 'height 200ms ease-out', // 添加过渡效果
}}
>
{expand ? (
<Box
sx={{
marginTop: '5px',
borderRadius: '12px',
padding: '4px 8px',
background: '#E4F0F0',
border: '3px solid #A9C6C5',
cursor: 'auto',
}}
className="description-detail"
>
<Box
sx={{
color: '#4F8A87',
fontWeight: 800,
fontSize: '16px',
marginBottom: '4px',
}}
>
Specification:
</Box>
{card.detail.map((node, index) => (
<AgentDetailCard
key={index}
node={node[0] as any}
id={node[1] as any}
render={render}
/>
))}
</Box>
) : (
<></>
)}
</Box>
</Box>
{globalStorage.focusingStepTaskId === step.id ? (
<Box
sx={{
cursor: 'pointer',
userSelect: 'none',
position: 'absolute',
right: '-4px',
bottom: '2px',
width: '32px',
height: '32px',
bgcolor: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '10px 0 0 0',
'&:hover': {
filter: 'brightness(0.9)',
},
}}
onClick={() => (globalStorage.taskProcessModificationWindow = true)}
>
<BranchIcon />
<Box
component="span"
sx={{
fontSize: '12px',
position: 'absolute',
right: '3px',
bottom: 0,
color: 'white',
fontWeight: 800,
textAlign: 'right',
}}
>
{step.agentSelection?.leaves?.length ?? 0}
</Box>
</Box>
) : (
<></>
)}
</Box>
);
});

View File

@@ -0,0 +1,57 @@
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import DescriptionCard from './DescriptionCard';
import { globalStorage } from '@/storage';
import LoadingMask from '@/components/LoadingMask';
import Title from '@/components/Title';
export default observer(({ style = {} }: { style?: SxProps }) => {
return (
<Box
sx={{
background: '#FFF',
border: '3px solid #E1E1E1',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
...style,
}}
>
<Title title="Task Process" />
<Stack
spacing={1}
sx={{
position: 'relative',
padding: '6px 12px',
paddingBottom: '44px',
borderRadius: '10px',
height: 0,
flexGrow: 1,
overflowY: 'auto',
}}
onScroll={() => {
globalStorage.renderLines({ delay: 0, repeat: 2 });
}}
>
{globalStorage.planManager.currentPlan.map(step => (
<DescriptionCard key={step.name} step={step} />
))}
{globalStorage.api.planGenerating ? (
<LoadingMask
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
) : (
<></>
)}
</Stack>
</Box>
);
});

View File

@@ -0,0 +1,286 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import EditIcon from '@mui/icons-material/Edit';
import TextField from '@mui/material/TextField';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import MarkdownBlock from '@/components/MarkdownBlock';
import { globalStorage } from '@/storage';
export interface IObjectNodeProps {
name: string;
content: string;
_ref?: React.RefObject<HTMLDivElement | HTMLElement>;
style?: SxProps;
editObjectName?: string;
stepId: string;
handleExpand?: () => void;
}
export default observer(
({
style = {},
name,
content,
_ref,
editObjectName,
stepId,
handleExpand,
}: IObjectNodeProps) => {
const inputStringRef = React.useRef<string>('');
const [edit, setEdit] = React.useState(false);
const [expand, setExpand] = React.useState(false);
const refDetail = React.useRef<HTMLDivElement>(null);
// 使用useEffect来更新detail容器的高度
React.useEffect(() => {
if (refDetail.current) {
refDetail.current.style.height = expand
? `${refDetail.current.scrollHeight}px`
: '0px';
}
if (handleExpand) {
let count = 0;
const intervalId = setInterval(() => {
handleExpand();
count++;
if (count >= 20) {
clearInterval(intervalId);
}
}, 10);
}
}, [expand]);
return (
<Box
sx={{
userSelect: 'none',
borderRadius: '8px',
background: '#F6F6F6',
padding: '10px',
border: '2px solid #E5E5E5',
fontSize: '14px',
position: 'relative',
...(stepId
? {
cursor: 'pointer',
transition: 'all 200ms ease-in-out',
'&:hover': {
border: '2px solid #0002',
backgroundImage: 'linear-gradient(0, #0000000A, #0000000A)',
},
}
: {}),
...style,
}}
ref={_ref}
onClick={() => {
if (stepId) {
globalStorage.setFocusingStepTaskId(stepId);
}
}}
>
<Box component="span" sx={{ fontWeight: 800 }}>
{name}
</Box>
{content && !editObjectName ? (
<Box
ref={refDetail}
sx={{
overflow: 'hidden',
transition: 'height 200ms ease-out', // 添加过渡效果
}}
>
{expand ? (
<MarkdownBlock
text={content}
style={{
marginTop: '5px',
borderRadius: '6px',
padding: '4px',
background: '#E6E6E6',
fontSize: '12px',
maxHeight: '240px',
overflowY: 'auto',
border: '1px solid #0003',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
marginBottom: '0',
}}
/>
) : (
<></>
)}
<Box
onClick={e => {
setExpand(v => !v);
e.stopPropagation();
}}
sx={{
position: 'absolute',
right: '18px',
bottom: '14px',
cursor: 'pointer',
userSelect: 'none',
height: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#0002',
borderRadius: '8px',
marginLeft: '4px',
padding: '0 4px',
'&:hover': {
background: '#0003',
},
}}
>
{expand ? (
<UnfoldLessIcon
sx={{ fontSize: '16px', transform: 'rotate(90deg)' }}
/>
) : (
<MoreHorizIcon sx={{ fontSize: '16px' }} />
)}
</Box>
</Box>
) : (
<></>
)}
{editObjectName ? (
<Box sx={{ position: 'relative' }}>
{edit ? (
<>
<TextField
fullWidth
multiline
rows={1}
inputRef={ele => {
if (ele) {
ele.value = inputStringRef.current;
}
}}
onChange={event =>
(inputStringRef.current = event.target.value)
}
size="small"
sx={{
fontSize: '12px',
paddingTop: '10px',
paddingBottom: '10px',
borderRadius: '10px',
border: 'none !important',
borderBottom: 'none !important',
'&::before': {
borderBottom: 'none !important',
border: 'none !important',
},
'& > .MuiInputBase-root': {
border: 'none',
background: '#0001',
'& > .MuiOutlinedInput-notchedOutline': {
border: 'none !important',
},
},
}}
/>
<Box
onClick={() => {
setEdit(false);
globalStorage.form.inputs[editObjectName] =
inputStringRef.current;
}}
sx={{
position: 'absolute',
right: '8px',
bottom: '2px',
cursor: 'pointer',
userSelect: 'none',
height: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backdropFilter: 'contrast(0.6)',
borderRadius: '3px',
marginLeft: '4px',
}}
>
<CheckIcon sx={{ fontSize: '18px', color: '#1d7d09' }} />
</Box>
<Box
onClick={() => setEdit(false)}
sx={{
position: 'absolute',
right: '34px',
bottom: '2px',
cursor: 'pointer',
userSelect: 'none',
height: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backdropFilter: 'contrast(0.6)',
borderRadius: '3px',
marginLeft: '4px',
}}
>
<CloseIcon sx={{ fontSize: '18px', color: '#8e0707' }} />
</Box>
</>
) : (
<>
<MarkdownBlock
text={globalStorage.form.inputs[editObjectName]}
style={{
marginTop: '5px',
borderRadius: '6px',
padding: '4px',
background: '#E6E6E6',
fontSize: '12px',
maxHeight: '240px',
overflowY: 'auto',
border: '1px solid #0003',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
marginBottom: '0',
}}
/>
<Box
onClick={() => {
inputStringRef.current =
globalStorage.form.inputs[editObjectName];
setEdit(true);
}}
sx={{
position: 'absolute',
right: '6px',
bottom: '8px',
cursor: 'pointer',
userSelect: 'none',
height: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '3px',
marginLeft: '4px',
opacity: 0.5,
transition: 'opacity 200ms ease-out',
'&:hover': {
opacity: 1,
},
}}
>
<EditIcon sx={{ fontSize: '14px' }} />
</Box>
</>
)}
</Box>
) : (
<></>
)}
</Box>
);
},
);

View File

@@ -0,0 +1,289 @@
import React from 'react';
import Box from '@mui/material/Box';
import { observer } from 'mobx-react-lite';
import { globalStorage } from '@/storage';
import { ActionType, getAgentActionStyle } from '@/storage/plan';
interface ILine<T = string> {
type: T;
from: string;
to: string;
}
interface RehearsalSvgProps {
cardRefMap: Map<string, React.RefObject<HTMLElement | HTMLDivElement>>;
renderCount: number;
objectStepOrder: string[];
importantLines: ILine<ActionType>[];
actionIsHovered: string | undefined;
}
const getIOLineHeight = (nodeOrder: string[], lines: ILine[]) => {
const edgeHeightIndexMap_ = new Map<number, number[][]>();
const compareFunction = (a: ILine, b: ILine): number => {
const [afrom, ato] = [a.from, a.to];
const [bfrom, bto] = [b.from, b.to];
const afromPos = nodeOrder.indexOf(afrom);
const bfromPos = nodeOrder.indexOf(bfrom);
const atoPos = nodeOrder.indexOf(ato);
const btoPos = nodeOrder.indexOf(bto);
// 如果最小位置相同,则比较位置之间的距离
const aDistance = atoPos - afromPos;
const bDistance = btoPos - bfromPos;
if (aDistance !== bDistance) {
return aDistance - bDistance;
} else {
return afromPos - bfromPos;
}
};
lines.sort(compareFunction);
const isCrossOver = (ptPair: number[], ptList: number[][]) => {
for (const pt of ptList) {
if (pt[1] <= ptPair[0] || pt[0] >= ptPair[1]) {
continue;
}
return true;
}
return false;
};
lines.forEach(line => {
const fromIndex = nodeOrder.indexOf(line.from);
const toIndex = nodeOrder.indexOf(line.to);
let h = 1;
while (
isCrossOver([fromIndex, toIndex], edgeHeightIndexMap_.get(h) || [])
) {
h += 1;
}
edgeHeightIndexMap_.set(h, [
...(edgeHeightIndexMap_.get(h) || []),
[fromIndex, toIndex],
]);
});
const edgeHeightMap_ = new Map<string, number>();
edgeHeightIndexMap_.forEach((pairList, height) => {
// 遍历当前条目的数组将数字替换为数组b中对应的名称
pairList.forEach(pair => {
edgeHeightMap_.set(pair.map(index => nodeOrder[index]).join('.'), height);
});
});
return edgeHeightMap_;
};
const getOffset = (child: HTMLElement, parent: HTMLElement) => {
const parentRect = parent.getBoundingClientRect();
const childRect = child.getBoundingClientRect();
// 计算相对位置
return new DOMRect(
childRect.left - parentRect.left,
childRect.top - parentRect.top,
childRect.width,
childRect.height,
);
};
const calcCurve = (fromCard: DOMRect, toCard: DOMRect, height: number) => {
// calc bezier curve
// from [fromCard.left, fromCard.top+0.5*fromCard.height]
// to [toCard.left, toCard.top+0.5*toCard.height
const h = fromCard.left * height;
return `M ${fromCard.left + fromCard.width},${
fromCard.top + 0.5 * fromCard.height
}
C ${fromCard.left + fromCard.width + 1.5 * h},${
fromCard.top + 0.5 * fromCard.height
},
${toCard.left + toCard.width + 1.5 * h},${toCard.top + 0.5 * toCard.height},
${toCard.left + toCard.width},${toCard.top + 0.5 * toCard.height}`;
};
const calcPath = (objectCard: DOMRect, stepCard: DOMRect, height: number) => {
// console.log('calcPath', fromCard, toCard, height);
const fromCard = objectCard.top < stepCard.top ? objectCard : stepCard;
const toCard = objectCard.top < stepCard.top ? stepCard : objectCard;
const h = fromCard.left * height;
const ptList = [
{ x: fromCard.left, y: fromCard.top + fromCard.height - 10 },
{ x: fromCard.left - h, y: fromCard.top + fromCard.height + 10 - 10 },
{ x: toCard.left - h, y: toCard.top + 0 * toCard.height - 10 + 10 },
{ x: toCard.left, y: toCard.top + 0 * toCard.height + 10 },
];
const path = [
`M ${ptList[0].x},${ptList[0].y}`,
`L ${ptList[1].x},${ptList[1].y}`,
`L ${ptList[2].x},${ptList[2].y}`,
`L ${ptList[3].x},${ptList[3].y}`,
].join(' ');
return path;
};
export default observer(
({
cardRefMap,
renderCount,
objectStepOrder,
importantLines,
actionIsHovered,
}: RehearsalSvgProps) => {
const IOLines = globalStorage.renderingIOLines;
const edgeHeightMap = React.useMemo(
() => getIOLineHeight(objectStepOrder, IOLines),
[objectStepOrder, IOLines],
);
const [ioLineRects, setIOLineRects] = React.useState<
[ILine, DOMRect, DOMRect][]
>([]);
const [importantLineRects, setImportantLineRects] = React.useState<
[ILine, DOMRect, DOMRect][]
>([]);
const refreshCurrentIdRef = React.useRef(-1);
React.useEffect(() => {
refreshCurrentIdRef.current = (refreshCurrentIdRef.current + 1) % 100000;
const currentId = refreshCurrentIdRef.current;
const sleep = (time: number) =>
new Promise(resolve => setTimeout(resolve, time));
(async () => {
let ioAllReady = false;
let importantAllReady = false;
const ioLineRects: [ILine, DOMRect, DOMRect][] = [];
const importantLineRects: [ILine, DOMRect, DOMRect][] = [];
while (true) {
if (refreshCurrentIdRef.current !== currentId) {
return;
}
const rootElement = cardRefMap.get('root')?.current;
if (!rootElement) {
await sleep(5);
continue;
}
if (!ioAllReady) {
ioAllReady = true;
for (const line of IOLines) {
const fromElement = cardRefMap.get(line.from)?.current;
const toElement = cardRefMap.get(line.to)?.current;
if (fromElement && toElement) {
ioLineRects.push([
line,
getOffset(fromElement, rootElement),
getOffset(toElement, rootElement),
]);
} else {
ioAllReady = false;
continue;
}
}
if (!ioAllReady) {
ioLineRects.length = 0;
await sleep(5);
continue;
}
}
if (!importantAllReady) {
importantAllReady = true;
for (const line of importantLines) {
const fromElement = cardRefMap.get(line.from)?.current;
const toElement = cardRefMap.get(line.to)?.current;
if (fromElement && toElement) {
importantLineRects.push([
line,
getOffset(fromElement, rootElement),
getOffset(toElement, rootElement),
]);
} else {
importantAllReady = false;
break;
}
}
if (!importantAllReady) {
importantLineRects.length = 0;
await sleep(5);
continue;
}
}
break;
}
setIOLineRects(ioLineRects);
setImportantLineRects(importantLineRects);
})();
}, [edgeHeightMap, renderCount, cardRefMap]);
const ioLinesEle = React.useMemo(
() =>
ioLineRects.map(([line, from, to]) => {
const key = `${line.from}.${line.to}`;
const height = edgeHeightMap.get(key) || 0;
return (
<path
key={`Rehearsal.IOLine.${key}`}
fill="none"
strokeWidth="3"
stroke={line.type === 'output' ? '#FFCA8C' : '#B9DCB0'}
d={calcPath(
from,
to,
height / (Math.max(...edgeHeightMap.values()) + 1),
)}
/>
);
}),
[ioLineRects],
);
const importantLinesEle = React.useMemo(
() =>
importantLineRects
.sort((a, b) => {
// eslint-disable-next-line no-nested-ternary
return a[0].to === b[0].to
? 0
: a[0].to === actionIsHovered
? 1
: -1;
})
.map(([line, from, to]) => {
const key = `${line.from}.${line.to}`;
return (
<path
key={`Rehearsal.ImportantLine.${key}`}
fill="none"
strokeWidth="3"
stroke={
(getAgentActionStyle(line.type as any) as any).borderColor
}
d={calcCurve(from, to, 0.5)}
strokeOpacity={actionIsHovered === line.to ? 1 : 0.2}
// style={{
// ...(actionIsHovered === line.to
// ? { filter: 'brightness(110%) saturate(100%)' }
// : { filter: 'brightness(100%) saturate(20%)' }),
// }}
/>
);
}),
[importantLineRects, actionIsHovered],
);
const height = cardRefMap
.get('root')
?.current?.querySelector?.('.contents-stack')?.scrollHeight;
return (
<Box
component="svg"
sx={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: height ? `${height}px` : '100%',
zIndex: 999,
pointerEvents: 'none',
}}
>
<g>{ioLinesEle}</g>
<g>{importantLinesEle}</g>
</Box>
);
},
);

View File

@@ -0,0 +1,170 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { Divider, SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import MarkdownBlock from '@/components/MarkdownBlock';
import { globalStorage } from '@/storage';
import type { IExecuteStepHistoryItem } from '@/apis/execute-plan';
import AgentIcon from '@/components/AgentIcon';
import { getAgentActionStyle } from '@/storage/plan';
export interface IStepHistoryItemProps {
item: IExecuteStepHistoryItem;
actionRef: React.RefObject<HTMLDivElement | HTMLElement>;
style?: SxProps;
hoverCallback: (isHovered: boolean) => void;
handleExpand?: () => void;
}
export default observer(
({
item,
actionRef,
hoverCallback,
handleExpand,
style = {},
}: IStepHistoryItemProps) => {
const [expand, setExpand] = React.useState(false);
const refDetail = React.useRef<HTMLDivElement>(null);
// 使用useEffect来更新detail容器的高度
React.useEffect(() => {
if (refDetail.current) {
refDetail.current.style.height = expand
? `${refDetail.current.scrollHeight}px`
: '0px';
}
if (handleExpand) {
let count = 0;
const intervalId = setInterval(() => {
handleExpand();
count++;
if (count >= 20) {
clearInterval(intervalId);
}
}, 10);
}
}, [expand]);
const s = { ...getAgentActionStyle(item.type), ...style } as SxProps;
React.useEffect(() => {
console.log(item);
}, [item]);
return (
<Box
ref={actionRef}
className="step-history-item"
sx={{
userSelect: 'none',
borderRadius: '10px',
padding: '4px',
fontSize: '14px',
position: 'relative',
marginTop: '4px',
backgroundColor: (s as any).backgroundColor,
border: `2px solid ${(s as any).borderColor}`,
}}
onMouseOver={() => hoverCallback(true)}
onMouseOut={() => hoverCallback(false)}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<AgentIcon
name={globalStorage.agentIconMap.get(item.agent)}
style={{ height: '36px', width: 'auto', margin: '0px' }}
tooltipInfo={globalStorage.agentMap.get(item.agent)}
/>
<Box component="span" sx={{ fontWeight: 500, marginLeft: '4px' }}>
{item.agent}
</Box>
<Box component="span" sx={{ fontWeight: 400 }}>
: {item.type}
</Box>
</Box>
{item.result ? (
<Box
ref={refDetail}
sx={{
overflow: 'hidden',
transition: 'height 200ms ease-out', // 添加过渡效果
}}
>
{expand ? (
<>
<Divider
sx={{
margin: '4px 0px',
borderBottom: '2px dashed', // 设置为虚线
borderColor: '#0003',
}}
/>
<Box
sx={{
marginLeft: '6px',
marginBottom: '4px',
color: '#0009',
fontWeight: 400,
}}
>
{item.description}
</Box>
<MarkdownBlock
text={item.result}
style={{
marginTop: '5px',
borderRadius: '10px',
padding: '6px',
background: '#FFF9',
fontSize: '12px',
maxHeight: '240px',
overflowY: 'auto',
border: '1px solid #0003',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
marginBottom: '5px',
}}
/>
</>
) : (
<></>
)}
<Box
onClick={e => {
setExpand(v => !v);
e.stopPropagation();
}}
sx={{
position: 'absolute',
right: '8px',
// bottom: '12px',
top: '24px',
cursor: 'pointer',
userSelect: 'none',
height: '14px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#0002',
borderRadius: '8px',
marginLeft: '4px',
padding: '0 4px',
'&:hover': {
background: '#0003',
},
}}
>
{expand ? (
<UnfoldLessIcon
sx={{ fontSize: '16px', transform: 'rotate(90deg)' }}
/>
) : (
<MoreHorizIcon sx={{ fontSize: '16px' }} />
)}
</Box>
</Box>
) : (
<></>
)}
</Box>
);
},
);

View File

@@ -0,0 +1,86 @@
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import React from 'react';
import StepHistoryItem from './StepHistoryItem';
import type { IExecuteStepHistoryItem } from '@/apis/execute-plan';
import { globalStorage } from '@/storage';
export interface IStepNodeProps {
name: string;
history: IExecuteStepHistoryItem[];
touchRef: (
id: string,
existingRef?: React.RefObject<HTMLElement>,
) => React.RefObject<HTMLElement>;
_ref?: React.RefObject<HTMLDivElement | HTMLElement>;
style?: SxProps;
id: string;
actionHoverCallback: (actionName: string | undefined) => void;
handleExpand?: () => void;
}
export default observer(
({
style = {},
name,
history,
_ref,
touchRef,
id,
actionHoverCallback,
handleExpand,
}: IStepNodeProps) => {
return (
<Box
sx={{
userSelect: 'none',
borderRadius: '12px',
background: '#F6F6F6',
padding: '10px',
border: '2px solid #E5E5E5',
fontSize: '14px',
position: 'relative',
display: 'flex',
flexDirection: 'column',
cursor: 'pointer',
transition: 'all 200ms ease-in-out',
'&:hover': {
border: '2px solid #0002',
backgroundImage: 'linear-gradient(0, #0000000A, #0000000A)',
},
'& > .step-history-item:first-of-type': {
marginTop: '10px',
},
...style,
}}
ref={_ref}
onClick={() => {
if (id) {
globalStorage.setFocusingStepTaskId(id);
}
}}
>
<Box component="span" sx={{ fontWeight: 800 }}>
{name}
</Box>
{history.map((item, index) => (
<StepHistoryItem
// eslint-disable-next-line react/no-array-index-key
key={`${item.id}.${item.agent}.${index}`}
item={item}
actionRef={touchRef(`Action.${name}.${item.id}`)!}
hoverCallback={(isHovered: boolean) => {
// console.log(isHovered, 'hover', `action.${name}.${item.id}`);
actionHoverCallback(
isHovered ? `Action.${name}.${item.id}` : undefined,
);
}}
handleExpand={handleExpand}
/>
))}
</Box>
);
},
);

View File

@@ -0,0 +1,219 @@
import React from 'react';
import throttle from 'lodash/throttle';
import { SxProps } from '@mui/material';
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 RefreshIcon from '@mui/icons-material/Refresh';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import CircularProgress from '@mui/material/CircularProgress';
import ObjectNode from './ObjectNode';
import StepNode from './StepNode';
import RehearsalSvg from './RehearsalSvg';
import { globalStorage } from '@/storage';
import Title from '@/components/Title';
import { ExecuteNodeType } from '@/apis/execute-plan';
import { useResize } from '@/utils/resize-hook';
export default observer(({ style = {} }: { style?: SxProps }) => {
const [renderCount, setRenderCount] = React.useState(0);
const cardRefMap = React.useMemo(
() => new Map<string, React.RefObject<HTMLElement>>(),
[],
);
const touchRef = React.useCallback(
(id: string, existingRef?: React.RefObject<HTMLElement>) => {
if (existingRef) {
cardRefMap.set(id, existingRef);
return existingRef;
}
if (cardRefMap.has(id)) {
return cardRefMap.get(id)!;
} else {
cardRefMap.set(id, React.createRef<HTMLElement>());
return cardRefMap.get(id)!;
}
},
[cardRefMap],
);
const render = React.useMemo(
() =>
throttle(
() =>
// eslint-disable-next-line max-nested-callbacks
requestAnimationFrame(() => setRenderCount(old => (old + 1) % 100)),
5,
{
leading: false,
trailing: true,
},
),
[],
);
const stackRef = useResize(render);
const [actionIsHovered, setActionIsHovered] = React.useState<
string | undefined
>();
return (
<Box
sx={{
background: '#FFF',
border: '3px solid #E1E1E1',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
width: '100%',
...style,
}}
>
<Box
sx={{
fontWeight: 600,
fontSize: '18px',
userSelect: 'none',
padding: '0 6px 0 0',
position: 'relative',
display: 'flex',
alignItems: 'center',
}}
>
<Title title="Execution Result" />
<Box
sx={{
flexGrow: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'end',
}}
>
{globalStorage.api.planExecuting ? (
<CircularProgress
sx={{
width: '12px !important',
height: '12px !important',
marginLeft: '6px',
}}
/>
) : (
<></>
)}
{globalStorage.api.planExecuting ? (
<></>
) : (
<>
<IconButton
disabled={!globalStorage.logManager.outdate}
size="small"
onClick={() => globalStorage.logManager.clearLog()}
sx={{
color: 'error.main',
'&:hover': {
color: 'error.dark',
},
}}
>
<RefreshIcon />
</IconButton>
<IconButton
disabled={!globalStorage.api.ready}
size="small"
onClick={() => globalStorage.executePlan(Infinity)}
sx={{
color: 'primary.main',
'&:hover': {
color: 'primary.dark',
},
}}
>
<PlayArrowIcon />
</IconButton>
</>
)}
</Box>
</Box>
<Box
sx={{
position: 'relative',
height: 0,
flexGrow: 1,
overflowY: 'auto',
overflowX: 'hidden',
padding: '6px 6px',
}}
onScroll={() => {
globalStorage.renderLines({ delay: 0, repeat: 2 });
}}
>
<Box sx={{ height: '100%', width: '100%' }}>
<Box sx={{ position: 'relative' }} ref={touchRef('root', stackRef)}>
<Stack
spacing="12px"
sx={{
position: 'absolute',
zIndex: 1,
marginLeft: '12px',
marginRight: '12px',
width: 'calc(100% - 24px)',
paddingBottom: '24px',
}}
className="contents-stack"
>
{Object.keys(globalStorage.form.inputs).map(name => (
<ObjectNode
key={`rehearsal.object.${name}`}
name={name}
content=""
stepId=""
editObjectName={name}
_ref={touchRef(`Object.${name}`)}
handleExpand={() => {
// render();
setRenderCount(old => (old + 1) % 100);
}}
/>
))}
{globalStorage.logManager.renderingLog.map(node =>
node.type === ExecuteNodeType.Step ? (
<StepNode
key={`rehearsal.step.${node.id}`}
id={node.stepId}
_ref={touchRef(`Step.${node.id}`, node.ref)}
name={node.id}
history={node.history}
touchRef={touchRef}
actionHoverCallback={setActionIsHovered}
handleExpand={() => {
// render();
setRenderCount(old => (old + 1) % 100);
}}
/>
) : (
<ObjectNode
key={`rehearsal.object.${node.id}`}
_ref={touchRef(`Object.${node.id}`, node.ref)}
name={node.id}
content={node.content}
stepId={node.stepId}
handleExpand={() => {
// render();
setRenderCount(old => (old + 1) % 100);
}}
/>
),
)}
</Stack>
<RehearsalSvg
renderCount={renderCount}
cardRefMap={cardRefMap}
actionIsHovered={actionIsHovered}
objectStepOrder={globalStorage.rehearsalSvgObjectOrder}
importantLines={globalStorage.rehearsalSvgLines}
/>
</Box>
</Box>
</Box>
</Box>
);
});

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Resizable } from 're-resizable';
import { globalStorage } from '@/storage';
export default React.memo<{
children: React.ReactNode;
columnWidth: string;
}>(({ children, columnWidth }) => {
return (
<Resizable
defaultSize={{ height: '100%', width: columnWidth }}
enable={{
top: false,
right: true,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
minWidth="50px"
maxWidth="75%"
style={{
flexShrink: 0,
display: 'flex',
flexWrap: 'nowrap',
position: 'relative',
flexGrow: 0,
}}
handleClasses={{
right: 'side-resize-bar',
}}
handleStyles={{
right: {
width: '4px',
right: '1px',
},
}}
onResize={() => globalStorage.renderLines({ delay: 0, repeat: 3 })}
>
{children}
</Resizable>
);
});

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;

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
export default React.memo<{ title: string; style?: SxProps }>(
({ title, style = {} }) => {
return (
<Box
component="span"
sx={{
position: 'relative',
height: '35px', // 容器的高度
userSelect: 'none',
display: 'flex',
marginTop: '-4px',
...style,
}}
>
<Box
component="span"
sx={{
display: 'inline-flex',
height: '100%',
backgroundColor: '#E1E1E1',
alignItems: 'center',
justifyContent: 'center',
color: 'primary.main',
fontWeight: 800,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
paddingLeft: '10px',
paddingRight: '10px',
fontSize: '20px',
}}
>
{title}
</Box>
<Box
component="span"
sx={{
height: 0,
width: 0,
borderRight: '26px solid transparent',
borderTop: '35px solid #E1E1E1', // 与矩形相同的颜色
}}
/>
</Box>
);
},
);

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import FormControl from '@mui/material/FormControl';
import FilledInput from '@mui/material/FilledInput';
import TelegramIcon from '@mui/icons-material/Telegram';
import CircularProgress from '@mui/material/CircularProgress';
import { globalStorage } from '@/storage';
export interface UserGoalInputProps {
style?: SxProps;
}
export default observer(({ style = {} }: UserGoalInputProps) => {
const inputRef = React.useRef<string>('');
const inputElementRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (inputElementRef.current) {
if (globalStorage.planManager) {
inputElementRef.current.value = globalStorage.planManager.goal;
} else {
inputElementRef.current.value = globalStorage.briefGoal;
}
}
}, [globalStorage.planManager]);
return (
<FormControl
sx={{
position: 'relative',
...style,
}}
>
<FilledInput
disabled={
globalStorage.api.busy ||
!globalStorage.api.agentsReady ||
globalStorage.api.planReady
}
placeholder="Yout Goal"
fullWidth
inputRef={inputElementRef}
onChange={event => (inputRef.current = event.target.value)}
size="small"
sx={{
borderRadius: '10px',
background: '#E1E1E1',
borderBottom: 'none !important',
'&::before': {
borderBottom: 'none !important',
},
'& > input': {
padding: '10px',
},
}}
startAdornment={
<Box
sx={{
color: '#4A9C9E',
fontWeight: 800,
fontSize: '18px',
textWrap: 'nowrap',
userSelect: 'none',
}}
>
\General Goal:
</Box>
}
/>
{globalStorage.api.planGenerating ? (
<CircularProgress
sx={{
position: 'absolute',
right: '12px',
top: '20px',
width: '24px !important',
height: '24px !important',
}}
/>
) : (
<IconButton
disabled={
globalStorage.api.busy ||
!globalStorage.api.agentsReady ||
globalStorage.api.planReady
}
color="primary"
aria-label="提交"
sx={{ position: 'absolute', right: '6px', top: '12px' }}
onClick={() => {
globalStorage.form.goal = inputRef.current;
globalStorage.generatePlanBase();
}}
>
<TelegramIcon />
</IconButton>
)}
</FormControl>
);
});

View File

@@ -0,0 +1,153 @@
import React from 'react';
import Box from '@mui/material/Box';
import { observer } from 'mobx-react-lite';
import { min, max } from 'lodash';
import { globalStorage } from '@/storage';
const mergeRects = (...rects: DOMRect[]) => {
const minX = min(rects.map(rect => rect.left)) ?? 0;
const minY = min(rects.map(rect => rect.top)) ?? 0;
const maxX = max(rects.map(rect => rect.left + rect.width)) ?? 0;
const maxY = max(rects.map(rect => rect.top + rect.height)) ?? 0;
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
};
const drawConnectors = (() => {
let lastRectString = '';
let lastRenderCache = <></>;
return (...rects: DOMRect[]) => {
const rectString = rects
.map(rect => `${rect.left},${rect.top},${rect.width},${rect.height}`)
.join('');
if (rectString === lastRectString) {
return lastRenderCache;
}
lastRectString = rectString;
const margin = 5;
const connectors: React.ReactNode[] = [];
for (const rect of rects) {
connectors.push(
<g>
<line
x1={rect.left - margin}
y1={rect.top}
x2={rect.left - margin}
y2={rect.top + rect.height}
stroke="#43A8AA"
strokeWidth="4"
></line>
<line
x1={rect.left + rect.width + margin}
y1={rect.top}
x2={rect.left + rect.width + margin}
y2={rect.top + rect.height}
stroke="#43A8AA"
strokeWidth="4"
></line>
</g>,
);
}
for (let i = 0; i <= rects.length - 2; i++) {
const rect1 = rects[i];
const rect2 = rects[i + 1];
connectors.push(
<g>
<path
d={`M ${rect1.left + rect1.width + margin},${rect1.top}
L ${rect2.left - margin},${rect2.top}
${rect2.left - margin},${rect2.top + rect2.height}
${rect1.left + rect1.width + margin},${
rect1.top + rect1.height
}`}
fill="#86D2CE"
fillOpacity={0.5}
// strokeWidth="2"
></path>
</g>,
);
}
lastRenderCache = <>{connectors}</>;
return lastRenderCache;
};
})();
export default observer(() => {
const [drawCallId, setDrawCallId] = React.useState(0);
React.useEffect(() => {
globalStorage.setRenderLinesMethod(() =>
setDrawCallId(old => (old + 1) % 100),
);
return () => globalStorage.setRenderLinesMethod(undefined);
}, []);
const [connectors, setConnectors] = React.useState(<></>);
React.useEffect(() => {
try {
const { ElementToLink } = globalStorage;
if (!globalStorage.focusingStepTask) {
setConnectors(<></>);
}
const outlinePosition =
ElementToLink[0]?.current?.getBoundingClientRect?.();
if (!outlinePosition) {
return;
}
const agentRects = ElementToLink[1]
.map(ele => ele.current?.getBoundingClientRect?.() as DOMRect)
.filter(rect => rect);
if (agentRects.length === 0) {
return;
}
const agentsPosition = mergeRects(...agentRects);
const descriptionPosition =
ElementToLink[2]?.current?.getBoundingClientRect?.();
if (!descriptionPosition) {
return;
}
const LogRects = ElementToLink[3]
.map(ele => ele?.current?.getBoundingClientRect?.() as DOMRect)
.filter(rect => rect);
if (LogRects.length > 0) {
const logPosition = mergeRects(...LogRects);
logPosition.x -= 5;
logPosition.width += 10;
setConnectors(
drawConnectors(
outlinePosition,
agentsPosition,
descriptionPosition,
logPosition,
),
);
} else {
setConnectors(
drawConnectors(outlinePosition, agentsPosition, descriptionPosition),
);
}
} catch (e) {
console.error(e);
}
}, [drawCallId, globalStorage.ElementToLink]);
return (
<Box
component="svg"
sx={{
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
right: 0,
height: '100%',
width: '100%',
pointerEvents: 'none',
zIndex: 1,
clipPath: 'inset(136px 0px 5px 0px)',
}}
>
{connectors}
</Box>
);
});