diff --git a/JobTrackerApi/Controllers/AiUsageController.cs b/JobTrackerApi/Controllers/AiUsageController.cs index 2ff7cfb..3f5d3c3 100644 --- a/JobTrackerApi/Controllers/AiUsageController.cs +++ b/JobTrackerApi/Controllers/AiUsageController.cs @@ -22,7 +22,7 @@ public sealed class AiUsageController : ControllerBase } public sealed record UsagePeriodDto(int Calls, long InputCharacters, long OutputCharacters, long EstimatedTokens); - public sealed record UsageDto(UsagePeriodDto CurrentMonth, UsagePeriodDto AllTime, string Plan, int MonthlyCallLimit, long MonthlyTokenLimit); + public sealed record UsageDto(UsagePeriodDto CurrentMonth, UsagePeriodDto AllTime, string Plan, int MonthlyCallLimit, long MonthlyTokenLimit, long StorageUsedBytes, long StorageLimitBytes); [HttpGet] public async Task> Get(CancellationToken cancellationToken) @@ -38,7 +38,9 @@ public sealed class AiUsageController : ControllerBase await SumAsync(_db.AiInteractions.Where(x => x.OwnerUserId == user.Id), cancellationToken), entitlements.AdvancedAi ? "premium" : "free", entitlements.MonthlyAiCalls, - entitlements.MonthlyAiTokens)); + entitlements.MonthlyAiTokens, + await _db.Attachments.Where(x => x.JobApplication.OwnerUserId == user.Id).SumAsync(x => (long?)x.FileSize, cancellationToken) ?? 0, + entitlements.StorageBytes)); } private static async Task SumAsync(IQueryable query, CancellationToken cancellationToken) diff --git a/job-tracker-ui/src/ai-usage-card.test.tsx b/job-tracker-ui/src/ai-usage-card.test.tsx index ec7a7e6..4e079cf 100644 --- a/job-tracker-ui/src/ai-usage-card.test.tsx +++ b/job-tracker-ui/src/ai-usage-card.test.tsx @@ -13,6 +13,8 @@ test("shows monthly AI call and token limits", async () => { plan: "free", monthlyCallLimit: 25, monthlyTokenLimit: 100000, + storageUsedBytes: 50000000, + storageLimitBytes: 250000000, }, }); @@ -20,5 +22,6 @@ test("shows monthly AI call and token limits", async () => { expect(await screen.findByText("4 of 25 generations this month")).toBeInTheDocument(); expect(screen.getByText("12,000 of 100,000 estimated tokens")).toBeInTheDocument(); + expect(screen.getByText("50.0 MB of 250.0 MB attachment storage")).toBeInTheDocument(); expect(screen.getByLabelText("Monthly AI generations used")).toHaveAttribute("aria-valuenow", "16"); }); diff --git a/job-tracker-ui/src/components/AiUsageCard.tsx b/job-tracker-ui/src/components/AiUsageCard.tsx index 2d7de81..da529aa 100644 --- a/job-tracker-ui/src/components/AiUsageCard.tsx +++ b/job-tracker-ui/src/components/AiUsageCard.tsx @@ -7,8 +7,16 @@ type Usage = { plan: string; monthlyCallLimit: number; monthlyTokenLimit: number; + storageUsedBytes: number; + storageLimitBytes: number; }; +function formatBytes(bytes: number) { + if (bytes < 1_000_000) return `${Math.round(bytes / 1_000)} KB`; + if (bytes < 1_000_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`; + return `${(bytes / 1_000_000_000).toFixed(1)} GB`; +} + export default function AiUsageCard() { const [usage, setUsage] = useState(null); const [failed, setFailed] = useState(false); @@ -26,6 +34,7 @@ export default function AiUsageCard() { const callsPercent = Math.min(100, usage.currentMonth.calls / usage.monthlyCallLimit * 100); const tokensPercent = Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100); + const storagePercent = Math.min(100, usage.storageUsedBytes / usage.storageLimitBytes * 100); return ( @@ -41,7 +50,11 @@ export default function AiUsageCard() { {usage.currentMonth.estimatedTokens.toLocaleString()} of {usage.monthlyTokenLimit.toLocaleString()} estimated tokens - Limits reset at the start of each calendar month. + + {formatBytes(usage.storageUsedBytes)} of {formatBytes(usage.storageLimitBytes)} attachment storage + + + AI limits reset at the start of each calendar month. ); }