Add backend and frontend

This commit is contained in:
Gk0Wk
2024-04-07 15:04:00 +08:00
parent 49fdd9cc43
commit 84a7cb1b7e
233 changed files with 29927 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
// eslint-disable-next-line import/no-commonjs
module.exports = {
root: true,
extends: ['@modern-js-app'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['../tsconfig.json'],
},
};

View File

@@ -0,0 +1,86 @@
import { api } from '@sttot/api-hooks';
import { IApiStepTask } from './generate-base-plan';
export interface IAgentSelectModifyInitRequest {
goal: string;
stepTask: IApiStepTask;
}
export interface IAgentSelectModifyAddRequest {
aspects: string[];
}
export interface IScoreItem {
reason: string;
score: number;
}
type IRawAgentSelectModifyInitResponse = Record<
string,
Record<string, { Reason: string; Score: number }>
>;
export const agentSelectModifyInitApi = api<
IAgentSelectModifyInitRequest,
Record<string, Record<string, IScoreItem>>
>(
({ goal, stepTask }) => ({
baseURL: 'api',
url: '/agentSelectModify_init',
method: 'POST',
data: {
'General Goal': goal,
stepTask: {
StepName: stepTask.name,
TaskContent: stepTask.content,
// eslint-disable-next-line @typescript-eslint/naming-convention
InputObject_List: stepTask.inputs,
OutputObject: stepTask.output,
},
},
}),
({ data }: { data: IRawAgentSelectModifyInitResponse }) =>
Object.fromEntries(
Object.entries(data).map(([agent, reasons]) => [
agent,
Object.fromEntries(
Object.entries(reasons).map(([reason, score]) => [
reason,
{
reason: score.Reason,
score: score.Score,
},
]),
),
]),
),
);
export const agentSelectModifyAddApi = api<
IAgentSelectModifyAddRequest,
Record<string, Record<string, IScoreItem>>
>(
data => ({
baseURL: 'api',
url: '/agentSelectModify_addAspect',
method: 'POST',
data: {
aspectList: data.aspects,
},
}),
({ data }: { data: IRawAgentSelectModifyInitResponse }) =>
Object.fromEntries(
Object.entries(data).map(([agent, reasons]) => [
agent,
Object.fromEntries(
Object.entries(reasons).map(([reason, score]) => [
reason,
{
reason: score.Reason,
score: score.Score,
},
]),
),
]),
),
);

View File

@@ -0,0 +1,144 @@
import { api } from '@sttot/api-hooks';
import type { IGeneratedPlan } from './generate-base-plan';
import { ActionType } from '@/storage/plan';
export interface IExecutePlanRequest {
plan: IGeneratedPlan;
stepsToRun: number;
rehearsalLog: IExecuteNode[];
}
export enum ExecuteNodeType {
Step = 'step',
Object = 'object',
}
type IExecuteRawResponse = {
LogNodeType: string;
NodeId: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
InputName_List?: string[] | null;
OutputName?: string;
content?: string;
ActionHistory?: {
ID: string;
ActionType: string;
AgentName: string;
Description: string;
ImportantInput: string[];
// eslint-disable-next-line @typescript-eslint/naming-convention
Action_Result: string;
}[];
};
export interface IExecuteStepHistoryItem {
id: string;
type: ActionType;
agent: string;
description: string;
inputs: string[];
result: string;
}
export interface IExecuteStep {
type: ExecuteNodeType.Step;
id: string;
inputs: string[];
output: string;
history: IExecuteStepHistoryItem[];
}
export interface IExecuteObject {
type: ExecuteNodeType.Object;
id: string;
content: string;
}
export type IExecuteNode = IExecuteStep | IExecuteObject;
export const executePlanApi = api<IExecutePlanRequest, IExecuteNode[]>(
({ plan, stepsToRun, rehearsalLog }) => ({
baseURL: '/api',
url: '/executePlan',
method: 'POST',
timeout: Infinity,
data: {
// eslint-disable-next-line @typescript-eslint/naming-convention
num_StepToRun: Number.isFinite(stepsToRun)
? Math.max(stepsToRun, 1)
: null,
plan: {
'Initial Input Object': plan.inputs,
'General Goal': plan.goal,
'Collaboration Process': plan.process.map(step => ({
StepName: step.name,
TaskContent: step.content,
// eslint-disable-next-line @typescript-eslint/naming-convention
InputObject_List: step.inputs,
OutputObject: step.output,
AgentSelection: step.agents,
// eslint-disable-next-line @typescript-eslint/naming-convention
Collaboration_Brief_frontEnd: step.brief,
TaskProcess: step.process.map(action => ({
ActionType: action.type,
AgentName: action.agent,
Description: action.description,
ID: action.id,
ImportantInput: action.inputs,
})),
})),
},
RehearsalLog: rehearsalLog.map(h =>
h.type === ExecuteNodeType.Object
? {
LogNodeType: 'object',
NodeId: h.id,
content: h.content,
}
: {
LogNodeType: 'step',
NodeId: h.id,
// eslint-disable-next-line @typescript-eslint/naming-convention
InputName_List: h.inputs,
OutputName: h.output,
chatLog: [],
// eslint-disable-next-line @typescript-eslint/naming-convention
inputObject_Record: [],
ActionHistory: h.history.map(item => ({
ID: item.id,
ActionType: item.type,
AgentName: item.agent,
Description: item.description,
ImportantInput: item.inputs,
// eslint-disable-next-line @typescript-eslint/naming-convention
Action_Result: item.result,
})),
},
),
},
}),
({ data }) =>
data.map((raw: IExecuteRawResponse) =>
raw.LogNodeType === 'step'
? {
type: ExecuteNodeType.Step,
id: raw.NodeId,
inputs: raw.InputName_List || [],
output: raw.OutputName ?? '',
history:
raw.ActionHistory?.map(item => ({
id: item.ID,
type: item.ActionType,
agent: item.AgentName,
description: item.Description,
inputs: item.ImportantInput,
result: item.Action_Result,
})) || [],
}
: {
type: ExecuteNodeType.Object,
id: raw.NodeId,
content: raw.content || '',
},
),
);

View File

@@ -0,0 +1,61 @@
import { api } from '@sttot/api-hooks';
import { IApiStepTask, vec2Hsl, IRawStepTask } from './generate-base-plan';
export interface IFillAgentSelectionRequest {
goal: string;
stepTask: IApiStepTask;
agents: string[];
}
export const fillAgentSelectionApi = api<
IFillAgentSelectionRequest,
IApiStepTask
>(
({ goal, stepTask, agents }) => ({
baseURL: 'api',
url: '/fill_stepTask_TaskProcess',
method: 'POST',
data: {
'General Goal': goal,
// eslint-disable-next-line @typescript-eslint/naming-convention
stepTask_lackTaskProcess: {
StepName: stepTask.name,
TaskContent: stepTask.content,
// eslint-disable-next-line @typescript-eslint/naming-convention
InputObject_List: stepTask.inputs,
OutputObject: stepTask.output,
AgentSelection: agents,
},
},
}),
({ data }: { data: IRawStepTask }) => ({
name: data.StepName ?? '',
content: data.TaskContent ?? '',
inputs: data.InputObject_List ?? [],
output: data.OutputObject ?? '',
agents: data.AgentSelection ?? [],
brief: {
template: data.Collaboration_Brief_FrontEnd?.template ?? '',
data: Object.fromEntries(
Object.entries(data.Collaboration_Brief_FrontEnd?.data ?? {}).map(
([key, value]) => [
key,
{
text: value.text,
style: {
background: vec2Hsl(value.color),
},
},
],
),
),
},
process: data.TaskProcess.map(process => ({
id: process.ID,
type: process.ActionType,
agent: process.AgentName,
description: process.Description,
inputs: process.ImportantInput,
})),
}),
);

View File

@@ -0,0 +1,55 @@
import { api } from '@sttot/api-hooks';
import { IApiStepTask, IRawStepTask, vec2Hsl } from './generate-base-plan';
export interface IFillStepTaskRequest {
goal: string;
stepTask: IApiStepTask;
}
export const fillStepTaskApi = api<IFillStepTaskRequest, IApiStepTask>(
({ goal, stepTask }) => ({
baseURL: 'api',
url: '/fill_stepTask',
method: 'POST',
data: {
'General Goal': goal,
stepTask: {
StepName: stepTask.name,
TaskContent: stepTask.content,
// eslint-disable-next-line @typescript-eslint/naming-convention
InputObject_List: stepTask.inputs,
OutputObject: stepTask.output,
},
},
}),
({ data }: { data: IRawStepTask }) => ({
name: data.StepName ?? '',
content: data.TaskContent ?? '',
inputs: data.InputObject_List ?? [],
output: data.OutputObject ?? '',
agents: data.AgentSelection ?? [],
brief: {
template: data.Collaboration_Brief_FrontEnd?.template ?? '',
data: Object.fromEntries(
Object.entries(data.Collaboration_Brief_FrontEnd?.data ?? {}).map(
([key, value]) => [
key,
{
text: value.text,
style: {
background: vec2Hsl(value.color),
},
},
],
),
),
},
process: data.TaskProcess.map(process => ({
id: process.ID,
type: process.ActionType,
agent: process.AgentName,
description: process.Description,
inputs: process.ImportantInput,
})),
}),
);

View File

@@ -0,0 +1,119 @@
import { api } from '@sttot/api-hooks';
import { SxProps } from '@mui/material';
export const vec2Hsl = (vec: HslColorVector): string =>
`hsl(${vec[0]},${vec[1]}%,${vec[2]}%)`;
export interface IPlanGeneratingRequest {
goal: string;
inputs: string[];
}
export interface IRichText {
template: string;
data: Record<string, { text: string; style: SxProps }>;
}
export interface IApiAgentAction {
id: string;
type: string;
agent: string;
description: string;
inputs: string[];
}
export interface IApiStepTask {
name: string;
content: string;
inputs: string[];
output: string;
agents: string[];
brief: IRichText;
process: IApiAgentAction[];
}
export interface IGeneratedPlan {
inputs: string[];
goal: string;
process: IApiStepTask[];
}
type HslColorVector = [number, number, number];
export interface IRawStepTask {
StepName?: string;
TaskContent?: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
InputObject_List?: string[];
OutputObject?: string;
AgentSelection?: string[];
// eslint-disable-next-line @typescript-eslint/naming-convention
Collaboration_Brief_FrontEnd: {
template: string;
data: Record<string, { text: string; color: HslColorVector }>;
};
TaskProcess: {
ActionType: string;
AgentName: string;
Description: string;
ID: string;
ImportantInput: string[];
}[];
}
export interface IRawPlanResponse {
'Initial Input Object'?: string[] | string;
'General Goal'?: string;
'Collaboration Process'?: IRawStepTask[];
}
export const genBasePlanApi = api<IPlanGeneratingRequest, IGeneratedPlan>(
data => ({
baseURL: '/api',
url: '/generate_basePlan',
method: 'POST',
data: {
'General Goal': data.goal,
'Initial Input Object': data.inputs,
},
timeout: Infinity,
}),
({ data }: { data: IRawPlanResponse }) => ({
inputs:
(typeof data['Initial Input Object'] === 'string'
? [data['Initial Input Object']]
: data['Initial Input Object']) || [],
goal: data['General Goal'] || '',
process:
data['Collaboration Process']?.map(step => ({
name: step.StepName || '',
content: step.TaskContent || '',
inputs: step.InputObject_List || [],
output: step.OutputObject || '',
agents: step.AgentSelection || [],
brief: {
template: step.Collaboration_Brief_FrontEnd.template,
data: Object.fromEntries(
Object.entries(step.Collaboration_Brief_FrontEnd.data).map(
([key, value]) => [
key,
{
text: value.text,
style: {
background: vec2Hsl(value.color),
},
},
],
),
),
},
process: step.TaskProcess.map(process => ({
id: process.ID,
type: process.ActionType,
agent: process.AgentName,
description: process.Description,
inputs: process.ImportantInput,
})),
})) ?? [],
}),
);

View File

