From 6834a5dff9731dce2f0fd6d3debb9169597a20b4 Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Tue, 4 Aug 2026 17:33:48 -0400 Subject: [PATCH 01/13] Explore AI credit usage and purchase interfaces --- .../components/app-message-cards/index.tsx | 75 +++--- .../purchase-credits-dialog/events.ts | 5 + .../purchase-credits-dialog/index.tsx | 94 +++++++ .../purchase-credits-dialog/style.module.css | 51 ++++ .../components/settings-view/style.module.css | 113 ++++++++- .../settings-view/usage-panel.test.tsx | 104 ++++---- .../components/settings-view/usage-panel.tsx | 236 +++++++++++++----- .../data/queries/use-app-messages.test.tsx | 69 +++++ apps/ui/src/data/queries/use-app-messages.ts | 32 ++- apps/ui/src/data/usage-exploration.ts | 100 ++++++++ .../session-view/composer/index.tsx | 4 + .../session-view/composer/style.module.css | 4 +- .../components/session-view/index.test.tsx | 83 +++++- .../components/session-view/index.tsx | 50 ++-- .../session-chat-actions.test.tsx | 20 ++ .../session-view/session-chat-actions.tsx | 59 +++-- .../components/session-view/style.module.css | 77 ++++++ .../session-view/usage-limit-lock.tsx | 38 +++ .../session-view/usage-warning-strip.tsx | 39 +++ .../usage-interfaces/01-monthly-36.png | Bin 0 -> 33611 bytes .../usage-interfaces/02-monthly-80.png | Bin 0 -> 33686 bytes .../usage-interfaces/03-monthly-90.png | Bin 0 -> 33836 bytes .../usage-interfaces/04-monthly-100.png | Bin 0 -> 35078 bytes .../usage-interfaces/05-extra-in-reserve.png | Bin 0 -> 34099 bytes .../usage-interfaces/06-extra-36.png | Bin 0 -> 34878 bytes .../usage-interfaces/07-extra-80.png | Bin 0 -> 34996 bytes .../usage-interfaces/08-extra-90.png | Bin 0 -> 35210 bytes .../usage-interfaces/09-extra-100.png | Bin 0 -> 35725 bytes .../usage-interfaces/10-chat-monthly-80.png | Bin 0 -> 53935 bytes .../usage-interfaces/11-chat-monthly-90.png | Bin 0 -> 53466 bytes .../usage-interfaces/12-chat-extra-80.png | Bin 0 -> 54570 bytes .../usage-interfaces/13-chat-extra-90.png | Bin 0 -> 54751 bytes .../14-chat-blocked-monthly.png | Bin 0 -> 54601 bytes .../15-chat-blocked-extra.png | Bin 0 -> 54373 bytes .../16-add-credits-checkout.png | Bin 0 -> 42341 bytes .../17-credits-added-success.png | Bin 0 -> 52010 bytes 36 files changed, 1058 insertions(+), 195 deletions(-) create mode 100644 apps/ui/src/components/purchase-credits-dialog/events.ts create mode 100644 apps/ui/src/components/purchase-credits-dialog/index.tsx create mode 100644 apps/ui/src/components/purchase-credits-dialog/style.module.css create mode 100644 apps/ui/src/data/queries/use-app-messages.test.tsx create mode 100644 apps/ui/src/data/usage-exploration.ts create mode 100644 apps/ui/src/ui-classic/components/session-view/usage-limit-lock.tsx create mode 100644 apps/ui/src/ui-classic/components/session-view/usage-warning-strip.tsx create mode 100644 docs/design-explorations/usage-interfaces/01-monthly-36.png create mode 100644 docs/design-explorations/usage-interfaces/02-monthly-80.png create mode 100644 docs/design-explorations/usage-interfaces/03-monthly-90.png create mode 100644 docs/design-explorations/usage-interfaces/04-monthly-100.png create mode 100644 docs/design-explorations/usage-interfaces/05-extra-in-reserve.png create mode 100644 docs/design-explorations/usage-interfaces/06-extra-36.png create mode 100644 docs/design-explorations/usage-interfaces/07-extra-80.png create mode 100644 docs/design-explorations/usage-interfaces/08-extra-90.png create mode 100644 docs/design-explorations/usage-interfaces/09-extra-100.png create mode 100644 docs/design-explorations/usage-interfaces/10-chat-monthly-80.png create mode 100644 docs/design-explorations/usage-interfaces/11-chat-monthly-90.png create mode 100644 docs/design-explorations/usage-interfaces/12-chat-extra-80.png create mode 100644 docs/design-explorations/usage-interfaces/13-chat-extra-90.png create mode 100644 docs/design-explorations/usage-interfaces/14-chat-blocked-monthly.png create mode 100644 docs/design-explorations/usage-interfaces/15-chat-blocked-extra.png create mode 100644 docs/design-explorations/usage-interfaces/16-add-credits-checkout.png create mode 100644 docs/design-explorations/usage-interfaces/17-credits-added-success.png diff --git a/apps/ui/src/components/app-message-cards/index.tsx b/apps/ui/src/components/app-message-cards/index.tsx index 48ad895b8d..21775d34ff 100644 --- a/apps/ui/src/components/app-message-cards/index.tsx +++ b/apps/ui/src/components/app-message-cards/index.tsx @@ -1,48 +1,59 @@ import { __ } from '@wordpress/i18n'; import { Button, Notice } from '@wordpress/ui'; import { clsx } from 'clsx'; +import { useEffect, useState } from 'react'; import toastStyles from '@/components/app-toasts/style.module.css'; +import { PurchaseCreditsDialog } from '@/components/purchase-credits-dialog'; +import { OPEN_PURCHASE_CREDITS_EVENT } from '@/components/purchase-credits-dialog/events'; import { useActivePersistentMessages } from '@/data/queries/use-app-messages'; import styles from './style.module.css'; export function AppMessageCards( { className }: { className?: string } ) { const { messages, dismiss } = useActivePersistentMessages(); + const [ purchaseOpen, setPurchaseOpen ] = useState( false ); - if ( ! messages.length ) { - return null; - } + useEffect( () => { + const openPurchase = () => setPurchaseOpen( true ); + window.addEventListener( OPEN_PURCHASE_CREDITS_EVENT, openPurchase ); + return () => window.removeEventListener( OPEN_PURCHASE_CREDITS_EVENT, openPurchase ); + }, [] ); return ( -
- { messages.map( ( message ) => ( -
- - { message.title } - { message.description ? ( - { message.description } - ) : null } - { message.cta ? ( - - - - ) : null } - dismiss( message ) } /> - + <> + { messages.length ? ( +
+ { messages.map( ( message ) => ( +
+ + { message.title } + { message.description ? ( + { message.description } + ) : null } + { message.cta ? ( + + + + ) : null } + dismiss( message ) } /> + +
+ ) ) }
- ) ) } -
+ ) : null } + + ); } diff --git a/apps/ui/src/components/purchase-credits-dialog/events.ts b/apps/ui/src/components/purchase-credits-dialog/events.ts new file mode 100644 index 0000000000..8c3fe5f131 --- /dev/null +++ b/apps/ui/src/components/purchase-credits-dialog/events.ts @@ -0,0 +1,5 @@ +export const OPEN_PURCHASE_CREDITS_EVENT = 'studio:open-purchase-credits'; + +export function openPurchaseCreditsDialog() { + window.dispatchEvent( new Event( OPEN_PURCHASE_CREDITS_EVENT ) ); +} diff --git a/apps/ui/src/components/purchase-credits-dialog/index.tsx b/apps/ui/src/components/purchase-credits-dialog/index.tsx new file mode 100644 index 0000000000..b974124822 --- /dev/null +++ b/apps/ui/src/components/purchase-credits-dialog/index.tsx @@ -0,0 +1,94 @@ +import { __, sprintf } from '@wordpress/i18n'; +import { privateApis } from '@wordpress/theme'; +import { Button, Dialog, Tooltip } from '@wordpress/ui'; +import { useState } from 'react'; +import { toast } from '@/data/app-messages'; +import { addExplorationCredits } from '@/data/usage-exploration'; +import { useColorScheme } from '@/hooks/use-color-scheme'; +import { unlock } from '@/lock-unlock'; +import styles from './style.module.css'; + +const CREDIT_AMOUNTS = [ 10, 20, 50, 100 ]; +const { ThemeProvider } = unlock( privateApis ); + +export function PurchaseCreditsDialog( { + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: ( open: boolean ) => void; +} ) { + const [ amount, setAmount ] = useState( 20 ); + const colorScheme = useColorScheme(); + const dialogBackground = colorScheme === 'dark' ? '#1e1e1e' : '#ffffff'; + + const continueToCheckout = () => { + addExplorationCredits( amount ); + onOpenChange( false ); + toast.success( __( 'Credits added' ), { + description: sprintf( + /* translators: %s: dollar amount of AI credits purchased. */ + __( '$%s in AI credits is now available.' ), + String( amount ) + ), + } ); + }; + + return ( + + + + + { __( 'Add AI credits' ) } + + + + { __( + 'Choose a one-time credit amount to check out securely on WordPress.com. Credits do not expire and are used after your monthly allowance.' + ) } + +
+ { CREDIT_AMOUNTS.map( ( option ) => ( + + ) ) } +
+
+ + + { __( 'Cancel' ) } + + + } + > + { sprintf( + /* translators: %s: dollar amount of AI credits selected. */ + __( 'Continue with $%s' ), + String( amount ) + ) } + + }> + { __( 'Checkout on WordPress.com' ) } + + + +
+
+
+ ); +} diff --git a/apps/ui/src/components/purchase-credits-dialog/style.module.css b/apps/ui/src/components/purchase-credits-dialog/style.module.css new file mode 100644 index 0000000000..95794e3e3b --- /dev/null +++ b/apps/ui/src/components/purchase-credits-dialog/style.module.css @@ -0,0 +1,51 @@ +.amounts { + display: grid; + grid-template-columns: repeat( 2, minmax( 0, 1fr ) ); + gap: var( --wpds-dimension-gap-sm ); + margin-block-start: var( --wpds-dimension-padding-lg ); +} + +.amount { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var( --wpds-dimension-padding-sm ); + padding: var( --wpds-dimension-padding-md ); + border: 1px solid var( --wpds-color-stroke-surface-neutral ); + border-radius: 8px; + background: var( --wpds-color-bg-surface-neutral-weak ); + color: var( --wpds-color-fg-content-neutral ); + cursor: pointer; + text-align: start; +} + +.amount:hover { + border-color: var( --wpds-color-stroke-interactive-neutral ); + background: var( --wpds-color-bg-surface-neutral ); +} + +.amount[data-selected] { + border-color: transparent; + outline: 2px solid var( --wpds-color-stroke-interactive-brand ); + outline-offset: -1px; + background: color-mix( + in srgb, + var( --wpds-color-bg-interactive-brand-weak ) 35%, + var( --wpds-color-bg-surface-neutral-strong ) + ); +} + +.amount:focus-visible { + outline: 2px solid var( --wpds-color-stroke-focus-brand ); + outline-offset: -1px; +} + +.amountValue { + font-size: var( --wpds-typography-font-size-lg ); + font-weight: 600; +} + +.amountLabel { + color: var( --wpds-color-fg-content-neutral-weak ); + font-size: var( --wpds-typography-font-size-xs ); +} diff --git a/apps/ui/src/components/settings-view/style.module.css b/apps/ui/src/components/settings-view/style.module.css index 7344356b45..f2c3821084 100644 --- a/apps/ui/src/components/settings-view/style.module.css +++ b/apps/ui/src/components/settings-view/style.module.css @@ -868,7 +868,118 @@ .progressValue { block-size: 100%; border-radius: inherit; - background: var( --wpds-color-fg-interactive-brand ); + background: var( --wpds-color-fg-content-neutral ); +} + +.progressValueWarning { + background: var( --wpds-color-fg-content-caution-weak ); + filter: saturate( 2 ) brightness( 1.8 ); +} + +.progressValueCritical { + background: var( --wpds-color-fg-content-warning-weak ); + filter: saturate( 1.6 ) brightness( 1.35 ); +} + +.progressValueExhausted { + background: var( --wpds-color-bg-interactive-error-strong ); +} + +.creditMeterHeader { + color: var( --wpds-color-fg-content-neutral-weak ); + font-size: var( --wpds-typography-font-size-xs ); + line-height: var( --wpds-typography-line-height-sm ); +} + +.creditMeterHeader { + display: flex; + justify-content: space-between; + gap: var( --wpds-dimension-padding-md ); +} + +.creditMeterHeader span:first-child { + color: var( --wpds-color-fg-content-neutral ); + font-weight: 500; +} + +.creditTopUpText { + min-width: 0; +} + +.creditTopUpText strong { + color: var( --wpds-color-fg-content-neutral ); + font-size: var( --wpds-typography-font-size-xs ); + font-weight: 500; + line-height: var( --wpds-typography-line-height-sm ); +} + +.creditTopUpText p { + margin-block-start: 2px; + font-size: var( --wpds-typography-font-size-xs ); +} + +.creditTopUpButton { + align-self: flex-start; +} + +.explorationControls { + gap: var( --wpds-dimension-padding-lg ); +} + +.explorationControls strong { + color: var( --wpds-color-fg-content-neutral ); + font-size: var( --wpds-typography-font-size-sm ); +} + +.explorationControls p { + margin-block-start: var( --wpds-dimension-padding-xs ); +} + +.explorationButtons { + display: flex; + flex-wrap: wrap; + gap: var( --wpds-dimension-gap-xs ); +} + +.explorationScenarioRows { + display: flex; + flex-direction: column; + gap: var( --wpds-dimension-padding-sm ); +} + +.explorationScenarioRow { + display: grid; + grid-template-columns: 112px 1fr; + align-items: center; + gap: var( --wpds-dimension-padding-md ); + color: var( --wpds-color-fg-content-neutral-weak ); + font-size: var( --wpds-typography-font-size-xs ); +} + +.explorationButton { + padding: 5px 10px; + border: 1px solid var( --wpds-color-stroke-surface-neutral ); + border-radius: 999px; + background: var( --wpds-color-bg-surface-neutral-strong ); + color: var( --wpds-color-fg-content-neutral-weak ); + cursor: pointer; + font-size: var( --wpds-typography-font-size-xs ); +} + +.explorationButton:hover { + border-color: var( --wpds-color-stroke-interactive-neutral ); + color: var( --wpds-color-fg-content-neutral ); +} + +.explorationButton[data-selected] { + border-color: var( --wpds-color-stroke-interactive-brand ); + background: var( --wpds-color-bg-interactive-brand-weak ); + color: var( --wpds-color-fg-interactive-brand ); +} + +.explorationButton:focus-visible { + outline: var( --wpds-border-width-focus ) solid var( --wpds-color-stroke-focus-brand ); + outline-offset: 2px; } .previewActionsButton { diff --git a/apps/ui/src/components/settings-view/usage-panel.test.tsx b/apps/ui/src/components/settings-view/usage-panel.test.tsx index 217b64987a..82a730c3f1 100644 --- a/apps/ui/src/components/settings-view/usage-panel.test.tsx +++ b/apps/ui/src/components/settings-view/usage-panel.test.tsx @@ -2,7 +2,6 @@ import '@testing-library/jest-dom/vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useConnector } from '@/data/core'; -import { useStudioAssistantQuota } from '@/data/queries/use-assistant-quota'; import { useAuthUser, useLogin } from '@/data/queries/use-auth-user'; import { useDeleteAllSnapshots, @@ -10,6 +9,7 @@ import { useSnapshots, } from '@/data/queries/use-snapshots'; import { useUserLocale } from '@/data/queries/use-user-locale'; +import { setUsageExplorationScenario } from '@/data/usage-exploration'; import { useOffline } from '@/hooks/use-offline'; import { UsagePanel } from './usage-panel'; import type { ButtonHTMLAttributes, ReactNode } from 'react'; @@ -79,14 +79,14 @@ vi.mock( '@/hooks/use-offline', () => ( { useOffline: vi.fn(), } ) ); -vi.mock( '@/data/queries/use-assistant-quota', () => ( { - useStudioAssistantQuota: vi.fn(), -} ) ); - vi.mock( '@/data/queries/use-user-locale', () => ( { useUserLocale: vi.fn(), } ) ); +vi.mock( '@/components/purchase-credits-dialog', () => ( { + PurchaseCreditsDialog: () => null, +} ) ); + // Reached through `useAgenticFeatures`, which reads the agentic-features // preference; this panel has no QueryClientProvider. vi.mock( '@/data/queries/use-user-preferences', () => ( { @@ -101,7 +101,6 @@ const useDeleteAllSnapshotsMock = vi.mocked( useDeleteAllSnapshots ); const useSnapshotUsageMock = vi.mocked( useSnapshotUsage ); const useSnapshotsMock = vi.mocked( useSnapshots ); const useOfflineMock = vi.mocked( useOffline ); -const useStudioAssistantQuotaMock = vi.mocked( useStudioAssistantQuota ); const useUserLocaleMock = vi.mocked( useUserLocale ); describe( 'UsagePanel', () => { @@ -111,6 +110,7 @@ describe( 'UsagePanel', () => { beforeEach( () => { vi.clearAllMocks(); + setUsageExplorationScenario( 'warning' ); confirmDeleteAllPreviewSites.mockResolvedValue( true ); // `agenticRequiresAuth` lets the real useAgenticFeatures derive the @@ -120,10 +120,6 @@ describe( 'UsagePanel', () => { agenticRequiresAuth: true, } as never ); useOfflineMock.mockReturnValue( false ); - useStudioAssistantQuotaMock.mockReturnValue( { - data: undefined, - isLoading: false, - } as never ); useUserLocaleMock.mockReturnValue( 'en' ); useAuthUserMock.mockReturnValue( { data: { id: 1, displayName: 'Ada Lovelace', email: 'ada@example.com' }, @@ -146,67 +142,58 @@ describe( 'UsagePanel', () => { render( ); expect( screen.getByRole( 'heading', { name: 'Usage' } ) ).toBeInTheDocument(); - expect( - screen.getByText( - 'AI credits are currently free while Studio Code is in Alpha. Build, iterate, and experiment, but know that credits will eventually have a cost.' - ) - ).toBeInTheDocument(); + expect( screen.getByText( '40 / 50' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Extra AI credits' ) ).toBeInTheDocument(); + expect( screen.getByRole( 'button', { name: 'Add credits' } ) ).toBeInTheDocument(); expect( screen.getByText( '2 of 10 active preview sites' ) ).toBeInTheDocument(); expect( useSnapshotsMock ).toHaveBeenCalledWith( 1 ); expect( useSnapshotUsageMock ).toHaveBeenCalledWith( 1 ); expect( useDeleteAllSnapshotsMock ).toHaveBeenCalledWith( 1 ); } ); - it( 'renders AI usage when a quota with a cost cap is available', () => { - useStudioAssistantQuotaMock.mockReturnValue( { - data: { costUsage: 25, costCap: 100, costResetDate: '2026-08-01T12:00:00' }, - isLoading: false, - } as never ); - + it( 'switches between exploration states', () => { render( ); + fireEvent.click( screen.getByRole( 'button', { name: 'Monthly allowance 100%' } ) ); + + expect( screen.getByText( '50 / 50' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Monthly allowance used' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Keep chatting with extra credits' ) ).toBeInTheDocument(); expect( - screen.getByText( '25% of monthly limit used (resets on August 1, 2026)' ) + screen.getByText( 'Add credits to continue now. Extra credits do not expire.' ) ).toBeInTheDocument(); - expect( - screen.queryByText( - 'AI credits are currently free while Studio Code is in Alpha. Build, iterate, and experiment, but know that credits will eventually have a cost.' - ) - ).not.toBeInTheDocument(); + expect( screen.getByRole( 'button', { name: 'Add credits' } ) ).toBeInTheDocument(); } ); - it( 'shows an unavailable message when the quota fetch fails', () => { - useStudioAssistantQuotaMock.mockReturnValue( { - data: undefined, - isLoading: false, - isError: true, - } as never ); + it( 'shows purchased credits as a second usage graph', () => { + setUsageExplorationScenario( 'extra-healthy' ); render( ); - expect( - screen.getByText( 'Studio Code limits are temporarily unavailable.' ) - ).toBeInTheDocument(); - expect( - screen.queryByText( - 'AI credits are currently free while Studio Code is in Alpha. Build, iterate, and experiment, but know that credits will eventually have a cost.' - ) - ).not.toBeInTheDocument(); + expect( screen.getByText( '18 / 50 used' ) ).toBeInTheDocument(); + expect( screen.getByRole( 'button', { name: 'Add credits' } ) ).toBeInTheDocument(); + expect( screen.getAllByTestId( 'usage-progress-bar' ) ).toHaveLength( 3 ); } ); - it( 'falls back to the Alpha copy when the quota has no cost cap', () => { - useStudioAssistantQuotaMock.mockReturnValue( { - data: { costUsage: 0, costCap: 0 }, - isLoading: false, - } as never ); + it( 'offers prototype states for extra-credit usage', () => { + render( ); + + fireEvent.click( screen.getByRole( 'button', { name: 'Extra credits 100%' } ) ); + expect( screen.getByText( 'Extra AI credits used' ) ).toBeInTheDocument(); + expect( screen.getByText( '50 / 50 used' ) ).toBeInTheDocument(); + expect( screen.getAllByTestId( 'usage-progress-bar' ) ).toHaveLength( 3 ); + } ); + + it( 'shows extra credits held in reserve before the monthly allowance is used', () => { render( ); - expect( - screen.getByText( - 'AI credits are currently free while Studio Code is in Alpha. Build, iterate, and experiment, but know that credits will eventually have a cost.' - ) - ).toBeInTheDocument(); + fireEvent.click( screen.getByRole( 'button', { name: 'Extra credits In reserve' } ) ); + + expect( screen.getByText( '18 / 50' ) ).toBeInTheDocument(); + expect( screen.getByText( '0 / 50 used' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Extra AI credits' ) ).toBeInTheDocument(); + expect( screen.getAllByTestId( 'usage-progress-bar' ) ).toHaveLength( 3 ); } ); it( 'confirms through the connector before deleting all preview sites', async () => { @@ -229,8 +216,7 @@ describe( 'UsagePanel', () => { expect( deleteSnapshotsMutate ).not.toHaveBeenCalled(); } ); - it( 'shows a loading row with an empty progress bar in both sections', () => { - useStudioAssistantQuotaMock.mockReturnValue( { data: undefined, isLoading: true } as never ); + it( 'keeps AI usage visible while preview usage loads', () => { // Preview usage is still cached from before the delete, so the bar would // otherwise keep its old fill next to a "Loading…" row. useDeleteAllSnapshotsMock.mockReturnValue( { @@ -241,21 +227,15 @@ describe( 'UsagePanel', () => { render( ); - expect( screen.getAllByText( 'Loading…' ) ).toHaveLength( 2 ); + expect( screen.getAllByText( 'Loading…' ) ).toHaveLength( 1 ); const bars = screen.getAllByTestId( 'usage-progress-bar' ); expect( bars ).toHaveLength( 2 ); - for ( const bar of bars ) { - expect( bar.firstElementChild ).toHaveStyle( { inlineSize: '0%' } ); - } + expect( bars[ 0 ].firstElementChild ).toHaveStyle( { inlineSize: '80%' } ); + expect( bars[ 1 ].firstElementChild ).toHaveStyle( { inlineSize: '0%' } ); } ); it( 'replaces figures and actions with the offline notice while offline', () => { useOfflineMock.mockReturnValue( true ); - useStudioAssistantQuotaMock.mockReturnValue( { - data: { costUsage: 25, costCap: 100, costResetDate: '2026-08-01T12:00:00' }, - isLoading: false, - } as never ); - render( ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( "You're offline" ); diff --git a/apps/ui/src/components/settings-view/usage-panel.tsx b/apps/ui/src/components/settings-view/usage-panel.tsx index bf0691f45d..70db05d91b 100644 --- a/apps/ui/src/components/settings-view/usage-panel.tsx +++ b/apps/ui/src/components/settings-view/usage-panel.tsx @@ -1,19 +1,15 @@ -import { - clampQuotaFraction, - formatQuotaPercentage, - formatQuotaResetDate, - formatAiBlockedNotice, -} from '@studio/common/lib/studio-assistant-quota'; +import { clampQuotaFraction } from '@studio/common/lib/studio-assistant-quota'; import { __, _n, sprintf } from '@wordpress/i18n'; import { moreHorizontal } from '@wordpress/icons'; -import { IconButton } from '@wordpress/ui'; +import { Button, IconButton } from '@wordpress/ui'; import { clsx } from 'clsx'; +import { useState } from 'react'; import { SigninNotice } from '@/components/agentic-signin-banner'; import * as Menu from '@/components/menu'; import { OfflineNotice } from '@/components/offline-banner'; +import { PurchaseCreditsDialog } from '@/components/purchase-credits-dialog'; import { useConnector } from '@/data/core'; import { useAgenticFeatures } from '@/data/queries/use-agentic-features'; -import { useStudioAssistantQuota } from '@/data/queries/use-assistant-quota'; import { useAuthUser } from '@/data/queries/use-auth-user'; import { useDeleteAllSnapshots, @@ -21,6 +17,11 @@ import { useSnapshots, } from '@/data/queries/use-snapshots'; import { useUserLocale } from '@/data/queries/use-user-locale'; +import { + setUsageExplorationScenario, + useUsageExploration, + type UsageExplorationScenario, +} from '@/data/usage-exploration'; import styles from './style.module.css'; const DEFAULT_PREVIEW_SITE_LIMIT = 10; @@ -38,70 +39,192 @@ function UnavailableSection( { title }: { title: string } ) { ); } -function UsageProgressBar( { fraction }: { fraction: number } ) { +function UsageProgressBar( { + fraction, + valueClassName, +}: { + fraction: number; + valueClassName?: string; +} ) { return (