setup(frontend): rename frontend-react
This commit is contained in:
@@ -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';
|
||||
561
frontend-react/src/components/AgentAssignment/index.tsx
Normal file
561
frontend-react/src/components/AgentAssignment/index.tsx
Normal 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 */
|
||||
165
frontend-react/src/components/AgentBoard/AgentCard.tsx
Normal file
165
frontend-react/src/components/AgentBoard/AgentCard.tsx
Normal 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>
|
||||
));
|
||||
123
frontend-react/src/components/AgentBoard/DutyItem.tsx
Normal file
123
frontend-react/src/components/AgentBoard/DutyItem.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
163
frontend-react/src/components/AgentBoard/index.tsx
Normal file
163
frontend-react/src/components/AgentBoard/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
),
|
||||
);
|
||||
@@ -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 <></>;
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
28
frontend-react/src/components/AgentHiring/index.tsx
Normal file
28
frontend-react/src/components/AgentHiring/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
119
frontend-react/src/components/AgentIcon/agents.ts
Normal file
119
frontend-react/src/components/AgentIcon/agents.ts
Normal 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,
|
||||
};
|
||||
119
frontend-react/src/components/AgentIcon/index.tsx
Normal file
119
frontend-react/src/components/AgentIcon/index.tsx
Normal 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';
|
||||
120
frontend-react/src/components/FloatWindow/index.tsx
Normal file
120
frontend-react/src/components/FloatWindow/index.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
231
frontend-react/src/components/HeadBar/index.tsx
Normal file
231
frontend-react/src/components/HeadBar/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
22
frontend-react/src/components/LoadingMask/index.tsx
Normal file
22
frontend-react/src/components/LoadingMask/index.tsx
Normal 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>
|
||||
));
|
||||
1210
frontend-react/src/components/MarkdownBlock/github-mardown.css
Normal file
1210
frontend-react/src/components/MarkdownBlock/github-mardown.css
Normal file
File diff suppressed because it is too large
Load Diff
33
frontend-react/src/components/MarkdownBlock/index.tsx
Normal file
33
frontend-react/src/components/MarkdownBlock/index.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
702
frontend-react/src/components/MarkdownBlock/markdown-style.css
Normal file
702
frontend-react/src/components/MarkdownBlock/markdown-style.css
Normal 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;
|
||||
}
|
||||
344
frontend-react/src/components/Outline/Cards.tsx
Normal file
344
frontend-react/src/components/Outline/Cards.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import React, { useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { IconButton, SxProps } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import AdjustIcon from '@mui/icons-material/Adjust';
|
||||
import { ObjectProps, ProcessProps } from './interface';
|
||||
import AgentIcon from '@/components/AgentIcon';
|
||||
import { globalStorage } from '@/storage';
|
||||
|
||||
export interface IEditObjectProps {
|
||||
finishEdit: (objectName: string) => void;
|
||||
}
|
||||
|
||||
export const EditObjectCard: React.FC<IEditObjectProps> = React.memo(
|
||||
({ finishEdit }) => {
|
||||
const handleKeyPress = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
finishEdit(event.target.value);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<TextField
|
||||
onKeyPress={handleKeyPress}
|
||||
sx={{
|
||||
backgroundColor: '#D9D9D9',
|
||||
borderRadius: '6px',
|
||||
padding: '8px',
|
||||
userSelect: 'none',
|
||||
margin: '6px 0px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface IHoverIconButtonProps {
|
||||
onAddClick: () => void;
|
||||
isActive: boolean;
|
||||
style: SxProps;
|
||||
responseToHover?: boolean;
|
||||
addOrRemove: boolean | undefined; // true for add, false for remove,undefined for adjust
|
||||
}
|
||||
|
||||
const HoverIconButton: React.FC<IHoverIconButtonProps> = ({
|
||||
onAddClick,
|
||||
isActive,
|
||||
style,
|
||||
addOrRemove,
|
||||
responseToHover = true,
|
||||
}) => {
|
||||
const [addIconHover, setAddIconHover] = useState(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseOver={() => {
|
||||
setAddIconHover(true);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setAddIconHover(false);
|
||||
}}
|
||||
onClick={() => {
|
||||
onAddClick();
|
||||
}}
|
||||
sx={{ ...style, justifySelf: 'start' }}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'primary',
|
||||
'&:hover': {
|
||||
color: 'primary.dark',
|
||||
},
|
||||
padding: '0px',
|
||||
borderRadius: 10,
|
||||
border: '1px dotted #333',
|
||||
visibility:
|
||||
(responseToHover && addIconHover) || isActive
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: '1.25rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{addOrRemove === undefined ? <AdjustIcon /> : <></>}
|
||||
{addOrRemove === true ? <AddIcon /> : <></>}
|
||||
{addOrRemove === false ? <RemoveIcon /> : <></>}
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface IEditableBoxProps {
|
||||
text: string;
|
||||
inputCallback: (text: string) => void;
|
||||
}
|
||||
|
||||
const EditableBox: React.FC<IEditableBoxProps> = ({ text, inputCallback }) => {
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const handleDoubleClick = () => {
|
||||
setIsEditable(true);
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputCallback(event.target.value);
|
||||
setIsEditable(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{isEditable ? (
|
||||
<TextField
|
||||
defaultValue={text}
|
||||
multiline
|
||||
onKeyPress={handleKeyPress}
|
||||
onBlur={() => setIsEditable(false)} // 失去焦点时也关闭编辑状态
|
||||
autoFocus
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
// 目标 MUI 的输入基础根元素
|
||||
padding: '0px 0px', // 你可以设置为你希望的内边距值
|
||||
},
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={{
|
||||
color: '#707070',
|
||||
// textDecoration: 'underline',
|
||||
// textDecorationColor: '#C1C1C1',
|
||||
borderBottom: '1.5px solid #C1C1C1',
|
||||
fontSize: '15px',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IObjectCardProps {
|
||||
object: ObjectProps;
|
||||
isAddActive?: boolean;
|
||||
handleAddActive?: (objectName: string) => void;
|
||||
addOrRemove?: boolean;
|
||||
}
|
||||
|
||||
export const ObjectCard = React.memo<IObjectCardProps>(
|
||||
({
|
||||
object,
|
||||
isAddActive = false,
|
||||
handleAddActive = (objectName: string) => {
|
||||
console.log(objectName);
|
||||
},
|
||||
addOrRemove = true,
|
||||
}) => {
|
||||
const onAddClick = () => {
|
||||
handleAddActive(object.name);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
// maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<HoverIconButton
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '100%',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)translateX(-50%)',
|
||||
}}
|
||||
onAddClick={onAddClick}
|
||||
isActive={isAddActive}
|
||||
responseToHover={false}
|
||||
addOrRemove={addOrRemove}
|
||||
/>
|
||||
<Box
|
||||
ref={object.cardRef}
|
||||
sx={{
|
||||
backgroundColor: '#F6F6F6',
|
||||
borderRadius: '15px',
|
||||
border: '2px solid #E5E5E5',
|
||||
padding: '10px 4px',
|
||||
userSelect: 'none',
|
||||
margin: '12px 0px',
|
||||
maxWidth: '100%',
|
||||
wordWrap: 'break-word',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: 800,
|
||||
textAlign: 'center',
|
||||
color: '#222',
|
||||
}}
|
||||
>
|
||||
{object.name}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export interface IProcessCardProps {
|
||||
process: ProcessProps;
|
||||
handleProcessClick: (stepId: string) => void;
|
||||
isFocusing: boolean;
|
||||
isAddActive?: boolean;
|
||||
handleAddActive?: (objectName: string) => void;
|
||||
|
||||
handleEditContent: (stepTaskId: string, newContent: string) => void;
|
||||
// handleSizeChange: () => void;
|
||||
}
|
||||
|
||||
export const ProcessCard: React.FC<IProcessCardProps> = React.memo(
|
||||
({
|
||||
process,
|
||||
handleProcessClick,
|
||||
isFocusing,
|
||||
isAddActive = false,
|
||||
handleAddActive = (objectName: string) => {
|
||||
console.log(objectName);
|
||||
},
|
||||
// handleSizeChange,
|
||||
handleEditContent,
|
||||
}) => {
|
||||
const onAddClick = () => {
|
||||
handleAddActive(process.id);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
// width: '100%',
|
||||
}}
|
||||
>
|
||||
<HoverIconButton
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)translateX(-50%)',
|
||||
}}
|
||||
onAddClick={onAddClick}
|
||||
isActive={isAddActive}
|
||||
addOrRemove={undefined}
|
||||
/>
|
||||
<Box
|
||||
ref={process.cardRef}
|
||||
sx={{
|
||||
backgroundColor: '#F6F6F6',
|
||||
borderRadius: '15px',
|
||||
padding: '8px',
|
||||
margin: '18px 0px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
border: isFocusing ? '2px solid #43b2aa' : '2px solid #E5E5E5',
|
||||
transition: 'all 80ms ease-in-out',
|
||||
'&:hover': {
|
||||
border: isFocusing ? '2px solid #03a89d' : '2px solid #b3b3b3',
|
||||
backgroundImage: 'linear-gradient(0, #0001, #0001)',
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => handleProcessClick(process.id)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 800,
|
||||
textAlign: 'center',
|
||||
color: '#222',
|
||||
marginTop: '4px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
{process.name}
|
||||
</Box>
|
||||
{/* Assuming AgentIcon is another component */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
margin: '8px 0',
|
||||
}}
|
||||
>
|
||||
{process.agents.map(agentName => (
|
||||
<AgentIcon
|
||||
key={`outline.${process.name}.${agentName}`}
|
||||
name={globalStorage.agentMap.get(agentName)?.icon ?? 'unknown'}
|
||||
style={{
|
||||
width: '40px',
|
||||
height: 'auto',
|
||||
marginRight: '3px',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
tooltipInfo={globalStorage.agentMap.get(agentName)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{isFocusing && (
|
||||
<Box onClick={e => e.stopPropagation()}>
|
||||
<Divider
|
||||
sx={{
|
||||
margin: '5px 0px',
|
||||
borderBottom: '2px dashed', // 设置为虚线
|
||||
borderColor: '#d4d4d4',
|
||||
}}
|
||||
/>
|
||||
<EditableBox
|
||||
text={process.content}
|
||||
inputCallback={(text: string) => {
|
||||
handleEditContent(process.id, text);
|
||||
// handleEditStep(step.name, { ...step, task: text });
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const Card: React.FC = React.memo(() => {
|
||||
return <></>; // Replace with your component JSX
|
||||
});
|
||||
|
||||
export default Card;
|
||||
241
frontend-react/src/components/Outline/D3Graph.tsx
Normal file
241
frontend-react/src/components/Outline/D3Graph.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
// D3Graph.tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface SvgLineProp {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
type: string;
|
||||
key: string;
|
||||
stepTaskId: string;
|
||||
stepName: string;
|
||||
objectName: string;
|
||||
}
|
||||
|
||||
const getRefOffset = (
|
||||
child: React.RefObject<HTMLElement>,
|
||||
grandParent: React.RefObject<HTMLElement>,
|
||||
) => {
|
||||
const offset = { top: 0, left: 0, width: 0, height: 0 };
|
||||
if (!child.current || !grandParent.current) {
|
||||
return offset;
|
||||
}
|
||||
let node = child.current;
|
||||
// Traverse up the DOM tree until we reach the grandparent or run out of elements
|
||||
while (node && node !== grandParent.current) {
|
||||
offset.top += node.offsetTop;
|
||||
offset.left += node.offsetLeft;
|
||||
// Move to the offset parent (the nearest positioned ancestor)
|
||||
node = node.offsetParent as HTMLElement;
|
||||
}
|
||||
// If we didn't reach the grandparent, return null
|
||||
if (node !== grandParent.current) {
|
||||
return offset;
|
||||
}
|
||||
offset.width = child.current.offsetWidth;
|
||||
offset.height = child.current.offsetHeight;
|
||||
return offset;
|
||||
};
|
||||
// 辅助函数来计算均值和最大值
|
||||
const calculateLineMetrics = (
|
||||
cardRect: Map<
|
||||
string,
|
||||
{
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
>,
|
||||
prefix: string,
|
||||
) => {
|
||||
const filteredRects = Array.from(cardRect.entries())
|
||||
.filter(([key]) => key.startsWith(prefix))
|
||||
.map(([, rect]) => rect);
|
||||
|
||||
return {
|
||||
x:
|
||||
filteredRects.reduce(
|
||||
(acc, rect) => acc + (rect.left + 0.5 * rect.width),
|
||||
0,
|
||||
) / filteredRects.length,
|
||||
y2: Math.max(...filteredRects.map(rect => rect.top + rect.height), 0),
|
||||
};
|
||||
};
|
||||
interface D3GraphProps {
|
||||
// objProCards_: ObjectProcessCardProps[];
|
||||
cardRefMap: Map<string, React.RefObject<HTMLElement>>;
|
||||
relations: {
|
||||
type: string;
|
||||
stepTaskId: string;
|
||||
stepCardName: string;
|
||||
objectCardName: string;
|
||||
}[];
|
||||
focusingStepId: string;
|
||||
forceRender: number;
|
||||
}
|
||||
|
||||
const D3Graph: React.FC<D3GraphProps> = ({
|
||||
cardRefMap,
|
||||
relations,
|
||||
forceRender,
|
||||
focusingStepId,
|
||||
}) => {
|
||||
const [svgLineProps, setSvgLineProps] = useState<SvgLineProp[]>([]);
|
||||
const [objectLine, setObjectLine] = useState({
|
||||
x: 0,
|
||||
y2: 0,
|
||||
});
|
||||
const [processLine, setProcessLine] = useState({
|
||||
x: 0,
|
||||
y2: 0,
|
||||
});
|
||||
const cardRect = new Map<
|
||||
string,
|
||||
{ top: number; left: number; width: number; height: number }
|
||||
>();
|
||||
React.useEffect(() => {
|
||||
const svgLines_ = relations
|
||||
.filter(({ stepCardName, objectCardName }) => {
|
||||
return cardRefMap.has(stepCardName) && cardRefMap.has(objectCardName);
|
||||
})
|
||||
.map(({ type, stepTaskId, stepCardName, objectCardName }) => {
|
||||
const stepRect = getRefOffset(
|
||||
cardRefMap.get(stepCardName)!,
|
||||
cardRefMap.get('root')!,
|
||||
);
|
||||
cardRect.set(stepCardName, stepRect);
|
||||
const objectRect = getRefOffset(
|
||||
cardRefMap.get(objectCardName)!,
|
||||
cardRefMap.get('root')!,
|
||||
);
|
||||
cardRect.set(objectCardName, objectRect);
|
||||
|
||||
return {
|
||||
key: `${type}.${stepCardName}.${objectCardName}`,
|
||||
stepTaskId,
|
||||
stepName: stepCardName,
|
||||
objectName: objectCardName,
|
||||
type,
|
||||
x1: objectRect.left + objectRect.width,
|
||||
y1: objectRect.top + 0.5 * objectRect.height,
|
||||
x2: stepRect.left,
|
||||
y2: stepRect.top + 0.5 * stepRect.height,
|
||||
};
|
||||
});
|
||||
const objectMetrics = calculateLineMetrics(cardRect, 'object');
|
||||
const processMetrics = calculateLineMetrics(cardRect, 'process');
|
||||
const maxY2 = Math.max(objectMetrics.y2, processMetrics.y2);
|
||||
|
||||
setObjectLine({ ...objectMetrics, y2: maxY2 });
|
||||
setProcessLine({ ...processMetrics, y2: maxY2 });
|
||||
|
||||
setSvgLineProps(svgLines_);
|
||||
}, [forceRender, focusingStepId, relations]);
|
||||
|
||||
return (
|
||||
// <Box
|
||||
// sx={{
|
||||
// width: '100%',
|
||||
// height: '100%',
|
||||
// position: 'absolute',
|
||||
// zIndex: 1,
|
||||
// }}
|
||||
// >
|
||||
<svg
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: objectLine.y2 + 50,
|
||||
zIndex: 1,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<marker
|
||||
id="arrowhead"
|
||||
markerWidth="4"
|
||||
markerHeight="4"
|
||||
refX="2"
|
||||
refY="2"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M0,0 L4,2 L0,4 z" fill="#E5E5E5" />
|
||||
</marker>
|
||||
<marker
|
||||
id="starter"
|
||||
markerWidth="4"
|
||||
markerHeight="4"
|
||||
refX="0"
|
||||
refY="2"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M0,0 L1,0 L1,4 L0,4 z" fill="#E5E5E5" />
|
||||
</marker>
|
||||
|
||||
<g>
|
||||
<text
|
||||
x={objectLine.x}
|
||||
y="15"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#898989"
|
||||
fontWeight="800"
|
||||
>
|
||||
Key Object
|
||||
</text>
|
||||
<line
|
||||
x1={objectLine.x}
|
||||
y1={30}
|
||||
x2={objectLine.x}
|
||||
y2={objectLine.y2 + 30}
|
||||
stroke="#E5E5E5"
|
||||
strokeWidth="8"
|
||||
markerEnd="url(#arrowhead)"
|
||||
markerStart="url(#starter)"
|
||||
></line>
|
||||
<text
|
||||
x={processLine.x}
|
||||
y="15"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#898989"
|
||||
fontWeight="800"
|
||||
>
|
||||
Process
|
||||
</text>
|
||||
<line
|
||||
x1={processLine.x}
|
||||
y1={30}
|
||||
x2={processLine.x}
|
||||
y2={processLine.y2 + 30}
|
||||
stroke="#E5E5E5"
|
||||
strokeWidth="8"
|
||||
markerEnd="url(#arrowhead)"
|
||||
markerStart="url(#starter)"
|
||||
></line>
|
||||
</g>
|
||||
<g>
|
||||
{svgLineProps.map(edgeValue => (
|
||||
<line
|
||||
key={edgeValue.key}
|
||||
x1={edgeValue.x1}
|
||||
y1={edgeValue.y1}
|
||||
x2={edgeValue.x2}
|
||||
y2={edgeValue.y2}
|
||||
strokeWidth="5"
|
||||
stroke={edgeValue.type === 'output' ? '#FFCA8C' : '#B9DCB0'}
|
||||
strokeOpacity={
|
||||
focusingStepId === edgeValue.stepTaskId ? '100%' : '20%'
|
||||
}
|
||||
></line>
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
// </Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default D3Graph;
|
||||
281
frontend-react/src/components/Outline/OutlineView.tsx
Normal file
281
frontend-react/src/components/Outline/OutlineView.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
// 已移除对d3的引用
|
||||
import React, { useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import D3Graph from './D3Graph';
|
||||
import { ObjectCard, ProcessCard, EditObjectCard } from './Cards';
|
||||
import { RectWatcher } from './RectWatcher';
|
||||
import { globalStorage } from '@/storage';
|
||||
|
||||
export default observer(() => {
|
||||
const { outlineRenderingStepTaskCards, focusingStepTaskId } = globalStorage;
|
||||
const [renderCount, setRenderCount] = useState(0);
|
||||
const [addObjectHover, setAddObjectHover] = useState(false);
|
||||
const [isAddingObject, setIsAddingObject] = useState(false);
|
||||
const [activeObjectAdd, setActiveObjectAdd] = useState('');
|
||||
const [activeProcessIdAdd, setactiveProcessIdAdd] = useState('');
|
||||
|
||||
const handleProcessClick = (processName: string) => {
|
||||
if (processName === focusingStepTaskId) {
|
||||
globalStorage.setFocusingStepTaskId(undefined);
|
||||
} else {
|
||||
globalStorage.setFocusingStepTaskId(processName);
|
||||
}
|
||||
};
|
||||
|
||||
const finishAddInitialObject = (objectName: string) => {
|
||||
setIsAddingObject(false);
|
||||
globalStorage.addUserInput(objectName);
|
||||
};
|
||||
|
||||
const addInitialObject = () => setIsAddingObject(true);
|
||||
|
||||
const handleObjectAdd = (objectName: string) =>
|
||||
setActiveObjectAdd(activeObjectAdd === objectName ? '' : objectName);
|
||||
const handleProcessAdd = (processName: string) =>
|
||||
setactiveProcessIdAdd(
|
||||
activeProcessIdAdd === processName ? '' : processName,
|
||||
);
|
||||
|
||||
const cardRefMap = new Map<string, React.RefObject<HTMLElement>>();
|
||||
const getCardRef = (cardId: string) => {
|
||||
if (cardRefMap.has(cardId)) {
|
||||
return cardRefMap.get(cardId);
|
||||
} else {
|
||||
cardRefMap.set(cardId, React.createRef<HTMLElement>());
|
||||
return cardRefMap.get(cardId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditContent = (stepTaskId: string, newContent: string) => {
|
||||
globalStorage.setStepTaskContent(stepTaskId, newContent);
|
||||
};
|
||||
const WidthRatio = ['30%', '15%', '52.5%'];
|
||||
|
||||
const [cardRefMapReady, setCardRefMapReady] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCardRefMapReady(true);
|
||||
setRenderCount(old => (old + 1) % 10);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeObjectAdd !== '' && activeProcessIdAdd !== '') {
|
||||
if (
|
||||
outlineRenderingStepTaskCards
|
||||
.filter(({ id }) => id === activeProcessIdAdd)[0]
|
||||
.inputs.includes(activeObjectAdd)
|
||||
) {
|
||||
globalStorage.removeStepTaskInput(activeProcessIdAdd, activeObjectAdd);
|
||||
} else {
|
||||
globalStorage.addStepTaskInput(activeProcessIdAdd, activeObjectAdd);
|
||||
}
|
||||
// globalStorage.addStepTaskInput(activeProcessIdAdd, activeObjectAdd);
|
||||
setActiveObjectAdd('');
|
||||
setactiveProcessIdAdd('');
|
||||
}
|
||||
}, [activeObjectAdd, activeProcessIdAdd]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
ref={getCardRef('root')}
|
||||
onScroll={() => {
|
||||
globalStorage.renderLines({ delay: 0, repeat: 2 });
|
||||
}}
|
||||
>
|
||||
<RectWatcher onRectChange={() => setRenderCount(old => (old + 1) % 10)}>
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
zIndex: 2,
|
||||
paddingTop: '30px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: WidthRatio[0],
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{isAddingObject ? (
|
||||
<EditObjectCard finishEdit={finishAddInitialObject} />
|
||||
) : (
|
||||
<Box
|
||||
onMouseOver={() => setAddObjectHover(true)}
|
||||
onMouseOut={() => setAddObjectHover(false)}
|
||||
onClick={() => addInitialObject()}
|
||||
sx={{ display: 'inline-flex', paddingTop: '6px' }}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: 'primary',
|
||||
'&:hover': {
|
||||
color: 'primary.dark',
|
||||
},
|
||||
padding: '0px',
|
||||
borderRadius: 0,
|
||||
border: '1px dotted #333',
|
||||
|
||||
visibility: addObjectHover ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{globalStorage.userInputs.map(initialInput => (
|
||||
<Box key={initialInput} sx={{ display: 'flex' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: `0 0 ${WidthRatio[0]}`,
|
||||
}}
|
||||
>
|
||||
<ObjectCard
|
||||
key={initialInput}
|
||||
object={{
|
||||
name: initialInput,
|
||||
cardRef: getCardRef(`object.${initialInput}`),
|
||||
}}
|
||||
// isAddActive={initialInput === activeObjectAdd}
|
||||
isAddActive={activeProcessIdAdd !== ''}
|
||||
{...(activeProcessIdAdd !== ''
|
||||
? {
|
||||
addOrRemove: !outlineRenderingStepTaskCards
|
||||
.filter(({ id }) => id === activeProcessIdAdd)[0]
|
||||
.inputs.includes(initialInput),
|
||||
}
|
||||
: {})}
|
||||
handleAddActive={handleObjectAdd}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{outlineRenderingStepTaskCards.map(
|
||||
({ id, name, output, agentIcons, agents, content, ref }, index) => (
|
||||
<Box
|
||||
key={`stepTaskCard.${id}`}
|
||||
sx={{ display: 'flex' }}
|
||||
ref={ref}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: WidthRatio[0],
|
||||
justifyContent: 'center',
|
||||
flex: `0 0 ${WidthRatio[0]}`,
|
||||
}}
|
||||
>
|
||||
{output && (
|
||||
<ObjectCard
|
||||
key={`objectCard.${output}`}
|
||||
object={{
|
||||
name: output,
|
||||
cardRef: getCardRef(`object.${output}`),
|
||||
}}
|
||||
// isAddActive={output === activeObjectAdd}
|
||||
isAddActive={
|
||||
activeProcessIdAdd !== '' &&
|
||||
outlineRenderingStepTaskCards
|
||||
.map(({ id }) => id)
|
||||
.indexOf(activeProcessIdAdd) > index
|
||||
}
|
||||
{...(activeProcessIdAdd !== ''
|
||||
? {
|
||||
addOrRemove: !outlineRenderingStepTaskCards
|
||||
.filter(({ id }) => id === activeProcessIdAdd)[0]
|
||||
.inputs.includes(output),
|
||||
}
|
||||
: {})}
|
||||
handleAddActive={handleObjectAdd}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ flex: `0 0 ${WidthRatio[1]}` }} />
|
||||
<Box
|
||||
sx={{
|
||||
// display: 'flex',
|
||||
alignItems: 'center',
|
||||
// width: WidthRatio[2],
|
||||
justifyContent: 'center',
|
||||
flex: `0 0 ${WidthRatio[2]}`,
|
||||
}}
|
||||
>
|
||||
{name && (
|
||||
<ProcessCard
|
||||
process={{
|
||||
id,
|
||||
name,
|
||||
icons: agentIcons,
|
||||
agents,
|
||||
content,
|
||||
cardRef: getCardRef(`process.${name}`),
|
||||
}}
|
||||
handleProcessClick={handleProcessClick}
|
||||
isFocusing={focusingStepTaskId === id}
|
||||
isAddActive={id === activeProcessIdAdd}
|
||||
handleAddActive={handleProcessAdd}
|
||||
handleEditContent={handleEditContent}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</RectWatcher>
|
||||
{cardRefMapReady && (
|
||||
<D3Graph
|
||||
cardRefMap={cardRefMap}
|
||||
focusingStepId={focusingStepTaskId || ''}
|
||||
relations={outlineRenderingStepTaskCards
|
||||
.map(({ id, name, inputs, output }) => {
|
||||
const relations: {
|
||||
type: string;
|
||||
stepTaskId: string;
|
||||
stepCardName: string;
|
||||
objectCardName: string;
|
||||
}[] = [];
|
||||
inputs.forEach(input => {
|
||||
relations.push({
|
||||
type: 'input',
|
||||
stepTaskId: id,
|
||||
stepCardName: `process.${name}`,
|
||||
objectCardName: `object.${input}`,
|
||||
});
|
||||
});
|
||||
if (output) {
|
||||
relations.push({
|
||||
type: 'output',
|
||||
stepTaskId: id,
|
||||
stepCardName: `process.${name}`,
|
||||
objectCardName: `object.${output}`,
|
||||
});
|
||||
}
|
||||
return relations;
|
||||
})
|
||||
.flat()}
|
||||
forceRender={renderCount}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
73
frontend-react/src/components/Outline/RectWatcher.tsx
Normal file
73
frontend-react/src/components/Outline/RectWatcher.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
// Define the props for the RectWatcher component
|
||||
interface RectWatcherProps {
|
||||
children: React.ReactNode;
|
||||
onRectChange: (size: { height: number; width: number }) => void;
|
||||
debounceDelay?: number; // Optional debounce delay with a default value
|
||||
}
|
||||
|
||||
// Rewrite the RectWatcher component with TypeScript
|
||||
export const RectWatcher = React.memo<RectWatcherProps>(
|
||||
({
|
||||
children,
|
||||
onRectChange,
|
||||
debounceDelay = 10, // Assuming the delay is meant to be in milliseconds
|
||||
}) => {
|
||||
const [lastSize, setLastSize] = React.useState<{
|
||||
height: number;
|
||||
width: number;
|
||||
}>({
|
||||
height: -1,
|
||||
width: -1,
|
||||
});
|
||||
const ref = React.createRef<HTMLElement>(); // Assuming the ref is attached to a div element
|
||||
|
||||
const debouncedHeightChange = React.useMemo(
|
||||
() =>
|
||||
debounce((newSize: { height: number; width: number }) => {
|
||||
if (
|
||||
newSize.height !== lastSize.height ||
|
||||
newSize.width !== lastSize.width
|
||||
) {
|
||||
onRectChange(newSize);
|
||||
setLastSize(newSize);
|
||||
}
|
||||
}, debounceDelay),
|
||||
[onRectChange, debounceDelay, lastSize],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
const resizeObserver = new ResizeObserver(
|
||||
(entries: ResizeObserverEntry[]) => {
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
const entry = entries[0];
|
||||
debouncedHeightChange({
|
||||
height: entry.contentRect.height,
|
||||
width: entry.contentRect.width,
|
||||
});
|
||||
},
|
||||
);
|
||||
resizeObserver.observe(ref.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}
|
||||
return () => undefined;
|
||||
}, [debouncedHeightChange]);
|
||||
|
||||
// Ensure children is a single React element
|
||||
if (
|
||||
React.Children.count(children) !== 1 ||
|
||||
!React.isValidElement(children)
|
||||
) {
|
||||
console.error('RectWatcher expects a single React element as children.');
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// Clone the child element with the ref attached
|
||||
return React.cloneElement(children, { ref } as any);
|
||||
},
|
||||
);
|
||||
97
frontend-react/src/components/Outline/index.tsx
Normal file
97
frontend-react/src/components/Outline/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { SxProps } from '@mui/material';
|
||||
import Box from '@mui/material/Box';
|
||||
import OutlineView from './OutlineView';
|
||||
|
||||
import Title from '@/components/Title';
|
||||
import LoadingMask from '@/components/LoadingMask';
|
||||
import { globalStorage } from '@/storage';
|
||||
import BranchIcon from '@/icons/BranchIcon';
|
||||
|
||||
export default observer(({ style = {} }: { style?: SxProps }) => {
|
||||
const {
|
||||
api: { planReady },
|
||||
} = globalStorage;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
background: '#FFF',
|
||||
border: '3px solid #E1E1E1',
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'column',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Title title="Plan Outline" />
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: 0,
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
padding: '6px 12px',
|
||||
}}
|
||||
>
|
||||
{planReady ? <OutlineView /> : <></>}
|
||||
{globalStorage.api.planGenerating ? (
|
||||
<LoadingMask
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Box>
|
||||
{planReady ? (
|
||||
<Box
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '36px',
|
||||
height: '32px',
|
||||
bgcolor: 'primary.main',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '10px 0 0 0',
|
||||
zIndex: 100,
|
||||
'&:hover': {
|
||||
filter: 'brightness(0.9)',
|
||||
},
|
||||
}}
|
||||
onClick={() => (globalStorage.planModificationWindow = true)}
|
||||
>
|
||||
<BranchIcon />
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '12px',
|
||||
position: 'absolute',
|
||||
right: '4px',
|
||||
bottom: '2px',
|
||||
color: 'white',
|
||||
fontWeight: 800,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{globalStorage.planManager.leaves.length}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
20
frontend-react/src/components/Outline/interface.tsx
Normal file
20
frontend-react/src/components/Outline/interface.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface ObjectProps {
|
||||
name: string;
|
||||
cardRef: any;
|
||||
}
|
||||
export interface ProcessProps {
|
||||
id: string;
|
||||
name: string;
|
||||
icons: string[];
|
||||
agents: string[];
|
||||
cardRef: any;
|
||||
content: string;
|
||||
}
|
||||
export interface ObjectProcessCardProps {
|
||||
process: ProcessProps;
|
||||
inputs: ObjectProps[];
|
||||
outputs: ObjectProps[];
|
||||
cardRef: any;
|
||||
focusStep?: (stepId: string) => void;
|
||||
focusing: boolean;
|
||||
}
|
||||
@@ -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;
|
||||
195
frontend-react/src/components/PlanModification/context.tsx
Normal file
195
frontend-react/src/components/PlanModification/context.tsx
Normal 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;
|
||||
};
|
||||
445
frontend-react/src/components/PlanModification/index.tsx
Normal file
445
frontend-react/src/components/PlanModification/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
// <> {node.content}</>
|
||||
<span> {node.content}</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
57
frontend-react/src/components/ProcessDiscription/index.tsx
Normal file
57
frontend-react/src/components/ProcessDiscription/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
286
frontend-react/src/components/ProcessRehearsal/ObjectNode.tsx
Normal file
286
frontend-react/src/components/ProcessRehearsal/ObjectNode.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
289
frontend-react/src/components/ProcessRehearsal/RehearsalSvg.tsx
Normal file
289
frontend-react/src/components/ProcessRehearsal/RehearsalSvg.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
86
frontend-react/src/components/ProcessRehearsal/StepNode.tsx
Normal file
86
frontend-react/src/components/ProcessRehearsal/StepNode.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
219
frontend-react/src/components/ProcessRehearsal/index.tsx
Normal file
219
frontend-react/src/components/ProcessRehearsal/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
45
frontend-react/src/components/ResizeableColumn/index.tsx
Normal file
45
frontend-react/src/components/ResizeableColumn/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
210
frontend-react/src/components/TaskModification/context.tsx
Normal file
210
frontend-react/src/components/TaskModification/context.tsx
Normal 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;
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
]);
|
||||
457
frontend-react/src/components/TaskModification/index.tsx
Normal file
457
frontend-react/src/components/TaskModification/index.tsx
Normal 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;
|
||||
51
frontend-react/src/components/Title/index.tsx
Normal file
51
frontend-react/src/components/Title/index.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
101
frontend-react/src/components/UserGoalInput/index.tsx
Normal file
101
frontend-react/src/components/UserGoalInput/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
153
frontend-react/src/components/ViewConnector/index.tsx
Normal file
153
frontend-react/src/components/ViewConnector/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user