mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-21 12:43:49 +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:
450
web/src/components/visualizations/s06-context-compact.tsx
Normal file
450
web/src/components/visualizations/s06-context-compact.tsx
Normal file
@@ -0,0 +1,450 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
|
||||
import { StepControls } from "@/components/visualizations/shared/step-controls";
|
||||
|
||||
type BlockType = "user" | "assistant" | "tool_result";
|
||||
|
||||
interface ContextBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
label: string;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
const BLOCK_COLORS: Record<BlockType, string> = {
|
||||
user: "bg-blue-500",
|
||||
assistant: "bg-zinc-500 dark:bg-zinc-600",
|
||||
tool_result: "bg-emerald-500",
|
||||
};
|
||||
|
||||
const BLOCK_LABELS: Record<BlockType, string> = {
|
||||
user: "USR",
|
||||
assistant: "AST",
|
||||
tool_result: "TRL",
|
||||
};
|
||||
|
||||
function generateBlocks(count: number, seed: number): ContextBlock[] {
|
||||
const types: BlockType[] = ["user", "assistant", "tool_result"];
|
||||
const blocks: ContextBlock[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const typeIndex = (i + seed) % 3;
|
||||
const type = types[typeIndex];
|
||||
const tokens = type === "tool_result" ? 4000 + (i % 3) * 1000 : 1500 + (i % 4) * 500;
|
||||
blocks.push({
|
||||
id: `b-${seed}-${i}`,
|
||||
type,
|
||||
label: `${BLOCK_LABELS[type]} ${i + 1}`,
|
||||
tokens,
|
||||
});
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
const MAX_TOKENS = 100000;
|
||||
const WINDOW_HEIGHT = 350;
|
||||
|
||||
interface StepState {
|
||||
blocks: { id: string; type: BlockType; label: string; heightPx: number; compressed?: boolean }[];
|
||||
tokenCount: number;
|
||||
fillPercent: number;
|
||||
compressionLabel: string | null;
|
||||
}
|
||||
|
||||
function computeStepState(step: number): StepState {
|
||||
switch (step) {
|
||||
case 0: {
|
||||
const raw = generateBlocks(8, 0);
|
||||
const tokenCount = 30000;
|
||||
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
|
||||
const blocks = raw.map((b) => ({
|
||||
...b,
|
||||
heightPx: Math.max(16, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.3),
|
||||
}));
|
||||
return { blocks, tokenCount, fillPercent: 30, compressionLabel: null };
|
||||
}
|
||||
case 1: {
|
||||
const raw = generateBlocks(16, 0);
|
||||
const tokenCount = 60000;
|
||||
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
|
||||
const blocks = raw.map((b) => ({
|
||||
...b,
|
||||
heightPx: Math.max(12, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.6),
|
||||
}));
|
||||
return { blocks, tokenCount, fillPercent: 60, compressionLabel: null };
|
||||
}
|
||||
case 2: {
|
||||
const raw = generateBlocks(20, 0);
|
||||
const tokenCount = 80000;
|
||||
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
|
||||
const blocks = raw.map((b) => ({
|
||||
...b,
|
||||
heightPx: Math.max(10, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.8),
|
||||
}));
|
||||
return { blocks, tokenCount, fillPercent: 80, compressionLabel: null };
|
||||
}
|
||||
case 3: {
|
||||
const raw = generateBlocks(20, 0);
|
||||
const tokenCount = 60000;
|
||||
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
|
||||
const blocks = raw.map((b) => ({
|
||||
...b,
|
||||
heightPx:
|
||||
b.type === "tool_result"
|
||||
? 6
|
||||
: Math.max(12, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.6),
|
||||
compressed: b.type === "tool_result",
|
||||
}));
|
||||
return {
|
||||
blocks,
|
||||
tokenCount,
|
||||
fillPercent: 60,
|
||||
compressionLabel: "MICRO-COMPACT",
|
||||
};
|
||||
}
|
||||
case 4: {
|
||||
const raw = generateBlocks(24, 1);
|
||||
const tokenCount = 85000;
|
||||
const totalRawTokens = raw.reduce((a, b) => a + b.tokens, 0);
|
||||
const blocks = raw.map((b) => ({
|
||||
...b,
|
||||
heightPx: Math.max(10, (b.tokens / totalRawTokens) * WINDOW_HEIGHT * 0.85),
|
||||
}));
|
||||
return { blocks, tokenCount, fillPercent: 85, compressionLabel: null };
|
||||
}
|
||||
case 5: {
|
||||
const tokenCount = 25000;
|
||||
const summaryBlock = {
|
||||
id: "auto-summary",
|
||||
type: "assistant" as BlockType,
|
||||
label: "SUMMARY",
|
||||
heightPx: 40,
|
||||
compressed: false,
|
||||
};
|
||||
const recentBlocks = generateBlocks(4, 2).map((b) => ({
|
||||
...b,
|
||||
heightPx: 20,
|
||||
}));
|
||||
return {
|
||||
blocks: [summaryBlock, ...recentBlocks],
|
||||
tokenCount,
|
||||
fillPercent: 25,
|
||||
compressionLabel: "AUTO-COMPACT",
|
||||
};
|
||||
}
|
||||
case 6: {
|
||||
const tokenCount = 8000;
|
||||
const compactBlock = {
|
||||
id: "compact-summary",
|
||||
type: "assistant" as BlockType,
|
||||
label: "COMPACT SUMMARY",
|
||||
heightPx: 24,
|
||||
compressed: false,
|
||||
};
|
||||
return {
|
||||
blocks: [compactBlock],
|
||||
tokenCount,
|
||||
fillPercent: 8,
|
||||
compressionLabel: "/compact",
|
||||
};
|
||||
}
|
||||
default:
|
||||
return { blocks: [], tokenCount: 0, fillPercent: 0, compressionLabel: null };
|
||||
}
|
||||
}
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
title: "Growing Context",
|
||||
description:
|
||||
"The context window holds the conversation. Each API call adds more messages.",
|
||||
},
|
||||
{
|
||||
title: "Context Growing",
|
||||
description:
|
||||
"As the agent works, messages accumulate. The context window fills up.",
|
||||
},
|
||||
{
|
||||
title: "Approaching Limit",
|
||||
description:
|
||||
"Old tool_results are the biggest consumers. Micro-compact targets these first.",
|
||||
},
|
||||
{
|
||||
title: "Stage 1: Micro-Compact",
|
||||
description:
|
||||
"Replace old tool_results with short summaries. Automatic, transparent to the model.",
|
||||
},
|
||||
{
|
||||
title: "Still Growing",
|
||||
description:
|
||||
"Work continues. Context grows again toward the threshold...",
|
||||
},
|
||||
{
|
||||
title: "Stage 2: Auto-Compact",
|
||||
description:
|
||||
"Entire conversation summarized into a compact block. Triggered at token threshold.",
|
||||
},
|
||||
{
|
||||
title: "Stage 3: /compact",
|
||||
description:
|
||||
"User-triggered, most aggressive. Three layers of strategic forgetting enable infinite sessions.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ContextCompact({ title }: { title?: string }) {
|
||||
const {
|
||||
currentStep,
|
||||
totalSteps,
|
||||
next,
|
||||
prev,
|
||||
reset,
|
||||
isPlaying,
|
||||
toggleAutoPlay,
|
||||
} = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2500 });
|
||||
|
||||
const state = useMemo(() => computeStepState(currentStep), [currentStep]);
|
||||
|
||||
const fillColor =
|
||||
state.fillPercent > 75
|
||||
? "bg-red-500"
|
||||
: state.fillPercent > 45
|
||||
? "bg-amber-500"
|
||||
: "bg-emerald-500";
|
||||
|
||||
const tokenDisplay = `${(state.tokenCount / 1000).toFixed(0)}K`;
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title || "Three-Layer Context Compression"}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900"
|
||||
style={{ minHeight: 500 }}
|
||||
>
|
||||
<div className="flex gap-6">
|
||||
{/* Token Window (tall vertical bar on the left) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 font-mono text-[10px] font-semibold text-zinc-500 dark:text-zinc-400">
|
||||
Context Window
|
||||
</div>
|
||||
<div
|
||||
className="relative w-24 overflow-hidden rounded-xl border-2 border-zinc-300 bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800"
|
||||
style={{ height: WINDOW_HEIGHT }}
|
||||
>
|
||||
{/* Blocks stacked from bottom up */}
|
||||
<div className="absolute bottom-0 left-0 right-0 flex flex-col-reverse gap-px p-1">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{state.blocks.map((block) => (
|
||||
<motion.div
|
||||
key={block.id}
|
||||
initial={{ opacity: 0, scaleY: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scaleY: 1,
|
||||
height: block.heightPx,
|
||||
}}
|
||||
exit={{ opacity: 0, scaleY: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className={`flex w-full items-center justify-center rounded-sm ${
|
||||
block.compressed
|
||||
? "bg-emerald-300 dark:bg-emerald-700"
|
||||
: BLOCK_COLORS[block.type]
|
||||
}`}
|
||||
style={{ originY: 1 }}
|
||||
>
|
||||
{block.heightPx >= 14 && (
|
||||
<span className="truncate px-1 text-[8px] font-medium text-white">
|
||||
{block.label}
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Fill level line */}
|
||||
<motion.div
|
||||
animate={{ bottom: `${state.fillPercent}%` }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="absolute left-0 right-0 border-t-2 border-dashed border-red-400 dark:border-red-500"
|
||||
>
|
||||
<span className="absolute -top-4 right-1 font-mono text-[9px] font-bold text-red-500 dark:text-red-400">
|
||||
{state.fillPercent}%
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Token count */}
|
||||
<motion.div
|
||||
key={state.tokenCount}
|
||||
initial={{ scale: 0.85 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="mt-2 font-mono text-sm font-bold text-zinc-700 dark:text-zinc-200"
|
||||
>
|
||||
{tokenDisplay}
|
||||
</motion.div>
|
||||
<div className="font-mono text-[10px] text-zinc-400">
|
||||
/ 100K
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side: state display and compression stage */}
|
||||
<div className="flex flex-1 flex-col justify-between">
|
||||
{/* Top: horizontal token bar */}
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Token usage
|
||||
</span>
|
||||
<span className="font-mono text-xs text-zinc-500">
|
||||
{state.tokenCount.toLocaleString()} / {MAX_TOKENS.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
|
||||
<motion.div
|
||||
animate={{ width: `${state.fillPercent}%` }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className={`h-full rounded-full ${fillColor}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message type legend */}
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-3 w-3 rounded bg-blue-500" />
|
||||
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">user</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-3 w-3 rounded bg-zinc-500" />
|
||||
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">assistant</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-3 w-3 rounded bg-emerald-500" />
|
||||
<span className="text-[10px] text-zinc-500 dark:text-zinc-400">tool_result</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Highlight old tool_results at step 2 */}
|
||||
<AnimatePresence>
|
||||
{currentStep === 2 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="mt-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 dark:border-amber-700 dark:bg-amber-900/20"
|
||||
>
|
||||
<div className="text-xs font-semibold text-amber-700 dark:text-amber-300">
|
||||
tool_results are the largest blocks
|
||||
</div>
|
||||
<div className="text-[11px] text-amber-600 dark:text-amber-400">
|
||||
File contents, command outputs, search results -- each one is thousands of tokens.
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Compression stage label */}
|
||||
<AnimatePresence>
|
||||
{state.compressionLabel && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="mt-4"
|
||||
>
|
||||
<div className={`rounded-lg border-2 p-4 text-center ${
|
||||
currentStep === 3
|
||||
? "border-amber-400 bg-amber-50 dark:border-amber-600 dark:bg-amber-900/20"
|
||||
: currentStep === 5
|
||||
? "border-blue-400 bg-blue-50 dark:border-blue-600 dark:bg-blue-900/20"
|
||||
: "border-emerald-400 bg-emerald-50 dark:border-emerald-600 dark:bg-emerald-900/20"
|
||||
}`}>
|
||||
<div className={`text-lg font-black ${
|
||||
currentStep === 3
|
||||
? "text-amber-600 dark:text-amber-300"
|
||||
: currentStep === 5
|
||||
? "text-blue-600 dark:text-blue-300"
|
||||
: "text-emerald-600 dark:text-emerald-300"
|
||||
}`}>
|
||||
{state.compressionLabel}
|
||||
</div>
|
||||
<div className={`mt-1 text-xs ${
|
||||
currentStep === 3
|
||||
? "text-amber-500 dark:text-amber-400"
|
||||
: currentStep === 5
|
||||
? "text-blue-500 dark:text-blue-400"
|
||||
: "text-emerald-500 dark:text-emerald-400"
|
||||
}`}>
|
||||
{currentStep === 3 && "Old tool_results shrunk to tiny summaries"}
|
||||
{currentStep === 5 && "Full conversation compressed to summary block"}
|
||||
{currentStep === 6 && "Most aggressive compression -- near-empty context"}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Three stages overview on final step */}
|
||||
{currentStep === 6 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="mt-4 space-y-2"
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded bg-amber-50 px-3 py-1.5 dark:bg-amber-900/10">
|
||||
<div className="h-2 w-2 rounded-full bg-amber-500" />
|
||||
<span className="text-xs text-amber-700 dark:text-amber-300">
|
||||
Stage 1: Micro -- shrink old tool_results
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] text-amber-500">
|
||||
automatic
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded bg-blue-50 px-3 py-1.5 dark:bg-blue-900/10">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
<span className="text-xs text-blue-700 dark:text-blue-300">
|
||||
Stage 2: Auto -- summarize entire conversation
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] text-blue-500">
|
||||
at threshold
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded bg-emerald-50 px-3 py-1.5 dark:bg-emerald-900/10">
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-500" />
|
||||
<span className="text-xs text-emerald-700 dark:text-emerald-300">
|
||||
Stage 3: /compact -- user-triggered, deepest compression
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] text-emerald-500">
|
||||
manual
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Controls */}
|
||||
<div className="mt-6">
|
||||
<StepControls
|
||||
currentStep={currentStep}
|
||||
totalSteps={totalSteps}
|
||||
onPrev={prev}
|
||||
onNext={next}
|
||||
onReset={reset}
|
||||
isPlaying={isPlaying}
|
||||
onToggleAutoPlay={toggleAutoPlay}
|
||||
stepTitle={STEPS[currentStep].title}
|
||||
stepDescription={STEPS[currentStep].description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user