@@ -0,0 +1,33 @@
import { api } from '@sttot/api-hooks';
import { IconName, IconMap } from '@/components/AgentIcon';
interface IRawAgent {
Name: string;
Profile: string;
Icon: string;
}
export interface IAgent {
name: string;
profile: string;
icon: IconName;
}
export const getAgentsApi = api<void, IAgent[]>(
() => ({
baseURL: '/api',
url: '/getAgent',
method: 'POST',
timeout: Infinity,
}),
({ data }) => {
const data_ = data as IRawAgent[];
return data_.map(agent => ({
name: agent.Name,
profile: agent.Profile,
icon: IconMap[agent.Icon.replace(/\.png$/, '')],
}));
},
);
export const useAgents = getAgentsApi.createMemoHook();

View File

@@ -0,0 +1,66 @@
import { api } from '@sttot/api-hooks';
import { IApiStepTask, IApiAgentAction } from './generate-base-plan';
export interface INewActionBranchRequest {
goal: string;
batch?: number;
requirement: string;
stepTask: IApiStepTask;
base: IApiAgentAction[];
existing: IApiAgentAction[];
}
export type INewActionBranchResponse = IApiAgentAction[][];
export const newActionBranchApi = api<
INewActionBranchRequest,
INewActionBranchResponse
>(
data => ({
baseURL: '/api',
url: '/branch_TaskProcess',
method: 'POST',
timeout: Infinity,
data: {
// eslint-disable-next-line @typescript-eslint/naming-convention
branch_Number: data.batch ?? 1,
// eslint-disable-next-line @typescript-eslint/naming-convention
Modification_Requirement: data.requirement,
// eslint-disable-next-line @typescript-eslint/naming-convention
Baseline_Completion: data.base.map(s => ({
ID: s.id,
ActionType: s.type,
AgentName: s.agent,
Description: s.description,
ImportantInput: s.inputs,
})),
// eslint-disable-next-line @typescript-eslint/naming-convention
Existing_Steps: data.existing.map(s => ({
ID: s.id,
ActionType: s.type,
AgentName: s.agent,
Description: s.description,
ImportantInput: s.inputs,
})),
'General Goal': data.goal,
stepTaskExisting: {
StepName: data.stepTask.name,
TaskContent: data.stepTask.content,
// eslint-disable-next-line @typescript-eslint/naming-convention
InputObject_List: data.stepTask.inputs,
OutputObject: data.stepTask.output,
AgentSelection: data.stepTask.agents,
},
},
}),
({ data }) =>
data.map((s: any) =>
s.map((s: any) => ({
id: s.ID,
type: s.ActionType,
agent: s.AgentName,
description: s.Description,
inputs: s.ImportantInput,
})),
),
);

View File

@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { api } from '@sttot/api-hooks';
import { IApiStepTask } from './generate-base-plan';
export interface INewPlanBranchRequest {
goal: string;
inputs: string[];
batch?: number;
requirement: string;
base: IApiStepTask[];
existing: IApiStepTask[];
}
export type INewPlanBranchResponse = IApiStepTask[][];
export const newPlanBranchApi = api<
INewPlanBranchRequest,
INewPlanBranchResponse
>(
data => ({
baseURL: '/api',
url: '/branch_PlanOutline',
method: 'POST',
timeout: Infinity,
data: {
branch_Number: data.batch ?? 1,
Modification_Requirement: data.requirement,
Baseline_Completion: data.base.map(s => ({
StepName: s.name,
TaskContent: s.content,
InputObject_List: s.inputs,
OutputObject: s.output,
})),
Existing_Steps: data.existing.map(s => ({
StepName: s.name,
TaskContent: s.content,
InputObject_List: s.inputs,
OutputObject: s.output,
})),
'General Goal': data.goal,
'Initial Input Object': data.inputs,
},
}),
({ data }) =>
data.map((s: any) =>
s.map((s: any) => ({
name: s.StepName,
content: s.TaskContent,
inputs: s.InputObject_List,
output: s.OutputObject,
agents: [],
brief: { template: '', data: {} },
process: [],
})),
),
);
/* eslint-enable @typescript-eslint/naming-convention */

View File

