diff --git a/apps/code/src/renderer/features/billing/components/TokenSpendAnalysisBanner.tsx b/apps/code/src/renderer/features/billing/components/TokenSpendAnalysisBanner.tsx index bbcb37b34..66c5c5e08 100644 --- a/apps/code/src/renderer/features/billing/components/TokenSpendAnalysisBanner.tsx +++ b/apps/code/src/renderer/features/billing/components/TokenSpendAnalysisBanner.tsx @@ -4,59 +4,32 @@ import type { SpendAnalysisProductRow, SpendAnalysisResponse, SpendAnalysisToolRow, - SpendAnalysisTraceRow, } from "@features/billing/types/spend-analysis"; +import { + formatTokens, + formatUsd, + formatWindow, +} from "@features/billing/utils/spendAnalysisFormat"; +import { buildAnalysisPrompt } from "@features/billing/utils/spendAnalysisPrompt"; +import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; import { ArrowSquareOut, ChartLine, Lightning, + Sparkle, WarningCircle, } from "@phosphor-icons/react"; import { Button, Callout, Flex, Spinner, Table, Text } from "@radix-ui/themes"; +import { ANALYTICS_EVENTS } from "@shared/types/analytics"; +import { useNavigationStore } from "@stores/navigationStore"; +import { track } from "@utils/analytics"; const DOCS_URL = "https://posthog.com/docs/llm-analytics"; -const SKILL_URL = - "https://github.com/PostHog/posthog/blob/master/products/llm_analytics/skills/exploring-llm-costs/SKILL.md"; - -function formatUsd(amount: number): string { - if (amount === 0) return "$0"; - if (amount < 0.01) return "<$0.01"; - if (amount < 100) return `$${amount.toFixed(2)}`; - return `$${Math.round(amount).toLocaleString()}`; -} - -function formatTokens(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(0)}k`; - return n.toString(); -} - -function formatTrace(traceId: string | null): string { - if (!traceId) return "(no trace id)"; - if (traceId.length <= 14) return traceId; - return `${traceId.slice(0, 8)}…${traceId.slice(-4)}`; -} - -function formatWindow(fromIso: string, toIso: string): string { - const fromMs = new Date(fromIso).getTime(); - const toMs = new Date(toIso).getTime(); - const days = Math.max(1, Math.round((toMs - fromMs) / (1000 * 60 * 60 * 24))); - return `${days} days`; -} - -function formatDate(iso: string | null): string { - if (!iso) return "—"; - return new Date(iso).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - }); -} function generateSuggestions(data: SpendAnalysisResponse): string[] { const suggestions: string[] = []; const { summary } = data; const toolItems = data.by_tool.items; - const traceItems = data.top_traces.items; if (summary.total_cost_usd === 0) { return ["No LLM spend in the selected window."]; @@ -88,19 +61,9 @@ function generateSuggestions(data: SpendAnalysisResponse): string[] { } } - if (traceItems.length > 0 && codeTotal > 0) { - const topTrace = traceItems[0]; - const share = topTrace.cost_usd / codeTotal; - if (share > 0.15) { - suggestions.push( - `Your top session cost ${formatUsd(topTrace.cost_usd)} — ${Math.round(share * 100)}% of PostHog Code spend in one trace. Long sessions compound context cost.`, - ); - } - } - if (suggestions.length === 0) { suggestions.push( - "Your spend is fairly evenly distributed across tools and sessions — no single hotspot stands out.", + "Your spend is fairly evenly distributed across tools — no single hotspot stands out.", ); } @@ -218,30 +181,6 @@ function ModelTable({ rows }: { rows: SpendAnalysisModelRow[] }) { ); } -function TraceTable({ rows }: { rows: SpendAnalysisTraceRow[] }) { - if (rows.length === 0) return null; - return ( - - {rows.map((r) => ( - - - - {formatTrace(r.trace_id)} - - - {r.generation_count.toLocaleString()} - {formatDate(r.started_at)} - {formatUsd(r.cost_usd)} - - ))} - - ); -} - function SectionTable({ title, headers, @@ -279,9 +218,39 @@ function SectionTable({ ); } -function FooterLinks() { +function FooterLinks({ data }: { data: SpendAnalysisResponse }) { + const navigateToTaskInput = useNavigationStore( + (state) => state.navigateToTaskInput, + ); + const closeSettings = useSettingsDialogStore((state) => state.close); + + const handleAnalyseClick = (): void => { + track(ANALYTICS_EVENTS.SPEND_ANALYSIS_TASK_OPENED, { + total_cost_usd: data.summary.total_cost_usd, + scoped_cost_usd: data.summary.scoped_cost_usd, + scoped_event_count: data.summary.scoped_event_count, + window_days: Math.max( + 1, + Math.round( + (new Date(data.summary.date_to).getTime() - + new Date(data.summary.date_from).getTime()) / + (1000 * 60 * 60 * 24), + ), + ), + tool_row_count: Math.min(data.by_tool.items.length, 10), + model_row_count: data.by_model.items.length, + }); + // This banner lives inside the Settings dialog (modal). `navigateToTaskInput` + // changes the underlying view but the dialog stays mounted on top, so the user + // doesn't see the prefilled task input. Close the dialog first. + closeSettings(); + navigateToTaskInput({ + initialPrompt: buildAnalysisPrompt(data), + }); + }; + return ( - + Use{" "} {" "} in your own project for the full slice-and-dice experience. - - Want an agent to run this kind of analysis on demand? Drop the{" "} - - exploring-llm-costs - {" "} - skill into your agent. - + ); } @@ -346,7 +312,6 @@ export function TokenSpendAnalysisBanner() { - ))} - + ); } @@ -406,7 +371,7 @@ export function TokenSpendAnalysisBanner() { Analyse your token usage with PostHog LLM analytics - See where your spend goes — by tool, by model, by trace — over the + See where your spend goes — by product, tool, and model — over the last 30 days, and get tips on where to optimise.