mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-21 04:33:36 +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:
82
web/src/app/[locale]/(learn)/[version]/client.tsx
Normal file
82
web/src/app/[locale]/(learn)/[version]/client.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { ArchDiagram } from "@/components/architecture/arch-diagram";
|
||||
import { WhatsNew } from "@/components/diff/whats-new";
|
||||
import { DesignDecisions } from "@/components/architecture/design-decisions";
|
||||
import { DocRenderer } from "@/components/docs/doc-renderer";
|
||||
import { SourceViewer } from "@/components/code/source-viewer";
|
||||
import { AgentLoopSimulator } from "@/components/simulator/agent-loop-simulator";
|
||||
import { ExecutionFlow } from "@/components/architecture/execution-flow";
|
||||
import { SessionVisualization } from "@/components/visualizations";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import { useTranslations } from "@/lib/i18n";
|
||||
|
||||
interface VersionDetailClientProps {
|
||||
version: string;
|
||||
diff: {
|
||||
from: string;
|
||||
to: string;
|
||||
newClasses: string[];
|
||||
newFunctions: string[];
|
||||
newTools: string[];
|
||||
locDelta: number;
|
||||
} | null;
|
||||
source: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export function VersionDetailClient({
|
||||
version,
|
||||
diff,
|
||||
source,
|
||||
filename,
|
||||
}: VersionDetailClientProps) {
|
||||
const t = useTranslations("version");
|
||||
|
||||
const tabs = [
|
||||
{ id: "learn", label: t("tab_learn") },
|
||||
{ id: "simulate", label: t("tab_simulate") },
|
||||
{ id: "code", label: t("tab_code") },
|
||||
{ id: "deep-dive", label: t("tab_deep_dive") },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Hero Visualization */}
|
||||
<SessionVisualization version={version} />
|
||||
|
||||
{/* Tabbed content */}
|
||||
<Tabs tabs={tabs} defaultTab="learn">
|
||||
{(activeTab) => (
|
||||
<>
|
||||
{activeTab === "learn" && <DocRenderer version={version} />}
|
||||
{activeTab === "simulate" && (
|
||||
<AgentLoopSimulator version={version} />
|
||||
)}
|
||||
{activeTab === "code" && (
|
||||
<SourceViewer source={source} filename={filename} />
|
||||
)}
|
||||
{activeTab === "deep-dive" && (
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<h2 className="mb-4 text-xl font-semibold">
|
||||
{t("execution_flow")}
|
||||
</h2>
|
||||
<ExecutionFlow version={version} />
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="mb-4 text-xl font-semibold">
|
||||
{t("architecture")}
|
||||
</h2>
|
||||
<ArchDiagram version={version} />
|
||||
</section>
|
||||
{diff && <WhatsNew diff={diff} />}
|
||||
<DesignDecisions version={version} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
202
web/src/app/[locale]/(learn)/[version]/diff/diff-content.tsx
Normal file
202
web/src/app/[locale]/(learn)/[version]/diff/diff-content.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { useLocale } from "@/lib/i18n";
|
||||
import { VERSION_META } from "@/lib/constants";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { LayerBadge } from "@/components/ui/badge";
|
||||
import { CodeDiff } from "@/components/diff/code-diff";
|
||||
import { ArrowLeft, Plus, Minus, FileCode, Wrench, Box, FunctionSquare } from "lucide-react";
|
||||
import type { AgentVersion, VersionDiff, VersionIndex } from "@/types/agent-data";
|
||||
import versionData from "@/data/generated/versions.json";
|
||||
|
||||
const data = versionData as VersionIndex;
|
||||
|
||||
interface DiffPageContentProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export function DiffPageContent({ version }: DiffPageContentProps) {
|
||||
const locale = useLocale();
|
||||
const meta = VERSION_META[version];
|
||||
|
||||
const { currentVersion, prevVersion, diff } = useMemo(() => {
|
||||
const current = data.versions.find((v) => v.id === version);
|
||||
const prevId = meta?.prevVersion;
|
||||
const prev = prevId ? data.versions.find((v) => v.id === prevId) : null;
|
||||
const d = data.diffs.find((d) => d.to === version);
|
||||
return { currentVersion: current, prevVersion: prev, diff: d };
|
||||
}, [version, meta]);
|
||||
|
||||
if (!meta || !currentVersion) {
|
||||
return (
|
||||
<div className="py-12 text-center">
|
||||
<p className="text-zinc-500">Version not found.</p>
|
||||
<Link href={`/${locale}/timeline`} className="mt-4 inline-block text-sm text-blue-600 hover:underline">
|
||||
Back to timeline
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!prevVersion || !diff) {
|
||||
return (
|
||||
<div className="py-12">
|
||||
<Link
|
||||
href={`/${locale}/${version}`}
|
||||
className="mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Back to {meta.title}
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold">{meta.title}</h1>
|
||||
<p className="mt-4 text-zinc-500">
|
||||
This is the first version -- there is no previous version to compare against.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const prevMeta = VERSION_META[prevVersion.id];
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<Link
|
||||
href={`/${locale}/${version}`}
|
||||
className="mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Back to {meta.title}
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{prevMeta?.title || prevVersion.id} → {meta.title}
|
||||
</h1>
|
||||
<p className="mt-2 text-zinc-500 dark:text-zinc-400">
|
||||
{prevVersion.id} ({prevVersion.loc} LOC) → {version} ({currentVersion.loc} LOC)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Structural Diff */}
|
||||
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<FileCode size={16} />
|
||||
<span className="text-sm">LOC Delta</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className={diff.locDelta >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
|
||||
{diff.locDelta >= 0 ? "+" : ""}{diff.locDelta}
|
||||
</span>
|
||||
<span className="ml-2 text-sm font-normal text-zinc-500">lines</span>
|
||||
</CardTitle>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<Wrench size={16} />
|
||||
<span className="text-sm">New Tools</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className="text-blue-600 dark:text-blue-400">{diff.newTools.length}</span>
|
||||
</CardTitle>
|
||||
{diff.newTools.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{diff.newTools.map((tool) => (
|
||||
<span key={tool} className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<Box size={16} />
|
||||
<span className="text-sm">New Classes</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className="text-purple-600 dark:text-purple-400">{diff.newClasses.length}</span>
|
||||
</CardTitle>
|
||||
{diff.newClasses.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{diff.newClasses.map((cls) => (
|
||||
<span key={cls} className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
||||
{cls}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<FunctionSquare size={16} />
|
||||
<span className="text-sm">New Functions</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className="text-amber-600 dark:text-amber-400">{diff.newFunctions.length}</span>
|
||||
</CardTitle>
|
||||
{diff.newFunctions.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{diff.newFunctions.map((fn) => (
|
||||
<span key={fn} className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
||||
{fn}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Version Info Comparison */}
|
||||
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<Card className="border-l-4 border-l-red-300 dark:border-l-red-700">
|
||||
<CardHeader>
|
||||
<CardTitle>{prevMeta?.title || prevVersion.id}</CardTitle>
|
||||
<p className="text-sm text-zinc-500">{prevMeta?.subtitle}</p>
|
||||
</CardHeader>
|
||||
<div className="space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p>{prevVersion.loc} LOC</p>
|
||||
<p>{prevVersion.tools.length} tools: {prevVersion.tools.join(", ")}</p>
|
||||
<LayerBadge layer={prevVersion.layer}>{prevVersion.layer}</LayerBadge>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="border-l-4 border-l-green-300 dark:border-l-green-700">
|
||||
<CardHeader>
|
||||
<CardTitle>{meta.title}</CardTitle>
|
||||
<p className="text-sm text-zinc-500">{meta.subtitle}</p>
|
||||
</CardHeader>
|
||||
<div className="space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p>{currentVersion.loc} LOC</p>
|
||||
<p>{currentVersion.tools.length} tools: {currentVersion.tools.join(", ")}</p>
|
||||
<LayerBadge layer={currentVersion.layer}>{currentVersion.layer}</LayerBadge>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Code Diff */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-semibold">Source Code Diff</h2>
|
||||
<CodeDiff
|
||||
oldSource={prevVersion.source}
|
||||
newSource={currentVersion.source}
|
||||
oldLabel={`${prevVersion.id} (${prevVersion.filename})`}
|
||||
newLabel={`${version} (${currentVersion.filename})`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
web/src/app/[locale]/(learn)/[version]/diff/page.tsx
Normal file
15
web/src/app/[locale]/(learn)/[version]/diff/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { LEARNING_PATH } from "@/lib/constants";
|
||||
import { DiffPageContent } from "./diff-content";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return LEARNING_PATH.map((version) => ({ version }));
|
||||
}
|
||||
|
||||
export default async function DiffPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string; version: string }>;
|
||||
}) {
|
||||
const { version } = await params;
|
||||
return <DiffPageContent version={version} />;
|
||||
}
|
||||
125
web/src/app/[locale]/(learn)/[version]/page.tsx
Normal file
125
web/src/app/[locale]/(learn)/[version]/page.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import Link from "next/link";
|
||||
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
|
||||
import { LayerBadge } from "@/components/ui/badge";
|
||||
import versionsData from "@/data/generated/versions.json";
|
||||
import { VersionDetailClient } from "./client";
|
||||
import { getTranslations } from "@/lib/i18n-server";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return LEARNING_PATH.map((version) => ({ version }));
|
||||
}
|
||||
|
||||
export default async function VersionPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string; version: string }>;
|
||||
}) {
|
||||
const { locale, version } = await params;
|
||||
|
||||
const versionData = versionsData.versions.find((v) => v.id === version);
|
||||
const meta = VERSION_META[version];
|
||||
const diff = versionsData.diffs.find((d) => d.to === version) ?? null;
|
||||
|
||||
if (!versionData || !meta) {
|
||||
return (
|
||||
<div className="py-20 text-center">
|
||||
<h1 className="text-2xl font-bold">Version not found</h1>
|
||||
<p className="mt-2 text-zinc-500">{version}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const t = getTranslations(locale, "version");
|
||||
const tSession = getTranslations(locale, "sessions");
|
||||
const tLayer = getTranslations(locale, "layer_labels");
|
||||
const layer = LAYERS.find((l) => l.id === meta.layer);
|
||||
|
||||
const pathIndex = LEARNING_PATH.indexOf(version as typeof LEARNING_PATH[number]);
|
||||
const prevVersion = pathIndex > 0 ? LEARNING_PATH[pathIndex - 1] : null;
|
||||
const nextVersion =
|
||||
pathIndex < LEARNING_PATH.length - 1
|
||||
? LEARNING_PATH[pathIndex + 1]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-10 py-4">
|
||||
{/* Header */}
|
||||
<header className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="rounded-lg bg-zinc-100 px-3 py-1 font-mono text-lg font-bold dark:bg-zinc-800">
|
||||
{version}
|
||||
</span>
|
||||
<h1 className="text-2xl font-bold sm:text-3xl">{tSession(version) || meta.title}</h1>
|
||||
{layer && (
|
||||
<LayerBadge layer={meta.layer}>{tLayer(layer.id)}</LayerBadge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-lg text-zinc-500 dark:text-zinc-400">
|
||||
{meta.subtitle}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<span className="font-mono">{versionData.loc} LOC</span>
|
||||
<span>{versionData.tools.length} {t("tools")}</span>
|
||||
{meta.coreAddition && (
|
||||
<span className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs dark:bg-zinc-800">
|
||||
{meta.coreAddition}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{meta.keyInsight && (
|
||||
<blockquote className="border-l-4 border-zinc-300 pl-4 text-sm italic text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
|
||||
{meta.keyInsight}
|
||||
</blockquote>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Client-rendered interactive sections */}
|
||||
<VersionDetailClient
|
||||
version={version}
|
||||
diff={diff}
|
||||
source={versionData.source}
|
||||
filename={versionData.filename}
|
||||
/>
|
||||
|
||||
{/* Prev / Next navigation */}
|
||||
<nav className="flex items-center justify-between border-t border-zinc-200 pt-6 dark:border-zinc-700">
|
||||
{prevVersion ? (
|
||||
<Link
|
||||
href={`/${locale}/${prevVersion}`}
|
||||
className="group flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white"
|
||||
>
|
||||
<span className="transition-transform group-hover:-translate-x-1">
|
||||
←
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-400">{t("prev")}</div>
|
||||
<div className="font-medium">
|
||||
{prevVersion} - {tSession(prevVersion) || VERSION_META[prevVersion]?.title}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{nextVersion ? (
|
||||
<Link
|
||||
href={`/${locale}/${nextVersion}`}
|
||||
className="group flex items-center gap-2 text-right text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white"
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-400">{t("next")}</div>
|
||||
<div className="font-medium">
|
||||
{tSession(nextVersion) || VERSION_META[nextVersion]?.title} - {nextVersion}
|
||||
</div>
|
||||
</div>
|
||||
<span className="transition-transform group-hover:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
308
web/src/app/[locale]/(learn)/compare/page.tsx
Normal file
308
web/src/app/[locale]/(learn)/compare/page.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useLocale, useTranslations } from "@/lib/i18n";
|
||||
import { LEARNING_PATH, VERSION_META } from "@/lib/constants";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { LayerBadge } from "@/components/ui/badge";
|
||||
import { CodeDiff } from "@/components/diff/code-diff";
|
||||
import { ArchDiagram } from "@/components/architecture/arch-diagram";
|
||||
import { ArrowRight, FileCode, Wrench, Box, FunctionSquare } from "lucide-react";
|
||||
import type { VersionIndex } from "@/types/agent-data";
|
||||
import versionData from "@/data/generated/versions.json";
|
||||
|
||||
const data = versionData as VersionIndex;
|
||||
|
||||
export default function ComparePage() {
|
||||
const t = useTranslations("compare");
|
||||
const locale = useLocale();
|
||||
const [versionA, setVersionA] = useState<string>("");
|
||||
const [versionB, setVersionB] = useState<string>("");
|
||||
|
||||
const infoA = useMemo(() => data.versions.find((v) => v.id === versionA), [versionA]);
|
||||
const infoB = useMemo(() => data.versions.find((v) => v.id === versionB), [versionB]);
|
||||
const metaA = versionA ? VERSION_META[versionA] : null;
|
||||
const metaB = versionB ? VERSION_META[versionB] : null;
|
||||
|
||||
const comparison = useMemo(() => {
|
||||
if (!infoA || !infoB) return null;
|
||||
const toolsA = new Set(infoA.tools);
|
||||
const toolsB = new Set(infoB.tools);
|
||||
const onlyA = infoA.tools.filter((t) => !toolsB.has(t));
|
||||
const onlyB = infoB.tools.filter((t) => !toolsA.has(t));
|
||||
const shared = infoA.tools.filter((t) => toolsB.has(t));
|
||||
|
||||
const classesA = new Set(infoA.classes.map((c) => c.name));
|
||||
const classesB = new Set(infoB.classes.map((c) => c.name));
|
||||
const newClasses = infoB.classes.map((c) => c.name).filter((c) => !classesA.has(c));
|
||||
|
||||
const funcsA = new Set(infoA.functions.map((f) => f.name));
|
||||
const funcsB = new Set(infoB.functions.map((f) => f.name));
|
||||
const newFunctions = infoB.functions.map((f) => f.name).filter((f) => !funcsA.has(f));
|
||||
|
||||
return {
|
||||
locDelta: infoB.loc - infoA.loc,
|
||||
toolsOnlyA: onlyA,
|
||||
toolsOnlyB: onlyB,
|
||||
toolsShared: shared,
|
||||
newClasses,
|
||||
newFunctions,
|
||||
};
|
||||
}, [infoA, infoB]);
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold">{t("title")}</h1>
|
||||
<p className="mt-2 text-zinc-500 dark:text-zinc-400">{t("subtitle")}</p>
|
||||
</div>
|
||||
|
||||
{/* Selectors */}
|
||||
<div className="mb-8 flex flex-col items-start gap-4 sm:flex-row sm:items-center">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{t("select_a")}
|
||||
</label>
|
||||
<select
|
||||
value={versionA}
|
||||
onChange={(e) => setVersionA(e.target.value)}
|
||||
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
|
||||
>
|
||||
<option value="">-- select --</option>
|
||||
{LEARNING_PATH.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v} - {VERSION_META[v]?.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ArrowRight size={20} className="mt-5 hidden text-zinc-400 sm:block" />
|
||||
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{t("select_b")}
|
||||
</label>
|
||||
<select
|
||||
value={versionB}
|
||||
onChange={(e) => setVersionB(e.target.value)}
|
||||
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
|
||||
>
|
||||
<option value="">-- select --</option>
|
||||
{LEARNING_PATH.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v} - {VERSION_META[v]?.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{infoA && infoB && comparison && (
|
||||
<div className="space-y-8">
|
||||
{/* Side-by-side version info */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{metaA?.title || versionA}</CardTitle>
|
||||
<p className="text-sm text-zinc-500">{metaA?.subtitle}</p>
|
||||
</CardHeader>
|
||||
<div className="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p>{infoA.loc} LOC</p>
|
||||
<p>{infoA.tools.length} tools</p>
|
||||
{metaA && <LayerBadge layer={metaA.layer}>{metaA.layer}</LayerBadge>}
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{metaB?.title || versionB}</CardTitle>
|
||||
<p className="text-sm text-zinc-500">{metaB?.subtitle}</p>
|
||||
</CardHeader>
|
||||
<div className="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p>{infoB.loc} LOC</p>
|
||||
<p>{infoB.tools.length} tools</p>
|
||||
{metaB && <LayerBadge layer={metaB.layer}>{metaB.layer}</LayerBadge>}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Side-by-side Architecture Diagrams */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-semibold">{t("architecture")}</h2>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{metaA?.title || versionA}
|
||||
</h3>
|
||||
<ArchDiagram version={versionA} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{metaB?.title || versionB}
|
||||
</h3>
|
||||
<ArchDiagram version={versionB} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Structural diff */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<FileCode size={16} />
|
||||
<span className="text-sm">{t("loc_delta")}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className={comparison.locDelta >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
|
||||
{comparison.locDelta >= 0 ? "+" : ""}{comparison.locDelta}
|
||||
</span>
|
||||
<span className="ml-2 text-sm font-normal text-zinc-500">{t("lines")}</span>
|
||||
</CardTitle>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<Wrench size={16} />
|
||||
<span className="text-sm">{t("new_tools_in_b")}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className="text-blue-600 dark:text-blue-400">{comparison.toolsOnlyB.length}</span>
|
||||
</CardTitle>
|
||||
{comparison.toolsOnlyB.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{comparison.toolsOnlyB.map((tool) => (
|
||||
<span key={tool} className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<Box size={16} />
|
||||
<span className="text-sm">{t("new_classes_in_b")}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className="text-purple-600 dark:text-purple-400">{comparison.newClasses.length}</span>
|
||||
</CardTitle>
|
||||
{comparison.newClasses.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{comparison.newClasses.map((cls) => (
|
||||
<span key={cls} className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
||||
{cls}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
|
||||
<FunctionSquare size={16} />
|
||||
<span className="text-sm">{t("new_functions_in_b")}</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardTitle>
|
||||
<span className="text-amber-600 dark:text-amber-400">{comparison.newFunctions.length}</span>
|
||||
</CardTitle>
|
||||
{comparison.newFunctions.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{comparison.newFunctions.map((fn) => (
|
||||
<span key={fn} className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
||||
{fn}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tool comparison */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("tool_comparison")}</CardTitle>
|
||||
</CardHeader>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{t("only_in")} {metaA?.title || versionA}
|
||||
</h4>
|
||||
{comparison.toolsOnlyA.length === 0 ? (
|
||||
<p className="text-xs text-zinc-400">{t("none")}</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{comparison.toolsOnlyA.map((tool) => (
|
||||
<span key={tool} className="rounded bg-red-100 px-1.5 py-0.5 text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{t("shared")}
|
||||
</h4>
|
||||
{comparison.toolsShared.length === 0 ? (
|
||||
<p className="text-xs text-zinc-400">{t("none")}</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{comparison.toolsShared.map((tool) => (
|
||||
<span key={tool} className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
{t("only_in")} {metaB?.title || versionB}
|
||||
</h4>
|
||||
{comparison.toolsOnlyB.length === 0 ? (
|
||||
<p className="text-xs text-zinc-400">{t("none")}</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{comparison.toolsOnlyB.map((tool) => (
|
||||
<span key={tool} className="rounded bg-green-100 px-1.5 py-0.5 text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Code Diff */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-semibold">{t("source_diff")}</h2>
|
||||
<CodeDiff
|
||||
oldSource={infoA.source}
|
||||
newSource={infoB.source}
|
||||
oldLabel={`${infoA.id} (${infoA.filename})`}
|
||||
newLabel={`${infoB.id} (${infoB.filename})`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{(!versionA || !versionB) && (
|
||||
<div className="rounded-lg border border-dashed border-zinc-300 p-12 text-center dark:border-zinc-700">
|
||||
<p className="text-zinc-400">{t("empty_hint")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
web/src/app/[locale]/(learn)/layers/page.tsx
Normal file
133
web/src/app/[locale]/(learn)/layers/page.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslations, useLocale } from "@/lib/i18n";
|
||||
import { LAYERS, VERSION_META } from "@/lib/constants";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { LayerBadge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import type { VersionIndex } from "@/types/agent-data";
|
||||
import versionData from "@/data/generated/versions.json";
|
||||
|
||||
const data = versionData as VersionIndex;
|
||||
|
||||
const LAYER_BORDER_CLASSES: Record<string, string> = {
|
||||
tools: "border-l-blue-500",
|
||||
planning: "border-l-emerald-500",
|
||||
memory: "border-l-purple-500",
|
||||
concurrency: "border-l-amber-500",
|
||||
collaboration: "border-l-red-500",
|
||||
};
|
||||
|
||||
const LAYER_HEADER_BG: Record<string, string> = {
|
||||
tools: "bg-blue-500",
|
||||
planning: "bg-emerald-500",
|
||||
memory: "bg-purple-500",
|
||||
concurrency: "bg-amber-500",
|
||||
collaboration: "bg-red-500",
|
||||
};
|
||||
|
||||
export default function LayersPage() {
|
||||
const t = useTranslations("layers");
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold">{t("title")}</h1>
|
||||
<p className="mt-2 text-zinc-500 dark:text-zinc-400">{t("subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{LAYERS.map((layer, index) => {
|
||||
const versionInfos = layer.versions.map((vId) => {
|
||||
const info = data.versions.find((v) => v.id === vId);
|
||||
const meta = VERSION_META[vId];
|
||||
return { id: vId, info, meta };
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800",
|
||||
"border-l-4",
|
||||
LAYER_BORDER_CLASSES[layer.id]
|
||||
)}
|
||||
>
|
||||
{/* Layer header */}
|
||||
<div className="flex items-center gap-3 px-6 py-4">
|
||||
<div className={cn("h-3 w-3 rounded-full", LAYER_HEADER_BG[layer.id])} />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">
|
||||
<span className="text-zinc-400 dark:text-zinc-600">L{index + 1}</span>
|
||||
{" "}
|
||||
{layer.label}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{t(layer.id)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version cards within this layer */}
|
||||
<div className="border-t border-zinc-200 bg-zinc-50/50 px-6 py-4 dark:border-zinc-800 dark:bg-zinc-900/50">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{versionInfos.map(({ id, info, meta }) => (
|
||||
<Link
|
||||
key={id}
|
||||
href={`/${locale}/${id}`}
|
||||
className="group"
|
||||
>
|
||||
<Card className="transition-shadow hover:shadow-md">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-zinc-400">{id}</span>
|
||||
<LayerBadge layer={layer.id}>{layer.id}</LayerBadge>
|
||||
</div>
|
||||
<h3 className="mt-1 font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{meta?.title || id}
|
||||
</h3>
|
||||
{meta?.subtitle && (
|
||||
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{meta.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
className="mt-1 shrink-0 text-zinc-300 transition-colors group-hover:text-zinc-600 dark:text-zinc-600 dark:group-hover:text-zinc-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-4 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<span>{info?.loc ?? "?"} LOC</span>
|
||||
<span>{info?.tools.length ?? "?"} tools</span>
|
||||
</div>
|
||||
{meta?.keyInsight && (
|
||||
<p className="mt-2 text-xs leading-relaxed text-zinc-500 dark:text-zinc-400 line-clamp-2">
|
||||
{meta.keyInsight}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Composition indicator */}
|
||||
{index < LAYERS.length - 1 && (
|
||||
<div className="flex items-center justify-center py-1 text-zinc-300 dark:text-zinc-700">
|
||||
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" className="text-current">
|
||||
<path d="M10 0 L10 12 M5 7 L10 12 L15 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
web/src/app/[locale]/(learn)/layout.tsx
Normal file
14
web/src/app/[locale]/(learn)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
|
||||
export default function LearnLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<Sidebar />
|
||||
<div className="min-w-0 flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
web/src/app/[locale]/(learn)/timeline/page.tsx
Normal file
20
web/src/app/[locale]/(learn)/timeline/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n";
|
||||
import { Timeline } from "@/components/timeline/timeline";
|
||||
|
||||
export default function TimelinePage() {
|
||||
const t = useTranslations("timeline");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold">{t("title")}</h1>
|
||||
<p className="mt-2 text-[var(--color-text-secondary)]">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<Timeline />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
web/src/app/[locale]/layout.tsx
Normal file
60
web/src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Metadata } from "next";
|
||||
import { I18nProvider } from "@/lib/i18n";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import en from "@/i18n/messages/en.json";
|
||||
import zh from "@/i18n/messages/zh.json";
|
||||
import ja from "@/i18n/messages/ja.json";
|
||||
import "../globals.css";
|
||||
|
||||
const locales = ["en", "zh", "ja"];
|
||||
const metaMessages: Record<string, typeof en> = { en, zh, ja };
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const messages = metaMessages[locale] || metaMessages.en;
|
||||
return {
|
||||
title: messages.meta?.title || "Learn Claude Code",
|
||||
description: messages.meta?.description || "Build an AI coding agent from scratch, one concept at a time",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: `
|
||||
(function() {
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
`}} />
|
||||
</head>
|
||||
<body className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] antialiased">
|
||||
<I18nProvider locale={locale}>
|
||||
<Header />
|
||||
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</main>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
234
web/src/app/[locale]/page.tsx
Normal file
234
web/src/app/[locale]/page.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslations, useLocale } from "@/lib/i18n";
|
||||
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
|
||||
import { LayerBadge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import versionsData from "@/data/generated/versions.json";
|
||||
import { MessageFlow } from "@/components/architecture/message-flow";
|
||||
|
||||
const LAYER_DOT_COLORS: Record<string, string> = {
|
||||
tools: "bg-blue-500",
|
||||
planning: "bg-emerald-500",
|
||||
memory: "bg-purple-500",
|
||||
concurrency: "bg-amber-500",
|
||||
collaboration: "bg-red-500",
|
||||
};
|
||||
|
||||
const LAYER_BORDER_COLORS: Record<string, string> = {
|
||||
tools: "border-blue-500/30 hover:border-blue-500/60",
|
||||
planning: "border-emerald-500/30 hover:border-emerald-500/60",
|
||||
memory: "border-purple-500/30 hover:border-purple-500/60",
|
||||
concurrency: "border-amber-500/30 hover:border-amber-500/60",
|
||||
collaboration: "border-red-500/30 hover:border-red-500/60",
|
||||
};
|
||||
|
||||
const LAYER_BAR_COLORS: Record<string, string> = {
|
||||
tools: "bg-blue-500",
|
||||
planning: "bg-emerald-500",
|
||||
memory: "bg-purple-500",
|
||||
concurrency: "bg-amber-500",
|
||||
collaboration: "bg-red-500",
|
||||
};
|
||||
|
||||
function getVersionData(id: string) {
|
||||
return versionsData.versions.find((v) => v.id === id);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const t = useTranslations("home");
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-20 pb-16">
|
||||
{/* Hero Section */}
|
||||
<section className="flex flex-col items-center px-2 pt-8 text-center sm:pt-20">
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
|
||||
{t("hero_title")}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-base text-[var(--color-text-secondary)] sm:text-xl">
|
||||
{t("hero_subtitle")}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<Link
|
||||
href={`/${locale}/timeline`}
|
||||
className="inline-flex min-h-[44px] items-center gap-2 rounded-lg bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
|
||||
>
|
||||
{t("start")}
|
||||
<span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Core Pattern Section */}
|
||||
<section>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl font-bold sm:text-3xl">{t("core_pattern")}</h2>
|
||||
<p className="mt-2 text-[var(--color-text-secondary)]">
|
||||
{t("core_pattern_desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto max-w-2xl overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950">
|
||||
<div className="flex items-center gap-2 border-b border-zinc-800 px-4 py-2.5">
|
||||
<span className="h-3 w-3 rounded-full bg-red-500/70" />
|
||||
<span className="h-3 w-3 rounded-full bg-yellow-500/70" />
|
||||
<span className="h-3 w-3 rounded-full bg-green-500/70" />
|
||||
<span className="ml-3 text-xs text-zinc-500">agent_loop.py</span>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-4 text-sm leading-relaxed">
|
||||
<code>
|
||||
<span className="text-purple-400">while</span>
|
||||
<span className="text-zinc-300"> </span>
|
||||
<span className="text-orange-300">True</span>
|
||||
<span className="text-zinc-500">:</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}response = client.messages.</span>
|
||||
<span className="text-blue-400">create</span>
|
||||
<span className="text-zinc-500">(</span>
|
||||
<span className="text-zinc-300">messages=</span>
|
||||
<span className="text-zinc-300">messages</span>
|
||||
<span className="text-zinc-500">,</span>
|
||||
<span className="text-zinc-300"> tools=</span>
|
||||
<span className="text-zinc-300">tools</span>
|
||||
<span className="text-zinc-500">)</span>
|
||||
{"\n"}
|
||||
<span className="text-purple-400">{" "}if</span>
|
||||
<span className="text-zinc-300"> response.stop_reason != </span>
|
||||
<span className="text-green-400">"tool_use"</span>
|
||||
<span className="text-zinc-500">:</span>
|
||||
{"\n"}
|
||||
<span className="text-purple-400">{" "}break</span>
|
||||
{"\n"}
|
||||
<span className="text-purple-400">{" "}for</span>
|
||||
<span className="text-zinc-300"> tool_call </span>
|
||||
<span className="text-purple-400">in</span>
|
||||
<span className="text-zinc-300"> response.content</span>
|
||||
<span className="text-zinc-500">:</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}result = </span>
|
||||
<span className="text-blue-400">execute_tool</span>
|
||||
<span className="text-zinc-500">(</span>
|
||||
<span className="text-zinc-300">tool_call.name</span>
|
||||
<span className="text-zinc-500">,</span>
|
||||
<span className="text-zinc-300"> tool_call.input</span>
|
||||
<span className="text-zinc-500">)</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}messages.</span>
|
||||
<span className="text-blue-400">append</span>
|
||||
<span className="text-zinc-500">(</span>
|
||||
<span className="text-zinc-300">result</span>
|
||||
<span className="text-zinc-500">)</span>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Message Flow Visualization */}
|
||||
<section>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl font-bold sm:text-3xl">{t("message_flow")}</h2>
|
||||
<p className="mt-2 text-[var(--color-text-secondary)]">
|
||||
{t("message_flow_desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<MessageFlow />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Learning Path Preview */}
|
||||
<section>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl font-bold sm:text-3xl">{t("learning_path")}</h2>
|
||||
<p className="mt-2 text-[var(--color-text-secondary)]">
|
||||
{t("learning_path_desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{LEARNING_PATH.map((versionId) => {
|
||||
const meta = VERSION_META[versionId];
|
||||
const data = getVersionData(versionId);
|
||||
if (!meta || !data) return null;
|
||||
return (
|
||||
<Link
|
||||
key={versionId}
|
||||
href={`/${locale}/${versionId}`}
|
||||
className="group block"
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
"h-full border transition-all duration-200",
|
||||
LAYER_BORDER_COLORS[meta.layer]
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<LayerBadge layer={meta.layer}>{versionId}</LayerBadge>
|
||||
<span className="text-xs tabular-nums text-[var(--color-text-secondary)]">
|
||||
{data.loc} {t("loc")}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="mt-3 text-sm font-semibold group-hover:underline">
|
||||
{meta.title}
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-[var(--color-text-secondary)]">
|
||||
{meta.keyInsight}
|
||||
</p>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Layer Overview */}
|
||||
<section>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl font-bold sm:text-3xl">{t("layers_title")}</h2>
|
||||
<p className="mt-2 text-[var(--color-text-secondary)]">
|
||||
{t("layers_desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{LAYERS.map((layer) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
className="flex items-center gap-4 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-1.5 self-stretch rounded-full",
|
||||
LAYER_BAR_COLORS[layer.id]
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">{layer.label}</h3>
|
||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||
{layer.versions.length} {t("versions_in_layer")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{layer.versions.map((vid) => {
|
||||
const meta = VERSION_META[vid];
|
||||
return (
|
||||
<Link key={vid} href={`/${locale}/${vid}`}>
|
||||
<LayerBadge
|
||||
layer={layer.id}
|
||||
className="cursor-pointer transition-opacity hover:opacity-80"
|
||||
>
|
||||
{vid}: {meta?.title}
|
||||
</LayerBadge>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
web/src/app/favicon.ico
Normal file
BIN
web/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
555
web/src/app/globals.css
Normal file
555
web/src/app/globals.css
Normal file
@@ -0,0 +1,555 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
--color-layer-tools: #3B82F6;
|
||||
--color-layer-planning: #10B981;
|
||||
--color-layer-memory: #8B5CF6;
|
||||
--color-layer-concurrency: #F59E0B;
|
||||
--color-layer-collaboration: #EF4444;
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-secondary: #f4f4f5;
|
||||
--color-text: #09090b;
|
||||
--color-text-secondary: #71717a;
|
||||
--color-border: #e4e4e7;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-bg: #09090b;
|
||||
--color-bg-secondary: #18181b;
|
||||
--color-text: #fafafa;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-border: #27272a;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
pre, code {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
PROSE-CUSTOM: Premium documentation rendering
|
||||
===================================================== */
|
||||
|
||||
/* -- Headings -- */
|
||||
|
||||
.prose-custom h1 {
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
.dark .prose-custom h1 {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.prose-custom h2 {
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: #09090b;
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
}
|
||||
|
||||
.dark .prose-custom h2 {
|
||||
color: #fafafa;
|
||||
border-bottom-color: #27272a;
|
||||
}
|
||||
|
||||
.prose-custom h3 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #18181b;
|
||||
}
|
||||
|
||||
.dark .prose-custom h3 {
|
||||
color: #e4e4e7;
|
||||
}
|
||||
|
||||
.prose-custom h4 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
.dark .prose-custom h4 {
|
||||
color: #d4d4d8;
|
||||
}
|
||||
|
||||
/* -- Paragraphs -- */
|
||||
|
||||
.prose-custom p {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
.dark .prose-custom p {
|
||||
color: #d4d4d8;
|
||||
}
|
||||
|
||||
/* -- Hero callout (first blockquote) -- */
|
||||
|
||||
.prose-custom blockquote.hero-callout {
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: none;
|
||||
border-radius: 0.75rem;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #f0fdf4 100%);
|
||||
padding: 1.25rem 1.5rem 1.25rem 1.75rem;
|
||||
font-style: normal;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prose-custom blockquote.hero-callout::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(to bottom, #3b82f6, #10b981);
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.prose-custom blockquote.hero-callout p {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
font-weight: 500;
|
||||
color: #1e40af;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .prose-custom blockquote.hero-callout {
|
||||
background: linear-gradient(135deg, #172554 0%, #052e16 100%);
|
||||
}
|
||||
|
||||
.dark .prose-custom blockquote.hero-callout p {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
/* -- Regular blockquotes -- */
|
||||
|
||||
.prose-custom blockquote {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-left: 3px solid #a5b4fc;
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
background-color: #eef2ff;
|
||||
padding: 0.75rem 1rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.prose-custom blockquote p {
|
||||
color: #4338ca;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .prose-custom blockquote {
|
||||
border-left-color: #6366f1;
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.dark .prose-custom blockquote p {
|
||||
color: #c7d2fe;
|
||||
}
|
||||
|
||||
/* -- Code blocks with language label -- */
|
||||
|
||||
.prose-custom pre {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #1e293b;
|
||||
background-color: #0f172a;
|
||||
padding: 1.25rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
color: #e2e8f0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.prose-custom pre.code-block {
|
||||
padding-top: 2.25rem;
|
||||
}
|
||||
|
||||
.prose-custom pre.code-block::before {
|
||||
content: attr(data-language);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0.75rem;
|
||||
padding: 0.125rem 0.625rem 0.25rem;
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
border-radius: 0 0 0.375rem 0.375rem;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.prose-custom pre.code-block[data-language="sh"]::before {
|
||||
background: #22c55e;
|
||||
content: "terminal";
|
||||
}
|
||||
|
||||
/* -- ASCII diagram containers -- */
|
||||
|
||||
.prose-custom pre.ascii-diagram {
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
border: 1px solid #cbd5e1;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.dark .prose-custom pre.ascii-diagram {
|
||||
background: linear-gradient(135deg, #1e1b4b, #172554);
|
||||
border-color: #312e81;
|
||||
color: #c7d2fe;
|
||||
}
|
||||
|
||||
/* -- Inline code -- */
|
||||
|
||||
.prose-custom :not(pre) > code {
|
||||
border-radius: 0.375rem;
|
||||
background-color: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 0.125rem 0.425rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #be185d;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.dark .prose-custom :not(pre) > code {
|
||||
background-color: #27272a;
|
||||
border-color: #3f3f46;
|
||||
color: #f9a8d4;
|
||||
}
|
||||
|
||||
/* -- Links -- */
|
||||
|
||||
.prose-custom a {
|
||||
color: #2563eb;
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #93c5fd;
|
||||
text-underline-offset: 2px;
|
||||
transition: text-decoration-color 0.15s;
|
||||
}
|
||||
|
||||
.prose-custom a:hover {
|
||||
text-decoration-color: #2563eb;
|
||||
}
|
||||
|
||||
.dark .prose-custom a {
|
||||
color: #60a5fa;
|
||||
text-decoration-color: #1e40af;
|
||||
}
|
||||
|
||||
.dark .prose-custom a:hover {
|
||||
text-decoration-color: #60a5fa;
|
||||
}
|
||||
|
||||
/* -- Lists -- */
|
||||
|
||||
.prose-custom ul {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
.dark .prose-custom ul {
|
||||
color: #d4d4d8;
|
||||
}
|
||||
|
||||
.prose-custom ul > li {
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.prose-custom ul > li::marker {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.prose-custom ol {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
counter-reset: step-counter;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
color: #3f3f46;
|
||||
}
|
||||
|
||||
.dark .prose-custom ol {
|
||||
color: #d4d4d8;
|
||||
}
|
||||
|
||||
.prose-custom ol > li {
|
||||
counter-increment: step-counter;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 2.75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.prose-custom ol > li::before {
|
||||
content: counter(step-counter);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||
color: #ffffff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Reset nested lists inside ol to normal style */
|
||||
.prose-custom ol > li > ul {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-custom ol > li > ul > li {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.prose-custom ol > li > ul > li::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* -- Tables -- */
|
||||
|
||||
.prose-custom table {
|
||||
width: 100%;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .prose-custom table {
|
||||
border-color: #27272a;
|
||||
}
|
||||
|
||||
.prose-custom thead {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.prose-custom th {
|
||||
padding: 0.625rem 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #64748b;
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .prose-custom th {
|
||||
color: #94a3b8;
|
||||
background-color: #18181b;
|
||||
border-bottom-color: #27272a;
|
||||
}
|
||||
|
||||
.prose-custom td {
|
||||
padding: 0.625rem 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.prose-custom td code {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.dark .prose-custom td {
|
||||
border-bottom-color: #1e1e22;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.prose-custom tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.prose-custom tbody tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.dark .prose-custom tbody tr:hover {
|
||||
background-color: #111113;
|
||||
}
|
||||
|
||||
/* -- Horizontal rules -- */
|
||||
|
||||
.prose-custom hr {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #d4d4d8, transparent);
|
||||
}
|
||||
|
||||
.dark .prose-custom hr {
|
||||
background: linear-gradient(to right, transparent, #3f3f46, transparent);
|
||||
}
|
||||
|
||||
/* -- Strong / Em -- */
|
||||
|
||||
.prose-custom strong {
|
||||
font-weight: 700;
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
.dark .prose-custom strong {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.prose-custom em {
|
||||
font-style: italic;
|
||||
color: #52525b;
|
||||
}
|
||||
|
||||
.dark .prose-custom em {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
HIGHLIGHT.JS TOKEN THEME (code syntax highlighting)
|
||||
===================================================== */
|
||||
|
||||
.hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #c084fc;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-doctag,
|
||||
.hljs-template-variable,
|
||||
.hljs-variable {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.hljs-number {
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #60a5fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hljs-title.function_,
|
||||
.hljs-title.class_ {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.hljs-built_in {
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-attribute {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.hljs-params {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.hljs-meta {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.hljs-name,
|
||||
.hljs-tag {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #fca5a5;
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #86efac;
|
||||
background-color: rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
Reference in New Issue
Block a user