@@ -0,0 +1,32 @@
import { api } from '@sttot/api-hooks';
import { IconName } from '@/components/AgentIcon';
export interface IAgent {
name: string;
profile: string;
icon: IconName;
}
export const setAgentsApi = api<IAgent[], any>(
agents => ({
baseURL: '/api',
url: '/setAgents',
method: 'POST',
data: agents.map(agent => ({
Name: agent.name,
Profile: agent.profile,
})),
timeout: Infinity,
}),
({ data }) => {
// const data_ = data as IRawAgent[];
// return data_.map(agent => ({
// name: agent.Name,
// profile: agent.Profile,
// icon: IconMap[agent.Icon.replace(/\.png$/, '')],
// }));
return data;
},
);
export const setagents = setAgentsApi.createMemoHook();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,344 @@
import React, { useState } from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import { IconButton, SxProps } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import Divider from '@mui/material/Divider';
import RemoveIcon from '@mui/icons-material/Remove';
import AdjustIcon from '@mui/icons-material/Adjust';
import { ObjectProps, ProcessProps } from './interface';
import AgentIcon from '@/components/AgentIcon';
import { globalStorage } from '@/storage';
export interface IEditObjectProps {
finishEdit: (objectName: string) => void;
}
export const EditObjectCard: React.FC<IEditObjectProps> = React.memo(
({ finishEdit }) => {
const handleKeyPress = (event: any) => {
if (event.key === 'Enter') {
finishEdit(event.target.value);
}
};
return (
<TextField
onKeyPress={handleKeyPress}
sx={{
backgroundColor: '#D9D9D9',
borderRadius: '6px',
padding: '8px',
userSelect: 'none',
margin: '6px 0px',
}}
/>
);
},
);
interface IHoverIconButtonProps {
onAddClick: () => void;
isActive: boolean;
style: SxProps;
responseToHover?: boolean;
addOrRemove: boolean | undefined; // true for add, false for remove,undefined for adjust
}
const HoverIconButton: React.FC<IHoverIconButtonProps> = ({
onAddClick,
isActive,
style,
addOrRemove,
responseToHover = true,
}) => {
const [addIconHover, setAddIconHover] = useState(false);
return (
<Box
onMouseOver={() => {
setAddIconHover(true);
}}
onMouseOut={() => {
setAddIconHover(false);
}}
onClick={() => {
onAddClick();
}}
sx={{ ...style, justifySelf: 'start' }}
>
<IconButton
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
padding: '0px',
borderRadius: 10,
border: '1px dotted #333',
visibility:
(responseToHover && addIconHover) || isActive
? 'visible'
: 'hidden',
'& .MuiSvgIcon-root': {
fontSize: '1.25rem',
},
}}
>
{addOrRemove === undefined ? <AdjustIcon /> : <></>}
{addOrRemove === true ? <AddIcon /> : <></>}
{addOrRemove === false ? <RemoveIcon /> : <></>}
</IconButton>
</Box>
);
};
interface IEditableBoxProps {
text: string;
inputCallback: (text: string) => void;
}
const EditableBox: React.FC<IEditableBoxProps> = ({ text, inputCallback }) => {
const [isEditable, setIsEditable] = useState(false);
const handleDoubleClick = () => {
setIsEditable(true);
};
const handleKeyPress = (event: any) => {
if (event.key === 'Enter') {
inputCallback(event.target.value);
setIsEditable(false);
}
};
return (
<Box>
{isEditable ? (
<TextField
defaultValue={text}
multiline
onKeyPress={handleKeyPress}
onBlur={() => setIsEditable(false)} // 失去焦点时也关闭编辑状态
autoFocus
sx={{
'& .MuiInputBase-root': {
// 目标 MUI 的输入基础根元素
padding: '0px 0px', // 你可以设置为你希望的内边距值
},
width: '100%',
}}
/>
) : (
<span
onDoubleClick={handleDoubleClick}
style={{
color: '#707070',
// textDecoration: 'underline',
// textDecorationColor: '#C1C1C1',
borderBottom: '1.5px solid #C1C1C1',
fontSize: '15px',
}}
>
{text}
</span>
)}
</Box>
);
};
export interface IObjectCardProps {
object: ObjectProps;
isAddActive?: boolean;
handleAddActive?: (objectName: string) => void;
addOrRemove?: boolean;
}
export const ObjectCard = React.memo<IObjectCardProps>(
({
object,
isAddActive = false,
handleAddActive = (objectName: string) => {
console.log(objectName);
},
addOrRemove = true,
}) => {
const onAddClick = () => {
handleAddActive(object.name);
};
return (
<Box
sx={{
position: 'relative',
// maxWidth: '100%',
}}
>
<HoverIconButton
style={{
position: 'absolute',
left: '100%',
top: '50%',
transform: 'translateY(-50%)translateX(-50%)',
}}
onAddClick={onAddClick}
isActive={isAddActive}
responseToHover={false}
addOrRemove={addOrRemove}
/>
<Box
ref={object.cardRef}
sx={{
backgroundColor: '#F6F6F6',
borderRadius: '15px',
border: '2px solid #E5E5E5',
padding: '10px 4px',
userSelect: 'none',
margin: '12px 0px',
maxWidth: '100%',
wordWrap: 'break-word',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontSize: '16px',
fontWeight: 800,
textAlign: 'center',
color: '#222',
}}
>
{object.name}
</Box>
</Box>
);
},
);
export interface IProcessCardProps {
process: ProcessProps;
handleProcessClick: (stepId: string) => void;
isFocusing: boolean;
isAddActive?: boolean;
handleAddActive?: (objectName: string) => void;
handleEditContent: (stepTaskId: string, newContent: string) => void;
// handleSizeChange: () => void;
}
export const ProcessCard: React.FC<IProcessCardProps> = React.memo(
({
process,
handleProcessClick,
isFocusing,
isAddActive = false,
handleAddActive = (objectName: string) => {
console.log(objectName);
},
// handleSizeChange,
handleEditContent,
}) => {
const onAddClick = () => {
handleAddActive(process.id);
};
return (
<Box
sx={{
position: 'relative',
// width: '100%',
}}
>
<HoverIconButton
style={{
position: 'absolute',
left: '0',
top: '50%',
transform: 'translateY(-50%)translateX(-50%)',
}}
onAddClick={onAddClick}
isActive={isAddActive}
addOrRemove={undefined}
/>
<Box
ref={process.cardRef}
sx={{
backgroundColor: '#F6F6F6',
borderRadius: '15px',
padding: '8px',
margin: '18px 0px',
userSelect: 'none',
cursor: 'pointer',
border: isFocusing ? '2px solid #43b2aa' : '2px solid #E5E5E5',
transition: 'all 80ms ease-in-out',
'&:hover': {
border: isFocusing ? '2px solid #03a89d' : '2px solid #b3b3b3',
backgroundImage: 'linear-gradient(0, #0001, #0001)',
},
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
onClick={() => handleProcessClick(process.id)}
>
<Box
sx={{
fontSize: '16px',
fontWeight: 800,
textAlign: 'center',
color: '#222',
marginTop: '4px',
marginBottom: '4px',
}}
>
{process.name}
</Box>
{/* Assuming AgentIcon is another component */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: 'center',
flexWrap: 'wrap',
margin: '8px 0',
}}
>
{process.agents.map(agentName => (
<AgentIcon
key={`outline.${process.name}.${agentName}`}
name={globalStorage.agentMap.get(agentName)?.icon ?? 'unknown'}
style={{
width: '40px',
height: 'auto',
marginRight: '3px',
userSelect: 'none',
}}
tooltipInfo={globalStorage.agentMap.get(agentName)}
/>
))}
</Box>
{isFocusing && (
<Box onClick={e => e.stopPropagation()}>
<Divider
sx={{
margin: '5px 0px',
borderBottom: '2px dashed', // 设置为虚线
borderColor: '#d4d4d4',
}}
/>
<EditableBox
text={process.content}
inputCallback={(text: string) => {
handleEditContent(process.id, text);
// handleEditStep(step.name, { ...step, task: text });
}}
/>
</Box>
)}
</Box>
</Box>
);
},
);
const Card: React.FC = React.memo(() => {
return <></>; // Replace with your component JSX
});
export default Card;

View File

@@ -0,0 +1,241 @@
// D3Graph.tsx
import React, { useState } from 'react';
export interface SvgLineProp {
x1: number;
y1: number;
x2: number;
y2: number;
type: string;
key: string;
stepTaskId: string;
stepName: string;
objectName: string;
}
const getRefOffset = (
child: React.RefObject<HTMLElement>,
grandParent: React.RefObject<HTMLElement>,
) => {
const offset = { top: 0, left: 0, width: 0, height: 0 };
if (!child.current || !grandParent.current) {
return offset;
}
let node = child.current;
// Traverse up the DOM tree until we reach the grandparent or run out of elements
while (node && node !== grandParent.current) {
offset.top += node.offsetTop;
offset.left += node.offsetLeft;
// Move to the offset parent (the nearest positioned ancestor)
node = node.offsetParent as HTMLElement;
}
// If we didn't reach the grandparent, return null
if (node !== grandParent.current) {
return offset;
}
offset.width = child.current.offsetWidth;
offset.height = child.current.offsetHeight;
return offset;
};
// 辅助函数来计算均值和最大值
const calculateLineMetrics = (
cardRect: Map<
string,
{
top: number;
left: number;
width: number;
height: number;
}
>,
prefix: string,
) => {
const filteredRects = Array.from(cardRect.entries())
.filter(([key]) => key.startsWith(prefix))
.map(([, rect]) => rect);
return {
x:
filteredRects.reduce(
(acc, rect) => acc + (rect.left + 0.5 * rect.width),
0,
) / filteredRects.length,
y2: Math.max(...filteredRects.map(rect => rect.top + rect.height), 0),
};
};
interface D3GraphProps {
// objProCards_: ObjectProcessCardProps[];
cardRefMap: Map<string, React.RefObject<HTMLElement>>;
relations: {
type: string;
stepTaskId: string;
stepCardName: string;
objectCardName: string;
}[];
focusingStepId: string;
forceRender: number;
}
const D3Graph: React.FC<D3GraphProps> = ({
cardRefMap,
relations,
forceRender,
focusingStepId,
}) => {
const [svgLineProps, setSvgLineProps] = useState<SvgLineProp[]>([]);
const [objectLine, setObjectLine] = useState({
x: 0,
y2: 0,
});
const [processLine, setProcessLine] = useState({
x: 0,
y2: 0,
});
const cardRect = new Map<
string,
{ top: number; left: number; width: number; height: number }
>();
React.useEffect(() => {
const svgLines_ = relations
.filter(({ stepCardName, objectCardName }) => {
return cardRefMap.has(stepCardName) && cardRefMap.has(objectCardName);
})
.map(({ type, stepTaskId, stepCardName, objectCardName }) => {
const stepRect = getRefOffset(
cardRefMap.get(stepCardName)!,
cardRefMap.get('root')!,
);
cardRect.set(stepCardName, stepRect);
const objectRect = getRefOffset(
cardRefMap.get(objectCardName)!,
cardRefMap.get('root')!,
);
cardRect.set(objectCardName, objectRect);
return {
key: `${type}.${stepCardName}.${objectCardName}`,
stepTaskId,
stepName: stepCardName,
objectName: objectCardName,
type,
x1: objectRect.left + objectRect.width,
y1: objectRect.top + 0.5 * objectRect.height,
x2: stepRect.left,
y2: stepRect.top + 0.5 * stepRect.height,
};
});
const objectMetrics = calculateLineMetrics(cardRect, 'object');
const processMetrics = calculateLineMetrics(cardRect, 'process');
const maxY2 = Math.max(objectMetrics.y2, processMetrics.y2);
setObjectLine({ ...objectMetrics, y2: maxY2 });
setProcessLine({ ...processMetrics, y2: maxY2 });
setSvgLineProps(svgLines_);
}, [forceRender, focusingStepId, relations]);
return (
// <Box
// sx={{
// width: '100%',
// height: '100%',
// position: 'absolute',
// zIndex: 1,
// }}
// >
<svg
style={{
position: 'absolute',
width: '100%',
height: objectLine.y2 + 50,
zIndex: 1,
userSelect: 'none',
}}
>
<marker
id="arrowhead"
markerWidth="4"
markerHeight="4"
refX="2"
refY="2"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L4,2 L0,4 z" fill="#E5E5E5" />
</marker>
<marker
id="starter"
markerWidth="4"
markerHeight="4"
refX="0"
refY="2"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L1,0 L1,4 L0,4 z" fill="#E5E5E5" />
</marker>
<g>
<text
x={objectLine.x}
y="15"
textAnchor="middle"
dominantBaseline="middle"
fill="#898989"
fontWeight="800"
>
Key Object
</text>
<line
x1={objectLine.x}
y1={30}
x2={objectLine.x}
y2={objectLine.y2 + 30}
stroke="#E5E5E5"
strokeWidth="8"
markerEnd="url(#arrowhead)"
markerStart="url(#starter)"
></line>
<text
x={processLine.x}
y="15"
textAnchor="middle"
dominantBaseline="middle"
fill="#898989"
fontWeight="800"
>
Process
</text>
<line
x1={processLine.x}
y1={30}
x2={processLine.x}
y2={processLine.y2 + 30}
stroke="#E5E5E5"
strokeWidth="8"
markerEnd="url(#arrowhead)"
markerStart="url(#starter)"
></line>
</g>
<g>
{svgLineProps.map(edgeValue => (
<line
key={edgeValue.key}
x1={edgeValue.x1}
y1={edgeValue.y1}
x2={edgeValue.x2}
y2={edgeValue.y2}
strokeWidth="5"
stroke={edgeValue.type === 'output' ? '#FFCA8C' : '#B9DCB0'}
strokeOpacity={
focusingStepId === edgeValue.stepTaskId ? '100%' : '20%'
}
></line>
))}
</g>
</svg>
// </Box>
);
};
export default D3Graph;

View File

@@ -0,0 +1,281 @@
// 已移除对d3的引用
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import D3Graph from './D3Graph';
import { ObjectCard, ProcessCard, EditObjectCard } from './Cards';
import { RectWatcher } from './RectWatcher';
import { globalStorage } from '@/storage';
export default observer(() => {
const { outlineRenderingStepTaskCards, focusingStepTaskId } = globalStorage;
const [renderCount, setRenderCount] = useState(0);
const [addObjectHover, setAddObjectHover] = useState(false);
const [isAddingObject, setIsAddingObject] = useState(false);
const [activeObjectAdd, setActiveObjectAdd] = useState('');
const [activeProcessIdAdd, setactiveProcessIdAdd] = useState('');
const handleProcessClick = (processName: string) => {
if (processName === focusingStepTaskId) {
globalStorage.setFocusingStepTaskId(undefined);
} else {
globalStorage.setFocusingStepTaskId(processName);
}
};
const finishAddInitialObject = (objectName: string) => {
setIsAddingObject(false);
globalStorage.addUserInput(objectName);
};
const addInitialObject = () => setIsAddingObject(true);
const handleObjectAdd = (objectName: string) =>
setActiveObjectAdd(activeObjectAdd === objectName ? '' : objectName);
const handleProcessAdd = (processName: string) =>
setactiveProcessIdAdd(
activeProcessIdAdd === processName ? '' : processName,
);
const cardRefMap = new Map<string, React.RefObject<HTMLElement>>();
const getCardRef = (cardId: string) => {
if (cardRefMap.has(cardId)) {
return cardRefMap.get(cardId);
} else {
cardRefMap.set(cardId, React.createRef<HTMLElement>());
return cardRefMap.get(cardId);
}
};
const handleEditContent = (stepTaskId: string, newContent: string) => {
globalStorage.setStepTaskContent(stepTaskId, newContent);
};
const WidthRatio = ['30%', '15%', '52.5%'];
const [cardRefMapReady, setCardRefMapReady] = React.useState(false);
React.useEffect(() => {
setCardRefMapReady(true);
setRenderCount(old => (old + 1) % 10);
}, []);
React.useEffect(() => {
if (activeObjectAdd !== '' && activeProcessIdAdd !== '') {
if (
outlineRenderingStepTaskCards
.filter(({ id }) => id === activeProcessIdAdd)[0]
.inputs.includes(activeObjectAdd)
) {
globalStorage.removeStepTaskInput(activeProcessIdAdd, activeObjectAdd);
} else {
globalStorage.addStepTaskInput(activeProcessIdAdd, activeObjectAdd);
}
// globalStorage.addStepTaskInput(activeProcessIdAdd, activeObjectAdd);
setActiveObjectAdd('');
setactiveProcessIdAdd('');
}
}, [activeObjectAdd, activeProcessIdAdd]);
return (
<Box
sx={{
position: 'relative',
height: '100%',
overflow: 'auto',
}}
ref={getCardRef('root')}
onScroll={() => {
globalStorage.renderLines({ delay: 0, repeat: 2 });
}}
>
<RectWatcher onRectChange={() => setRenderCount(old => (old + 1) % 10)}>
<Stack
sx={{
position: 'absolute',
zIndex: 2,
paddingTop: '30px',
width: '100%',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: WidthRatio[0],
flexDirection: 'column',
justifyContent: 'center',
}}
>
{isAddingObject ? (
<EditObjectCard finishEdit={finishAddInitialObject} />
) : (
<Box
onMouseOver={() => setAddObjectHover(true)}
onMouseOut={() => setAddObjectHover(false)}
onClick={() => addInitialObject()}
sx={{ display: 'inline-flex', paddingTop: '6px' }}
>
<IconButton
sx={{
color: 'primary',
'&:hover': {
color: 'primary.dark',
},
padding: '0px',
borderRadius: 0,
border: '1px dotted #333',
visibility: addObjectHover ? 'visible' : 'hidden',
}}
>
<AddIcon />
</IconButton>
</Box>
)}
</Box>
{globalStorage.userInputs.map(initialInput => (
<Box key={initialInput} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: `0 0 ${WidthRatio[0]}`,
}}
>
<ObjectCard
key={initialInput}
object={{
name: initialInput,
cardRef: getCardRef(`object.${initialInput}`),
}}
// isAddActive={initialInput === activeObjectAdd}
isAddActive={activeProcessIdAdd !== ''}
{...(activeProcessIdAdd !== ''
? {
addOrRemove: !outlineRenderingStepTaskCards
.filter(({ id }) => id === activeProcessIdAdd)[0]
.inputs.includes(initialInput),
}
: {})}
handleAddActive={handleObjectAdd}
/>
</Box>
</Box>
))}
{outlineRenderingStepTaskCards.map(
({ id, name, output, agentIcons, agents, content, ref }, index) => (
<Box
key={`stepTaskCard.${id}`}
sx={{ display: 'flex' }}
ref={ref}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: WidthRatio[0],
justifyContent: 'center',
flex: `0 0 ${WidthRatio[0]}`,
}}
>
{output && (
<ObjectCard
key={`objectCard.${output}`}
object={{
name: output,
cardRef: getCardRef(`object.${output}`),
}}
// isAddActive={output === activeObjectAdd}
isAddActive={
activeProcessIdAdd !== '' &&
outlineRenderingStepTaskCards
.map(({ id }) => id)
.indexOf(activeProcessIdAdd) > index
}
{...(activeProcessIdAdd !== ''
? {
addOrRemove: !outlineRenderingStepTaskCards
.filter(({ id }) => id === activeProcessIdAdd)[0]
.inputs.includes(output),
}
: {})}
handleAddActive={handleObjectAdd}
/>
)}
</Box>
<Box sx={{ flex: `0 0 ${WidthRatio[1]}` }} />
<Box
sx={{
// display: 'flex',
alignItems: 'center',
// width: WidthRatio[2],
justifyContent: 'center',
flex: `0 0 ${WidthRatio[2]}`,
}}
>
{name && (
<ProcessCard
process={{
id,
name,
icons: agentIcons,
agents,
content,
cardRef: getCardRef(`process.${name}`),
}}
handleProcessClick={handleProcessClick}
isFocusing={focusingStepTaskId === id}
isAddActive={id === activeProcessIdAdd}
handleAddActive={handleProcessAdd}
handleEditContent={handleEditContent}
/>
)}
</Box>
</Box>
),
)}
</Stack>
</RectWatcher>
{cardRefMapReady && (
<D3Graph
cardRefMap={cardRefMap}
focusingStepId={focusingStepTaskId || ''}
relations={outlineRenderingStepTaskCards
.map(({ id, name, inputs, output }) => {
const relations: {
type: string;
stepTaskId: string;
stepCardName: string;
objectCardName: string;
}[] = [];
inputs.forEach(input => {
relations.push({
type: 'input',
stepTaskId: id,
stepCardName: `process.${name}`,
objectCardName: `object.${input}`,
});
});
if (output) {
relations.push({
type: 'output',
stepTaskId: id,
stepCardName: `process.${name}`,
objectCardName: `object.${output}`,
});
}
return relations;
})
.flat()}
forceRender={renderCount}
/>
)}
</Box>
);
});

