Files
AgentCoord/frontend-react/src/components/AgentAssignment/index.tsx
zhaoweijie 6392301833 refactor(LLMAPI): 重构LLM接口以支持新版本OpenAI SDK
- 升级openai依赖至2.x版本并替换旧版SDK调用方式
- 引入OpenAI和AsyncOpenAI客户端实例替代全局配置
- 更新所有聊天完成请求方法以适配新版API格式
- 为异步流式响应处理添加异常捕获和错误提示
- 统一超时时间和最大token数等默认参数设置
- 修复部分变量命名冲突和潜在的空值引用问题
- 添加打印彩色日志的辅助函数避免循环导入问题
2025-11-22 17:01:25 +08:00

562 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 */