Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@
"analytics_top_skills_empty": "No skill usage data",
"usage_model": "Model",
"usage_models": "Models",
"usage_branch": "Branch",
"usage_refresh": "Refresh usage data",
"usage_summary_total_cost": "Total Cost",
"usage_summary_copilot_ai_credits": "Copilot AI Credits",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@
"analytics_top_skills_empty": "无 skill 使用数据",
"usage_model": "模型",
"usage_models": "模型",
"usage_branch": "分支",
"usage_refresh": "刷新用量数据",
"usage_summary_total_cost": "总成本",
"usage_summary_copilot_ai_credits": "Copilot AI Credits",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/api/generated/index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions frontend/src/lib/api/generated/models/BranchTotal.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions frontend/src/lib/api/generated/models/DbBranchBreakdown.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/lib/api/generated/models/DbDailyUsageEntry.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions frontend/src/lib/api/types/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export interface AgentBreakdown {
cost: number;
}

export interface BranchBreakdown {
project: string;
branch: string;
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
cost: number;
}

export interface DailyUsageEntry {
date: string;
inputTokens: number;
Expand All @@ -48,6 +58,7 @@ export interface DailyUsageEntry {
modelBreakdowns?: ModelBreakdown[];
projectBreakdowns?: ProjectBreakdown[];
agentBreakdowns?: AgentBreakdown[];
branchBreakdowns?: BranchBreakdown[];
}

export interface ProjectTotal {
Expand Down Expand Up @@ -77,6 +88,16 @@ export interface AgentTotal {
cost: number;
}

export interface BranchTotal {
project: string;
branch: string;
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
cost: number;
}

export interface CacheStats {
cacheReadTokens: number;
cacheCreationTokens: number;
Expand Down Expand Up @@ -107,6 +128,7 @@ export interface UsageSummaryResponse {
projectTotals: ProjectTotal[];
modelTotals: ModelTotal[];
agentTotals: AgentTotal[];
branchTotals: BranchTotal[];
sessionCounts: UsageSessionCounts;
cacheStats: CacheStats;
comparison?: UsageComparison;
Expand All @@ -129,6 +151,7 @@ export interface UsageParams {
to?: string;
project?: string;
machine?: string;
git_branch?: string;
agent?: string;
model?: string;
exclude_project?: string;
Expand Down
53 changes: 40 additions & 13 deletions frontend/src/lib/components/usage/AttributionPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
type GroupBy,
type AttributionView,
} from "../../stores/usage.svelte.js";
import {
branchFilterToken,
branchLabel,
} from "../../branchFilters.js";
import { projectColor } from "../../utils/projectColor.js";
import Treemap from "./Treemap.svelte";
import { m } from "../../i18n/index.js";
Expand All @@ -20,6 +24,8 @@

const groupBy = $derived(usage.toggles.attribution.groupBy);
const view = $derived(usage.toggles.attribution.view);
const canSelectRows = $derived(groupBy !== "branch");
const noBranchLabel = $derived(m.shared_no_branch());

interface Row {
id: string;
Expand Down Expand Up @@ -51,6 +57,12 @@
label: m.model,
cost: m.cost,
}));
} else if (groupBy === "branch") {
items = s.branchTotals.map((b) => ({
id: branchFilterToken(b.project, b.branch),
label: branchLabel(b.project, b.branch, noBranchLabel),
cost: b.cost,
}));
} else {
items = s.agentTotals.map((a) => ({
id: a.agent,
Expand Down Expand Up @@ -88,7 +100,7 @@
usage.toggleProject(id);
} else if (groupBy === "agent") {
usage.toggleAgent(id);
} else {
} else if (groupBy === "model") {
usage.toggleModel(id);
}
}
Expand Down Expand Up @@ -128,6 +140,13 @@
>
{m.analytics_col_agent()}
</button>
<button
class="toggle-btn"
class:active={groupBy === "branch"}
onclick={() => handleGroupByChange("branch")}
>
{m.usage_branch()}
</button>
</div>
<div class="segment-toggle">
<button
Expand All @@ -151,14 +170,16 @@
{#if rows.length === 0}
<div class="empty">{m.shared_no_data_for_period()}</div>
{:else}
<div class="hint">{m.usage_click_to_hide_hint()}</div>
{#if canSelectRows}
<div class="hint">{m.usage_click_to_hide_hint()}</div>
{/if}
{#if view === "treemap"}
<div class="treemap-layout">
<div class="treemap-main">
<Treemap
items={treemapItems}
height={260}
onSelect={handleSelect}
onSelect={canSelectRows ? handleSelect : undefined}
/>
</div>
<div class="side-rail">
Expand All @@ -167,8 +188,9 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="rail-row"
title={m.usage_click_to_hide({ label: row.label })}
onclick={() => handleSelect(row.id)}
class:interactive={canSelectRows}
title={canSelectRows ? m.usage_click_to_hide({ label: row.label }) : undefined}
onclick={canSelectRows ? () => handleSelect(row.id) : undefined}
>
<span class="rail-rank">{i + 1}</span>
<span
Expand All @@ -188,8 +210,9 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="list-row"
title={m.usage_click_to_hide({ label: row.label })}
onclick={() => handleSelect(row.id)}
class:interactive={canSelectRows}
title={canSelectRows ? m.usage_click_to_hide({ label: row.label }) : undefined}
onclick={canSelectRows ? () => handleSelect(row.id) : undefined}
>
<span class="list-rank">{i + 1}</span>
<span
Expand Down Expand Up @@ -270,7 +293,6 @@
color: var(--text-secondary);
}

/* Treemap layout: main + side rail */
.treemap-layout {
display: grid;
grid-template-columns: 2.4fr 1fr;
Expand All @@ -297,11 +319,14 @@
gap: 6px;
padding: 3px 4px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.1s;
}

.rail-row:hover {
.rail-row.interactive {
cursor: pointer;
}

.rail-row.interactive:hover {
background: var(--bg-surface-hover);
}

Expand Down Expand Up @@ -337,7 +362,6 @@
color: var(--text-primary);
}

/* List view */
.list-view {
display: flex;
flex-direction: column;
Expand All @@ -350,11 +374,14 @@
gap: 8px;
padding: 4px 6px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.1s;
}

.list-row:hover {
.list-row.interactive {
cursor: pointer;
}

.list-row.interactive:hover {
background: var(--bg-surface-hover);
}

Expand Down
34 changes: 27 additions & 7 deletions frontend/src/lib/components/usage/CostTimeSeriesChart.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<script lang="ts">
import { usage, type GroupBy } from "../../stores/usage.svelte.js";
import {
branchFilterToken,
branchTokenLabel,
} from "../../branchFilters.js";
import { projectColor } from "../../utils/projectColor.js";
import { m } from "../../i18n/index.js";

Expand Down Expand Up @@ -36,6 +40,7 @@
}

const groupBy = $derived(usage.toggles.timeSeries.groupBy);
const noBranchLabel = $derived(m.shared_no_branch());

const seriesData = $derived.by((): {
points: Point[];
Expand All @@ -47,7 +52,6 @@
return { points: [], keys: [], maxY: 0 };
}

// Sum cost per key across the whole range to find top N.
const totals = new Map<string, number>();
for (const day of daily) {
if (groupBy === "project" && day.projectBreakdowns) {
Expand All @@ -65,10 +69,14 @@
totals.set(b.agent,
(totals.get(b.agent) ?? 0) + b.cost);
}
} else if (groupBy === "branch" && day.branchBreakdowns) {
for (const b of day.branchBreakdowns) {
const key = branchFilterToken(b.project, b.branch);
totals.set(key, (totals.get(key) ?? 0) + b.cost);
}
}
}

// If only one key or few keys, no need for "Other".
if (totals.size === 0) {
const points = daily.map((d) => ({
date: d.date,
Expand All @@ -81,7 +89,6 @@
return { points, keys: ["total"], maxY: maxY || 1 };
}

// Pick top N by total cost, group the rest as "Other".
const ranked = [...totals.entries()]
.sort((a, b) => b[1] - a[1]);
const topKeys = new Set(
Expand All @@ -106,6 +113,10 @@
items = day.agentBreakdowns.map((b) => ({
key: b.agent, cost: b.cost,
}));
} else if (groupBy === "branch" && day.branchBreakdowns) {
items = day.branchBreakdowns.map((b) => ({
key: branchFilterToken(b.project, b.branch), cost: b.cost,
}));
}

for (const { key, cost } of items) {
Expand All @@ -119,8 +130,6 @@
points.push({ date: day.date, values });
}

// Build ordered key list: top N by cost desc, then
// __other__ (displayed as "Other" in legend/labels).
const keys = ranked
.slice(0, MAX_SERIES)
.map(([k]) => k);
Expand Down Expand Up @@ -240,7 +249,6 @@
d += i === 0 ? `M${x},${top}` : `L${x},${top}`;
}

// Close area back along baseline
for (let i = points.length - 1; i >= 0; i--) {
const x = Y_LABEL_W + i * xStep;
const base = scaleY(baselines[i]!, maxY, h);
Expand Down Expand Up @@ -332,6 +340,11 @@
function handleGroupByChange(g: GroupBy) {
usage.setTimeSeriesGroupBy(g);
}

function legendLabel(key: string): string {
if (key === "__other__") return m.shared_other();
return groupBy === "branch" ? branchTokenLabel(key, noBranchLabel) : key;
}
</script>

<div class="chart-container">
Expand Down Expand Up @@ -359,6 +372,13 @@
>
{m.analytics_col_agent()}
</button>
<button
class="toggle-btn"
class:active={groupBy === "branch"}
onclick={() => handleGroupByChange("branch")}
>
{m.usage_branch()}
</button>
</div>
</div>

Expand Down Expand Up @@ -416,7 +436,7 @@
class="legend-dot"
style="background: {key === '__other__' ? 'var(--text-muted)' : projectColor(key)}"
></span>
{key === "__other__" ? m.shared_other() : key}
{legendLabel(key)}
</span>
{/each}
</div>
Expand Down
Loading
Loading