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 14:37:42 +08:00
committed by CrazyBoyM
commit c6a27ef1d7
156 changed files with 28059 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
"use client";
import { useState, useMemo } from "react";
import { diffLines, Change } from "diff";
import { cn } from "@/lib/utils";
interface CodeDiffProps {
oldSource: string;
newSource: string;
oldLabel: string;
newLabel: string;
}
export function CodeDiff({ oldSource, newSource, oldLabel, newLabel }: CodeDiffProps) {
const [viewMode, setViewMode] = useState<"unified" | "split">("unified");
const changes = useMemo(() => diffLines(oldSource, newSource), [oldSource, newSource]);
return (
<div>
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 truncate text-sm text-zinc-500 dark:text-zinc-400">
<span className="font-medium text-zinc-700 dark:text-zinc-300">{oldLabel}</span>
{" -> "}
<span className="font-medium text-zinc-700 dark:text-zinc-300">{newLabel}</span>
</div>
<div className="flex shrink-0 rounded-lg border border-zinc-200 dark:border-zinc-700">
<button
onClick={() => setViewMode("unified")}
className={cn(
"min-h-[36px] px-3 text-xs font-medium transition-colors",
viewMode === "unified"
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400"
)}
>
Unified
</button>
<button
onClick={() => setViewMode("split")}
className={cn(
"min-h-[36px] px-3 text-xs font-medium transition-colors sm:inline-flex hidden",
viewMode === "split"
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "text-zinc-500 hover:text-zinc-700 dark:text-zinc-400"
)}
>
Split
</button>
</div>
</div>
{viewMode === "unified" ? (
<UnifiedView changes={changes} />
) : (
<SplitView changes={changes} />
)}
</div>
);
}
function UnifiedView({ changes }: { changes: Change[] }) {
let oldLine = 1;
let newLine = 1;
const rows: { oldNum: number | null; newNum: number | null; type: "add" | "remove" | "context"; text: string }[] = [];
for (const change of changes) {
const lines = change.value.replace(/\n$/, "").split("\n");
for (const line of lines) {
if (change.added) {
rows.push({ oldNum: null, newNum: newLine++, type: "add", text: line });
} else if (change.removed) {
rows.push({ oldNum: oldLine++, newNum: null, type: "remove", text: line });
} else {
rows.push({ oldNum: oldLine++, newNum: newLine++, type: "context", text: line });
}
}
}
return (
<div className="overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700">
<table className="w-full border-collapse font-mono text-xs leading-5">
<tbody>
{rows.map((row, i) => (
<tr
key={i}
className={cn(
row.type === "add" && "bg-green-50 dark:bg-green-950/30",
row.type === "remove" && "bg-red-50 dark:bg-red-950/30"
)}
>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.oldNum ?? ""}
</td>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.newNum ?? ""}
</td>
<td className="w-4 select-none px-1 text-center">
{row.type === "add" && <span className="text-green-600 dark:text-green-400">+</span>}
{row.type === "remove" && <span className="text-red-600 dark:text-red-400">-</span>}
</td>
<td className="whitespace-pre px-2">
<span
className={cn(
row.type === "add" && "text-green-800 dark:text-green-300",
row.type === "remove" && "text-red-800 dark:text-red-300",
row.type === "context" && "text-zinc-700 dark:text-zinc-300"
)}
>
{row.text}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function SplitView({ changes }: { changes: Change[] }) {
let oldLine = 1;
let newLine = 1;
type SplitRow = {
left: { num: number | null; text: string; type: "remove" | "context" | "empty" };
right: { num: number | null; text: string; type: "add" | "context" | "empty" };
};
const rows: SplitRow[] = [];
for (const change of changes) {
const lines = change.value.replace(/\n$/, "").split("\n");
if (change.removed) {
for (const line of lines) {
rows.push({
left: { num: oldLine++, text: line, type: "remove" },
right: { num: null, text: "", type: "empty" },
});
}
} else if (change.added) {
let filled = 0;
for (const line of lines) {
// Try to fill in empty right-side slots from preceding removes
const lastUnfilled = rows.length - lines.length + filled;
if (
lastUnfilled >= 0 &&
lastUnfilled < rows.length &&
rows[lastUnfilled].right.type === "empty" &&
rows[lastUnfilled].left.type === "remove"
) {
rows[lastUnfilled].right = { num: newLine++, text: line, type: "add" };
} else {
rows.push({
left: { num: null, text: "", type: "empty" },
right: { num: newLine++, text: line, type: "add" },
});
}
filled++;
}
} else {
for (const line of lines) {
rows.push({
left: { num: oldLine++, text: line, type: "context" },
right: { num: newLine++, text: line, type: "context" },
});
}
}
}
const cellClass = (type: string) =>
cn(
"whitespace-pre px-2",
type === "add" && "bg-green-50 text-green-800 dark:bg-green-950/30 dark:text-green-300",
type === "remove" && "bg-red-50 text-red-800 dark:bg-red-950/30 dark:text-red-300",
type === "context" && "text-zinc-700 dark:text-zinc-300",
type === "empty" && "bg-zinc-50 dark:bg-zinc-900"
);
return (
<div className="overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-700">
<table className="w-full border-collapse font-mono text-xs leading-5">
<tbody>
{rows.map((row, i) => (
<tr key={i}>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.left.num ?? ""}
</td>
<td className={cn("w-1/2 border-r border-zinc-200 dark:border-zinc-700", cellClass(row.left.type))}>
{row.left.text}
</td>
<td className="w-10 select-none border-r border-zinc-200 px-2 text-right text-zinc-400 dark:border-zinc-700 dark:text-zinc-600">
{row.right.num ?? ""}
</td>
<td className={cn("w-1/2", cellClass(row.right.type))}>
{row.right.text}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import { motion } from "framer-motion";
import { useTranslations } from "@/lib/i18n";
import { Card } from "@/components/ui/card";
interface WhatsNewProps {
diff: {
from: string;
to: string;
newClasses: string[];
newFunctions: string[];
newTools: string[];
locDelta: number;
} | null;
}
export function WhatsNew({ diff }: WhatsNewProps) {
const t = useTranslations("version");
const td = useTranslations("diff");
if (!diff) {
return null;
}
const hasContent =
diff.newClasses.length > 0 ||
diff.newTools.length > 0 ||
diff.newFunctions.length > 0 ||
diff.locDelta !== 0;
if (!hasContent) {
return null;
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">{t("whats_new")}</h2>
<div className="grid gap-4 sm:grid-cols-2">
{diff.newClasses.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="h-full">
<h3 className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("new_classes")}
</h3>
<div className="space-y-1.5">
{diff.newClasses.map((cls) => (
<div
key={cls}
className="rounded-md bg-emerald-50 px-3 py-1.5 font-mono text-sm font-medium text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300"
>
{cls}
</div>
))}
</div>
</Card>
</motion.div>
)}
{diff.newTools.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<Card className="h-full">
<h3 className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("new_tools")}
</h3>
<div className="flex flex-wrap gap-1.5">
{diff.newTools.map((tool) => (
<span
key={tool}
className="rounded-full bg-blue-50 px-3 py-1 font-mono text-xs font-medium text-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{tool}
</span>
))}
</div>
</Card>
</motion.div>
)}
{diff.newFunctions.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="h-full">
<h3 className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("new_functions")}
</h3>
<ul className="space-y-1 text-sm text-zinc-700 dark:text-zinc-300">
{diff.newFunctions.map((fn) => (
<li key={fn} className="font-mono">
<span className="text-zinc-400 dark:text-zinc-500">
def{" "}
</span>
{fn}()
</li>
))}
</ul>
</Card>
</motion.div>
)}
{diff.locDelta !== 0 && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.25 }}
>
<Card className="flex h-full items-center">
<div>
<h3 className="mb-1 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{td("loc_delta")}
</h3>
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">
+{diff.locDelta} lines
</p>
</div>
</Card>
</motion.div>
)}
</div>
</div>
);
}