mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-08-12 17:13:38 +08:00
feat: build an AI agent from 0 to 1 -- 11 progressive sessions
- 11 sessions from basic agent loop to autonomous teams - Python MVP implementations for each session - Mental-model-first docs in en/zh/ja - Interactive web platform with step-through visualizations - Incremental architecture: each session adds one mechanism
This commit is contained in:
228
web/src/components/architecture/arch-diagram.tsx
Normal file
228
web/src/components/architecture/arch-diagram.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LAYERS } from "@/lib/constants";
|
||||
import versionsData from "@/data/generated/versions.json";
|
||||
|
||||
const CLASS_DESCRIPTIONS: Record<string, string> = {
|
||||
TodoManager: "Visible task planning with constraints",
|
||||
SkillLoader: "Dynamic knowledge injection from SKILL.md files",
|
||||
ContextManager: "Three-layer context compression pipeline",
|
||||
Task: "File-based persistent task with dependencies",
|
||||
TaskManager: "File-based persistent task CRUD with dependencies",
|
||||
BackgroundTask: "Single background execution unit",
|
||||
BackgroundManager: "Non-blocking thread execution + notification queue",
|
||||
TeammateManager: "Multi-agent team lifecycle and coordination",
|
||||
Teammate: "Individual agent identity and state tracking",
|
||||
SharedBoard: "Cross-agent shared state coordination",
|
||||
};
|
||||
|
||||
interface ArchDiagramProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
function getLayerColor(versionId: string): string {
|
||||
const layer = LAYERS.find((l) => (l.versions as readonly string[]).includes(versionId));
|
||||
return layer?.color ?? "#71717a";
|
||||
}
|
||||
|
||||
function getLayerColorClasses(versionId: string): {
|
||||
border: string;
|
||||
bg: string;
|
||||
} {
|
||||
const v =
|
||||
versionsData.versions.find((v) => v.id === versionId) as { layer?: string } | undefined;
|
||||
const layer = v?.layer;
|
||||
switch (layer) {
|
||||
case "tools":
|
||||
return {
|
||||
border: "border-blue-500",
|
||||
bg: "bg-blue-500/10",
|
||||
};
|
||||
case "planning":
|
||||
return {
|
||||
border: "border-emerald-500",
|
||||
bg: "bg-emerald-500/10",
|
||||
};
|
||||
case "memory":
|
||||
return {
|
||||
border: "border-purple-500",
|
||||
bg: "bg-purple-500/10",
|
||||
};
|
||||
case "concurrency":
|
||||
return {
|
||||
border: "border-amber-500",
|
||||
bg: "bg-amber-500/10",
|
||||
};
|
||||
case "collaboration":
|
||||
return {
|
||||
border: "border-red-500",
|
||||
bg: "bg-red-500/10",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
border: "border-zinc-500",
|
||||
bg: "bg-zinc-500/10",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function collectClassesUpTo(
|
||||
targetId: string
|
||||
): { name: string; introducedIn: string }[] {
|
||||
const { versions, diffs } = versionsData;
|
||||
const order = versions.map((v) => v.id);
|
||||
const targetIdx = order.indexOf(targetId);
|
||||
if (targetIdx < 0) return [];
|
||||
|
||||
const result: { name: string; introducedIn: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (let i = 0; i <= targetIdx; i++) {
|
||||
const v = versions[i];
|
||||
if (!v.classes) continue;
|
||||
for (const cls of v.classes) {
|
||||
if (!seen.has(cls.name)) {
|
||||
seen.add(cls.name);
|
||||
result.push({ name: cls.name, introducedIn: v.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getNewClassNames(version: string): Set<string> {
|
||||
const diff = versionsData.diffs.find((d) => d.to === version);
|
||||
if (!diff) {
|
||||
const v = versionsData.versions.find((ver) => ver.id === version);
|
||||
return new Set(v?.classes?.map((c) => c.name) ?? []);
|
||||
}
|
||||
return new Set(diff.newClasses ?? []);
|
||||
}
|
||||
|
||||
export function ArchDiagram({ version }: ArchDiagramProps) {
|
||||
const allClasses = collectClassesUpTo(version);
|
||||
const newClassNames = getNewClassNames(version);
|
||||
const versionData = versionsData.versions.find((v) => v.id === version);
|
||||
const tools = versionData?.tools ?? [];
|
||||
|
||||
const reversed = [...allClasses].reverse();
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{reversed.map((cls, i) => {
|
||||
const isNew = newClassNames.has(cls.name);
|
||||
const colorClasses = getLayerColorClasses(cls.introducedIn);
|
||||
|
||||
return (
|
||||
<div key={cls.name}>
|
||||
{i > 0 && (
|
||||
<div className="flex justify-center py-1">
|
||||
<motion.svg
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 24 20"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: i * 0.08 + 0.05 }}
|
||||
>
|
||||
<motion.line
|
||||
x1={12}
|
||||
y1={0}
|
||||
x2={12}
|
||||
y2={14}
|
||||
stroke="var(--color-text-secondary)"
|
||||
strokeWidth={1.5}
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.08 }}
|
||||
/>
|
||||
<motion.polygon
|
||||
points="7,12 12,19 17,12"
|
||||
fill="var(--color-text-secondary)"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: i * 0.08 + 0.2 }}
|
||||
/>
|
||||
</motion.svg>
|
||||
</div>
|
||||
)}
|
||||
<motion.div
|
||||
key={cls.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.08, duration: 0.3 }}
|
||||
className={cn(
|
||||
"rounded-lg border-2 px-4 py-3 transition-colors",
|
||||
isNew
|
||||
? cn(colorClasses.border, colorClasses.bg)
|
||||
: "border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono text-sm font-semibold",
|
||||
isNew
|
||||
? "text-zinc-900 dark:text-white"
|
||||
: "text-zinc-400 dark:text-zinc-500"
|
||||
)}
|
||||
>
|
||||
{cls.name}
|
||||
</span>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-0.5 text-xs",
|
||||
isNew
|
||||
? "text-zinc-600 dark:text-zinc-300"
|
||||
: "text-zinc-400 dark:text-zinc-500"
|
||||
)}
|
||||
>
|
||||
{CLASS_DESCRIPTIONS[cls.name] || ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
{cls.introducedIn}
|
||||
</span>
|
||||
{isNew && (
|
||||
<span className="rounded-full bg-zinc-900 px-2 py-0.5 text-[10px] font-bold uppercase text-white dark:bg-white dark:text-zinc-900">
|
||||
NEW
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{allClasses.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed border-zinc-300 px-4 py-6 text-center text-sm text-zinc-400 dark:border-zinc-600">
|
||||
No classes in this version (functions only)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tools.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: reversed.length * 0.08 + 0.1 }}
|
||||
className="flex flex-wrap gap-1.5 pt-2"
|
||||
>
|
||||
{tools.map((tool) => (
|
||||
<span
|
||||
key={tool}
|
||||
className="rounded-md bg-zinc-100 px-2 py-1 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
145
web/src/components/architecture/design-decisions.tsx
Normal file
145
web/src/components/architecture/design-decisions.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useTranslations, useLocale } from "@/lib/i18n";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import s01Annotations from "@/data/annotations/s01.json";
|
||||
import s02Annotations from "@/data/annotations/s02.json";
|
||||
import s03Annotations from "@/data/annotations/s03.json";
|
||||
import s04Annotations from "@/data/annotations/s04.json";
|
||||
import s05Annotations from "@/data/annotations/s05.json";
|
||||
import s06Annotations from "@/data/annotations/s06.json";
|
||||
import s07Annotations from "@/data/annotations/s07.json";
|
||||
import s08Annotations from "@/data/annotations/s08.json";
|
||||
import s09Annotations from "@/data/annotations/s09.json";
|
||||
import s10Annotations from "@/data/annotations/s10.json";
|
||||
import s11Annotations from "@/data/annotations/s11.json";
|
||||
|
||||
interface Decision {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
alternatives: string;
|
||||
zh?: { title: string; description: string };
|
||||
ja?: { title: string; description: string };
|
||||
}
|
||||
|
||||
interface AnnotationFile {
|
||||
version: string;
|
||||
decisions: Decision[];
|
||||
}
|
||||
|
||||
const ANNOTATIONS: Record<string, AnnotationFile> = {
|
||||
s01: s01Annotations as AnnotationFile,
|
||||
s02: s02Annotations as AnnotationFile,
|
||||
s03: s03Annotations as AnnotationFile,
|
||||
s04: s04Annotations as AnnotationFile,
|
||||
s05: s05Annotations as AnnotationFile,
|
||||
s06: s06Annotations as AnnotationFile,
|
||||
s07: s07Annotations as AnnotationFile,
|
||||
s08: s08Annotations as AnnotationFile,
|
||||
s09: s09Annotations as AnnotationFile,
|
||||
s10: s10Annotations as AnnotationFile,
|
||||
s11: s11Annotations as AnnotationFile,
|
||||
};
|
||||
|
||||
interface DesignDecisionsProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
function DecisionCard({
|
||||
decision,
|
||||
locale,
|
||||
}: {
|
||||
decision: Decision;
|
||||
locale: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = useTranslations("version");
|
||||
|
||||
const localized =
|
||||
locale !== "en" ? (decision as unknown as Record<string, unknown>)[locale] as { title?: string; description?: string } | undefined : undefined;
|
||||
|
||||
const title = localized?.title || decision.title;
|
||||
const description = localized?.description || decision.description;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex w-full items-center justify-between px-4 py-3 text-left"
|
||||
>
|
||||
<span className="pr-4 text-sm font-semibold text-zinc-900 dark:text-white">
|
||||
{title}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn(
|
||||
"shrink-0 text-zinc-400 transition-transform duration-200",
|
||||
open && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="border-t border-zinc-100 px-4 py-3 dark:border-zinc-800">
|
||||
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-300">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{decision.alternatives && (
|
||||
<div className="mt-3">
|
||||
<h4 className="text-xs font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
|
||||
{t("alternatives")}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
|
||||
{decision.alternatives}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DesignDecisions({ version }: DesignDecisionsProps) {
|
||||
const t = useTranslations("version");
|
||||
const locale = useLocale();
|
||||
|
||||
const annotations = ANNOTATIONS[version];
|
||||
if (!annotations || annotations.decisions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">{t("design_decisions")}</h2>
|
||||
<div className="space-y-2">
|
||||
{annotations.decisions.map((decision, i) => (
|
||||
<motion.div
|
||||
key={decision.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
>
|
||||
<DecisionCard decision={decision} locale={locale} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
243
web/src/components/architecture/execution-flow.tsx
Normal file
243
web/src/components/architecture/execution-flow.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "@/lib/i18n";
|
||||
import { getFlowForVersion } from "@/data/execution-flows";
|
||||
import type { FlowNode, FlowEdge } from "@/types/agent-data";
|
||||
|
||||
const NODE_WIDTH = 140;
|
||||
const NODE_HEIGHT = 40;
|
||||
const DIAMOND_SIZE = 50;
|
||||
|
||||
const LAYER_COLORS: Record<string, string> = {
|
||||
start: "#3B82F6",
|
||||
process: "#10B981",
|
||||
decision: "#F59E0B",
|
||||
subprocess: "#8B5CF6",
|
||||
end: "#EF4444",
|
||||
};
|
||||
|
||||
function getNodeCenter(node: FlowNode): { cx: number; cy: number } {
|
||||
return { cx: node.x, cy: node.y };
|
||||
}
|
||||
|
||||
function getEdgePath(from: FlowNode, to: FlowNode): string {
|
||||
const { cx: x1, cy: y1 } = getNodeCenter(from);
|
||||
const { cx: x2, cy: y2 } = getNodeCenter(to);
|
||||
|
||||
const halfH = from.type === "decision" ? DIAMOND_SIZE / 2 : NODE_HEIGHT / 2;
|
||||
const halfHTo = to.type === "decision" ? DIAMOND_SIZE / 2 : NODE_HEIGHT / 2;
|
||||
|
||||
if (Math.abs(x1 - x2) < 10) {
|
||||
const startY = y1 + halfH;
|
||||
const endY = y2 - halfHTo;
|
||||
return `M ${x1} ${startY} L ${x2} ${endY}`;
|
||||
}
|
||||
|
||||
const startY = y1 + halfH;
|
||||
const endY = y2 - halfHTo;
|
||||
const midY = (startY + endY) / 2;
|
||||
return `M ${x1} ${startY} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${endY}`;
|
||||
}
|
||||
|
||||
function NodeShape({ node }: { node: FlowNode }) {
|
||||
const color = LAYER_COLORS[node.type];
|
||||
const lines = node.label.split("\n");
|
||||
|
||||
if (node.type === "decision") {
|
||||
const half = DIAMOND_SIZE / 2;
|
||||
return (
|
||||
<g>
|
||||
<polygon
|
||||
points={`${node.x},${node.y - half} ${node.x + half},${node.y} ${node.x},${node.y + half} ${node.x - half},${node.y}`}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
{lines.map((line, i) => (
|
||||
<text
|
||||
key={i}
|
||||
x={node.x}
|
||||
y={node.y + (i - (lines.length - 1) / 2) * 12}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={10}
|
||||
fontFamily="monospace"
|
||||
fill="currentColor"
|
||||
>
|
||||
{line}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === "start" || node.type === "end") {
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
x={node.x - NODE_WIDTH / 2}
|
||||
y={node.y - NODE_HEIGHT / 2}
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
rx={NODE_HEIGHT / 2}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<text
|
||||
x={node.x}
|
||||
y={node.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={12}
|
||||
fontWeight={600}
|
||||
fontFamily="monospace"
|
||||
fill="currentColor"
|
||||
>
|
||||
{node.label}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
const isSubprocess = node.type === "subprocess";
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
x={node.x - NODE_WIDTH / 2}
|
||||
y={node.y - NODE_HEIGHT / 2}
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
rx={4}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={isSubprocess ? "6 3" : undefined}
|
||||
/>
|
||||
{lines.map((line, i) => (
|
||||
<text
|
||||
key={i}
|
||||
x={node.x}
|
||||
y={node.y + (i - (lines.length - 1) / 2) * 13}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={11}
|
||||
fontFamily="monospace"
|
||||
fill="currentColor"
|
||||
>
|
||||
{line}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function EdgePath({
|
||||
edge,
|
||||
nodes,
|
||||
index,
|
||||
}: {
|
||||
edge: FlowEdge;
|
||||
nodes: FlowNode[];
|
||||
index: number;
|
||||
}) {
|
||||
const from = nodes.find((n) => n.id === edge.from);
|
||||
const to = nodes.find((n) => n.id === edge.to);
|
||||
if (!from || !to) return null;
|
||||
|
||||
const d = getEdgePath(from, to);
|
||||
const midX = (from.x + to.x) / 2;
|
||||
const midY = (from.y + to.y) / 2;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<motion.path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="var(--color-text-secondary)"
|
||||
strokeWidth={1.5}
|
||||
markerEnd="url(#arrowhead)"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.12 }}
|
||||
/>
|
||||
{edge.label && (
|
||||
<motion.text
|
||||
x={midX + 8}
|
||||
y={midY - 4}
|
||||
fontSize={10}
|
||||
fill="var(--color-text-secondary)"
|
||||
fontFamily="monospace"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: index * 0.12 + 0.3 }}
|
||||
>
|
||||
{edge.label}
|
||||
</motion.text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExecutionFlowProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export function ExecutionFlow({ version }: ExecutionFlowProps) {
|
||||
const t = useTranslations("version");
|
||||
const [flow, setFlow] = useState<ReturnType<typeof getFlowForVersion>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFlow(getFlowForVersion(version));
|
||||
}, [version]);
|
||||
|
||||
if (!flow) return null;
|
||||
|
||||
const maxY = Math.max(...flow.nodes.map((n) => n.y)) + 50;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-4 text-xl font-semibold">{t("execution_flow")}</h2>
|
||||
<div className="overflow-x-auto rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4">
|
||||
<svg
|
||||
viewBox={`0 0 600 ${maxY}`}
|
||||
className="mx-auto w-full max-w-[600px]"
|
||||
style={{ minHeight: 300 }}
|
||||
>
|
||||
<defs>
|
||||
<marker
|
||||
id="arrowhead"
|
||||
markerWidth={8}
|
||||
markerHeight={6}
|
||||
refX={8}
|
||||
refY={3}
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0 0, 8 3, 0 6"
|
||||
fill="var(--color-text-secondary)"
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{flow.edges.map((edge, i) => (
|
||||
<EdgePath key={`${edge.from}-${edge.to}`} edge={edge} nodes={flow.nodes} index={i} />
|
||||
))}
|
||||
|
||||
{flow.nodes.map((node, i) => (
|
||||
<motion.g
|
||||
key={node.id}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.06, duration: 0.3 }}
|
||||
>
|
||||
<NodeShape node={node} />
|
||||
</motion.g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
70
web/src/components/architecture/message-flow.tsx
Normal file
70
web/src/components/architecture/message-flow.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
const FLOW_STEPS = [
|
||||
{ role: "user", label: "user", color: "bg-blue-500" },
|
||||
{ role: "assistant", label: "assistant", color: "bg-zinc-600" },
|
||||
{ role: "tool_call", label: "tool_call", color: "bg-amber-500" },
|
||||
{ role: "tool_result", label: "tool_result", color: "bg-emerald-500" },
|
||||
{ role: "assistant", label: "assistant", color: "bg-zinc-600" },
|
||||
{ role: "tool_call", label: "tool_call", color: "bg-amber-500" },
|
||||
{ role: "tool_result", label: "tool_result", color: "bg-emerald-500" },
|
||||
{ role: "assistant", label: "assistant (final)", color: "bg-zinc-600" },
|
||||
];
|
||||
|
||||
export function MessageFlow() {
|
||||
const [count, setCount] = useState(0);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCount((prev) => {
|
||||
if (prev >= FLOW_STEPS.length) {
|
||||
setTimeout(() => setCount(0), 1500);
|
||||
return prev;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, 800);
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
|
||||
messages[]
|
||||
</span>
|
||||
<span className="ml-auto rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-xs tabular-nums dark:bg-zinc-800">
|
||||
len={count}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1.5 overflow-x-auto pb-1">
|
||||
<AnimatePresence>
|
||||
{FLOW_STEPS.slice(0, count).map((step, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, scale: 0.7, width: 0 }}
|
||||
animate={{ opacity: 1, scale: 1, width: "auto" }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className={`flex shrink-0 items-center rounded-md px-2.5 py-1.5 ${step.color}`}
|
||||
>
|
||||
<span className="whitespace-nowrap font-mono text-[10px] font-medium text-white">
|
||||
{step.label}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
{count === 0 && (
|
||||
<div className="flex h-7 items-center text-xs text-[var(--color-text-secondary)]">
|
||||
[]
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user