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:
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user