feat: show account AI usage
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Deferred inputs
|
||||
|
||||
Updated: 2026-07-31
|
||||
|
||||
## Stripe billing — Phase 7.5
|
||||
|
||||
Needed from the operator before implementation can finish:
|
||||
|
||||
- Add `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` to the production environment.
|
||||
- Create the recurring Premium price and add `STRIPE_PRICE_PREMIUM`.
|
||||
- Create the billing webhook and add `STRIPE_WEBHOOK_SECRET`.
|
||||
- Do not store or paste secret values in the repository or chat.
|
||||
|
||||
## Production key rotation — Phase 1.4
|
||||
|
||||
Confirm the DataProtection keys exposed in git history have been rotated on the production host.
|
||||
|
||||
## Progress
|
||||
|
||||
Phases 2 through 7.9 are complete except Stripe billing. Public CV sharing, premium themes, AI call and token limits, storage limits, NAV discovery, provider market dual-write, CORS hardening, the prospect workflow, and migration-history reconciliation are pushed to `main`.
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import AiUsageCard from "./components/AiUsageCard";
|
||||
import { api } from "./api";
|
||||
|
||||
jest.mock("./api", () => ({ api: { get: jest.fn() } }));
|
||||
|
||||
test("shows monthly AI call and token limits", async () => {
|
||||
(api.get as jest.Mock).mockResolvedValue({
|
||||
data: {
|
||||
currentMonth: { calls: 4, estimatedTokens: 12000 },
|
||||
plan: "free",
|
||||
monthlyCallLimit: 25,
|
||||
monthlyTokenLimit: 100000,
|
||||
},
|
||||
});
|
||||
|
||||
render(<AiUsageCard />);
|
||||
|
||||
expect(await screen.findByText("4 of 25 generations this month")).toBeInTheDocument();
|
||||
expect(screen.getByText("12,000 of 100,000 estimated tokens")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Monthly AI generations used")).toHaveAttribute("aria-valuenow", "16");
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, Box, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { api } from "../api";
|
||||
|
||||
type Usage = {
|
||||
currentMonth: { calls: number; estimatedTokens: number };
|
||||
plan: string;
|
||||
monthlyCallLimit: number;
|
||||
monthlyTokenLimit: number;
|
||||
};
|
||||
|
||||
export default function AiUsageCard() {
|
||||
const [usage, setUsage] = useState<Usage | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
api.get<Usage>("/ai/usage")
|
||||
.then((response) => { if (active) setUsage(response.data); })
|
||||
.catch(() => { if (active) setFailed(true); });
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
if (failed) return <Alert severity="warning">AI usage is temporarily unavailable.</Alert>;
|
||||
if (!usage) return <Skeleton variant="rounded" height={150} />;
|
||||
|
||||
const callsPercent = Math.min(100, usage.currentMonth.calls / usage.monthlyCallLimit * 100);
|
||||
const tokensPercent = Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2.5, borderRadius: 4, border: "none" }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary", fontWeight: 800 }}>AI usage</Typography>
|
||||
<Typography variant="caption" sx={{ textTransform: "capitalize", fontWeight: 700 }}>{usage.plan} plan</Typography>
|
||||
</Stack>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">{usage.currentMonth.calls.toLocaleString()} of {usage.monthlyCallLimit.toLocaleString()} generations this month</Typography>
|
||||
<LinearProgress variant="determinate" value={callsPercent} aria-label="Monthly AI generations used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2">{usage.currentMonth.estimatedTokens.toLocaleString()} of {usage.monthlyTokenLimit.toLocaleString()} estimated tokens</Typography>
|
||||
<LinearProgress variant="determinate" value={tokensPercent} aria-label="Monthly AI tokens used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>Limits reset at the start of each calendar month.</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import ImportExportJobs from "./ImportExportJobs";
|
||||
import RulesSettingsCard from "./RulesSettingsCard";
|
||||
import BackupCard from "./BackupCard";
|
||||
import QuickCaptureCard from "./QuickCaptureCard";
|
||||
import AiUsageCard from "./AiUsageCard";
|
||||
import { ThemeModePref } from "../themePrefs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
@@ -197,6 +198,7 @@ export default function SettingsView({
|
||||
</Box>
|
||||
</SectionCard>
|
||||
|
||||
<AiUsageCard />
|
||||
<QuickCaptureCard />
|
||||
<SectionCard title="Connected accounts" subtitle="Manage inbox connections separately from your account and security settings.">
|
||||
<Button variant="outlined" onClick={() => navigate("/settings/connected-accounts")}>Manage connected accounts</Button>
|
||||
|
||||
Reference in New Issue
Block a user