feat(desktop/vscode): add quota providers API and UI integration (#266)
* feat(openchamber): persist usage auto-refresh settings Enable a switch to toggle automatic usage refresh Provide input to configure refresh interval in milliseconds Persist changes to desktop settings and server API when changed * feat: add UsageCard component Add a new UsageCard component to display a usage window with title, optional subtitle, and a progress bar Show current usage percentage and a reset time label for the window Render a formatted window label and a compact subtitle for concise UI * feat(usage): add UsagePage UI for quota usage Add a dedicated UsagePage with provider-based usage details Show last updated time and auto-refresh status Handle empty, not configured, and error states with informative banners * feat(usage): add UsageProgressBar component Introduce a new UsageProgressBar component to visualize quota usage Display a gradient fill that changes with critical, warn, or normal tones Expose accessible progress attributes for screen readers * feat(usage): add UsageSidebar component Display quotas for all providers in a scrollable sidebar Refresh quotas with a button and loading indicator Colorize provider rows based on usage status and runtime context * feat: add usage section to Settings Add a new Usage item to the Settings sidebar for desktop and mobile Render UsagePage when the Usage tab is selected in Settings Wire up new UsageSidebar and UsagePage components under usage * feat: add Usage section to sidebar Add new Usage section in the sidebar for API quota monitoring. Display a bar chart icon and description for the Usage item. Monitor and display API quota usage across providers. * feat(desktop): add usage auto-refresh settings Enable automatic refresh for usage data with new settings Store refresh interval in milliseconds for usage updates * feat(persistence): support usageAutoRefresh and usageRefreshIntervalMs Persist usageAutoRefresh in desktop settings Persist usageRefreshIntervalMs in desktop settings Validate types for new fields during sanitizeWebSettings * feat(quota): export providers and utilities Expose QUOTA_PROVIDERS and QUOTA_PROVIDER_MAP for consumers Export QuotaProviderMeta type for user code Make formatting and usage resolution utilities available from quota module * feat(quota): define base quota provider interface Define QuotaProvider interface with id, name, isConfigured, and fetchQuota Expose ProviderResult type in fetchQuota contract * feat: add quota providers index and map Expose QUOTA_PROVIDERS with OpenAI, Google and z.ai Provide QUOTA_PROVIDER_MAP for quick provider lookup by id * feat: add quota utils for percent formatting and tone Add clampPercent to sanitize and clamp numbers to 0-100 Add formatPercent to render '-' for null and 'x%' for values Add resolveUsageTone to categorize percent as safe, warn, or critical * feat: add useQuotaStore for quota data Load usage settings from desktop, VSCode, or API at startup Fetch quotas for all providers in parallel and update loading state Expose lastUpdated timestamp and error state for UI feedback * feat: export quota types from quota module Expose quota-related types in UI type definitions Allow downstream code to import QuotaProviderId and related types Aggregate quota exports under the quota module in index * feat: add quota types for usage providers Add QuotaProviderId and UsageWindow shapes to model quota data Add ProviderUsage, ProviderResult, and related usage mapping for providers * feat: add quota provider endpoints API List available quota providers via GET /api/quota/providers Retrieve quota details for a specific provider with GET /api/quota/:providerId Log errors and return 500 with error message on quota fetch failures * feat: add quota providers discovery and formatting Detect configured quota providers from auth and account files Normalize auth entries to tokens or objects for API usage Expose formatted reset times and remaining window metrics * feat: persist usage settings in UsageSidebar and remove from defaults Load usage settings on mount for the sidebar Persist changes to auto-refresh and refresh interval to server Remove usage settings state and effects from DefaultsSettings * fix(usage): guard auto-select in UsagePage when results empty Guard auto-select in UsagePage when results are empty Prevent unexpected provider selection on initial render * fix(quota): set error state during quota updates Reset error to null when a new quota result is added Set error to the error message on fetch failure or fallback Keep error state alongside results in all update paths * feat(desktop/vscode): add quota providers API and UI integration * fix(quotaProviders): correct quota payload parsing --------- Co-authored-by: Nelson Pires <nelsonpires.sn@gmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Nelson Pires
parent
3e145bdbd6
commit
05e95410d7
@@ -15,7 +15,7 @@ export const UsageCard: React.FC<UsageCardProps> = ({ title, window, subtitle })
|
||||
const windowLabel = formatWindowLabel(title);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border/60 bg-card/40 p-4 shadow-sm">
|
||||
<div className="rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/60 p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="typography-ui-label text-foreground truncate">{windowLabel}</div>
|
||||
|
||||
@@ -69,20 +69,20 @@ export const UsagePage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{!selectedResult && (
|
||||
<div className="rounded-lg border border-border/60 bg-card/40 p-4 text-muted-foreground">
|
||||
<div className="rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/60 p-4 text-muted-foreground">
|
||||
<p className="typography-body">No usage data available yet.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-border/60 bg-card/40 p-4 text-muted-foreground">
|
||||
<div className="rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/60 p-4 text-muted-foreground">
|
||||
<p className="typography-body">Failed to refresh usage data.</p>
|
||||
<p className="typography-meta mt-1">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedResult && !selectedResult.configured && (
|
||||
<div className="rounded-lg border border-border/60 bg-card/40 p-4 text-muted-foreground">
|
||||
<div className="rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/60 p-4 text-muted-foreground">
|
||||
<p className="typography-body">Provider is not configured yet.</p>
|
||||
<p className="typography-meta mt-1">
|
||||
Add credentials in the Providers tab to enable usage tracking.
|
||||
@@ -114,7 +114,7 @@ export const UsagePage: React.FC = () => {
|
||||
|
||||
{selectedResult?.configured && usage && Object.keys(usage.windows ?? {}).length === 0 &&
|
||||
Object.keys(usage.models ?? {}).length === 0 && (
|
||||
<div className="rounded-lg border border-border/60 bg-card/40 p-4 text-muted-foreground">
|
||||
<div className="rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/60 p-4 text-muted-foreground">
|
||||
<p className="typography-body">No quota windows reported for this provider.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,17 +11,17 @@ export const UsageProgressBar: React.FC<UsageProgressBarProps> = ({ percent, cla
|
||||
const clamped = clampPercent(percent) ?? 0;
|
||||
const tone = resolveUsageTone(percent);
|
||||
|
||||
const fillClass = tone === 'critical'
|
||||
? 'from-rose-500 to-rose-400'
|
||||
const fillStyle = tone === 'critical'
|
||||
? { backgroundColor: 'var(--status-error)' }
|
||||
: tone === 'warn'
|
||||
? 'from-amber-500 to-amber-400'
|
||||
: 'from-emerald-500 to-emerald-400';
|
||||
? { backgroundColor: 'var(--status-warning)' }
|
||||
: { backgroundColor: 'var(--status-success)' };
|
||||
|
||||
return (
|
||||
<div className={cn('h-2.5 rounded-full bg-muted/60 overflow-hidden', className)}>
|
||||
<div className={cn('h-2.5 rounded-full bg-[var(--interactive-border)] overflow-hidden', className)}>
|
||||
<div
|
||||
className={cn('h-full bg-gradient-to-r transition-all duration-300', fillClass)}
|
||||
style={{ width: `${clamped}%` }}
|
||||
className="h-full transition-all duration-300"
|
||||
style={{ ...fillStyle, width: `${clamped}%` }}
|
||||
role="progressbar"
|
||||
aria-valuenow={clamped}
|
||||
aria-valuemin={0}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
@@ -90,11 +91,21 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Total {QUOTA_PROVIDERS.length}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={usageAutoRefresh}
|
||||
onCheckedChange={handleUsageAutoRefreshChange}
|
||||
aria-label="Toggle auto refresh"
|
||||
/>
|
||||
<Tooltip delayDuration={700}>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<Switch
|
||||
checked={usageAutoRefresh}
|
||||
onCheckedChange={handleUsageAutoRefreshChange}
|
||||
aria-label="Toggle auto refresh"
|
||||
className="data-[state=checked]:bg-[var(--status-info)]"
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Auto-refresh usage data at set interval
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Select
|
||||
value={String(usageRefreshIntervalMs)}
|
||||
onValueChange={handleUsageRefreshIntervalChange}
|
||||
@@ -113,7 +124,7 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 -my-1 text-muted-foreground"
|
||||
className="h-7 w-7 -my-1 text-muted-foreground overflow-hidden"
|
||||
onClick={() => fetchAllQuotas()}
|
||||
aria-label="Refresh usage"
|
||||
title="Refresh usage"
|
||||
@@ -133,20 +144,20 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
const isSelected = provider.id === selectedProviderId;
|
||||
const configured = result?.configured ?? false;
|
||||
|
||||
const statusClass = !configured
|
||||
? 'bg-muted-foreground/40'
|
||||
const statusStyle = !configured
|
||||
? { backgroundColor: 'var(--surface-muted-foreground)', opacity: 0.4 }
|
||||
: tone === 'critical'
|
||||
? 'bg-rose-500'
|
||||
? { backgroundColor: 'var(--status-error)' }
|
||||
: tone === 'warn'
|
||||
? 'bg-amber-500'
|
||||
: 'bg-emerald-500';
|
||||
? { backgroundColor: 'var(--status-warning)' }
|
||||
: { backgroundColor: 'var(--status-success)' };
|
||||
|
||||
return (
|
||||
<div
|
||||
key={provider.id}
|
||||
className={cn(
|
||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
||||
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -157,7 +168,7 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
}}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
>
|
||||
<span className={cn('h-2.5 w-2.5 rounded-full flex-shrink-0', statusClass)} />
|
||||
<span className="h-2.5 w-2.5 rounded-full flex-shrink-0" style={statusStyle} />
|
||||
<ProviderLogo providerId={provider.id} className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="typography-ui-label font-normal truncate flex-1 min-w-0 text-foreground">
|
||||
{provider.name}
|
||||
|
||||
Reference in New Issue
Block a user