mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-20 16:39:06 +08:00
feat: re-implement MultiAgentVisualization
This commit is contained in:
parent
cdb1492cef
commit
b152e787cc
@ -6,199 +6,39 @@
|
|||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
useNodesState,
|
|
||||||
useEdgesState,
|
|
||||||
Handle,
|
Handle,
|
||||||
Position,
|
Position,
|
||||||
type Node,
|
|
||||||
type Edge,
|
type Edge,
|
||||||
|
type ReactFlowInstance,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import {
|
import {
|
||||||
Brain,
|
Play,
|
||||||
FilePen,
|
|
||||||
MessageSquareQuote,
|
|
||||||
Microscope,
|
|
||||||
RotateCcw,
|
|
||||||
SquareTerminal,
|
|
||||||
UserCheck,
|
|
||||||
Users,
|
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronLeft,
|
||||||
|
Pause,
|
||||||
|
Fullscreen,
|
||||||
|
Minimize,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
type Dispatch,
|
|
||||||
type SetStateAction,
|
|
||||||
} from "react";
|
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||||
import { ShineBorder } from "~/components/magicui/shine-border";
|
import { ShineBorder } from "~/components/magicui/shine-border";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Slider } from "~/components/ui/slider";
|
||||||
import { useIntersectionObserver } from "~/hooks/use-intersection-observer";
|
import { useIntersectionObserver } from "~/hooks/use-intersection-observer";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const ROW_HEIGHT = 85;
|
import { playbook, type GraphNode } from "../store";
|
||||||
const ROW_1 = 0;
|
import {
|
||||||
const ROW_2 = ROW_HEIGHT;
|
activateStep,
|
||||||
const ROW_3 = ROW_HEIGHT * 2;
|
nextStep,
|
||||||
const ROW_4 = ROW_HEIGHT * 2;
|
play,
|
||||||
const ROW_5 = ROW_HEIGHT * 3;
|
prevStep,
|
||||||
const ROW_6 = ROW_HEIGHT * 4;
|
togglePlay,
|
||||||
|
useMAVStore,
|
||||||
export type WorkflowNode = Node<{
|
} from "../store/mav-store";
|
||||||
label: string;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
active?: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const initialNodes: WorkflowNode[] = [
|
|
||||||
{
|
|
||||||
id: "Start",
|
|
||||||
type: "circle",
|
|
||||||
data: { label: "Start" },
|
|
||||||
position: { x: -75, y: ROW_1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coordinator",
|
|
||||||
data: { icon: MessageSquareQuote, label: "Coordinator" },
|
|
||||||
position: { x: 150, y: ROW_1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Planner",
|
|
||||||
data: { icon: Brain, label: "Planner" },
|
|
||||||
position: { x: 150, y: ROW_2 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Reporter",
|
|
||||||
data: { icon: FilePen, label: "Reporter" },
|
|
||||||
position: { x: 275, y: ROW_3 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "HumanFeedback",
|
|
||||||
data: { icon: UserCheck, label: "Human Feedback" },
|
|
||||||
position: { x: 25, y: ROW_4 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam",
|
|
||||||
data: { icon: Users, label: "Research Team" },
|
|
||||||
position: { x: 25, y: ROW_5 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Researcher",
|
|
||||||
data: { icon: Microscope, label: "Researcher" },
|
|
||||||
position: { x: -75, y: ROW_6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coder",
|
|
||||||
data: { icon: SquareTerminal, label: "Coder" },
|
|
||||||
position: { x: 125, y: ROW_6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "End",
|
|
||||||
type: "circle",
|
|
||||||
data: { label: "End" },
|
|
||||||
position: { x: 330, y: ROW_6 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
|
||||||
{
|
|
||||||
id: "Start->Coordinator",
|
|
||||||
source: "Start",
|
|
||||||
target: "Coordinator",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "left",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coordinator->Planner",
|
|
||||||
source: "Coordinator",
|
|
||||||
target: "Planner",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Planner->Reporter",
|
|
||||||
source: "Planner",
|
|
||||||
target: "Reporter",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Planner->HumanFeedback",
|
|
||||||
source: "Planner",
|
|
||||||
target: "HumanFeedback",
|
|
||||||
sourceHandle: "left",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "HumanFeedback->Planner",
|
|
||||||
source: "HumanFeedback",
|
|
||||||
target: "Planner",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "bottom",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "HumanFeedback->ResearchTeam",
|
|
||||||
source: "HumanFeedback",
|
|
||||||
target: "ResearchTeam",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Reporter->End",
|
|
||||||
source: "Reporter",
|
|
||||||
target: "End",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam->Researcher",
|
|
||||||
source: "ResearchTeam",
|
|
||||||
target: "Researcher",
|
|
||||||
sourceHandle: "left",
|
|
||||||
targetHandle: "top",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam->Coder",
|
|
||||||
source: "ResearchTeam",
|
|
||||||
target: "Coder",
|
|
||||||
sourceHandle: "bottom",
|
|
||||||
targetHandle: "left",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ResearchTeam->Planner",
|
|
||||||
source: "ResearchTeam",
|
|
||||||
target: "Planner",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "bottom",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Researcher->ResearchTeam",
|
|
||||||
source: "Researcher",
|
|
||||||
target: "ResearchTeam",
|
|
||||||
sourceHandle: "right",
|
|
||||||
targetHandle: "bottom",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Coder->ResearchTeam",
|
|
||||||
source: "Coder",
|
|
||||||
target: "ResearchTeam",
|
|
||||||
sourceHandle: "top",
|
|
||||||
targetHandle: "right",
|
|
||||||
animated: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
circle: CircleNode,
|
circle: CircleNode,
|
||||||
@ -206,196 +46,121 @@ const nodeTypes = {
|
|||||||
default: AgentNode,
|
default: AgentNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const WORKFLOW_STEPS = [
|
export function MultiAgentVisualization({ className }: { className?: string }) {
|
||||||
{
|
const {
|
||||||
description:
|
graph: { nodes, edges },
|
||||||
"The Coordinator is responsible for engaging with the user to understand their problem and requirements.",
|
activeStepIndex,
|
||||||
tooltipPosition: "right",
|
playing,
|
||||||
activeNodes: ["Start", "Coordinator"],
|
} = useMAVStore((state) => state);
|
||||||
activeEdges: ["Start->Coordinator"],
|
const flowRef = useRef<ReactFlowInstance<GraphNode, Edge>>(null);
|
||||||
},
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
{
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
description:
|
const { ref: anchorRef } = useIntersectionObserver({
|
||||||
"If the user's problem is clearly defined, the Coordinator will hand it over to the Planner.",
|
|
||||||
tooltipPosition: "left",
|
|
||||||
activeNodes: ["Coordinator", "Planner"],
|
|
||||||
activeEdges: ["Coordinator->Planner"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Awaiting human feedback to refine the plan.",
|
|
||||||
tooltipPosition: "left",
|
|
||||||
activeNodes: ["Planner", "HumanFeedback"],
|
|
||||||
activeEdges: ["Planner->HumanFeedback"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Updating the plan based on human feedback.",
|
|
||||||
tooltipPosition: "left",
|
|
||||||
activeNodes: ["HumanFeedback", "Planner"],
|
|
||||||
activeEdges: ["HumanFeedback->Planner"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"The Research Team is responsible for conducting the core research tasks.",
|
|
||||||
tooltipPosition: "left",
|
|
||||||
activeNodes: ["HumanFeedback", "ResearchTeam"],
|
|
||||||
activeEdges: ["HumanFeedback->ResearchTeam", "ResearchTeam->HumanFeedback"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"The Researcher is responsible for gathering information using search and crawling tools.",
|
|
||||||
tooltipPosition: "left",
|
|
||||||
activeNodes: ["ResearchTeam", "Researcher"],
|
|
||||||
activeEdges: ["ResearchTeam->Researcher", "Researcher->ResearchTeam"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"The Coder is responsible for writing Python code to solve math problems, data analysis, and more.",
|
|
||||||
tooltipPosition: "right",
|
|
||||||
activeNodes: ["ResearchTeam", "Coder"],
|
|
||||||
activeEdges: ["ResearchTeam->Coder", "Coder->ResearchTeam"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Once the research tasks are completed, the Researcher will hand over to the Planner.",
|
|
||||||
tooltipPosition: "left",
|
|
||||||
activeNodes: ["ResearchTeam", "Planner"],
|
|
||||||
activeEdges: ["ResearchTeam->Planner"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"If no additional information is required, the Planner will handoff to the Reporter.",
|
|
||||||
tooltipPosition: "right",
|
|
||||||
activeNodes: ["Reporter", "Planner"],
|
|
||||||
activeEdges: ["Planner->Reporter"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "The Reporter will prepare a report summarizing the results.",
|
|
||||||
tooltipPosition: "bottom",
|
|
||||||
activeNodes: ["End", "Reporter"],
|
|
||||||
activeEdges: ["Reporter->End"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function useWorkflowRun(
|
|
||||||
setNodes: Dispatch<SetStateAction<WorkflowNode[]>>,
|
|
||||||
setEdges: Dispatch<SetStateAction<Edge[]>>,
|
|
||||||
) {
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
|
||||||
|
|
||||||
const clearAnimation = useCallback(() => {
|
|
||||||
setEdges((edges) => {
|
|
||||||
return edges.map((edge) => ({
|
|
||||||
...edge,
|
|
||||||
animated: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
setNodes((nodes) => {
|
|
||||||
return nodes.map((node) => ({
|
|
||||||
...node,
|
|
||||||
data: { ...node.data, active: false },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}, [setEdges, setNodes]);
|
|
||||||
|
|
||||||
const run = useCallback(async () => {
|
|
||||||
setIsRunning(true);
|
|
||||||
clearAnimation();
|
|
||||||
for (const step of WORKFLOW_STEPS) {
|
|
||||||
setNodes((nodes) => {
|
|
||||||
return nodes.map((node) => ({
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
active: step.activeNodes.includes(node.id),
|
|
||||||
stepDescription:
|
|
||||||
step.activeNodes.indexOf(node.id) === step.activeNodes.length - 1
|
|
||||||
? step.description
|
|
||||||
: undefined,
|
|
||||||
stepTooltipPosition: step.tooltipPosition,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
setEdges((edges) => {
|
|
||||||
return edges.map((edge) => ({
|
|
||||||
...edge,
|
|
||||||
animated: step.activeEdges.includes(edge.id),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(step.description.split(" ").length * 360);
|
|
||||||
}
|
|
||||||
clearAnimation();
|
|
||||||
setIsRunning(false);
|
|
||||||
}, [setNodes, setEdges, clearAnimation]);
|
|
||||||
|
|
||||||
return { run, isRunning };
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms: number) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MultiAgentVisualization() {
|
|
||||||
const [nodes, setNodes] = useNodesState(initialNodes);
|
|
||||||
const [edges, setEdges] = useEdgesState(initialEdges);
|
|
||||||
|
|
||||||
const { run, isRunning } = useWorkflowRun(setNodes, setEdges);
|
|
||||||
const [hasAutoRun, setHasAutoRun] = useState(false);
|
|
||||||
|
|
||||||
const { ref } = useIntersectionObserver({
|
|
||||||
rootMargin: "0%",
|
rootMargin: "0%",
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
onChange: (isIntersecting) => {
|
onChange: (isIntersecting) => {
|
||||||
if (isIntersecting && !hasAutoRun) {
|
if (isIntersecting && !playing) {
|
||||||
setHasAutoRun(true);
|
void play();
|
||||||
void run();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const toggleFullscreen = useCallback(async () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
setFullscreen(true);
|
||||||
|
await containerRef.current.requestFullscreen();
|
||||||
|
setTimeout(() => {
|
||||||
|
void flowRef.current?.fitView({ maxZoom: 2.5 });
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
setFullscreen(false);
|
||||||
|
await document.exitFullscreen();
|
||||||
|
setTimeout(() => {
|
||||||
|
void flowRef.current?.fitView();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<div
|
||||||
style={{
|
ref={containerRef}
|
||||||
["--xy-background-color-default" as string]: "transparent",
|
className={cn("flex h-full w-full flex-col pb-4", className)}
|
||||||
}}
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
nodeTypes={nodeTypes}
|
|
||||||
fitView
|
|
||||||
proOptions={{ hideAttribution: true }}
|
|
||||||
colorMode="dark"
|
|
||||||
panOnScroll={false}
|
|
||||||
zoomOnScroll={false}
|
|
||||||
preventScrolling={false}
|
|
||||||
panOnDrag={false}
|
|
||||||
>
|
>
|
||||||
<Background
|
<ReactFlow
|
||||||
className="[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]"
|
className={cn("flex min-h-0 flex-grow")}
|
||||||
bgColor="var(--background)"
|
style={{
|
||||||
/>
|
["--xy-background-color-default" as string]: "transparent",
|
||||||
<div
|
}}
|
||||||
id="auto-run-animation-trigger"
|
nodes={nodes}
|
||||||
ref={ref}
|
edges={edges}
|
||||||
className="absolute bottom-0 left-[50%] h-px w-px"
|
nodeTypes={nodeTypes}
|
||||||
/>
|
fitView
|
||||||
<div className="absolute top-0 right-0 left-0 z-200 flex items-center justify-center">
|
proOptions={{ hideAttribution: true }}
|
||||||
{!isRunning && (
|
colorMode="dark"
|
||||||
<Button
|
panOnScroll={false}
|
||||||
className="translate-x-[22vw]"
|
zoomOnScroll={false}
|
||||||
variant="outline"
|
preventScrolling={false}
|
||||||
size="sm"
|
panOnDrag={false}
|
||||||
onClick={() => {
|
onInit={(instance) => {
|
||||||
void run();
|
flowRef.current = instance;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RotateCcw />
|
<Background
|
||||||
Replay
|
className="[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]"
|
||||||
</Button>
|
bgColor="var(--background)"
|
||||||
)}
|
/>
|
||||||
|
<div
|
||||||
|
ref={anchorRef}
|
||||||
|
id="auto-run-animation-trigger"
|
||||||
|
className="absolute bottom-0 left-[50%] h-px w-px"
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
<div className="flex h-6 w-full shrink-0 items-center justify-center">
|
||||||
|
<div className="bg-muted/50 z-[200] flex rounded-3xl px-4 py-2">
|
||||||
|
<Tooltip title="Move to the previous step">
|
||||||
|
<Button variant="ghost" onClick={prevStep}>
|
||||||
|
<ChevronLeft className="size-5" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Play / Pause">
|
||||||
|
<Button variant="ghost" onClick={togglePlay}>
|
||||||
|
{playing ? (
|
||||||
|
<Pause className="size-5" />
|
||||||
|
) : (
|
||||||
|
<Play className="size-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Move to the next step">
|
||||||
|
<Button variant="ghost" onClick={nextStep}>
|
||||||
|
<ChevronRight className="size-5" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="text-muted-foreground ml-2 flex items-center justify-center">
|
||||||
|
<Slider
|
||||||
|
className="w-120"
|
||||||
|
max={playbook.steps.length - 1}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
value={[activeStepIndex >= 0 ? activeStepIndex : 0]}
|
||||||
|
onValueChange={([value]) => {
|
||||||
|
console.info("jump", value);
|
||||||
|
activateStep(value!);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tooltip title="Toggle fullscreen">
|
||||||
|
<Button variant="ghost" size="icon" onClick={toggleFullscreen}>
|
||||||
|
{fullscreen ? (
|
||||||
|
<Minimize className="size-5" />
|
||||||
|
) : (
|
||||||
|
<Fullscreen className="size-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ReactFlow>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
183
web/src/app/landing/store/graph.ts
Normal file
183
web/src/app/landing/store/graph.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
|
import {
|
||||||
|
Brain,
|
||||||
|
FilePen,
|
||||||
|
MessageSquareQuote,
|
||||||
|
Microscope,
|
||||||
|
SquareTerminal,
|
||||||
|
UserCheck,
|
||||||
|
Users,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export type GraphNode = Node<{
|
||||||
|
label: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
active?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Graph = {
|
||||||
|
nodes: GraphNode[];
|
||||||
|
edges: Edge[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 85;
|
||||||
|
const ROW_1 = 0;
|
||||||
|
const ROW_2 = ROW_HEIGHT;
|
||||||
|
const ROW_3 = ROW_HEIGHT * 2;
|
||||||
|
const ROW_4 = ROW_HEIGHT * 2;
|
||||||
|
const ROW_5 = ROW_HEIGHT * 3;
|
||||||
|
const ROW_6 = ROW_HEIGHT * 4;
|
||||||
|
|
||||||
|
export const graph: Graph = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "Start",
|
||||||
|
type: "circle",
|
||||||
|
data: { label: "Start" },
|
||||||
|
position: { x: -75, y: ROW_1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coordinator",
|
||||||
|
data: { icon: MessageSquareQuote, label: "Coordinator" },
|
||||||
|
position: { x: 150, y: ROW_1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Planner",
|
||||||
|
data: { icon: Brain, label: "Planner" },
|
||||||
|
position: { x: 150, y: ROW_2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Reporter",
|
||||||
|
data: { icon: FilePen, label: "Reporter" },
|
||||||
|
position: { x: 275, y: ROW_3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "HumanFeedback",
|
||||||
|
data: { icon: UserCheck, label: "Human Feedback" },
|
||||||
|
position: { x: 25, y: ROW_4 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam",
|
||||||
|
data: { icon: Users, label: "Research Team" },
|
||||||
|
position: { x: 25, y: ROW_5 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Researcher",
|
||||||
|
data: { icon: Microscope, label: "Researcher" },
|
||||||
|
position: { x: -75, y: ROW_6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coder",
|
||||||
|
data: { icon: SquareTerminal, label: "Coder" },
|
||||||
|
position: { x: 125, y: ROW_6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "End",
|
||||||
|
type: "circle",
|
||||||
|
data: { label: "End" },
|
||||||
|
position: { x: 330, y: ROW_6 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: "Start->Coordinator",
|
||||||
|
source: "Start",
|
||||||
|
target: "Coordinator",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "left",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coordinator->Planner",
|
||||||
|
source: "Coordinator",
|
||||||
|
target: "Planner",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Planner->Reporter",
|
||||||
|
source: "Planner",
|
||||||
|
target: "Reporter",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Planner->HumanFeedback",
|
||||||
|
source: "Planner",
|
||||||
|
target: "HumanFeedback",
|
||||||
|
sourceHandle: "left",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "HumanFeedback->Planner",
|
||||||
|
source: "HumanFeedback",
|
||||||
|
target: "Planner",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "bottom",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "HumanFeedback->ResearchTeam",
|
||||||
|
source: "HumanFeedback",
|
||||||
|
target: "ResearchTeam",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Reporter->End",
|
||||||
|
source: "Reporter",
|
||||||
|
target: "End",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam->Researcher",
|
||||||
|
source: "ResearchTeam",
|
||||||
|
target: "Researcher",
|
||||||
|
sourceHandle: "left",
|
||||||
|
targetHandle: "top",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam->Coder",
|
||||||
|
source: "ResearchTeam",
|
||||||
|
target: "Coder",
|
||||||
|
sourceHandle: "bottom",
|
||||||
|
targetHandle: "left",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ResearchTeam->Planner",
|
||||||
|
source: "ResearchTeam",
|
||||||
|
target: "Planner",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "bottom",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Researcher->ResearchTeam",
|
||||||
|
source: "Researcher",
|
||||||
|
target: "ResearchTeam",
|
||||||
|
sourceHandle: "right",
|
||||||
|
targetHandle: "bottom",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Coder->ResearchTeam",
|
||||||
|
source: "Coder",
|
||||||
|
target: "ResearchTeam",
|
||||||
|
sourceHandle: "top",
|
||||||
|
targetHandle: "right",
|
||||||
|
animated: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
5
web/src/app/landing/store/index.ts
Normal file
5
web/src/app/landing/store/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export * from "./graph";
|
||||||
|
export * from "./playbook";
|
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
import { sleep } from "~/core/utils";
|
||||||
|
|
||||||
|
import { graph, type Graph } from "./graph";
|
||||||
|
import { playbook } from "./playbook";
|
||||||
|
|
||||||
|
// Store for MAV(Multi-Agent Visualization)
|
||||||
|
export const useMAVStore = create<{
|
||||||
|
graph: Graph;
|
||||||
|
activeStepIndex: number;
|
||||||
|
playing: boolean;
|
||||||
|
}>(() => ({
|
||||||
|
graph,
|
||||||
|
activeStepIndex: -1,
|
||||||
|
playing: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function activateStep(stepIndex: number) {
|
||||||
|
const nextStep = playbook.steps[stepIndex]!;
|
||||||
|
const currentGraph = useMAVStore.getState().graph;
|
||||||
|
const nextGraph: Graph = {
|
||||||
|
nodes: currentGraph.nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
active: nextStep.activeNodes.includes(node.id),
|
||||||
|
stepDescription:
|
||||||
|
nextStep.activeNodes.indexOf(node.id) ===
|
||||||
|
nextStep.activeNodes.length - 1 && nextStep.description,
|
||||||
|
stepTooltipPosition:
|
||||||
|
nextStep.activeNodes.indexOf(node.id) ===
|
||||||
|
nextStep.activeNodes.length - 1 && nextStep.tooltipPosition,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
edges: currentGraph.edges.map((edge) => ({
|
||||||
|
...edge,
|
||||||
|
animated: nextStep.activeEdges.includes(edge.id),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
useMAVStore.setState({
|
||||||
|
activeStepIndex: stepIndex,
|
||||||
|
graph: nextGraph,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextStep() {
|
||||||
|
let stepIndex = useMAVStore.getState().activeStepIndex;
|
||||||
|
if (stepIndex >= playbook.steps.length - 1) {
|
||||||
|
stepIndex = 0;
|
||||||
|
} else {
|
||||||
|
stepIndex++;
|
||||||
|
}
|
||||||
|
activateStep(stepIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prevStep() {
|
||||||
|
let stepIndex = useMAVStore.getState().activeStepIndex;
|
||||||
|
if (stepIndex <= 0) {
|
||||||
|
stepIndex = playbook.steps.length - 1;
|
||||||
|
} else {
|
||||||
|
stepIndex--;
|
||||||
|
}
|
||||||
|
activateStep(stepIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function play() {
|
||||||
|
const state = useMAVStore.getState();
|
||||||
|
const activeStepIndex = state.activeStepIndex;
|
||||||
|
if (activeStepIndex >= playbook.steps.length - 1) {
|
||||||
|
if (state.playing) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useMAVStore.setState({
|
||||||
|
playing: true,
|
||||||
|
});
|
||||||
|
nextStep();
|
||||||
|
await sleep(4000);
|
||||||
|
const playing = useMAVStore.getState().playing;
|
||||||
|
if (playing) {
|
||||||
|
await play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pause() {
|
||||||
|
useMAVStore.setState({
|
||||||
|
playing: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function togglePlay() {
|
||||||
|
const playing = useMAVStore.getState().playing;
|
||||||
|
if (playing) {
|
||||||
|
pause();
|
||||||
|
} else {
|
||||||
|
await play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stop() {
|
||||||
|
useMAVStore.setState({
|
||||||
|
playing: false,
|
||||||
|
activeStepIndex: -1,
|
||||||
|
graph,
|
||||||
|
});
|
||||||
|
}
|
78
web/src/app/landing/store/playbook.ts
Normal file
78
web/src/app/landing/store/playbook.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export const playbook = {
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"The Coordinator is responsible for engaging with the user to understand their problem and requirements.",
|
||||||
|
activeNodes: ["Start", "Coordinator"],
|
||||||
|
activeEdges: ["Start->Coordinator"],
|
||||||
|
tooltipPosition: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"If the user's problem is clearly defined, the Coordinator will hand it over to the Planner.",
|
||||||
|
activeNodes: ["Coordinator", "Planner"],
|
||||||
|
activeEdges: ["Coordinator->Planner"],
|
||||||
|
tooltipPosition: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Awaiting human feedback to refine the plan.",
|
||||||
|
activeNodes: ["Planner", "HumanFeedback"],
|
||||||
|
activeEdges: ["Planner->HumanFeedback"],
|
||||||
|
tooltipPosition: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Updating the plan based on human feedback.",
|
||||||
|
activeNodes: ["HumanFeedback", "Planner"],
|
||||||
|
activeEdges: ["HumanFeedback->Planner"],
|
||||||
|
tooltipPosition: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"The Research Team is responsible for conducting the core research tasks.",
|
||||||
|
activeNodes: ["HumanFeedback", "ResearchTeam"],
|
||||||
|
activeEdges: [
|
||||||
|
"HumanFeedback->ResearchTeam",
|
||||||
|
"ResearchTeam->HumanFeedback",
|
||||||
|
],
|
||||||
|
tooltipPosition: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"The Researcher is responsible for gathering information using search and crawling tools.",
|
||||||
|
activeNodes: ["ResearchTeam", "Researcher"],
|
||||||
|
activeEdges: ["ResearchTeam->Researcher", "Researcher->ResearchTeam"],
|
||||||
|
tooltipPosition: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"The Coder is responsible for writing Python code to solve math problems, data analysis, and more.",
|
||||||
|
tooltipPosition: "right",
|
||||||
|
activeNodes: ["ResearchTeam", "Coder"],
|
||||||
|
activeEdges: ["ResearchTeam->Coder", "Coder->ResearchTeam"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Once the research tasks are completed, the Researcher will hand over to the Planner.",
|
||||||
|
activeNodes: ["ResearchTeam", "Planner"],
|
||||||
|
activeEdges: ["ResearchTeam->Planner"],
|
||||||
|
tooltipPosition: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"If no additional information is required, the Planner will handoff to the Reporter.",
|
||||||
|
activeNodes: ["Reporter", "Planner"],
|
||||||
|
activeEdges: ["Planner->Reporter"],
|
||||||
|
tooltipPosition: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"The Reporter will prepare a report summarizing the results.",
|
||||||
|
activeNodes: ["End", "Reporter"],
|
||||||
|
activeEdges: ["Reporter->End"],
|
||||||
|
tooltipPosition: "bottom",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user