setup(frontend): rename frontend-react
This commit is contained in:
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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user