mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-19 22:29:12 +08:00
feat: re-implement MultiAgentVisualization
This commit is contained in:
parent
cdb1492cef
commit
b152e787cc
@ -6,199 +6,39 @@
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Handle,
|
||||
Position,
|
||||
type Node,
|
||||
type Edge,
|
||||
type ReactFlowInstance,
|
||||
} from "@xyflow/react";
|
||||
import {
|
||||
Brain,
|
||||
FilePen,
|
||||
MessageSquareQuote,
|
||||
Microscope,
|
||||
RotateCcw,
|
||||
SquareTerminal,
|
||||
UserCheck,
|
||||
Users,
|
||||
Play,
|
||||
type LucideIcon,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
Pause,
|
||||
Fullscreen,
|
||||
Minimize,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { ShineBorder } from "~/components/magicui/shine-border";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Slider } from "~/components/ui/slider";
|
||||
import { useIntersectionObserver } from "~/hooks/use-intersection-observer";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
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 type WorkflowNode = Node<{
|
||||
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,
|
||||
},
|
||||
];
|
||||
import { playbook, type GraphNode } from "../store";
|
||||
import {
|
||||
activateStep,
|
||||
nextStep,
|
||||
play,
|
||||
prevStep,
|
||||
togglePlay,
|
||||
useMAVStore,
|
||||
} from "../store/mav-store";
|
||||
|
||||
const nodeTypes = {
|
||||
circle: CircleNode,
|
||||
@ -206,196 +46,121 @@ const nodeTypes = {
|
||||
default: AgentNode,
|
||||
};
|
||||
|
||||
const WORKFLOW_STEPS = [
|
||||
{
|
||||
description:
|
||||
"The Coordinator is responsible for engaging with the user to understand their problem and requirements.",
|
||||
tooltipPosition: "right",
|
||||
activeNodes: ["Start", "Coordinator"],
|
||||
activeEdges: ["Start->Coordinator"],
|
||||
},
|
||||
{
|
||||
description:
|
||||
"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({
|
||||
export function MultiAgentVisualization({ className }: { className?: string }) {
|
||||
const {
|
||||
graph: { nodes, edges },
|
||||
activeStepIndex,
|
||||
playing,
|
||||
} = useMAVStore((state) => state);
|
||||
const flowRef = useRef<ReactFlowInstance<GraphNode, Edge>>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const { ref: anchorRef } = useIntersectionObserver({
|
||||
rootMargin: "0%",
|
||||
threshold: 0,
|
||||
onChange: (isIntersecting) => {
|
||||
if (isIntersecting && !hasAutoRun) {
|
||||
setHasAutoRun(true);
|
||||
void run();
|
||||
if (isIntersecting && !playing) {
|
||||
void play();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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 (
|
||||
<ReactFlow
|
||||
style={{
|
||||
["--xy-background-color-default" as string]: "transparent",
|
||||
}}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
proOptions={{ hideAttribution: true }}
|
||||
colorMode="dark"
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
preventScrolling={false}
|
||||
panOnDrag={false}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("flex h-full w-full flex-col pb-4", className)}
|
||||
>
|
||||
<Background
|
||||
className="[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]"
|
||||
bgColor="var(--background)"
|
||||
/>
|
||||
<div
|
||||
id="auto-run-animation-trigger"
|
||||
ref={ref}
|
||||
className="absolute bottom-0 left-[50%] h-px w-px"
|
||||
/>
|
||||
<div className="absolute top-0 right-0 left-0 z-200 flex items-center justify-center">
|
||||
{!isRunning && (
|
||||
<Button
|
||||
className="translate-x-[22vw]"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
void run();
|
||||
}}
|
||||
>
|
||||
<RotateCcw />
|
||||
Replay
|
||||
</Button>
|
||||
)}
|
||||
<ReactFlow
|
||||
className={cn("flex min-h-0 flex-grow")}
|
||||
style={{
|
||||
["--xy-background-color-default" as string]: "transparent",
|
||||
}}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
proOptions={{ hideAttribution: true }}
|
||||
colorMode="dark"
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
preventScrolling={false}
|
||||
panOnDrag={false}
|
||||
onInit={(instance) => {
|
||||
flowRef.current = instance;
|
||||
}}
|
||||
>
|
||||
<Background
|
||||
className="[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]"
|
||||
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>
|
||||
</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