-
Notifications
You must be signed in to change notification settings - Fork 598
Expand file tree
/
Copy pathagent-context-indicator.tsx
More file actions
157 lines (146 loc) · 3.96 KB
/
agent-context-indicator.tsx
File metadata and controls
157 lines (146 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"use client"
import { memo } from "react"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../../components/ui/tooltip"
import { cn } from "../../../lib/utils"
// Claude model context windows
const CONTEXT_WINDOWS = {
opus: 200_000,
"opus[1m]": 1_000_000,
sonnet: 200_000,
haiku: 200_000,
} as const
type ModelId = keyof typeof CONTEXT_WINDOWS
// Pre-computed token data to avoid re-computing on every render
export interface MessageTokenData {
totalInputTokens: number
totalOutputTokens: number
totalCostUsd: number
messageCount: number
contextWindow?: number
}
interface AgentContextIndicatorProps {
tokenData: MessageTokenData
modelId?: ModelId
className?: string
onCompact?: () => void
isCompacting?: boolean
disabled?: boolean
}
function formatTokens(tokens: number): string {
if (tokens >= 1_000_000) {
return `${(tokens / 1_000_000).toFixed(1)}M`
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}K`
}
return tokens.toString()
}
// Circular progress component
function CircularProgress({
percent,
size = 18,
strokeWidth = 2,
className,
}: {
percent: number
size?: number
strokeWidth?: number
className?: string
}) {
const radius = (size - strokeWidth) / 2
const circumference = 2 * Math.PI * radius
const offset = circumference - (percent / 100) * circumference
return (
<svg
width={size}
height={size}
className={cn("transform -rotate-90", className)}
>
{/* Background circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
className="text-muted-foreground/20"
/>
{/* Progress circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
className="transition-all duration-300 text-muted-foreground/60"
/>
</svg>
)
}
export const AgentContextIndicator = memo(function AgentContextIndicator({
tokenData,
modelId = "sonnet",
className,
onCompact,
isCompacting,
disabled,
}: AgentContextIndicatorProps) {
const contextTokens = tokenData.totalInputTokens
const contextWindow = tokenData.contextWindow ?? CONTEXT_WINDOWS[modelId]
const percentUsed = Math.min(100, (contextTokens / contextWindow) * 100)
const isEmpty = contextTokens === 0
const isClickable = onCompact && !disabled && !isCompacting
return (
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<div
onClick={isClickable ? onCompact : undefined}
className={cn(
"h-4 w-4 flex items-center justify-center",
isClickable
? "cursor-pointer hover:opacity-70 transition-opacity"
: "cursor-default",
disabled && "opacity-50",
className,
)}
>
<CircularProgress
percent={percentUsed}
size={14}
strokeWidth={2.5}
className={isCompacting ? "animate-pulse" : undefined}
/>
</div>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={8}>
<p className="text-xs">
{isEmpty ? (
<span className="text-muted-foreground">
Context: 0 / {formatTokens(contextWindow)}
</span>
) : (
<>
<span className="font-mono font-medium text-foreground">
{percentUsed.toFixed(1)}%
</span>
<span className="text-muted-foreground mx-1">·</span>
<span className="text-muted-foreground">
{formatTokens(contextTokens)} /{" "}
{formatTokens(contextWindow)} context
</span>
</>
)}
</p>
</TooltipContent>
</Tooltip>
)
})