Skip to content
Merged
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 src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ function App() {
rangeEnd={rangeEnd}
licenseAmount={licenseAmount}
licenseSeatCounts={licenseSeatCounts}
includedAicCredits={includedAicPoolSize}
reportPlanScope={reportPlanScope}
upgradeRecommendation={individualUpgradeRecommendation}
onAdjustSeatCounts={reportPlanScope === 'organization' && !isIndividualReport ? () => setActiveView('users') : undefined}
Expand Down
42 changes: 32 additions & 10 deletions src/components/DualAxisLineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Tooltip,
Legend,
type ChartOptions,
type LegendItem,
type ScriptableLineSegmentContext,
} from 'chart.js'
import { Line } from 'react-chartjs-2'
Expand All @@ -27,12 +28,23 @@ export interface LineSeries {
segmentColor?: (startValue: number, endValue: number) => string
}

export interface ExtraLegendItem {
label: string
color: string
legendOrder?: number
}

export interface DualAxisLineChartProps {
title: string
labels: string[]
series: [LineSeries, LineSeries, ...LineSeries[]]
height?: number
formatYAsCurrency?: boolean
extraLegendItems?: ExtraLegendItem[]
}

type OrderedLegendItem = LegendItem & {
legendOrder?: number
}

function formatTick(value: number): string {
Expand All @@ -52,6 +64,7 @@ export function DualAxisLineChart({
series,
height = 320,
formatYAsCurrency = false,
extraLegendItems = [],
}: DualAxisLineChartProps) {
const tickFormatter = formatYAsCurrency ? formatUsdTick : formatTick
const usesSecondaryAxis = series.some((dataset) => dataset.yAxisID === 'y1')
Expand Down Expand Up @@ -111,24 +124,33 @@ export function DualAxisLineChart({
font: { size: 11, weight: 500 },
generateLabels: (chart) => {
const defaultLabels = ChartJS.defaults.plugins.legend.labels.generateLabels?.(chart) ?? []
return defaultLabels.map((item) => {
const datasetLabels: OrderedLegendItem[] = defaultLabels.map((item) => {
const dataset = typeof item.datasetIndex === 'number'
? chart.data.datasets[item.datasetIndex] as { legendLabel?: string, legendOrder?: number }
: undefined

return dataset?.legendLabel ? { ...item, text: dataset.legendLabel } : item
}).sort((a, b) => {
const datasetA = typeof a.datasetIndex === 'number'
? chart.data.datasets[a.datasetIndex] as { legendOrder?: number }
: undefined
const datasetB = typeof b.datasetIndex === 'number'
? chart.data.datasets[b.datasetIndex] as { legendOrder?: number }
: undefined
return {
...item,
text: dataset?.legendLabel ?? item.text,
legendOrder: dataset?.legendOrder,
}
})

const additionalLabels: OrderedLegendItem[] = extraLegendItems.map((item) => ({
text: item.label,
fillStyle: item.color,
strokeStyle: item.color,
hidden: false,
lineWidth: 2,
legendOrder: item.legendOrder,
}))

return (datasetA?.legendOrder ?? a.datasetIndex ?? 0) - (datasetB?.legendOrder ?? b.datasetIndex ?? 0)
return [...datasetLabels, ...additionalLabels].sort((a, b) => {
return (a.legendOrder ?? a.datasetIndex ?? 0) - (b.legendOrder ?? b.datasetIndex ?? 0)
})
},
},
onClick: () => undefined,
},
title: {
display: true,
Expand Down
16 changes: 7 additions & 9 deletions src/views/CostManagementView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -441,15 +441,6 @@ export function CostManagementView({
: SIMULATED_ADDITIONAL_COLOR
),
},
{
label: 'Simulated - additional usage',
legendOrder: 2,
color: SIMULATED_ADDITIONAL_COLOR,
data: cumulativeSimulationSeries.labels.map(() => null),
yAxisID: 'y',
order: 4,
pointRadius: 0,
},
{
label: 'Included AI Credits pool',
legendOrder: 4,
Expand All @@ -461,6 +452,13 @@ export function CostManagementView({
pointRadius: 0,
},
]}
extraLegendItems={[
{
label: 'Simulated - additional usage',
color: SIMULATED_ADDITIONAL_COLOR,
legendOrder: 2,
},
]}
formatYAsCurrency
height={320}
/>
Expand Down
120 changes: 84 additions & 36 deletions src/views/OverviewView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BillingProjectionDisclaimer, BillingTotalsCards } from '../components/u
import { appLinks } from '../config/links'
import type { ReportPlanScope } from '../pipeline/aicIncludedCredits'
import type { DailyUsageData } from '../pipeline/aggregators/dailyUsageAggregator'
import { AIC_UNIT_PRICE_USD } from '../utils/billingConstants'
import { fillDataForRange } from '../utils/fillDataForRange'
import { formatUsd } from '../utils/format'
import type { IndividualPlanUpgradeRecommendation } from '../utils/individualPlanUpgrade'
Expand All @@ -18,11 +19,16 @@ type OverviewViewProps = {
business: number
enterprise: number
}
includedAicCredits: number
reportPlanScope?: ReportPlanScope
upgradeRecommendation?: IndividualPlanUpgradeRecommendation | null
onAdjustSeatCounts?: () => void
}

const CURRENT_AIC_COLOR = '#1a7f37'
const ADDITIONAL_AIC_COLOR = '#cf222e'
const INCLUDED_CREDITS_COLOR = '#0969da'

function createEmptyDailyUsage(date: string): DailyUsageData {
return {
date,
Expand All @@ -44,6 +50,7 @@ export function OverviewView({
rangeEnd,
licenseAmount,
licenseSeatCounts,
includedAicCredits,
reportPlanScope = 'organization',
upgradeRecommendation = null,
onAdjustSeatCounts,
Expand Down Expand Up @@ -73,6 +80,23 @@ export function OverviewView({
const usageBasedBillingDocsUrl = reportPlanScope === 'individual'
? appLinks.usageBasedBillingForIndividualsDocs
: appLinks.usageBasedBillingForOrganizationsDocs
const includedCreditsValue = includedAicCredits * AIC_UNIT_PRICE_USD
const includedCreditsLabel = 'Included value'
const includedCreditsLegendLabel = reportPlanScope === 'individual' ? 'Included AI Credits' : 'Included AI Credits pool'
const includedCreditsDescription = reportPlanScope === 'individual'
? 'Your plan\'s included AI Credits are consumed first. Additional usage spend starts after cumulative AIC gross cost exceeds the included value.'
: 'The account-wide included AI Credits pool is consumed first. Additional usage spend starts after cumulative AIC gross cost exceeds the included value.'
const includedCreditsCardTitle = reportPlanScope === 'individual' ? 'Included credits are coming' : 'Pooled included credits are coming'
const includedCreditsCardBody = reportPlanScope === 'individual'
? 'Under usage-based billing, your Copilot plan includes AI Credits each month. Usage consumes those included credits first; additional usage is billed only after they are used.'
: 'Under usage-based billing, included credits will be pooled across all licensed users in your account. No more unused capacity going to waste from idle users.'
const includedCreditsDocsUrl = reportPlanScope === 'individual'
? appLinks.usageBasedBillingForIndividualsDocs
: appLinks.aiCreditsForOrganizationsDocs
const cumulativeAicGrossAmount = filledDailyUsageData.reduce<number[]>((totals, day) => {
totals.push((totals[totals.length - 1] ?? 0) + day.aicGrossAmount)
return totals
}, [])

return (
<div className="max-w-[var(--width-content-max)] w-full mx-auto px-6 pt-8 pb-12 flex flex-col gap-6">
Expand Down Expand Up @@ -146,25 +170,51 @@ export function OverviewView({
<BillingProjectionDisclaimer className="mb-6" />

<section className="grid grid-cols-1 gap-6 w-full">
<DualAxisLineChart
title="Daily Requests & AI Credits"
labels={filledDailyUsageData.map((day) => day.date)}
series={[
{
label: 'Premium Requests',
color: '#6366f1',
data: filledDailyUsageData.map((day) => day.requests),
yAxisID: 'y',
},
{
label: 'AI Credits',
color: '#22c55e',
data: filledDailyUsageData.map((day) => day.aicQuantity),
yAxisID: 'y1',
},
]}
height={320}
/>
<div className="flex flex-col gap-2">
<DualAxisLineChart
title="Cumulative AIC gross cost: included vs additional"
labels={filledDailyUsageData.map((day) => day.date)}
series={[
{
label: 'AIC gross cost',
legendLabel: 'Usage - within included value',
legendOrder: 1,
color: CURRENT_AIC_COLOR,
data: cumulativeAicGrossAmount,
yAxisID: 'y',
order: 1,
segmentColor: (_startValue, endValue) => (
endValue <= includedCreditsValue
? CURRENT_AIC_COLOR
: ADDITIONAL_AIC_COLOR
),
},
{
label: includedCreditsLabel,
legendLabel: includedCreditsLegendLabel,
legendOrder: 3,
color: INCLUDED_CREDITS_COLOR,
data: filledDailyUsageData.map(() => includedCreditsValue),
yAxisID: 'y',
borderDash: [2, 4],
order: 2,
pointRadius: 0,
},
]}
extraLegendItems={[
{
label: 'Usage - additional spend',
color: ADDITIONAL_AIC_COLOR,
legendOrder: 2,
},
]}
formatYAsCurrency
height={320}
/>
<p className="m-0 text-center text-[13px] text-fg-muted leading-normal">
{includedCreditsDescription}
</p>
</div>
<DualAxisLineChart
title="Daily cost: PRU cost vs AIC cost"
labels={filledDailyUsageData.map((day) => day.date)}
Expand Down Expand Up @@ -213,24 +263,22 @@ export function OverviewView({
/>
</section>

{reportPlanScope === 'organization' && (
<div className="bg-bg-default border border-border-default rounded-md py-5 px-6 mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5">
<div className="flex-1 flex flex-col gap-1">
<strong className="text-sm font-semibold text-fg-default">Pooled included credits are coming</strong>
<p className="m-0 text-[13px] text-fg-muted leading-normal">
Under usage-based billing, included credits will be pooled across all licensed users in your account. No more unused capacity going to waste from idle users.
</p>
</div>
<a
href={appLinks.aiCreditsForOrganizationsDocs}
className="text-sm font-medium text-fg-accent no-underline whitespace-nowrap hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Learn more &rarr;
</a>
<div className="bg-bg-default border border-border-default rounded-md py-5 px-6 mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5">
<div className="flex-1 flex flex-col gap-1">
<strong className="text-sm font-semibold text-fg-default">{includedCreditsCardTitle}</strong>
<p className="m-0 text-[13px] text-fg-muted leading-normal">
{includedCreditsCardBody}
</p>
</div>
)}
<a
href={includedCreditsDocsUrl}
className="text-sm font-medium text-fg-accent no-underline whitespace-nowrap hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Learn more &rarr;
</a>
</div>

<section className="mt-8">
<h2 className="text-base font-semibold text-fg-default pb-[10px] border-b border-border-default mb-4">Recommended next steps</h2>
Expand Down