diff --git a/static/app/components/seer/outOfCredits/seerOutOfCreditsCta.tsx b/static/app/components/seer/outOfCredits/seerOutOfCreditsCta.tsx
index 74763423b38c..1679d860b5db 100644
--- a/static/app/components/seer/outOfCredits/seerOutOfCreditsCta.tsx
+++ b/static/app/components/seer/outOfCredits/seerOutOfCreditsCta.tsx
@@ -1,4 +1,5 @@
import {OverrideOrDefault} from 'sentry/components/overrideOrDefault';
+import type {SeerCreditDenialReason} from 'sentry/utils/seer/useSeerCreditCheck';
/**
* What the organization can't afford, which picks the copy:
@@ -11,6 +12,11 @@ import {OverrideOrDefault} from 'sentry/components/overrideOrDefault';
export type SeerOutOfCreditsReason = 'agent-chat' | 'autofix-start' | 'autofix-continue';
export interface SeerOutOfCreditsCtaProps {
+ /**
+ * Why the surface's credit check denied credit, which also picks the copy:
+ * hitting the daily usage limit reads differently from running out.
+ */
+ denialReason: SeerCreditDenialReason | null;
reason: SeerOutOfCreditsReason;
/**
* The action button's size. Defaults to `md` for `agent-chat`, which sits
diff --git a/static/app/utils/seer/useSeerCreditCheck.tsx b/static/app/utils/seer/useSeerCreditCheck.tsx
index d9b5d41f81aa..033f0d943310 100644
--- a/static/app/utils/seer/useSeerCreditCheck.tsx
+++ b/static/app/utils/seer/useSeerCreditCheck.tsx
@@ -17,7 +17,20 @@ export type SeerCreditOperation =
| 'autofix_open_pr'
| 'autofix_pr_iteration';
+/**
+ * Why the check denied credit:
+ *
+ * - `not_enough_credits`: the wallet can't cover the operation.
+ * - `daily_cap_exceeded`: the operation would take today's spend over the
+ * org's daily usage limit.
+ */
+export type SeerCreditDenialReason = 'not_enough_credits' | 'daily_cap_exceeded';
+
interface SeerCreditCheckResponse {
+ /**
+ * Null when the check allows the operation, or doesn't say why it doesn't.
+ */
+ denialReason: SeerCreditDenialReason | null;
hasEnoughCredits: boolean;
}
@@ -93,6 +106,7 @@ export function useSeerCreditCheck(
return {
hasEnoughCredits: isChecking ? (query.data?.hasEnoughCredits ?? true) : true,
+ denialReason: isChecking ? (query.data?.denialReason ?? null) : null,
/**
* Whether the check has answered, or failed, since the caller mounted. A
* cached answer from an earlier mount doesn't count, since credits can
diff --git a/static/app/views/issueList/pages/inbox/issuePreview/issuePreview.tsx b/static/app/views/issueList/pages/inbox/issuePreview/issuePreview.tsx
index 69d2daea4a93..d658886a4497 100644
--- a/static/app/views/issueList/pages/inbox/issuePreview/issuePreview.tsx
+++ b/static/app/views/issueList/pages/inbox/issuePreview/issuePreview.tsx
@@ -317,6 +317,7 @@ function IssuePreviewContent() {
? 'autofix-start'
: 'autofix-continue'
}
+ denialReason={previewSeer.creditCheck.denialReason}
/>
)}
{summary && }
diff --git a/static/app/views/seerExplorer/components/seerExplorerContent.spec.tsx b/static/app/views/seerExplorer/components/seerExplorerContent.spec.tsx
index fee2aff7b96b..58a467019eb5 100644
--- a/static/app/views/seerExplorer/components/seerExplorerContent.spec.tsx
+++ b/static/app/views/seerExplorer/components/seerExplorerContent.spec.tsx
@@ -23,7 +23,10 @@ import type {SeerOutOfCreditsCtaProps} from 'sentry/components/seer/outOfCredits
import {registerOverride} from 'sentry/overrideRegistry';
import {ConfigStore} from 'sentry/stores/configStore';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import {seerCreditCheckApiOptions} from 'sentry/utils/seer/useSeerCreditCheck';
+import {
+ seerCreditCheckApiOptions,
+ type SeerCreditDenialReason,
+} from 'sentry/utils/seer/useSeerCreditCheck';
import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext';
import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext';
import * as useSeerExplorerModule from 'sentry/views/seerExplorer/hooks/useSeerExplorer';
@@ -2136,11 +2139,14 @@ describe('SeerExplorerContent', () => {
hideAiFeatures: false,
});
- function mockCreditCheck(hasEnoughCredits: boolean) {
+ function mockCreditCheck(
+ hasEnoughCredits: boolean,
+ denialReason: SeerCreditDenialReason | null = null
+ ) {
return MockApiClient.addMockResponse({
url: `/organizations/${creditsOrganization.slug}/seer/credit-check/`,
match: [MockApiClient.matchQuery({operation: 'agent_chat_message'})],
- body: {hasEnoughCredits},
+ body: {hasEnoughCredits, denialReason},
});
}
@@ -2188,6 +2194,26 @@ describe('SeerExplorerContent', () => {
expect(textarea).toHaveTextContent('Test message');
});
+ it('tells the CTA why the chat was denied credit', async () => {
+ // getsentry fills in the CTA; this stand-in shows what it was asked for.
+ registerOverride(
+ 'component:seer-out-of-credits-cta',
+ () =>
+ function StubOutOfCreditsCta({reason, denialReason}: SeerOutOfCreditsCtaProps) {
+ return (
+ {`${reason} ${denialReason}`}
+ );
+ }
+ );
+ mockCreditCheck(false, 'daily_cap_exceeded');
+
+ renderContent();
+
+ expect(await screen.findByTestId('seer-out-of-credits-cta')).toHaveTextContent(
+ 'agent-chat daily_cap_exceeded'
+ );
+ });
+
it('hides the suggested questions', async () => {
mockCreditCheck(false);
@@ -2479,6 +2505,29 @@ describe('SeerExplorerContent', () => {
).not.toBeInTheDocument();
});
+ it('tells the autofix CTA why the card was denied credit', async () => {
+ registerOverride(
+ 'component:seer-out-of-credits-cta',
+ () =>
+ function DenialReasonCta({reason, denialReason}: SeerOutOfCreditsCtaProps) {
+ return (
+ {`${reason} ${denialReason}`}
+ );
+ }
+ );
+ mockCreditCheck(true);
+ MockApiClient.addMockResponse({
+ url: `/organizations/${creditsOrganization.slug}/seer/credit-check/`,
+ match: [MockApiClient.matchQuery({operation: 'autofix_solution'})],
+ body: {hasEnoughCredits: false, denialReason: 'daily_cap_exceeded'},
+ });
+
+ renderContent();
+
+ const cta = await screen.findByTestId('seer-explorer-autofix-out-of-credits');
+ expect(cta).toHaveTextContent('autofix-continue daily_cap_exceeded');
+ });
+
it('shows only the chat CTA when the chat is out of credits too', async () => {
mockCreditCheck(false);
diff --git a/static/app/views/seerExplorer/components/seerExplorerContent.tsx b/static/app/views/seerExplorer/components/seerExplorerContent.tsx
index 729d2f576b83..c2f94d2b7d0a 100644
--- a/static/app/views/seerExplorer/components/seerExplorerContent.tsx
+++ b/static/app/views/seerExplorer/components/seerExplorerContent.tsx
@@ -364,7 +364,7 @@ export function SeerExplorerContent({
// waits on an answer to a message it already has.
const isComposerShown =
!readOnly && !showLoadError && !showFileApprovalBar && !showQuestionBar;
- const {hasEnoughCredits, hasCheckedSinceMount} = useSeerCreditCheck(
+ const {hasEnoughCredits, denialReason, hasCheckedSinceMount} = useSeerCreditCheck(
'agent_chat_message',
{enabled: isComposerShown}
);
@@ -399,18 +399,23 @@ export function SeerExplorerContent({
// own notice wins, so the card's waits for the chat's check to answer. While
// the approval or question bar stands in for the composer, there's nowhere to
// put it, so cards show their own.
- const {isAutofixOutOfCredits, reportOutOfCredits} = useAutofixOutOfCreditsEmbeds();
+ const {isAutofixOutOfCredits, autofixDenialReason, reportOutOfCredits} =
+ useAutofixOutOfCreditsEmbeds();
let outOfCreditsNotice: ReactNode = null;
if (isOutOfCredits) {
outOfCreditsNotice = (
-
+
);
} else if (isAutofixOutOfCredits && !isCheckingCredits) {
outOfCreditsNotice = (
-
+
);
}
diff --git a/static/gsApp/components/ai/seerOutOfCreditsCta.spec.tsx b/static/gsApp/components/ai/seerOutOfCreditsCta.spec.tsx
index be18fe45fb55..935e2a8fde47 100644
--- a/static/gsApp/components/ai/seerOutOfCreditsCta.spec.tsx
+++ b/static/gsApp/components/ai/seerOutOfCreditsCta.spec.tsx
@@ -50,7 +50,9 @@ describe('SeerOutOfCreditsCta', () => {
},
});
- render(, {organization});
+ render(, {
+ organization,
+ });
renderGlobalModal({organization});
expect(
@@ -66,35 +68,115 @@ describe('SeerOutOfCreditsCta', () => {
expect(screen.queryByRole('button', {name: 'Contact Admin'})).not.toBeInTheDocument();
});
- it('tells billing members when auto-reload has no top-ups left', async () => {
+ it('tells billing members with auto-reload on that the org hit its daily usage limit', async () => {
const organization = OrganizationFixture({access: ['org:read', 'org:billing']});
act(() =>
SubscriptionStore.set(organization.slug, SubscriptionFixture({organization}))
);
mockAutoTopUp(organization, true);
- render(, {organization});
+ render(, {
+ organization,
+ });
// Nothing shows until the setting loads, so the "off" copy never flashes.
expect(screen.queryByTestId('seer-out-of-credits')).not.toBeInTheDocument();
+ // Auto-reload tops the org up until the daily usage limit stops it.
+ expect(
+ await screen.findByText(
+ 'Your organization hit its daily usage limit, so you cannot continue messaging Seer.'
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', {name: 'Configure auto-reload'})
+ ).toBeInTheDocument();
+ });
+
+ it('tells billing members when the chat hit the daily usage limit', async () => {
+ const organization = OrganizationFixture({access: ['org:read', 'org:billing']});
+ act(() =>
+ SubscriptionStore.set(organization.slug, SubscriptionFixture({organization}))
+ );
+ // Auto-reload being off doesn't matter: the limit stopped the chat, and
+ // it's set alongside auto-reload.
+ mockAutoTopUp(organization, false);
+
+ render(
+ ,
+ {organization}
+ );
+
expect(
await screen.findByText(
- 'Your organization does not have enough credits left for you to message Seer, and you have no auto top-ups remaining. Adjust your spend and turn on auto top-up.'
+ 'Your organization hit its daily usage limit, so you cannot continue messaging Seer.'
)
).toBeInTheDocument();
+ // Tests run in UTC with the clock frozen at 2017-10-17T02:41:20Z, so the
+ // limit resets at the next midnight UTC.
+ expect(screen.getByTestId('seer-out-of-credits')).toHaveTextContent(
+ 'This will reset at 12:00 AM UTC.'
+ );
expect(
screen.getByRole('button', {name: 'Configure auto-reload'})
).toBeInTheDocument();
});
+ it('tells members to ask their admin about the daily usage limit', () => {
+ const organization = OrganizationFixture({access: ['org:read']});
+ act(() =>
+ SubscriptionStore.set(
+ organization.slug,
+ SubscriptionFixture({organization, canSelfServe: true})
+ )
+ );
+
+ render(
+ ,
+ {organization}
+ );
+
+ expect(
+ screen.getByText(
+ 'Your organization hit its daily usage limit, so you cannot continue messaging Seer. If you want to increase your limit, reach out to your admin.'
+ )
+ ).toBeInTheDocument();
+ // "Contact Admin" asks for credits, which won't lift the limit.
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
+ it('tells members when Autofix hit the daily usage limit', () => {
+ const organization = OrganizationFixture({access: ['org:read']});
+ act(() =>
+ SubscriptionStore.set(
+ organization.slug,
+ SubscriptionFixture({organization, canSelfServe: true})
+ )
+ );
+
+ render(
+ ,
+ {organization}
+ );
+
+ expect(
+ screen.getByText(
+ 'Your organization hit its daily usage limit, so you cannot start an Autofix run. If you want to increase your limit, reach out to your admin.'
+ )
+ ).toBeInTheDocument();
+ });
+
it.each([
[
'autofix-start',
- 'You do not have enough credits to start an Autofix run. Set up auto top up to keep your flow uninterrupted.',
+ 'You do not have enough credits to start an Autofix run. Set up auto-reload to keep your flow uninterrupted.',
],
[
'autofix-continue',
- 'Your org does not have enough credits to continue your Autofix run. Set up auto top up to keep your flow uninterrupted.',
+ 'Your org does not have enough credits to continue your Autofix run. Set up auto-reload to keep your flow uninterrupted.',
],
] as const)('uses the %s copy', async (reason, copy) => {
const organization = OrganizationFixture({access: ['org:read', 'org:billing']});
@@ -103,7 +185,7 @@ describe('SeerOutOfCreditsCta', () => {
);
mockAutoTopUp(organization, false);
- render(, {organization});
+ render(, {organization});
expect(await screen.findByText(copy)).toBeInTheDocument();
expect(
@@ -137,7 +219,9 @@ describe('SeerOutOfCreditsCta', () => {
method: 'POST',
});
- render(, {organization});
+ render(, {
+ organization,
+ });
expect(
await screen.findByText(
@@ -165,11 +249,11 @@ describe('SeerOutOfCreditsCta', () => {
it.each([
[
'autofix-start',
- 'Your organization does not have enough credits to start an Autofix run. Ask your admin to increase your daily token limit or set up auto top-up.',
+ 'Your organization does not have enough credits to start an Autofix run. Ask your admin to increase your daily usage limit or set up auto-reload.',
],
[
'autofix-continue',
- 'Your organization does not have enough credits to continue your Autofix run. Ask your admin to increase your daily token limit or set up auto top-up.',
+ 'Your organization does not have enough credits to continue your Autofix run. Ask your admin to increase your daily usage limit or set up auto-reload.',
],
] as const)('uses the member %s copy', async (reason, copy) => {
const organization = OrganizationFixture({access: ['org:read']});
@@ -184,7 +268,9 @@ describe('SeerOutOfCreditsCta', () => {
body: {},
});
- render(, {organization});
+ render(, {
+ organization,
+ });
expect(await screen.findByText(copy)).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Contact Admin'})).toBeEnabled();
@@ -211,7 +297,9 @@ describe('SeerOutOfCreditsCta', () => {
},
});
- render(, {organization});
+ render(, {
+ organization,
+ });
// "Contact Admin" can't be clicked before the earlier request has loaded.
expect(screen.queryByRole('button', {name: 'Contact Admin'})).not.toBeInTheDocument();
@@ -240,7 +328,9 @@ describe('SeerOutOfCreditsCta', () => {
},
});
- render(, {organization});
+ render(, {
+ organization,
+ });
expect(await screen.findByRole('button', {name: 'Contact Admin'})).toBeEnabled();
expect(prompts).toHaveBeenCalled();
@@ -258,7 +348,9 @@ describe('SeerOutOfCreditsCta', () => {
)
);
- render(, {organization});
+ render(, {
+ organization,
+ });
expect(screen.getByText(/Ask your admin to set up auto-reload/)).toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
diff --git a/static/gsApp/components/ai/seerOutOfCreditsCta.tsx b/static/gsApp/components/ai/seerOutOfCreditsCta.tsx
index ead78e08df83..f9801b165df0 100644
--- a/static/gsApp/components/ai/seerOutOfCreditsCta.tsx
+++ b/static/gsApp/components/ai/seerOutOfCreditsCta.tsx
@@ -4,6 +4,7 @@ import {useQuery} from '@tanstack/react-query';
import {usePrompts} from 'sentry/actionCreators/prompts';
import type {SeerOutOfCreditsCtaProps} from 'sentry/components/seer/outOfCredits/seerOutOfCreditsCta';
import type {Organization} from 'sentry/types/organization';
+import type {SeerCreditDenialReason} from 'sentry/utils/seer/useSeerCreditCheck';
import {useApi} from 'sentry/utils/useApi';
import {useOrganization} from 'sentry/utils/useOrganization';
@@ -20,11 +21,17 @@ import {seerWalletAutoTopUpOptions} from 'getsentry/views/subscriptionPage/seerW
function getEscalation(
organization: Organization,
- subscription: Subscription | null
+ subscription: Subscription | null,
+ denialReason: SeerCreditDenialReason | null | undefined
): SeerOutOfCreditsEscalation {
if (organization.access.includes('org:billing')) {
return 'configure';
}
+ // "Contact Admin" asks the admins for more credits, which doesn't lift the
+ // daily usage limit, so members are only told to ask them about the limit.
+ if (denialReason === 'daily_cap_exceeded') {
+ return 'contact';
+ }
// Members of orgs that buy through an account team have nobody to send a
// self-serve request to, same as the upgrade and trial CTAs.
if (subscription?.canSelfServe) {
@@ -35,12 +42,17 @@ function getEscalation(
const TOP_UP_REQUESTED_PROMPT = 'seer_top_up_requested';
-export function SeerOutOfCreditsCta({reason, size, variant}: SeerOutOfCreditsCtaProps) {
+export function SeerOutOfCreditsCta({
+ reason,
+ denialReason,
+ size,
+ variant,
+}: SeerOutOfCreditsCtaProps) {
const api = useApi({persistInFlight: true});
const organization = useOrganization();
const subscription = useSubscription();
const [isRequestingCredits, setIsRequestingCredits] = useState(false);
- const escalation = getEscalation(organization, subscription);
+ const escalation = getEscalation(organization, subscription, denialReason);
const {
isLoading: isPromptLoading,
@@ -55,9 +67,8 @@ export function SeerOutOfCreditsCta({reason, size, variant}: SeerOutOfCreditsCta
options: {enabled: escalation === 'request'},
});
- // The copy changes when auto-reload is already on: the org has used up the
- // top-ups it allows. Only billing members see that copy, and only they can
- // read the setting.
+ // With auto-reload on, running out means the org hit its daily usage limit,
+ // which gets its own copy. Only billing members can read the setting.
const {data: autoTopUp, isPending: isAutoTopUpPending} = useQuery({
...seerWalletAutoTopUpOptions(organization),
enabled: escalation === 'configure',
@@ -89,6 +100,7 @@ export function SeerOutOfCreditsCta({reason, size, variant}: SeerOutOfCreditsCta
return (
-
+
+
```
## Agent chat
@@ -46,25 +47,50 @@ auto-reload modal. The chat uses full-size buttons; Autofix uses small ones.
/>
```
-## Agent chat, auto-reload already on
+## Daily usage limit
-With auto-reload on, running out means the org used every top-up it allows, so
-billing members configure it instead.
+The org has credits but hit its daily usage limit: the credit check's
+`denialReason` is `daily_cap_exceeded`, or a billing member has auto-reload on,
+which tops the org up until the limit stops it. The notice says when the limit
+resets, at the next midnight UTC in the viewer's timezone. Billing members
+configure auto-reload, where the limit is set. Members can't raise the limit,
+and more credits won't lift it, so they get no button and are told to ask their
+admin. Autofix uses the same copy for its own steps.
- {}}
- />
+
+ {}}
+ />
+
+ {}}
+ />
+
```jsx
+
+
```
@@ -219,9 +245,9 @@ The auto-reload button and "Contact Admin" do the real thing: the modal reads an
saves your org's Seer wallet, and "Contact Admin" emails your billing members.
-
+
```jsx
-
+
```
diff --git a/static/gsApp/components/ai/seerOutOfCreditsNotice.tsx b/static/gsApp/components/ai/seerOutOfCreditsNotice.tsx
index 9cd93ade4524..1a48e5159035 100644
--- a/static/gsApp/components/ai/seerOutOfCreditsNotice.tsx
+++ b/static/gsApp/components/ai/seerOutOfCreditsNotice.tsx
@@ -6,11 +6,13 @@ import {Button, type ButtonProps} from '@sentry/scraps/button';
import {Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
+import {DateTime} from 'sentry/components/dateTime';
import type {
SeerOutOfCreditsCtaProps,
SeerOutOfCreditsReason,
} from 'sentry/components/seer/outOfCredits/seerOutOfCreditsCta';
-import {t} from 'sentry/locale';
+import {t, tct} from 'sentry/locale';
+import type {SeerCreditDenialReason} from 'sentry/utils/seer/useSeerCreditCheck';
/**
* What the viewer can do about running out of credits:
@@ -22,33 +24,17 @@ import {t} from 'sentry/locale';
*/
export type SeerOutOfCreditsEscalation = 'configure' | 'request' | 'contact';
-// Billing members, from the Billing Gates designs. Auto-reload already on means
-// the org ran through the top-ups it allows, so the copy says so.
-const BILLING_MESSAGE: Record> = {
- 'agent-chat': {
- off: t(
- 'Your organization does not have enough credits left for you to message Seer. Adjust your spend and turn on auto-reload.'
- ),
- on: t(
- 'Your organization does not have enough credits left for you to message Seer, and you have no auto top-ups remaining. Adjust your spend and turn on auto top-up.'
- ),
- },
- 'autofix-start': {
- off: t(
- 'You do not have enough credits to start an Autofix run. Set up auto top up to keep your flow uninterrupted.'
- ),
- on: t(
- 'You do not have enough credits to start an Autofix run, and you have no auto top-ups remaining. Adjust your spend and turn on auto top-up.'
- ),
- },
- 'autofix-continue': {
- off: t(
- 'Your org does not have enough credits to continue your Autofix run. Set up auto top up to keep your flow uninterrupted.'
- ),
- on: t(
- 'Your org does not have enough credits to continue your Autofix run, and you have no auto top-ups remaining. Adjust your spend and turn on auto top-up.'
- ),
- },
+// Billing members, from the Billing Gates designs.
+const BILLING_MESSAGE: Record = {
+ 'agent-chat': t(
+ 'Your organization does not have enough credits left for you to message Seer. Adjust your spend and turn on auto-reload.'
+ ),
+ 'autofix-start': t(
+ 'You do not have enough credits to start an Autofix run. Set up auto-reload to keep your flow uninterrupted.'
+ ),
+ 'autofix-continue': t(
+ 'Your org does not have enough credits to continue your Autofix run. Set up auto-reload to keep your flow uninterrupted.'
+ ),
};
// Members, from the designs' member flow. "Contact Admin" says who to ask, so
@@ -58,10 +44,10 @@ const MEMBER_MESSAGE: Record = {
'Your organization does not have enough credits left for you to message Seer.'
),
'autofix-start': t(
- 'Your organization does not have enough credits to start an Autofix run. Ask your admin to increase your daily token limit or set up auto top-up.'
+ 'Your organization does not have enough credits to start an Autofix run. Ask your admin to increase your daily usage limit or set up auto-reload.'
),
'autofix-continue': t(
- 'Your organization does not have enough credits to continue your Autofix run. Ask your admin to increase your daily token limit or set up auto top-up.'
+ 'Your organization does not have enough credits to continue your Autofix run. Ask your admin to increase your daily usage limit or set up auto-reload.'
),
};
@@ -74,27 +60,81 @@ const CONTACT_MESSAGE: Record = {
),
};
+// From the Agent Chat designs, for an org that hit its daily usage limit, and
+// the same for Autofix. Members can't raise the limit, so their copy says who
+// can.
+const DAILY_LIMIT_MESSAGE: Record<
+ SeerOutOfCreditsReason,
+ Record<'configure' | 'member', string>
+> = {
+ 'agent-chat': {
+ configure: t(
+ 'Your organization hit its daily usage limit, so you cannot continue messaging Seer.'
+ ),
+ member: t(
+ 'Your organization hit its daily usage limit, so you cannot continue messaging Seer. If you want to increase your limit, reach out to your admin.'
+ ),
+ },
+ 'autofix-start': {
+ configure: t(
+ 'Your organization hit its daily usage limit, so you cannot start an Autofix run.'
+ ),
+ member: t(
+ 'Your organization hit its daily usage limit, so you cannot start an Autofix run. If you want to increase your limit, reach out to your admin.'
+ ),
+ },
+ 'autofix-continue': {
+ configure: t(
+ 'Your organization hit its daily usage limit, so you cannot continue your Autofix run.'
+ ),
+ member: t(
+ 'Your organization hit its daily usage limit, so you cannot continue your Autofix run. If you want to increase your limit, reach out to your admin.'
+ ),
+ },
+};
+
+/**
+ * When the daily usage limit resets. Billing counts a day's spend by its UTC
+ * date, so it resets at the next midnight UTC.
+ */
+function getDailyLimitResetsAt(): Date {
+ const now = new Date();
+ return new Date(
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)
+ );
+}
+
function getSeerOutOfCreditsMessage({
reason,
escalation,
- autoReloadEnabled,
+ isDailyLimitReached,
}: {
escalation: SeerOutOfCreditsEscalation;
+ isDailyLimitReached: boolean;
reason: SeerOutOfCreditsReason;
- autoReloadEnabled?: boolean;
}): string {
+ if (isDailyLimitReached) {
+ return DAILY_LIMIT_MESSAGE[reason][
+ escalation === 'configure' ? 'configure' : 'member'
+ ];
+ }
if (escalation === 'configure') {
- return BILLING_MESSAGE[reason][autoReloadEnabled ? 'on' : 'off'];
+ return BILLING_MESSAGE[reason];
}
return escalation === 'request' ? MEMBER_MESSAGE[reason] : CONTACT_MESSAGE[reason];
}
-export interface SeerOutOfCreditsNoticeProps extends SeerOutOfCreditsCtaProps {
+export interface SeerOutOfCreditsNoticeProps extends Omit<
+ SeerOutOfCreditsCtaProps,
+ 'denialReason'
+> {
escalation: SeerOutOfCreditsEscalation;
/**
- * Auto-reload is on, so the org has used up the top-ups it allows.
+ * Auto-reload is on. It tops the org up as it runs low, so running out anyway
+ * means the org hit its daily usage limit. Only billing members can read it.
*/
autoReloadEnabled?: boolean;
+ denialReason?: SeerCreditDenialReason | null;
/**
* The viewer already emailed their admins, so "Contact Admin" shows
* "Admins emailed" instead.
@@ -118,6 +158,7 @@ export interface SeerOutOfCreditsNoticeProps extends SeerOutOfCreditsCtaProps {
*/
export function SeerOutOfCreditsNotice({
reason,
+ denialReason,
size,
variant = 'warning',
escalation,
@@ -127,17 +168,29 @@ export function SeerOutOfCreditsNotice({
const NoticeAlert = variant === 'muted' ? MutedAlert : Alert;
// The chat has room for a full-size button; Autofix's spots are tighter.
const buttonSize = size ?? (reason === 'agent-chat' ? 'md' : 'sm');
+ const isDailyLimitReached =
+ denialReason === 'daily_cap_exceeded' || !!autoReloadEnabled;
return (
- {getSeerOutOfCreditsMessage({reason, escalation, autoReloadEnabled})}
+
+ {getSeerOutOfCreditsMessage({reason, escalation, isDailyLimitReached})}
+
+ {isDailyLimitReached && (
+
+ {tct('This will reset at [time].', {
+ time: ,
+ })}
+
+ )}