diff --git a/src/App.tsx b/src/App.tsx index c4d6385..dab1731 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,12 +26,11 @@ import { CostCenterAggregator, type CostCenterResult } from './pipeline/aggregat import { OrganizationAggregator, type OrganizationResult } from './pipeline/aggregators/organizationAggregator' import { UserUsageAggregator, type UserUsageResult } from './pipeline/aggregators/userUsageAggregator' import { - BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS, - ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS, calculateLicenseSummary, inferReportPlanScope, type AicIncludedCreditsOverrides, } from './pipeline/aicIncludedCredits' +import { resolveIncludedCreditsPolicy } from './pipeline/includedCreditsPolicy' import { PRODUCT_BUDGET_COPILOT, PRODUCT_BUDGET_COPILOT_CLOUD_AGENT, PRODUCT_BUDGET_SPARK } from './pipeline/productClassification' import { runPipeline } from './pipeline/runPipeline' import type { ReportFormatMetadata } from './pipeline/reportAdapters' @@ -39,6 +38,7 @@ import { runBudgetSimulation, type BudgetSimulationResult } from './utils/budget import { EMPTY_BUDGET_VALUES, getDefaultBudgetValues, getUserSpendSegmentsByUsername, type BudgetField, type BudgetValues } from './utils/costManagementBudgets' import { calculateIndividualPlanUpgradeRecommendation, getIndividualLicenseMonthlyCost } from './utils/individualPlanUpgrade' import { normalizeSeatCount } from './utils/seatCounts' +import { getReportMode, isNativeAiCreditsMode } from './utils/reportMode' import { useAppVersionCheck } from './hooks/useAppVersionCheck' type Status = 'idle' | 'processing' | 'done' @@ -116,25 +116,37 @@ function App() { includedCreditsOverrides: AicIncludedCreditsOverrides = {}, onProgress?: (progressInfo: { rowsProcessed: number; progressPercent: number }) => void, ) => { - const statsAggregator = new QuickStatsAggregator() - const contextAggregator = new ReportContextAggregator() - const dailyAggregator = new DailyUsageAggregator() - const modelAggregator = new ModelUsageAggregator() - const productAggregator = new ProductUsageAggregator() - const costCenterAggregator = new CostCenterAggregator() - const orgAggregator = new OrganizationAggregator() - const userAggregator = new UserUsageAggregator() - - const pipelineResult = await runPipeline(file, [ - statsAggregator, - contextAggregator, - dailyAggregator, - modelAggregator, - productAggregator, - costCenterAggregator, - orgAggregator, - userAggregator, - ], { + let statsAggregator!: QuickStatsAggregator + let contextAggregator!: ReportContextAggregator + let dailyAggregator!: DailyUsageAggregator + let modelAggregator!: ModelUsageAggregator + let productAggregator!: ProductUsageAggregator + let costCenterAggregator!: CostCenterAggregator + let orgAggregator!: OrganizationAggregator + let userAggregator!: UserUsageAggregator + + const pipelineResult = await runPipeline(file, (reportMetadata) => { + statsAggregator = new QuickStatsAggregator() + contextAggregator = new ReportContextAggregator() + dailyAggregator = new DailyUsageAggregator(reportMetadata) + modelAggregator = new ModelUsageAggregator(reportMetadata) + productAggregator = new ProductUsageAggregator(reportMetadata) + costCenterAggregator = new CostCenterAggregator(reportMetadata) + orgAggregator = new OrganizationAggregator(reportMetadata) + userAggregator = new UserUsageAggregator(reportMetadata) + + return [ + statsAggregator, + contextAggregator, + dailyAggregator, + modelAggregator, + productAggregator, + costCenterAggregator, + orgAggregator, + userAggregator, + ] + }, { + enableNativeAiCreditsProcessing: true, includedCreditsOverrides, progressResolution: 500, onProgress, @@ -156,8 +168,16 @@ function App() { } }, []) + const reportMode = getReportMode(reportMetadata) + const isNativeAiCreditsReport = isNativeAiCreditsMode(reportMode) + const rangeStart = reportContext?.startDate ?? null + const rangeEnd = reportContext?.endDate ?? null + const includedCreditsPolicy = resolveIncludedCreditsPolicy(reportMode, { startDate: rangeStart, endDate: rangeEnd }) + const showOrganizationPromotionalDataDisclaimer = reportMode === 'transition-period-billing-preview' + || includedCreditsPolicy.id === 'native-ai-credits-summer-promo' + const getDefaultSeatCounts = useCallback(() => { - const summary = calculateLicenseSummary(userUsage?.users ?? []) + const summary = calculateLicenseSummary(userUsage?.users ?? [], includedCreditsPolicy) return { business: normalizeSeatCount( summary.rows.find((row) => row.label === 'Copilot Business')?.users ?? 0, @@ -168,7 +188,7 @@ function App() { 0, ), } - }, [userUsage]) + }, [includedCreditsPolicy, userUsage]) const resetReportState = useCallback(({ status, fileName }: { status: Status; fileName: string | null }) => { setStatus(status) @@ -339,6 +359,11 @@ function App() { const handleApplyBudgetSimulation = useCallback(async () => { const file = currentFileRef.current if (!file) return + if (isNativeAiCreditsReport) { + setBudgetSimulation(null) + setBudgetSimulationError('Budget simulation is not available for native AI Credits reports yet.') + return + } const budgetReportUsers = userUsage?.users ?? [] const hasBudgetOrganizationContext = budgetReportUsers.some((user) => user.organizations.length > 0 || user.costCenters.length > 0) @@ -424,6 +449,7 @@ function App() { budgetValues.productCopilot, budgetValues.productSpark, budgetValues.user, + isNativeAiCreditsReport, resolveIncludedCreditOverrides, seatOverrides, userUsage, @@ -485,8 +511,6 @@ function App() { const hasReport = status === 'done' && fileName !== null && reportMetadata !== null const showSeatConfirmation = hasReport && seatConfirmationPending - const rangeStart = reportContext?.startDate ?? null - const rangeEnd = reportContext?.endDate ?? null const reportUsers = userUsage?.users ?? [] const hasOrganizationContext = reportUsers.some((user) => user.organizations.length > 0 || user.costCenters.length > 0) const reportPlanScope = inferReportPlanScope(reportUsers.length, hasOrganizationContext) @@ -505,15 +529,18 @@ function App() { ? { business: effectiveBusinessSeats, enterprise: effectiveEnterpriseSeats } : undefined const includedAicPoolSize = reportPlanScope === 'organization' - ? (effectiveBusinessSeats * BUSINESS_MONTHLY_AIC_INCLUDED_CREDITS) + (effectiveEnterpriseSeats * ENTERPRISE_MONTHLY_AIC_INCLUDED_CREDITS) - : calculateLicenseSummary(reportUsers).totalIncludedAic + ? (effectiveBusinessSeats * includedCreditsPolicy.organizationPlans.business.monthlyIncludedCredits) + (effectiveEnterpriseSeats * includedCreditsPolicy.organizationPlans.enterprise.monthlyIncludedCredits) + : calculateLicenseSummary(reportUsers, includedCreditsPolicy).totalIncludedAic const selectedUser = individualUser ?? (selectedUsername && userUsage ? userUsage.users.find((user) => user.username === selectedUsername) ?? null : null) const canShowSpendInsights = Boolean(userUsage) && !isIndividualReport && reportUsers.length > 1 - const visibleActiveView = activeView === 'spendInsights' && !canShowSpendInsights ? 'overview' : activeView + const visibleActiveView = (activeView === 'spendInsights' && !canShowSpendInsights) + || (isNativeAiCreditsReport && (activeView === 'guide' || activeView === 'faq')) + ? 'overview' + : activeView const userNavActive = isIndividualReport ? visibleActiveView === 'userDetails' : visibleActiveView === 'users' || visibleActiveView === 'userDetails' @@ -704,25 +731,29 @@ function App() { Cost Management -
{savings > 0 ? ( <> @@ -126,59 +138,31 @@ export function CostCentersView({ data, rangeStart }: { data: CostCenterResult;
)} -- 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. + {isNativeAiCredits + ? 'Included credits are pooled across all licensed users in your account.' + : '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.'}
Budget simulation is not available for usage-based billing reports yet.
+{savings > 0 ? ( <>{selectedModel}'s {periodLabel} usage would cost{' '}{formatUsd(savings)} less under usage-based billing> @@ -201,120 +211,96 @@ export function ModelsView({ modelUsage, isIndividualReport, rangeStart, rangeEn
)} -- Average AICs per PRU for {selectedModel} in the current report period:{' '} - {formatAverageAicPerRequest(overallAverageAicPerRequest)}, average gross per PRU{' '} - {formatUsd(overallAverageAicGrossPerRequest)} -
-- Note: PRU quantities include billing-period model multipliers, so AICs per PRU should not be read as AICs per actual request when comparing models. -
-+ Average AICs per PRU for {selectedModel} in the current report period:{' '} + {formatAverageAicPerRequest(overallAverageAicPerRequest)}, average gross per PRU{' '} + {formatUsd(overallAverageAicGrossPerRequest)} +
++ Note: PRU quantities include billing-period model multipliers, so AICs per PRU should not be read as AICs per actual request when comparing models. +
+Gross cost is shown before included-credits discounts. Under usage-based billing, included AICs from the account-wide pool will reduce net costs.
+Gross cost is shown before included-credits discounts. Included AICs from the account-wide pool reduce net costs.
{savings > 0 ? ( <> @@ -125,59 +137,31 @@ export function OrganizationsView({ data, rangeStart }: { data: OrganizationResu
)} -- Under usage-based billing, included credits will be pooled across all licensed users in your account (not per organization). Included credits are shared across your account-wide pool, not allocated separately to each organization. No more unused capacity going to waste from idle users. + {isNativeAiCredits + ? 'Included credits are shared across your account-wide pool, not allocated separately to each organization.' + : 'Under usage-based billing, included credits will be pooled across all licensed users in your account (not per organization). Included credits are shared across your account-wide pool, not allocated separately to each organization. No more unused capacity going to waste from idle users.'}
- Starting June 1, 2026, Copilot usage will be measured in AI Credits (AICs) instead of Premium Requests (PRUs). 1 AIC = $0.01. This is a preview estimate based on your uploaded report. Actual bills under usage-based billing may differ based on model mix and final pricing. -
- {fileName && ( -- Note: This is a preview estimate based on your uploaded report ({fileName}). Actual bills under usage-based billing may differ based on model mix and final pricing. + {!isNativeAiCredits && ( +
+ Starting June 1, 2026, Copilot usage will be measured in AI Credits (AICs) instead of Premium Requests (PRUs). 1 AIC = $0.01. This is a preview estimate based on your uploaded report. Actual bills under usage-based billing may differ based on model mix and final pricing.
- )} - - Learn more about usage-based billing → - -+ Note: This is a preview estimate based on your uploaded report ({fileName}). Actual bills under usage-based billing may differ based on model mix and final pricing. +
+ )} + + Learn more about usage-based billing → + +
{savings > 0 ? (
<>
@@ -163,11 +175,13 @@ export function OverviewView({
licenseSeatCounts={licenseSeatCounts}
showExistingDiscountDisclaimer={reportPlanScope !== 'individual'}
showPromotionalDataDisclaimer={reportPlanScope === 'individual'}
+ showOrganizationPromotionalDataDisclaimer={reportPlanScope !== 'individual' && showOrganizationPromotionalDataDisclaimer}
upgradeRecommendation={upgradeRecommendation}
onAdjustSeatCounts={onAdjustSeatCounts}
+ reportMode={reportMode}
className="mb-3"
/>
-
- {includedCreditsCardBody} + {isNativeAiCredits ? nativeIncludedCreditsCardBody : includedCreditsCardBody}
{savings > 0 ? ( <> @@ -252,58 +261,30 @@ export function UserDetailsView({
)} -Upgrading Copilot Business users to Copilot Enterprise during the promotional period reduces the additional usage cost by $20 per upgrade.
@@ -350,12 +360,14 @@ export function UsersView({ users, seatOverrides = {}, onSeatOverridesChange, on -