View File

@@ -0,0 +1,73 @@
import React from 'react';
import debounce from 'lodash/debounce';
// Define the props for the RectWatcher component
interface RectWatcherProps {
children: React.ReactNode;
onRectChange: (size: { height: number; width: number }) => void;
debounceDelay?: number; // Optional debounce delay with a default value
}
// Rewrite the RectWatcher component with TypeScript
export const RectWatcher = React.memo<RectWatcherProps>(
({
children,
onRectChange,
debounceDelay = 10, // Assuming the delay is meant to be in milliseconds
}) => {
const [lastSize, setLastSize] = React.useState<{
height: number;
width: number;
}>({
height: -1,
width: -1,
});
const ref = React.createRef<HTMLElement>(); // Assuming the ref is attached to a div element
const debouncedHeightChange = React.useMemo(
() =>
debounce((newSize: { height: number; width: number }) => {
if (
newSize.height !== lastSize.height ||
newSize.width !== lastSize.width
) {
onRectChange(newSize);
setLastSize(newSize);
}
}, debounceDelay),
[onRectChange, debounceDelay, lastSize],
);
React.useEffect(() => {
if (ref.current) {
const resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[]) => {
if (!entries.length) {
return;
}
const entry = entries[0];
debouncedHeightChange({
height: entry.contentRect.height,
width: entry.contentRect.width,
});
},
);
resizeObserver.observe(ref.current);
return () => resizeObserver.disconnect();
}
return () => undefined;
}, [debouncedHeightChange]);
// Ensure children is a single React element
if (
React.Children.count(children) !== 1 ||
!React.isValidElement(children)
) {
console.error('RectWatcher expects a single React element as children.');
return <></>;
}
// Clone the child element with the ref attached
return React.cloneElement(children, { ref } as any);
},
);

View File

@@ -0,0 +1,97 @@
import { observer } from 'mobx-react-lite';
import { SxProps } from '@mui/material';
import Box from '@mui/material/Box';
import OutlineView from './OutlineView';
import Title from '@/components/Title';
import LoadingMask from '@/components/LoadingMask';
import { globalStorage } from '@/storage';
import BranchIcon from '@/icons/BranchIcon';
export default observer(({ style = {} }: { style?: SxProps }) => {
const {
api: { planReady },
} = globalStorage;
return (
<Box
sx={{
position: 'relative',
background: '#FFF',
border: '3px solid #E1E1E1',
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
...style,
}}
>
<Title title="Plan Outline" />
<Box
sx={{
position: 'relative',
height: 0,
flexGrow: 1,
overflowY: 'auto',
overflowX: 'hidden',
padding: '6px 12px',
}}
>
{planReady ? <OutlineView /> : <></>}
{globalStorage.api.planGenerating ? (
<LoadingMask
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
) : (
<></>
)}
</Box>
{planReady ? (
<Box
sx={{
cursor: 'pointer',
userSelect: 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '36px',
height: '32px',
bgcolor: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '10px 0 0 0',
zIndex: 100,
'&:hover': {
filter: 'brightness(0.9)',
},
}}
onClick={() => (globalStorage.planModificationWindow = true)}
>
<BranchIcon />
<Box
component="span"
sx={{
fontSize: '12px',
position: 'absolute',
right: '4px',
bottom: '2px',
color: 'white',
fontWeight: 800,
textAlign: 'right',
}}
>
{globalStorage.planManager.leaves.length}
</Box>
</Box>
) : (
<></>
)}
</Box>
);
});

View File

@@ -0,0 +1,20 @@
export interface ObjectProps {
name: string;
cardRef: any;
}
export interface ProcessProps {
id: string;
name: string;
icons: string[];
agents: string[];
cardRef: any;
content: string;
}
export interface ObjectProcessCardProps {
process: ProcessProps;
inputs: ObjectProps[];
outputs: ObjectProps[];
cardRef: any;
focusStep?: (stepId: string) => void;
focusing: boolean;
}

View File

@@ -0,0 +1,134 @@
import React from 'react';
import _ from 'lodash';
import { observer } from 'mobx-react-lite';
import { usePlanModificationContext } from './context';
import { globalStorage } from '@/storage';
const getRefOffset = (
child: React.RefObject<HTMLElement>,
grandParent: React.RefObject<HTMLElement>,
) => {
const offset = { top: 0, left: 0, width: 0, height: 0 };
if (!child.current || !grandParent.current) {
return offset;
}
let node = child.current;
// Traverse up the DOM tree until we reach the grandparent or run out of elements
while (node && node !== grandParent.current) {
offset.top += node.offsetTop;
offset.left += node.offsetLeft;
// Move to the offset parent (the nearest positioned ancestor)
node = node.offsetParent as HTMLElement;
}
// If we didn't reach the grandparent, return null
if (node !== grandParent.current) {
return offset;
}
offset.width = child.current.offsetWidth;
offset.height = child.current.offsetHeight;
return offset;
};
const PlanModificationSvg = observer(() => {
const {
forestPaths,
whoIsAddingBranch,
nodeRefMap,
svgForceRenderCounter,
containerRef,
} = usePlanModificationContext();
const { currentStepTaskNodeSet } = globalStorage;
const [nodeRectMap, setNodeRectMap] = React.useState(
new Map<
string,
{
top: number;
left: number;
width: number;
height: number;
}
>(),
);
React.useEffect(() => {
if (containerRef) {
const nodeRectMap_ = new Map(
[...nodeRefMap].map(kv => {
return [kv[0], getRefOffset(kv[1], containerRef)];
}),
);
setNodeRectMap(nodeRectMap_);
}
}, [svgForceRenderCounter, whoIsAddingBranch]);
const renderLine = (startid: string, endid: string) => {
const startRect = nodeRectMap.get(startid);
const endRect = nodeRectMap.get(endid);
if (!startRect || !endRect) {
return <></>;
}
let isCurrent = false;
if (
currentStepTaskNodeSet.has(startid) &&
currentStepTaskNodeSet.has(endid)
) {
isCurrent = true;
}
if (startid === 'root' && currentStepTaskNodeSet.has(endid)) {
isCurrent = true;
}
// console.log(`line.${startid}${startRect.left}.${endid}${endRect.left}`);
return (
<path
key={`line.${startid}${startRect.left}.${endid}${endRect.left}`}
d={`M ${startRect.left + 0.5 * startRect.width} ${
startRect.top + 0.5 * startRect.height
}
C ${startRect.left + startRect.width * 0.5} ${
endRect.top + 0.5 * endRect.height
},
${endRect.left} ${endRect.top + 0.5 * endRect.height},
${endRect.left} ${endRect.top + 0.5 * endRect.height}`}
fill="none"
stroke={isCurrent ? '#4a9c9e' : '#D9D9D9'}
strokeWidth="6"
></path>
);
};
const renderRoot = () => {
const rootRect = nodeRectMap.get('root');
if (rootRect && forestPaths.length > 0) {
return (
<circle
key={`root${rootRect.left}`}
cx={rootRect.left + 0.5 * rootRect.width}
cy={rootRect.top + 0.5 * rootRect.height}
r="10"
fill="#4a9c9e"
/>
);
}
return <></>;
};
return (
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
// backgroundColor: 'red',
width: _.max(
[...nodeRectMap.values()].map(rect => rect.left + rect.width),
),
height: _.max(
[...nodeRectMap.values()].map(rect => rect.top + rect.height),
),
}}
>
{forestPaths.map(pair => renderLine(pair[0], pair[1]))}
{renderRoot()}
{whoIsAddingBranch && renderLine(whoIsAddingBranch, 'requirement')}
</svg>
);
});
export default PlanModificationSvg;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,284 @@
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) => {
return a.to === b.to ? 0 : a.to === actionIsHovered ? 1 : -1;
})
.map(([line, from, to]) => {
const key = `${line.from}.${line.to}`;
return (
<path
key={`Rehearsal.ImportantLine.${key}`}
fill="none"
strokeWidth="3"
stroke={
(getAgentActionStyle(line.type as any) as any).borderColor
}
d={calcCurve(from, to, 0.5)}
strokeOpacity={actionIsHovered === line.to ? 1 : 0.2}
// style={{
// ...(actionIsHovered === line.to
// ? { filter: 'brightness(110%) saturate(100%)' }
// : { filter: 'brightness(100%) saturate(20%)' }),
// }}
/>
);
}),
[importantLineRects, actionIsHovered],
);
const height = cardRefMap
.get('root')
?.current?.querySelector?.('.contents-stack')?.scrollHeight;
return (
<Box
component="svg"
sx={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: height ? `${height}px` : '100%',
zIndex: 999,
pointerEvents: 'none',
}}
>
<g>{ioLinesEle}</g>
<g>{importantLinesEle}</g>
</Box>
);
},
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,132 @@
import React from 'react';
import _ from 'lodash';
import { observer } from 'mobx-react-lite';
import { useTaskModificationContext } from './context';
import { globalStorage } from '@/storage';
const getRefOffset = (
child: React.RefObject<HTMLElement>,
grandParent: React.RefObject<HTMLElement>,
) => {
const offset = { top: 0, left: 0, width: 0, height: 0 };
if (!child.current || !grandParent.current) {
return offset;
}
let node = child.current;
// Traverse up the DOM tree until we reach the grandparent or run out of elements
while (node && node !== grandParent.current) {
offset.top += node.offsetTop;
offset.left += node.offsetLeft;
// Move to the offset parent (the nearest positioned ancestor)
node = node.offsetParent as HTMLElement;
}
// If we didn't reach the grandparent, return null
if (node !== grandParent.current) {
return offset;
}
offset.width = child.current.offsetWidth;
offset.height = child.current.offsetHeight;
return offset;
};
const TaskModificationSvg = observer(() => {
const {
forestPaths,
whoIsAddingBranch,
nodeRefMap,
svgForceRenderCounter,
containerRef,
} = useTaskModificationContext();
const { currentActionNodeSet } = globalStorage;
const [nodeRectMap, setNodeRectMap] = React.useState(
new Map<
string,
{
top: number;
left: number;
width: number;
height: number;
}
>(),
);
React.useEffect(() => {
if (containerRef) {
const nodeRectMap_ = new Map(
[...nodeRefMap].map(kv => {
return [kv[0], getRefOffset(kv[1], containerRef)];
}),
);
setNodeRectMap(nodeRectMap_);
}
}, [svgForceRenderCounter, whoIsAddingBranch]);
const renderLine = (startid: string, endid: string) => {
const startRect = nodeRectMap.get(startid);
const endRect = nodeRectMap.get(endid);
if (!startRect || !endRect) {
return <></>;
}
let isCurrent = false;
if (currentActionNodeSet.has(startid) && currentActionNodeSet.has(endid)) {
isCurrent = true;
}
if (startid === 'root' && currentActionNodeSet.has(endid)) {
isCurrent = true;
}
// console.log(`line.${startid}${startRect.left}.${endid}${endRect.left}`);
return (
<path
key={`line.${startid}${startRect.left}.${endid}${endRect.left}`}
d={`M ${startRect.left + 0.5 * startRect.width} ${
startRect.top + 0.5 * startRect.height
}
C ${startRect.left + startRect.width * 0.5} ${
endRect.top + 0.5 * endRect.height
},
${endRect.left} ${endRect.top + 0.5 * endRect.height},
${endRect.left} ${endRect.top + 0.5 * endRect.height}`}
fill="none"
stroke={isCurrent ? '#4a9c9e' : '#D9D9D9'}
strokeWidth="6"
></path>
);
};
const renderRoot = () => {
const rootRect = nodeRectMap.get('root');
if (rootRect && forestPaths.length > 0) {
return (
<circle
key={`root${rootRect.left}`}
cx={rootRect.left + 0.5 * rootRect.width}
cy={rootRect.top + 0.5 * rootRect.height}
r="10"
fill="#4a9c9e"
/>
);
}
return <></>;
};
return (
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
// backgroundColor: 'red',
width: _.max(
[...nodeRectMap.values()].map(rect => rect.left + rect.width),
),
height: _.max(
[...nodeRectMap.values()].map(rect => rect.top + rect.height),
),
}}
>
{forestPaths.map(pair => renderLine(pair[0], pair[1]))}
{whoIsAddingBranch && renderLine(whoIsAddingBranch, 'requirement')}
{renderRoot()}
</svg>
);
});
export default TaskModificationSvg;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import SvgIcon from '@mui/material/SvgIcon';
export default () => (
<SvgIcon>
<svg
width="30"
height="41"
viewBox="0 0 30 41"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M24.7241 11.135C24.4807 11.3619 24.37 11.6718 24.418 11.9785C24.4733 12.3268 24.5065 12.675 24.5176 13.0105C24.5692 14.6559 24.1081 16.0809 23.149 17.2503C21.4854 19.2695 18.8184 19.9181 16.8744 20.391C15.5538 20.7105 14.6021 20.7936 13.8348 20.8575C12.9938 20.931 12.5696 20.9661 11.9683 21.2664C11.8502 21.3271 11.7322 21.391 11.6178 21.4613C10.5297 22.1387 9.90625 23.241 9.90625 24.4008V25.3145C9.90625 25.6564 10.1091 25.9727 10.4374 26.1644C11.5551 26.8162 12.2892 27.9281 12.2671 29.1869C12.2376 31.1135 10.4227 32.6886 8.19465 32.7142C5.88177 32.7461 4.00049 31.1327 4.00049 29.139C4.00049 27.8961 4.73087 26.8002 5.8412 26.1612C6.16581 25.9727 6.36132 25.6532 6.36132 25.3145V7.40005C6.36132 7.05818 6.15843 6.74187 5.83013 6.55017C4.71242 5.89839 3.97835 4.78652 4.00049 3.52768C4.03 1.60108 5.84488 0.0291336 8.07292 0.000378473C10.3821 -0.0283767 12.2634 1.58511 12.2634 3.5788C12.2634 4.82166 11.533 5.91756 10.4227 6.55656C10.0981 6.74507 9.90256 7.06457 9.90256 7.40324V18.4612C9.90256 18.5922 10.0649 18.6721 10.1977 18.6082H10.2014C11.4666 17.9788 12.4921 17.8925 13.4844 17.8062C14.1779 17.7487 14.8972 17.6848 15.9153 17.4388C17.5125 17.0522 19.3237 16.6113 20.2644 15.4707C20.8915 14.7103 21.0243 13.7645 20.9689 12.9274C20.9431 12.5249 20.648 12.1766 20.2164 12.0296C18.6634 11.5088 17.5642 10.1989 17.5753 8.66527C17.59 6.69395 19.4676 5.09644 21.7436 5.11241C24.0085 5.12839 25.8382 6.7259 25.8382 8.69083C25.8382 9.63656 25.4139 10.496 24.7241 11.135Z"
fill="white"
/>
</g>
</svg>
</SvgIcon>
);

