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:
CrazyBoyM
2026-02-21 17:02:43 +08:00
committed by CrazyBoyM
commit c6a27ef1d7
156 changed files with 28059 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
"use client";
import { useState, useEffect } from "react";
export function useDarkMode(): boolean {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const html = document.documentElement;
setIsDark(html.classList.contains("dark"));
const observer = new MutationObserver(() => {
setIsDark(html.classList.contains("dark"));
});
observer.observe(html, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
return isDark;
}
export interface SvgPalette {
nodeFill: string;
nodeStroke: string;
nodeText: string;
activeNodeFill: string;
activeNodeStroke: string;
activeNodeText: string;
endNodeFill: string;
endNodeStroke: string;
edgeStroke: string;
activeEdgeStroke: string;
arrowFill: string;
labelFill: string;
bgSubtle: string;
}
export function useSvgPalette(): SvgPalette {
const isDark = useDarkMode();
if (isDark) {
return {
nodeFill: "#27272a",
nodeStroke: "#3f3f46",
nodeText: "#d4d4d8",
activeNodeFill: "#3b82f6",
activeNodeStroke: "#2563eb",
activeNodeText: "#ffffff",
endNodeFill: "#a855f7",
endNodeStroke: "#9333ea",
edgeStroke: "#52525b",
activeEdgeStroke: "#3b82f6",
arrowFill: "#71717a",
labelFill: "#a1a1aa",
bgSubtle: "#18181b",
};
}
return {
nodeFill: "#e2e8f0",
nodeStroke: "#cbd5e1",
nodeText: "#475569",
activeNodeFill: "#3b82f6",
activeNodeStroke: "#2563eb",
activeNodeText: "#ffffff",
endNodeFill: "#a855f7",
endNodeStroke: "#9333ea",
edgeStroke: "#cbd5e1",
activeEdgeStroke: "#3b82f6",
arrowFill: "#94a3b8",
labelFill: "#94a3b8",
bgSubtle: "#f8fafc",
};
}
+84
View File
@@ -0,0 +1,84 @@
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import type { SimStep } from "@/types/agent-data";
interface SimulatorState {
currentIndex: number;
isPlaying: boolean;
speed: number;
}
export function useSimulator(steps: SimStep[]) {
const [state, setState] = useState<SimulatorState>({
currentIndex: -1,
isPlaying: false,
speed: 1,
});
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const stepForward = useCallback(() => {
setState((prev) => {
if (prev.currentIndex >= steps.length - 1) {
return { ...prev, isPlaying: false };
}
return { ...prev, currentIndex: prev.currentIndex + 1 };
});
}, [steps.length]);
const play = useCallback(() => {
setState((prev) => {
if (prev.currentIndex >= steps.length - 1) {
return prev;
}
return { ...prev, isPlaying: true };
});
}, [steps.length]);
const pause = useCallback(() => {
clearTimer();
setState((prev) => ({ ...prev, isPlaying: false }));
}, [clearTimer]);
const reset = useCallback(() => {
clearTimer();
setState({ currentIndex: -1, isPlaying: false, speed: state.speed });
}, [clearTimer, state.speed]);
const setSpeed = useCallback((speed: number) => {
setState((prev) => ({ ...prev, speed }));
}, []);
useEffect(() => {
if (state.isPlaying && state.currentIndex < steps.length - 1) {
const delay = 1200 / state.speed;
timerRef.current = setTimeout(() => {
stepForward();
}, delay);
} else if (state.isPlaying && state.currentIndex >= steps.length - 1) {
setState((prev) => ({ ...prev, isPlaying: false }));
}
return () => clearTimer();
}, [state.isPlaying, state.currentIndex, state.speed, steps.length, stepForward, clearTimer]);
return {
currentIndex: state.currentIndex,
isPlaying: state.isPlaying,
speed: state.speed,
visibleSteps: steps.slice(0, state.currentIndex + 1),
totalSteps: steps.length,
isComplete: state.currentIndex >= steps.length - 1,
play,
pause,
stepForward,
reset,
setSpeed,
};
}
+84
View File
@@ -0,0 +1,84 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
interface SteppedVisualizationOptions {
totalSteps: number;
autoPlayInterval?: number; // ms, default 2000
}
interface SteppedVisualizationReturn {
currentStep: number;
totalSteps: number;
next: () => void;
prev: () => void;
reset: () => void;
goToStep: (step: number) => void;
isPlaying: boolean;
toggleAutoPlay: () => void;
isFirstStep: boolean;
isLastStep: boolean;
}
export function useSteppedVisualization({
totalSteps,
autoPlayInterval = 2000,
}: SteppedVisualizationOptions): SteppedVisualizationReturn {
const [currentStep, setCurrentStep] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const next = useCallback(() => {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps - 1));
}, [totalSteps]);
const prev = useCallback(() => {
setCurrentStep((prev) => Math.max(prev - 1, 0));
}, []);
const reset = useCallback(() => {
setCurrentStep(0);
setIsPlaying(false);
}, []);
const goToStep = useCallback(
(step: number) => {
setCurrentStep(Math.max(0, Math.min(step, totalSteps - 1)));
},
[totalSteps]
);
const toggleAutoPlay = useCallback(() => {
setIsPlaying((prev) => !prev);
}, []);
useEffect(() => {
if (isPlaying) {
intervalRef.current = setInterval(() => {
setCurrentStep((prev) => {
if (prev >= totalSteps - 1) {
setIsPlaying(false);
return prev;
}
return prev + 1;
});
}, autoPlayInterval);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [isPlaying, totalSteps, autoPlayInterval]);
return {
currentStep,
totalSteps,
next,
prev,
reset,
goToStep,
isPlaying,
toggleAutoPlay,
isFirstStep: currentStep === 0,
isLastStep: currentStep === totalSteps - 1,
};
}