{note}
+ + + ) +} + +export { ContextRing } diff --git a/src/components/ui/context-ring-helpers.ts b/src/components/ui/context-ring-helpers.ts new file mode 100644 index 0000000..df1c427 --- /dev/null +++ b/src/components/ui/context-ring-helpers.ts @@ -0,0 +1,11 @@ +/** One thing the context is full of, and the colour it is drawn in (a background class). */ +export type ContextPart = { key: string; label: string; tokens: number; tint: string } + +/** 168100 → "168.1k", 1000000 → "1M": the way a window's size is said aloud. */ +function compactTokens(n: number): string { + if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k` + return String(n) +} + +export { compactTokens } diff --git a/src/components/ui/context-ring.tsx b/src/components/ui/context-ring.tsx index 300c66f..9d627d8 100644 --- a/src/components/ui/context-ring.tsx +++ b/src/components/ui/context-ring.tsx @@ -1,132 +1,2 @@ -"use client" - -import * as React from "react" -import { ChevronDown } from "lucide-react" - -import { cn } from "@/lib/utils" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" - -/** One thing the context is full of, and the colour it is drawn in (a background class). */ -export type ContextPart = { key: string; label: string; tokens: number; tint: string } - -/** 168100 → "168.1k", 1000000 → "1M": the way a window's size is said aloud. */ -function compactTokens(n: number): string { - if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M` - if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k` - return String(n) -} - -const share = (tokens: number, window: number) => (window > 0 ? `${((tokens / window) * 100).toFixed(1)}%` : "—") - -/** - * How full a model's context window is, as a ring: read at a glance, opened - * for the numbers and for what the context is full of. - * - * Blue while there is room, which is nearly always; amber from 70%, the - * colour of failure from 90% — nearly full is worth a second look. - */ -function ContextRing({ - used, - window, - parts = [], - title = "Context window", - note = "How much of the window this conversation takes up. Earlier turns are compacted as it fills.", - freeLabel = "Free", - className, -}: { - used: number - window: number - parts?: ContextPart[] - title?: string - note?: string - freeLabel?: string - className?: string -}) { - const [detailed, setDetailed] = React.useState(false) - const fraction = window > 0 ? Math.min(used / window, 1) : 0 - const percent = Math.round(fraction * 100) - const r = 6 - const around = 2 * Math.PI * r - const tone = fraction >= 0.9 ? "text-destructive" : fraction >= 0.7 ? "text-amber-500" : "text-blue-500" - const counted = parts.reduce((sum, part) => sum + part.tokens, 0) - const free = Math.max(window - Math.max(used, counted), 0) - - return ( -{note}
- - - ) -} - -export { ContextRing, compactTokens } +export { ContextRing } from "@/components/ui/context-ring-client" +export { compactTokens, type ContextPart } from "@/components/ui/context-ring-helpers" diff --git a/src/components/ui/turn-status-client.tsx b/src/components/ui/turn-status-client.tsx new file mode 100644 index 0000000..d1f5ede --- /dev/null +++ b/src/components/ui/turn-status-client.tsx @@ -0,0 +1,104 @@ +"use client" + +import * as React from "react" +import { ChevronRight } from "lucide-react" + +import { cn } from "@/lib/utils" +import { formatDuration } from "@/components/ui/turn-status-helpers" +import { RollingNumber, StatusGlyph, type Activity, type Figure } from "@/components/ui/status-glyph" + +/** Whole seconds since a moment, counted for as long as the caller is mounted. */ +function useElapsed(since: number | undefined): number { + const [now, setNow] = React.useState(() => Date.now()) + React.useEffect(() => { + if (since === undefined) return + const tick = window.setInterval(() => setNow(Date.now()), 1000) + return () => window.clearInterval(tick) + }, [since]) + return since === undefined ? 0 : Math.max(0, Math.floor((now - since) / 1000)) +} + +/** + * The line a turn ends on for as long as it is going: that it is going (the + * glyph), what it is doing this moment (the words, with a slow glint across + * them), for how long, and about how much it has written. It is there from + * the first moment to the last, whatever else comes and goes above it. + * + * Given children, the line is a button, and opens to show them — the calls + * of the step in hand. + * + * Nothing is said of the time until there is a second of it: "0s" is a + * figure about nothing. + */ +function TurnStatus({ + activity = "thinking", + figure, + label, + since, + tokens = 0, + trailing, + children, + className, +}: { + activity?: Activity + /** One figure alone, instead of the activity's own; see StatusGlyph. */ + figure?: Figure + /** What is being done, in a few words. Left out, only the glyph and the figures show. */ + label?: string + /** When the turn began, in milliseconds since the epoch. */ + since?: number + /** Tokens written so far; the count climbs to each new value. */ + tokens?: number + /** Said after the line and apart from it: how many background tasks are running, say. */ + trailing?: React.ReactNode + children?: React.ReactNode + className?: string +}) { + const [open, setOpen] = React.useState(false) + const elapsed = useElapsed(since) + const opens = Boolean(children) + + const line = ( + <> +{note}
- - - ) -} - -export { ContextRing } diff --git a/src/components/ui/context-ring-helpers.ts b/src/components/ui/context-ring-helpers.ts deleted file mode 100644 index df1c427..0000000 --- a/src/components/ui/context-ring-helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** One thing the context is full of, and the colour it is drawn in (a background class). */ -export type ContextPart = { key: string; label: string; tokens: number; tint: string } - -/** 168100 → "168.1k", 1000000 → "1M": the way a window's size is said aloud. */ -function compactTokens(n: number): string { - if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M` - if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k` - return String(n) -} - -export { compactTokens } diff --git a/src/components/ui/context-ring.tsx b/src/components/ui/context-ring.tsx index 9d627d8..300c66f 100644 --- a/src/components/ui/context-ring.tsx +++ b/src/components/ui/context-ring.tsx @@ -1,2 +1,132 @@ -export { ContextRing } from "@/components/ui/context-ring-client" -export { compactTokens, type ContextPart } from "@/components/ui/context-ring-helpers" +"use client" + +import * as React from "react" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" + +/** One thing the context is full of, and the colour it is drawn in (a background class). */ +export type ContextPart = { key: string; label: string; tokens: number; tint: string } + +/** 168100 → "168.1k", 1000000 → "1M": the way a window's size is said aloud. */ +function compactTokens(n: number): string { + if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k` + return String(n) +} + +const share = (tokens: number, window: number) => (window > 0 ? `${((tokens / window) * 100).toFixed(1)}%` : "—") + +/** + * How full a model's context window is, as a ring: read at a glance, opened + * for the numbers and for what the context is full of. + * + * Blue while there is room, which is nearly always; amber from 70%, the + * colour of failure from 90% — nearly full is worth a second look. + */ +function ContextRing({ + used, + window, + parts = [], + title = "Context window", + note = "How much of the window this conversation takes up. Earlier turns are compacted as it fills.", + freeLabel = "Free", + className, +}: { + used: number + window: number + parts?: ContextPart[] + title?: string + note?: string + freeLabel?: string + className?: string +}) { + const [detailed, setDetailed] = React.useState(false) + const fraction = window > 0 ? Math.min(used / window, 1) : 0 + const percent = Math.round(fraction * 100) + const r = 6 + const around = 2 * Math.PI * r + const tone = fraction >= 0.9 ? "text-destructive" : fraction >= 0.7 ? "text-amber-500" : "text-blue-500" + const counted = parts.reduce((sum, part) => sum + part.tokens, 0) + const free = Math.max(window - Math.max(used, counted), 0) + + return ( +{note}
+ + + ) +} + +export { ContextRing, compactTokens } diff --git a/src/components/ui/turn-status-client.tsx b/src/components/ui/turn-status-client.tsx deleted file mode 100644 index dd40aa4..0000000 --- a/src/components/ui/turn-status-client.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client" - -import * as React from "react" -import { ChevronRight } from "lucide-react" - -import { cn } from "@/lib/utils" -import { formatDuration } from "@/components/ui/turn-status-helpers" -import { RollingNumber, StatusGlyph, type Activity, type Figure } from "@/components/ui/status-glyph" - -/** Whole seconds since a moment, counted for as long as the caller is mounted. */ -function useElapsed(since: number | undefined): number { - const [now, setNow] = React.useState(() => Date.now()) - React.useEffect(() => { - if (since === undefined) return - const tick = window.setInterval(() => setNow(Date.now()), 1000) - return () => window.clearInterval(tick) - }, [since]) - return since === undefined ? 0 : Math.max(0, Math.floor((now - since) / 1000)) -} - -/** - * The line a turn ends on for as long as it is going: that it is going (the - * glyph), what it is doing this moment (the words, with a slow glint across - * them), for how long, and about how much it has written. It is there from - * the first moment to the last, whatever else comes and goes above it. - * - * Given children, the line is a button, and opens to show them — the calls - * of the step in hand. - * - * Nothing is said of the time until there is a second of it: "0s" is a - * figure about nothing. - */ -function TurnStatus({ - activity = "thinking", - figure, - label, - since, - tokens = 0, - trailing, - children, - className, -}: { - activity?: Activity - /** One figure alone, instead of the activity's own; see StatusGlyph. */ - figure?: Figure - /** What is being done, in a few words. Left out, only the glyph and the figures show. */ - label?: string - /** When the turn began, in milliseconds since the epoch. */ - since?: number - /** Tokens written so far; the count climbs to each new value. */ - tokens?: number - /** Said after the line and apart from it: how many background tasks are running, say. */ - trailing?: React.ReactNode - children?: React.ReactNode - className?: string -}) { - const [open, setOpen] = React.useState(false) - const elapsed = useElapsed(since) - const opens = Boolean(children) - // The glyph is decorative. Even without visible words, expose the activity - // and keep the disclosure's name independent of the clock and token count. - const activityLabel = label?.trim() || activity - - const line = ( - <> -