View File

@@ -0,0 +1,17 @@
import SvgIcon from '@mui/material/SvgIcon';
export default () => (
<SvgIcon>
<svg
width="40"
height="42"
viewBox="0 0 40 42"
fill="white"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path d="M30.6096 18.3175L30.7811 18.5169L31.1242 18.9159C32.6296 20.6674 34.1437 22.4117 35.6663 24.1485H32.4663C32.4663 27.5995 31.1315 30.243 28.1968 32.368C27.1002 33.1439 25.8125 33.6515 24.4552 33.8788C24.27 33.9319 24.0781 33.9584 23.8853 33.9575C22.7741 33.9529 21.8738 33.0565 21.8738 31.9515C21.8738 30.8436 22.7778 29.9455 23.893 29.9455C23.9529 29.9455 24.0125 29.948 24.0712 29.9531C25.6074 29.6517 26.8862 28.6136 27.6663 27.3275C28.2053 26.2735 28.4706 25.211 28.7358 24.1485H25.5358C27.1444 22.2955 28.7444 20.1705 30.6096 18.3175ZM9.21925 15.6485C11.0845 13.7955 12.6845 11.6705 14.293 9.8175H11.093C11.3583 8.755 11.6235 7.6925 12.1626 6.6385C12.9668 5.3125 14.3016 4.25 15.9016 3.9865H15.9444V0C14.3786 0.17 12.8813 0.7055 11.6321 1.5895C8.69733 3.7145 7.36257 6.358 7.36257 9.809H4.15401C6.01925 11.934 7.61925 13.787 9.21925 15.6485ZM36 14.2062C36 16.5211 31.8041 17.34 28.2139 17.34C26.2778 17.34 24.4639 17.1061 23.1045 16.684C20.8907 15.9946 20.4278 14.9542 20.4278 14.2062C20.4278 11.7871 22.6711 9.60602 25.7434 8.84544C24.7543 8.11852 24.0051 6.95266 24.0051 5.64077V3.96933C24.0051 1.7799 25.7982 0 28.0034 0C30.2089 0 32.0017 1.7799 32.0017 3.9695V5.6406C32.0017 6.95266 31.463 8.11852 30.4739 8.84544C33.5463 9.60602 36 11.787 36 14.2062ZM19.5722 30.8662C19.5722 33.1811 15.3763 34 11.7861 34C9.85001 34 8.03611 33.7661 6.67671 33.344C4.46289 32.6546 4 31.6142 4 30.8662C4 28.4471 6.24325 26.266 9.31559 25.5054C8.3265 24.7785 7.57733 23.6127 7.57733 22.3008V20.6293C7.57733 18.4399 9.37035 16.66 11.5756 16.66C13.781 16.66 15.5739 18.4399 15.5739 20.6295V22.3006C15.5739 23.6127 15.0352 24.7785 14.0461 25.5054C17.1185 26.266 19.5722 28.447 19.5722 30.8662Z" />
</g>
</svg>
</SvgIcon>
);

View File

@@ -0,0 +1,18 @@
import SvgIcon from '@mui/material/SvgIcon';
export default () => (
<SvgIcon>
<svg
width="33"
height="35"
viewBox="0 0 33 35"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.4176 22.7961C12.6 23.5654 12.9833 24.222 13.4979 24.6464C14.0125 25.0709 14.6243 25.235 15.222 25.109C15.8197 24.9831 16.3636 24.5753 16.755 23.9599C17.1463 23.3445 17.359 22.5623 17.3545 21.7555C17.35 20.9487 17.1285 20.1709 16.7303 19.5634C16.3321 18.9559 15.7836 18.559 15.1846 18.445C14.5855 18.331 13.9757 18.5073 13.4659 18.942C12.9561 19.3766 12.5802 20.0408 12.4065 20.8136L12.4023 20.8377H4.95426C4.79861 20.1398 4.47853 19.5271 4.0414 19.0906C3.60427 18.6541 3.07334 18.4168 2.5272 18.4139H2.52303C1.85388 18.4139 1.21214 18.7684 0.738978 19.3992C0.265818 20.0301 0 20.8858 0 21.778V21.8021C0.00106744 22.6049 0.217216 23.3809 0.609477 23.99C1.00174 24.5991 1.5443 25.0014 2.13936 25.1242C2.73441 25.247 3.3428 25.0824 3.85485 24.6599C4.36689 24.2375 4.7489 23.585 4.93201 22.8202L4.93618 22.7961H12.4176ZM25.3416 4.82241H27.3986V6.77704H25.3416V4.82241ZM28.0023 4.82241H30.0594V6.77704H28.0023V4.82241ZM12.4023 32.6805H14.458V34.6333H12.4023V32.6805ZM15.0464 32.6805H17.1021V34.6333H15.0464V32.6805ZM17.6918 32.6805H19.7489V34.6333H17.6918V32.6805ZM5.40907 17.8502H7.46338V19.8067H5.40907V17.8502ZM8.0545 17.8502H10.1088V19.8067H8.0545V17.8502ZM10.6985 17.8502H12.7556V19.8067H10.6985V17.8502ZM19.6126 18.4584H21.6697V20.4131H19.6126V18.4584ZM22.258 18.4584H24.3137V20.4131H22.258V18.4584ZM24.902 18.4584H26.9591V20.4131H24.902V18.4584ZM7.22832 0.666504H9.28402V2.62113H7.22832V0.666504ZM9.87375 0.666504H11.9294V2.62113H9.87375V0.666504ZM12.5192 0.666504H14.5763V2.62113H12.5192V0.666504ZM2.63013 7.90271C3.72335 7.90271 4.65105 6.95692 5.00572 5.67733H10.8001C11.1047 5.67733 11.6262 5.96848 11.7959 6.28374L15.2328 13.4958C15.7237 14.4824 16.785 15.0239 17.5249 15.0239H20.9784C21.1747 15.6185 21.4964 16.1242 21.9043 16.4795C22.3123 16.8348 22.7889 17.0243 23.2761 17.0249H23.29C23.6217 17.0249 23.9502 16.9378 24.2566 16.7686C24.5631 16.5993 24.8415 16.3513 25.076 16.0385C25.3106 15.7258 25.4966 15.3546 25.6236 14.946C25.7505 14.5374 25.8158 14.0994 25.8158 13.6572V13.6516C25.8795 13.1903 25.8702 12.7163 25.7888 12.26C25.7074 11.8037 25.5555 11.375 25.3428 11.0013C25.1302 10.6276 24.8615 10.3171 24.5538 10.0896C24.2461 9.86214 23.9063 9.72266 23.5559 9.68009C23.2056 9.63752 22.8524 9.69278 22.519 9.84236C22.1855 9.99194 21.8792 10.2325 21.6194 10.5488C21.3596 10.8651 21.1521 11.2501 21.0101 11.6793C20.8681 12.1085 20.7948 12.5724 20.7948 13.0415V13.0452H17.5263C17.1549 13.0452 16.632 12.754 16.4831 12.4388L13.0616 5.25079C12.5887 4.2401 11.6123 3.76535 10.8529 3.76535H5.09057C4.96291 3.02543 4.65098 2.36501 4.20566 1.89182C3.76034 1.41863 3.20791 1.1606 2.63847 1.1598H2.62595C1.9568 1.1598 1.31506 1.51422 0.841902 2.1451C0.368742 2.77598 0.102924 3.63164 0.102924 4.52384V4.53867C0.102924 6.39687 1.23231 7.90271 2.62595 7.90271H2.63013ZM17.6918 8.86704C18.19 8.86518 18.6768 8.66845 19.0919 8.30122C19.507 7.93399 19.8322 7.41242 20.0271 6.80114L20.034 6.77889H24.7699V4.82241H20.187C20.0665 4.06426 19.7554 3.38376 19.3051 2.89369C18.8548 2.40363 18.2925 2.13342 17.7113 2.12784H17.7099C16.3107 2.12784 15.1827 3.62997 15.1827 5.49559V5.52155C15.1827 7.36862 16.3065 8.86704 17.6918 8.86704ZM12.0296 12.7967V12.7911C12.0296 11.8989 11.7638 11.0433 11.2906 10.4124C10.8175 9.78152 10.1757 9.4271 9.50656 9.4271H9.49822C8.82907 9.4271 8.18732 9.78152 7.71417 10.4124C7.24101 11.0433 6.97519 11.8989 6.97519 12.7911V12.7967C6.97519 14.6623 8.10318 16.1682 9.50239 16.1682C10.1723 16.1657 10.8143 15.8097 11.2879 15.1779C11.7614 14.5461 12.0281 13.6899 12.0296 12.7967ZM29.841 19.178H29.8285C29.2837 19.1778 28.7537 19.4145 28.319 19.8523C27.8842 20.29 27.5684 20.9049 27.4195 21.6037L27.4153 21.6278H22.6975C21.9395 21.6278 21.0118 21.7631 20.4902 22.7738L17.052 29.6002C16.9004 29.9173 16.3941 30.2103 16.0075 30.2103H11.7612C11.6011 29.5242 11.2816 28.9236 10.8489 28.4952C10.4161 28.0669 9.8925 27.833 9.35357 27.8273H9.34939C8.68024 27.8273 8.0385 28.1817 7.56534 28.8126C7.09218 29.4435 6.82636 30.2991 6.82636 31.1913V31.1969C6.82636 33.0607 7.95436 34.5665 9.35217 34.5665C10.4982 34.5665 11.4593 33.554 11.7792 32.1631H15.9755C16.7488 32.1631 17.7961 31.6661 18.2662 30.6999L21.7044 23.8513C21.7211 23.8068 21.856 23.625 22.6989 23.625H27.4682C27.8034 24.951 28.7478 25.8968 29.8591 25.8968H29.8619C30.5313 25.8963 31.1732 25.5415 31.6465 24.9104C32.1198 24.2793 32.3859 23.4234 32.3863 22.5309V22.5253C32.3736 21.6352 32.1005 20.7869 31.6252 20.1615C31.1499 19.5361 30.5101 19.1831 29.8424 19.178H29.841ZM22.9326 27.2877H22.9312C22.2613 27.2877 21.6189 27.6425 21.1452 28.274C20.6715 28.9056 20.4054 29.7622 20.4054 30.6554V30.661C20.4054 32.5247 21.5334 34.0269 22.9326 34.0269C24.3318 34.0269 25.4584 32.5229 25.4584 30.661C25.4584 28.7954 24.3318 27.2877 22.9326 27.2877Z"
fill="white"
/>
</svg>
</SvgIcon>
);

