feat: show account storage usage
This commit is contained in:
@@ -22,7 +22,7 @@ public sealed class AiUsageController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public sealed record UsagePeriodDto(int Calls, long InputCharacters, long OutputCharacters, long EstimatedTokens);
|
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]
|
[HttpGet]
|
||||||
public async Task<ActionResult<UsageDto>> Get(CancellationToken cancellationToken)
|
public async Task<ActionResult<UsageDto>> Get(CancellationToken cancellationToken)
|
||||||
@@ -38,7 +38,9 @@ public sealed class AiUsageController : ControllerBase
|
|||||||
await SumAsync(_db.AiInteractions.Where(x => x.OwnerUserId == user.Id), cancellationToken),
|
await SumAsync(_db.AiInteractions.Where(x => x.OwnerUserId == user.Id), cancellationToken),
|
||||||
entitlements.AdvancedAi ? "premium" : "free",
|
entitlements.AdvancedAi ? "premium" : "free",
|
||||||
entitlements.MonthlyAiCalls,
|
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<UsagePeriodDto> SumAsync(IQueryable<AiInteraction> query, CancellationToken cancellationToken)
|
private static async Task<UsagePeriodDto> SumAsync(IQueryable<AiInteraction> query, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ test("shows monthly AI call and token limits", async () => {
|
|||||||
plan: "free",
|
plan: "free",
|
||||||
monthlyCallLimit: 25,
|
monthlyCallLimit: 25,
|
||||||
monthlyTokenLimit: 100000,
|
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(await screen.findByText("4 of 25 generations this month")).toBeInTheDocument();
|
||||||
expect(screen.getByText("12,000 of 100,000 estimated tokens")).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");
|
expect(screen.getByLabelText("Monthly AI generations used")).toHaveAttribute("aria-valuenow", "16");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,16 @@ type Usage = {
|
|||||||
plan: string;
|
plan: string;
|
||||||
monthlyCallLimit: number;
|
monthlyCallLimit: number;
|
||||||
monthlyTokenLimit: 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() {
|
export default function AiUsageCard() {
|
||||||
const [usage, setUsage] = useState<Usage | null>(null);
|
const [usage, setUsage] = useState<Usage | null>(null);
|
||||||
const [failed, setFailed] = useState(false);
|
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 callsPercent = Math.min(100, usage.currentMonth.calls / usage.monthlyCallLimit * 100);
|
||||||
const tokensPercent = Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100);
|
const tokensPercent = Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100);
|
||||||
|
const storagePercent = Math.min(100, usage.storageUsedBytes / usage.storageLimitBytes * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 2.5, borderRadius: 4, border: "none" }}>
|
<Paper sx={{ p: 2.5, borderRadius: 4, border: "none" }}>
|
||||||
@@ -41,7 +50,11 @@ export default function AiUsageCard() {
|
|||||||
<Typography variant="body2">{usage.currentMonth.estimatedTokens.toLocaleString()} of {usage.monthlyTokenLimit.toLocaleString()} estimated tokens</Typography>
|
<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 }} />
|
<LinearProgress variant="determinate" value={tokensPercent} aria-label="Monthly AI tokens used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>Limits reset at the start of each calendar month.</Typography>
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="body2">{formatBytes(usage.storageUsedBytes)} of {formatBytes(usage.storageLimitBytes)} attachment storage</Typography>
|
||||||
|
<LinearProgress variant="determinate" value={storagePercent} aria-label="Attachment storage used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>AI limits reset at the start of each calendar month.</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user