View File

@@ -0,0 +1,33 @@
import { Box } from '@mui/material';
import React from 'react';
// 定义你的图标属性类型,这里可以扩展成任何你需要的属性
interface CustomIconProps {
size?: number;
color?: string;
// ...其他你需要的props
}
// 创建你的自定义SVG图标组件
const SendIcon: React.FC<CustomIconProps> = ({
size = 24,
color = 'currentColor',
}) => {
return (
<Box
component="svg"
width={size}
height={size}
viewBox="0 0 37 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M33.6537 0.409255L1.63031 19.3122C0.591998 19.9271 0.0578123 20.7469 0 21.5412V21.837C0.0739997 22.7872 0.832497 23.656 2.22693 24.0356L8.44059 25.7312C8.59912 25.7689 8.76347 25.7748 8.92425 25.7485C9.08503 25.7222 9.23909 25.6642 9.37761 25.5779C9.51614 25.4916 9.63642 25.3787 9.73157 25.2455C9.82672 25.1124 9.89488 24.9616 9.93215 24.8019C10.0176 24.479 9.97573 24.1354 9.81541 23.8428C9.65509 23.5502 9.38869 23.3314 9.0719 23.2321L2.87905 21.5435L34.4376 2.91537L30.9966 28.5168C30.9458 28.8965 30.6659 29.0874 30.3191 28.9896L17.1078 25.3399L27.8146 14.2092C28.0478 13.9627 28.1778 13.6354 28.1778 13.295C28.1778 12.9546 28.0478 12.6273 27.8146 12.3808C27.7009 12.2613 27.5643 12.1661 27.4131 12.1011C27.2619 12.0361 27.0991 12.0026 26.9347 12.0026C26.7703 12.0026 26.6076 12.0361 26.4564 12.1011C26.3052 12.1661 26.1686 12.2613 26.0548 12.3808L14.3189 24.5853C14.2183 24.6903 14.1233 24.8007 14.0345 24.916C13.8502 25.0368 13.6999 25.2033 13.5978 25.3995C13.4958 25.5957 13.4455 25.815 13.4518 26.0363C13.3261 26.4196 13.2621 26.8206 13.2621 27.2242V34.7355C13.2621 36.5429 15.1977 37.6166 16.643 36.6174L22.6508 32.4553C23.0208 32.2015 23.229 31.7589 23.2012 31.2954C23.1875 31.0714 23.1164 30.8547 22.9948 30.6665C22.8732 30.4783 22.7053 30.3251 22.5075 30.2217C22.3131 30.1212 22.0953 30.0759 21.8773 30.0906C21.6594 30.1054 21.4495 30.1797 21.2703 30.3056L15.7481 34.1299V27.6364L29.6762 31.4794C31.4684 31.9755 33.1981 30.7854 33.4548 28.8662L36.9652 2.75233C37.2704 0.55133 35.5199 -0.694738 33.6537 0.409255Z"
fill={color}
/>
</Box>
);
};
export default SendIcon;

View File

@@ -0,0 +1,33 @@
import { Box } from '@mui/material';
import React from 'react';
// 定义你的图标属性类型,这里可以扩展成任何你需要的属性
interface CustomIconProps {
size?: number | string;
color?: string;
// ...其他你需要的props
}
// 创建你的自定义SVG图标组件
const checkIcon: React.FC<CustomIconProps> = ({
size = '100%',
color = 'currentColor',
}) => {
return (
<Box
component="svg"
width={size}
height="auto"
viewBox="0 0 11 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.7204 0C7.37522 1.94391 4.95071 4.40066 3.85635 5.63171L1.18331 3.64484L0 4.54699L4.61463 9C5.4068 7.07085 7.92593 3.30116 11 0.620227L10.7204 0Z"
fill={color}
/>
</Box>
);
};
export default checkIcon;

3
frontend/src/modern-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types='@modern-js/app-tools/types' />
/// <reference types='@modern-js/runtime/types' />
/// <reference types='@modern-js/runtime/types/router' />

View File

@@ -0,0 +1,28 @@
html,
body,
body > div#root {
margin: 0;
height: 100%;
width: 100%;
overflow: hidden;
font-family: PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
div#root {
position: relative;
}
.side-resize-bar {
transition: all 300ms;
z-index: 1000;
&:hover,
&:active,
&:focus {
background-color: #0093ffcc;
}
}
/*webkit内核*/
::-webkit-scrollbar {
display: none;
}

View File

@@ -0,0 +1,81 @@
import { observer } from 'mobx-react-lite';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { Outlet } from '@modern-js/runtime/router';
import React from 'react';
import FloatWindow from '@/components/FloatWindow';
import { globalStorage } from '@/storage';
import AgentAssignment from '@/components/AgentAssignment';
import TaskModification from '@/components/TaskModification';
import PlanModification from '@/components/PlanModification';
const theme = createTheme({
palette: {
primary: {
main: '#43A8AA',
},
},
});
export default observer(() => {
const [resizePlanOutline, setResizePlanOutline] = React.useState(0);
const [resizeProcessModification, setResizeProcessModification] =
React.useState(0);
return (
<ThemeProvider theme={theme}>
<Outlet />
{globalStorage.planModificationWindow ? (
<FloatWindow
title="Plan Outline Exploration"
onClose={() => (globalStorage.planModificationWindow = false)}
onResize={() => {
setResizePlanOutline(old => (old + 1) % 100);
}}
>
<PlanModification
style={{
height: '100%',
width: '100%',
}}
resizeSignal={resizePlanOutline}
/>
</FloatWindow>
) : (
<></>
)}
{globalStorage.agentAssigmentWindow ? (
<FloatWindow
title="Assignment Exploration"
onClose={() => (globalStorage.agentAssigmentWindow = false)}
>
<AgentAssignment
style={{
height: '100%',
width: '100%',
}}
/>
</FloatWindow>
) : (
<></>
)}
{globalStorage.taskProcessModificationWindow ? (
<FloatWindow
title="Task Process Exploration"
onClose={() => (globalStorage.taskProcessModificationWindow = false)}
onResize={() => {
setResizeProcessModification(old => (old + 1) % 100);
}}
>
<TaskModification
style={{
height: '100%',
width: '100%',
}}
resizeSignal={resizeProcessModification}
/>
</FloatWindow>
) : (
<></>
)}
</ThemeProvider>
);
});

View File

@@ -0,0 +1,156 @@
import React from 'react';
import Box from '@mui/material/Box';
import localForage from 'localforage';
import Drawer from '@mui/material/Drawer';
import { Helmet } from '@modern-js/runtime/head';
import ViewConnector from '@/components/ViewConnector';
import './index.scss';
import ResizeableColumn from '@/components/ResizeableColumn';
import HeadBar from '@/components/HeadBar';
import Outline from '@/components/Outline';
import AgentHiring from '@/components/AgentHiring';
import AgentBoard from '@/components/AgentBoard';
import ProcessRehearsal from '@/components/ProcessRehearsal';
import UserGoalInput from '@/components/UserGoalInput';
import ProcessDiscrption from '@/components/ProcessDiscription';
import { globalStorage } from '@/storage';
// 持久化
localForage.config({
name: 'AgentCoord',
storeName: 'app',
});
export default React.memo(() => {
React.useEffect(() => {
let id: NodeJS.Timer | undefined;
if (!globalStorage.devMode) {
localForage.getItem('globalStorage').then(v => {
if (v) {
globalStorage.load(v);
}
});
id = setInterval(() => {
localForage.setItem('globalStorage', globalStorage.dump());
}, 1000);
}
// globalStorage.getAgents();
const refreshLines = () =>
globalStorage.renderLines({ delay: 0, repeat: 1, interval: 15 });
window.addEventListener('resize', refreshLines);
// window.addEventListener('pointermove', refreshLines);
return () => {
if (id) {
clearInterval(id);
}
// window.removeEventListener('pointermove', refreshLines);
window.removeEventListener('resize', refreshLines);
};
}, []);
const [showAgentHiring, setShowAgentHiring] = React.useState(false);
return (
<>
<Helmet>
<link
rel="stylesheet"
href="/assets/katex.min.css"
type="text/css"
media="all"
/>
<title>AgentCoord</title>
</Helmet>
{/* Columns */}
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
<HeadBar
style={{ height: '40px', flexGrow: 0, flexShrink: 0, zIndex: 2 }}
/>
<Box
sx={{
height: 0,
padding: '0 10px 10px 10px',
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
<UserGoalInput
style={{
height: '50px',
flexGrow: 0,
flexShrink: 0,
zIndex: 2,
background: '#FFF9',
paddingTop: '10px',
backdropFilter: 'blur(5px)',
}}
/>
<Box sx={{ height: 0, flexGrow: 1, display: 'flex' }}>
<ResizeableColumn columnWidth="25%">
<Outline
style={{
height: '100%',
width: '100%',
marginRight: '6px',
}}
/>
</ResizeableColumn>
<ResizeableColumn columnWidth="25%">
<AgentBoard
style={{
height: '100%',
width: '100%',
marginRight: '6px',
}}
onAddAgent={() => setShowAgentHiring(true)}
/>
</ResizeableColumn>
<ResizeableColumn columnWidth="25%">
<ProcessDiscrption
style={{
height: '100%',
width: '100%',
marginRight: '6px',
}}
/>
</ResizeableColumn>
<ProcessRehearsal
style={{
height: '100%',
flexGrow: 1,
width: '100%',
}}
/>
<ViewConnector />
</Box>
</Box>
</Box>
{/* Drawers */}
<Drawer
anchor="right"
open={showAgentHiring}
onClose={() => setShowAgentHiring(false)}
>
<AgentHiring
style={{
height: '100%',
minWidth: '25vw',
}}
/>
</Drawer>
</>
);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,82 @@
import { autorun } from 'mobx';
import { GlobalStorage } from '.';
export const debug = (store: GlobalStorage) => {
autorun(() => {
const { availiableAgents } = store;
console.groupCollapsed('[LOG] GetAgent:', availiableAgents.length);
for (const agent of availiableAgents) {
// console 输出name 粗体profile 正常
console.info('%c%s', 'font-weight: bold', agent.name, agent.profile);
}
console.groupEnd();
});
autorun(() => {
const { agentCards } = store;
console.groupCollapsed('[LOG] AgentCards:', agentCards.length);
for (const agentCard of agentCards) {
console.groupCollapsed(
`agent: ${agentCard.name}`,
'inuse:',
agentCard.inuse,
'action:',
agentCard.actions.length,
);
for (const action of agentCard.actions) {
console.groupCollapsed(
'%c%s',
`font-weight: bold`,
action.type,
action.description,
'input:',
action.inputs.length,
);
for (const input of action.inputs) {
console.info(input);
}
console.groupEnd();
}
console.groupEnd();
}
console.groupEnd();
});
autorun(() => {
const { refMap } = store;
console.groupCollapsed('[LOG] RefMap');
for (const [key, map] of Object.entries(refMap)) {
console.groupCollapsed(key);
for (const [k, v] of map) {
console.info(k, v);
}
console.groupEnd();
}
console.groupEnd();
});
autorun(() => {
const { planManager } = store;
console.groupCollapsed('[LOG] planManager');
console.info(planManager);
const currentLeafId = planManager.currentStepTaskLeaf;
if (currentLeafId) {
const currentPath = planManager.stepTaskMap.get(currentLeafId)?.path;
const outline = {
initialInputs: planManager.inputs,
processes: currentPath
?.map(nodeId => planManager.stepTaskMap.get(nodeId))
.filter(node => node)
.map(node => ({
inputs: node?.inputs,
output: node?.output,
StepName: node?.name,
AgentSelection: node?.agentSelection,
})),
};
console.log(outline);
}
console.groupEnd();
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
import { makeAutoObservable } from 'mobx';
import { SxProps } from '@mui/material';
import { INodeBase } from './base';
import { PlanManager } from './manager';
import { IApiAgentAction } from '@/apis/generate-base-plan';
export enum ActionType {
Propose = 'Propose',
Critique = 'Critique',
Improve = 'Improve',
Finalize = 'Finalize',
}
const AgentActionStyles = new Map<ActionType | '', SxProps>([
[ActionType.Propose, { backgroundColor: '#B9EBF9', borderColor: '#94c2dc' }],
[ActionType.Critique, { backgroundColor: '#EFF9B9', borderColor: '#c0dc94' }],
[ActionType.Improve, { backgroundColor: '#E0DEFC', borderColor: '#bbb8e5' }],
[ActionType.Finalize, { backgroundColor: '#F9C7B9', borderColor: '#dc9e94' }],
['', { backgroundColor: '#000000', borderColor: '#000000' }],
]);
export const getAgentActionStyle = (action: ActionType): SxProps => {
return AgentActionStyles.get(action) ?? AgentActionStyles.get('')!;
};
export interface IRichSentence {
who: string;
whoStyle: SxProps;
content: string;
style: SxProps;
}
export class AgentActionNode implements INodeBase {
public name: string = '';
public plan: PlanManager;
public type: ActionType = ActionType.Propose;
public agent: string = '';
public description: string = '';
public inputs: string[] = [];
public path: string[] = []; // ['A', 'B']
public children: string[] = []; // ['D', 'E']
public get id() {
return this.path[this.path.length - 1];
}
public get parent() {
return this.path[this.path.length - 2];
}
public get last() {
return this.plan.agentActionMap.get(this.parent);
}
public get renderCard(): IRichSentence {
const style = getAgentActionStyle(this.type);
return {
who: this.agent,
whoStyle: style,
// this.description 首字母小写
content: this.description[0].toLowerCase() + this.description.slice(1),
style: { ...style, backgroundColor: 'transparent' },
};
}
public get apiAgentAction(): IApiAgentAction {
return {
id: this.name,
type: this.type,
agent: this.agent,
description: this.description,
inputs: this.inputs,
};
}
public get belongingSelection() {
return this.plan.agentSelectionMap.get(
this.plan.actionSelectionMap.get(this.id) ?? '',
);
}
constructor(plan: PlanManager, json?: any) {
this.plan = plan;
if (json) {
this.name = json.name ?? '';
this.agent = json.agent ?? '';
this.description = json.description ?? '';
this.inputs = [...(json.inputs ?? [])];
this.type = json.type ?? ActionType.Propose;
this.path = [...(json.path ?? [])];
this.children = [...(json.children ?? [])];
}
makeAutoObservable(this);
}
public dump() {
return {
name: this.name,
agent: this.agent,
description: this.description,
inputs: [...this.inputs],
type: this.type,
path: [...this.path],
children: [...this.children],
};
}
}

View File

@@ -0,0 +1,13 @@
/* A
/ \
B C
/ \
D E */
export interface INodeBase {
id: string; // B
parent?: string; // A
path: string[]; // ['A', 'B']
children: string[]; // ['D', 'E']
}
export type NodeMap<T extends INodeBase> = Map<string, T>;

View File

@@ -0,0 +1,5 @@
export * from './action';
export * from './manager';
export * from './selection';
export * from './stepTask';
export * from './log';

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { makeAutoObservable } from 'mobx';
import { PlanManager } from './manager';
import {
IExecuteNode,
ExecuteNodeType,
IExecuteObject,
} from '@/apis/execute-plan';
export class RehearsalLog {
public planManager: PlanManager;
public outdate: boolean = false;
oldLog: IExecuteNode[] = [];
userInputs: Record<string, string> = {};
public get logWithoutUserInput(): IExecuteNode[] {
if (this.oldLog.length > 0) {
return this.oldLog;
} else {
const log: IExecuteNode[] = [];
// build skeleton logs
for (const step of this.planManager.currentPlan) {
log.push({
type: ExecuteNodeType.Step,
id: step.name,
inputs: Array.from(step.inputs),
output: step.output,
history: (step.agentSelection?.currentTaskProcess ?? []).map(
action => ({
id: action.name,
type: action.type,
agent: action.agent,
description: action.description,
inputs: action.inputs,
result: '',
}),
),
});
// push output of the step
log.push({
type: ExecuteNodeType.Object,
id: step.output,
content: '',
});
}
return log;
}
}
public get renderingLog(): (IExecuteNode & {
ref: React.RefObject<HTMLDivElement>;
stepId: string;
})[] {
const outputTaskIdMap = new Map<string, string>();
const taskNameTaskIdMap = new Map<string, string>();
this.planManager.currentPlan.forEach(step => {
taskNameTaskIdMap.set(step.name, step.id);
outputTaskIdMap.set(step.output, step.id);
});
return this.logWithoutUserInput.map(node => {
let stepId = '';
if (node.type === ExecuteNodeType.Step) {
stepId = taskNameTaskIdMap.get(node.id) ?? '';
} else {
stepId = outputTaskIdMap.get(node.id) ?? '';
}
return {
...node,
ref: React.createRef(),
stepId,
};
});
}
constructor(plan: PlanManager, json?: any) {
this.planManager = plan;
if (json) {
this.oldLog = JSON.parse(JSON.stringify(json.oldLog));
this.userInputs = JSON.parse(JSON.stringify(json.userInputs));
}
makeAutoObservable(this);
}
public dump() {
return {
oldLog: JSON.parse(JSON.stringify(this.oldLog)),
userInputs: JSON.parse(JSON.stringify(this.userInputs)),
};
}
public updateLog(newLog: IExecuteNode[]) {
const log = [...newLog];
const userInputs: Record<string, string> = {};
while (log.length > 0 && log[0].type === ExecuteNodeType.Object) {
const userInputObject = log.shift()! as IExecuteObject;
userInputs[userInputObject.id] = userInputObject.content;
}
this.oldLog = log;
this.userInputs = userInputs;
}
public clearLog() {
this.oldLog.length = 0;
this.outdate = false;
}
public setOutdate() {
if (this.oldLog.length > 0) {
this.outdate = true;
}
}
}

View File

@@ -0,0 +1,196 @@
import { makeAutoObservable } from 'mobx';
import { NodeMap } from './base';
import { StepTaskNode } from './stepTask';
import { AgentActionNode } from './action';
import { AgentSelection } from './selection';
import { IApiStepTask, IGeneratedPlan } from '@/apis/generate-base-plan';
export class PlanManager {
public goal: string = '';
public nextStepTaskId: number = 0;
public nextAgentSelectionId: number = 0;
public nextAgentActionId: number = 0;
public stepTaskMap: NodeMap<StepTaskNode> = new Map();
public agentActionMap: NodeMap<AgentActionNode> = new Map();
public agentSelectionMap: Map<string, AgentSelection> = new Map();
public actionSelectionMap: Map<string, string> = new Map();
public selectionStepMap: Map<string, string> = new Map();
public stepTaskRoots: string[] = [];
public currentStepTaskLeaf?: string;
public previewStepTaskLeaf?: string;
public inputs: string[] = [];
public branches: Record<
string,
{ start?: string; requirement?: string; base?: string }
> = {};
public get leaves() {
return Object.keys(this.branches);
}
public get currentPlan() {
const path: StepTaskNode[] = [];
let node = this.stepTaskMap.get(
this.previewStepTaskLeaf ?? this.currentStepTaskLeaf ?? '',
);
while (node) {
path.push(node);
node = node.last;
}
return path.reverse();
}
public get currentPath() {
return this.currentPlan.map(node => node.id);
}
public get activeStepNodeIds() {
return new Set<string>(this.currentPath);
}
public get apiPlan() {
return {
goal: this.goal,
inputs: this.inputs,
process: this.currentPlan.map(node => node.apiStepTask),
};
}
constructor() {
makeAutoObservable(this);
}
public parseApiPlan(plan: IGeneratedPlan) {
this.goal = plan.goal;
this.inputs = [...plan.inputs];
const leaf = this.insertProcess(plan.process);
this.currentStepTaskLeaf = leaf;
}
public insertProcess(
process: IApiStepTask[],
start?: string,
requirement?: string,
base?: string,
): string {
if (start && !this.stepTaskMap.has(start)) {
throw new Error(`StekTask node ${start} does not exist!`);
}
let lastChidrenList =
this.stepTaskMap.get(start ?? '')?.children ?? this.stepTaskRoots;
const path = [...(this.stepTaskMap.get(start ?? '')?.path ?? [])];
for (const task of process) {
// create agentSelection
const agentSelection = new AgentSelection(this, {
id: (this.nextAgentSelectionId++).toString(),
agents: task.agents,
});
const leaf = agentSelection.insertActions(task.process);
this.agentSelectionMap.set(agentSelection.id, agentSelection);
agentSelection.currentActionLeaf = leaf;
// create stepTask
path.push((this.nextStepTaskId++).toString());
const node = new StepTaskNode(this, {
name: task.name,
content: task.content,
inputs: task.inputs,
output: task.output,
brief: task.brief,
path,
agentSelectionIds: [agentSelection.id],
currentAgentSelection: agentSelection.id,
});
lastChidrenList.push(node.id);
lastChidrenList = node.children;
this.selectionStepMap.set(agentSelection.id, node.id);
this.stepTaskMap.set(node.id, node);
}
const leaf = path[path.length - 1];
this.branches[leaf] = { start, requirement, base };
return leaf;
}
public reset() {
this.goal = '';
this.stepTaskMap.clear();
this.agentActionMap.clear();
this.agentSelectionMap.clear();
this.stepTaskRoots = [];
this.inputs = [];
this.currentStepTaskLeaf = undefined;
this.previewStepTaskLeaf = undefined;
this.nextAgentActionId = 0;
this.nextAgentSelectionId = 0;
this.nextStepTaskId = 0;
}
public dump() {
return {
goal: this.goal,
nextStepTaskId: this.nextStepTaskId,
nextAgentSelectionId: this.nextAgentSelectionId,
nextAgentActionId: this.nextAgentActionId,
stepTaskMap: Array.from(this.stepTaskMap).map(([k, v]) => [k, v.dump()]),
agentActionMap: Array.from(this.agentActionMap).map(([k, v]) => [
k,
v.dump(),
]),
agentSelectionMap: Array.from(this.agentSelectionMap).map(([k, v]) => [
k,
v.dump(),
]),
stepTaskRoots: [...this.stepTaskRoots],
inputs: [...this.inputs],
currentStepTaskLeaf: this.currentStepTaskLeaf,
previewStepTaskLeaf: this.previewStepTaskLeaf,
branch: JSON.parse(JSON.stringify(this.branches)),
};
}
public load(json: any) {
this.reset();
this.goal = json.goal;
this.nextStepTaskId = json.nextStepTaskId;
this.nextAgentSelectionId = json.nextAgentSelectionId;
this.nextAgentActionId = json.nextAgentActionId;
for (const [k, v] of json.agentActionMap) {
this.agentActionMap.set(k, new AgentActionNode(this, v));
}
for (const [k, v] of json.agentSelectionMap) {
const selection = new AgentSelection(this, v);
for (const leaf of selection.leaves) {
let node = this.agentActionMap.get(leaf);
while (node) {
this.actionSelectionMap.set(node.id, selection.id);
node = node.last;
}
}
this.agentSelectionMap.set(k, selection);
}
for (const [k, v] of json.stepTaskMap) {
const stepTask = new StepTaskNode(this, v);
for (const id of stepTask.agentSelectionIds) {
this.selectionStepMap.set(id, stepTask.id);
}
this.stepTaskMap.set(k, stepTask);
}
this.stepTaskRoots = [...json.stepTaskRoots];
this.inputs = [...json.inputs];
this.currentStepTaskLeaf = json.currentStepTaskLeaf;
this.previewStepTaskLeaf = json.previewStepTaskLeaf;
this.branches = JSON.parse(JSON.stringify(json.branch));
}
}

View File

@@ -0,0 +1,112 @@
import { makeAutoObservable } from 'mobx';
import { PlanManager } from './manager';
import { AgentActionNode } from './action';
import { IApiAgentAction } from '@/apis/generate-base-plan';
export class AgentSelection {
public id: string = '';
public plan: PlanManager;
public agents: string[] = [];
public actionRoot: string[] = [];
public currentActionLeaf?: string;
public previewActionLeaf?: string;
public branches: Record<
string,
{ start?: string; requirement?: string; base?: string }
> = {};
public get leaves() {
return Object.keys(this.branches);
}
public get currentTaskProcess() {
const path: AgentActionNode[] = [];
let node = this.plan.agentActionMap.get(
this.previewActionLeaf ?? this.currentActionLeaf ?? '',
);
while (node) {
path.push(node);
node = node.last;
}
return path.reverse();
}
public get currentTaskProcessIds() {
return this.currentTaskProcess.map(node => node.id);
}
public get activeTaskIds() {
return new Set<string>(this.currentTaskProcessIds);
}
public get belongingStepTask() {
return this.plan.stepTaskMap.get(
this.plan.selectionStepMap.get(this.id) ?? '',
);
}
constructor(plan: PlanManager, json?: any) {
this.plan = plan;
if (json) {
this.id = json.id ?? '';
this.agents = [...(json.agents ?? [])];
this.branches = JSON.parse(JSON.stringify(json.branches ?? {}));
this.actionRoot = [...(json.actionRoot ?? [])];
this.currentActionLeaf = json.currentActionLeaf;
this.previewActionLeaf = json.previewActionLeaf;
}
makeAutoObservable(this);
}
public dump() {
return {
id: this.id,
agents: [...this.agents],
branches: JSON.parse(JSON.stringify(this.branches)),
actionRoot: [...this.actionRoot],
currentActionLeaf: this.currentActionLeaf,
previewActionLeaf: this.previewActionLeaf,
};
}
public insertActions(
actions: IApiAgentAction[],
start?: string,
requirement?: string,
base?: string,
) {
if (actions.length === 0) {
return undefined;
}
if (start && !this.plan.agentActionMap.has(start)) {
throw new Error(`AgentAction node ${start} does not exist!`);
}
let lastChidrenList =
this.plan.agentActionMap.get(start ?? '')?.children ?? this.actionRoot;
const path = [...(this.plan.agentActionMap.get(start ?? '')?.path ?? [])];
for (const action of actions) {
path.push((this.plan.nextAgentActionId++).toString());
const node = new AgentActionNode(this.plan, {
name: action.id,
path,
type: action.type,
agent: action.agent,
description: action.description,
inputs: action.inputs,
});
this.plan.agentActionMap.set(node.id, node);
this.plan.actionSelectionMap.set(node.id, this.id);
lastChidrenList.push(node.id);
lastChidrenList = node.children;
}
const leaf = path[path.length - 1];
this.branches[leaf] = { start, requirement, base };
return leaf;
}
}

View File

@@ -0,0 +1,280 @@
import React from 'react';
import { SxProps } from '@mui/material';
import { makeAutoObservable } from 'mobx';
import { INodeBase } from './base';
import { PlanManager } from './manager';
import {
IRichText,
IApiStepTask,
IApiAgentAction,
} from '@/apis/generate-base-plan';
export interface IRichSpan {
text: string;
style?: SxProps;
}
const nameJoin = (names: string[]) => {
// join names with comma, and 'and' for the last one
const tmp = [...names];
const last = tmp.pop()!;
let t = tmp.join(', ');
if (t.length > 0) {
t = `${t} and ${last}`;
} else {
t = last;
}
return t;
};
export class StepTaskNode implements INodeBase {
public name: string = '';
public content: string = '';
public inputs: string[] = [];
public output: string = '';
public _brief: IRichText = {
template: '',
data: {},
};
public path: string[] = [];
public children: string[] = [];
public plan: PlanManager;
public agentSelectionIds: string[] = [];
public currentAgentSelection?: string;
public previewAgentSelection?: string;
public agentAspectScores: Record<
string,
Record<string, { score: number; reason: string }>
> = {};
public get brief(): IRichText {
if (!this.agentSelection) {
return this._brief;
}
const agents = [...this.agentSelection.agents];
if (agents.length === 0) {
return this._brief;
}
const data: IRichText['data'] = {};
let indexOffset = 0;
const inputPlaceHolders = this.inputs.map((text, index) => {
data[(index + indexOffset).toString()] = {
text,
style: { background: '#ACDBA0' },
};
return `!<${index + indexOffset}>!`;
});
const inputSentence = nameJoin(inputPlaceHolders);
indexOffset += this.inputs.length;
const namePlaceholders = agents.map((text, index) => {
data[(index + indexOffset).toString()] = {
text,
style: { background: '#E5E5E5', boxShadow: '1px 1px 4px 1px #0003' },
};
return `!<${index + indexOffset}>!`;
});
const nameSentence = nameJoin(namePlaceholders);
indexOffset += agents.length;
let actionSentence = this.content;
// delete the last '.' of actionSentence
if (actionSentence[actionSentence.length - 1] === '.') {
actionSentence = actionSentence.slice(0, -1);
}
const actionIndex = indexOffset++;
data[actionIndex.toString()] = {
text: actionSentence,
style: { background: '#DDD', border: '1.5px solid #ddd' },
};
let outputSentence = '';
if (this.output) {
data[indexOffset.toString()] = {
text: this.output,
style: { background: '#FFCA8C' },
};
outputSentence = `to obtain !<${indexOffset}>!`;
}
// Join them togeter
let content = inputSentence;
if (content) {
content = `Based on ${content}, ${nameSentence} perform the task of !<${actionIndex}>!`;
} else {
content = `${nameSentence} perform the task of !<${actionIndex}>!`;
}
if (outputSentence) {
content = `${content}, ${outputSentence}.`;
} else {
content = `${content}.`;
}
content = content.trim();
return {
template: content,
data,
};
}
public get id() {
return this.path[this.path.length - 1];
}
public get last() {
return this.plan.stepTaskMap.get(this.path[this.path.length - 2]);
}
public get next() {
return this.children
.map(id => this.plan.stepTaskMap.get(id)!)
.filter(node => node);
}
public get agentSelection() {
const id = this.previewAgentSelection ?? this.currentAgentSelection ?? '';
return this.plan.agentSelectionMap.get(id);
}
public get allSelections() {
return this.agentSelectionIds
.map(id => this.plan.agentSelectionMap.get(id)!)
.filter(node => node);
}
public get apiStepTask(): IApiStepTask {
const actionsProcess: IApiAgentAction[] = [];
const actions = this.agentSelection?.currentTaskProcess ?? [];
for (const action of actions) {
actionsProcess.push({
id: action.name,
type: action.type,
agent: action.agent,
description: action.description,
inputs: [...action.inputs],
});
}
return {
name: this.name,
content: this.content,
inputs: [...this.inputs],
output: this.output,
agents: this.agentSelection?.agents ?? [],
brief: JSON.parse(JSON.stringify(this.brief)) as IRichText,
process: actionsProcess,
};
}
public get descriptionCard() {
const briefSpan: IRichSpan[] = [];
for (const substring of this.brief.template.split(/(!<[^>]+>!)/)) {
if (substring[0] === '!') {
const key = substring.slice(2, -2);
const { text, style } = this.brief.data[key];
briefSpan.push({ text, style });
} else {
briefSpan.push({ text: substring });
}
}
const actions = this.agentSelection?.currentTaskProcess ?? [];
const detailParagraph = actions.map(action => [
action.renderCard,
action.id,
]);
return {
id: this.id,
name: this.name,
content: this.content,
ref: React.createRef<HTMLElement>(),
brief: briefSpan,
detail: detailParagraph,
};
}
public get outlineCard() {
return {
id: this.id,
name: this.name,
inputs: this.inputs,
output: this.output,
agents: this.agentSelection?.agents ?? [],
content: this.content,
ref: React.createRef<HTMLElement>(),
};
}
public get heatmap() {
return Object.fromEntries(
Object.entries(this.agentAspectScores).map(([aspect, scores]) => [
aspect,
Object.fromEntries(
Object.entries(scores).map(([agent, score]) => [agent, score]),
),
]),
);
}
constructor(plan: PlanManager, json?: any) {
this.plan = plan;
if (json) {
this.name = json.name ?? '';
this.content = json.content ?? '';
this.inputs = [...(json.inputs ?? [])];
this.output = json.output;
this._brief = JSON.parse(
JSON.stringify(json.brief ?? '{ template: "", data: {} }'),
) as IRichText;
this.path = [...(json.path ?? [])];
this.children = [...(json.children ?? [])];
this.agentAspectScores = JSON.parse(
JSON.stringify(json.agentAspectScores ?? {}),
);
this.agentSelectionIds = [...(json.agentSelectionIds ?? [])];
this.currentAgentSelection = json.currentAgentSelection;
this.previewAgentSelection = json.previewAgentSelection;
}
makeAutoObservable(this);
}
public dump() {
return {
name: this.name,
content: this.content,
inputs: [...this.inputs],
output: this.output,
brief: JSON.parse(JSON.stringify(this.brief)),
path: [...this.path],
children: [...this.children],
agentSelectionIds: [...this.agentSelectionIds],
agentAspectScores: JSON.parse(JSON.stringify(this.agentAspectScores)),
currentAgentSelection: this.currentAgentSelection,
previewAgentSelection: this.previewAgentSelection,
};
}
public appendAspectScore(
aspect: string,
agentScores: Record<string, { score: number; reason: string }>,
) {
if (this.agentAspectScores[aspect]) {
for (const [agent, score] of Object.entries(agentScores)) {
if (this.agentAspectScores[aspect][agent]) {
this.agentAspectScores[aspect][agent].score = score.score;
this.agentAspectScores[aspect][agent].reason = score.reason;
} else {
this.agentAspectScores[aspect][agent] = score;
}
}
} else {
this.agentAspectScores[aspect] = agentScores;
}
}
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
export const useResize = <T extends HTMLElement = HTMLElement>(
onResize: () => void,
) => {
const ref = React.useRef<T>(null);
React.useEffect(() => {
if (ref.current) {
const observer = new ResizeObserver(onResize);
observer.observe(ref.current);
return () => observer.disconnect();
}
return () => undefined;
}, [ref.current]);
return ref;
};