feat: redesign settings pages to match canonical flat UI patterns (#493)
* refactor(settings): new IA shell + projects section + skills catalog discoverability * chore(settings): split providers list by scope; show user before project * fix: navigation flow in mobile Settings * feat: redesign settings pages to use modern elevated surface patterns * feat: replace helper text with tooltips in settings * ui: redesign update dialog and fix external link routing - Restructures UpdateDialog to focus on changelog readability with a wider max-w-4xl canvas - Highlights @username contributor mentions with theme primary color - Strips excessive vertical padding and right-aligns compact action buttons - Disables streamdown's internal link safety dialog in favor of direct Tauri shell routing * feat: refactor Git identities into dedicated Git settings page * feat: unify sidebar background styling across VS Code and web/mobile * fix: adjust button styling and layout for mobile settings pages * feat: add MCP settings page and sidebar * feat: hide models in provider view (thanks to @nguyenngothuong) * feat: add "Add new provider" option to model selector dropdown * fix: local evroc logo + provider dropdown icons * fix: increase width of provider menu * fix: dark theme background color for better contrast * feat: update @opencode-ai/sdk dependency to v1.2.10 * fix: restore session sorting to only use updated time * fix: added settings for sessions deletion dialog * fix: adjust padding on settings pages for better layout * fix: standardize select dropdown height across UI * fix: agent selector UI and notification settings * fix: remove redundant helper text from settings pages * fix: update UI layout for description fields * fix: remove border-none and shadow-none from textarea classes * fix: enable context menu on sidebar items * feat: refactor UI controls and layout patterns across settings pages * fix: use headerless blocks when page title already provides context * fix: remove subtask option from command settings * fix: refactor mcp page settings * fix: reduce spacing in skills configuration pages * feat: refactor voice settings * feat: refactor settings sidebar sections
This commit is contained in:
committed by
GitHub
parent
d2d39c48ac
commit
d2358c2c03
@@ -4,7 +4,7 @@ import { formatPercent, formatWindowLabel, calculatePace, calculateExpectedUsage
|
||||
import { UsageProgressBar } from './UsageProgressBar';
|
||||
import { PaceIndicator } from './PaceIndicator';
|
||||
import { useQuotaStore } from '@/stores/useQuotaStore';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
interface UsageCardProps {
|
||||
title: string;
|
||||
@@ -30,67 +30,63 @@ export const UsageCard: React.FC<UsageCardProps> = ({
|
||||
const resetLabel = window.resetAfterFormatted ?? window.resetAtFormatted ?? '';
|
||||
const windowLabel = formatWindowLabel(title);
|
||||
|
||||
// Calculate pace info for the usage window
|
||||
// Pass the title (window label) to infer windowSeconds when not provided by the API
|
||||
const paceInfo = React.useMemo(() => {
|
||||
return calculatePace(window.usedPercent, window.resetAt, window.windowSeconds, title);
|
||||
}, [window.usedPercent, window.resetAt, window.windowSeconds, title]);
|
||||
|
||||
// Calculate expected marker position for weekly/monthly quotas
|
||||
const expectedMarkerPercent = React.useMemo(() => {
|
||||
if (!paceInfo || paceInfo.dailyAllocationPercent === null) {
|
||||
return null;
|
||||
}
|
||||
// Show marker based on elapsed time ratio
|
||||
const expectedUsed = calculateExpectedUsagePercent(paceInfo.elapsedRatio);
|
||||
// If displaying remaining, invert the marker position
|
||||
return displayMode === 'remaining' ? 100 - expectedUsed : expectedUsed;
|
||||
}, [paceInfo, displayMode]);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)]/60 p-4 shadow-sm">
|
||||
<div className="py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="typography-ui-label text-foreground truncate">{windowLabel}</div>
|
||||
{subtitle && (
|
||||
<div className="typography-micro text-muted-foreground truncate">{subtitle}</div>
|
||||
<div className="min-w-0 flex-1 flex items-center gap-2">
|
||||
{showToggle && (
|
||||
<Checkbox
|
||||
checked={toggleEnabled}
|
||||
onChange={(checked) => onToggle?.(checked)}
|
||||
ariaLabel="Show in dropdown"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showToggle ? (
|
||||
<Switch
|
||||
checked={toggleEnabled}
|
||||
onCheckedChange={onToggle}
|
||||
aria-label="Show in dropdown"
|
||||
/>
|
||||
) : (
|
||||
<div className="typography-ui-label text-foreground tabular-nums">
|
||||
{percentLabel === '-' ? '' : percentLabel}
|
||||
<div className="min-w-0 flex flex-col">
|
||||
<span className="typography-ui-label text-foreground truncate">{windowLabel}</span>
|
||||
{subtitle && (
|
||||
<span className="typography-meta text-muted-foreground truncate">{subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="typography-ui-label text-foreground tabular-nums flex items-center justify-end">
|
||||
{percentLabel === '-' ? '' : percentLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="mt-2.5">
|
||||
<UsageProgressBar
|
||||
percent={displayPercent}
|
||||
tonePercent={window.usedPercent}
|
||||
expectedMarkerPercent={expectedMarkerPercent}
|
||||
className="h-1.5"
|
||||
/>
|
||||
<div className="mt-1 text-right typography-micro text-muted-foreground text-[10px]">
|
||||
{barLabel}
|
||||
<div className="mt-1 flex items-center justify-between">
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
{resetLabel ? `Resets ${resetLabel}` : ''}
|
||||
</span>
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
{barLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pace indicator - only shown when we have pace info */}
|
||||
{paceInfo && (
|
||||
<div className="mt-2">
|
||||
<div className="mt-1.5">
|
||||
<PaceIndicator paceInfo={paceInfo} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex items-center justify-between text-muted-foreground">
|
||||
<span className="typography-micro">Resets</span>
|
||||
<span className="typography-micro tabular-nums">{resetLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { UsageCard } from './UsageCard';
|
||||
import { QUOTA_PROVIDERS } from '@/lib/quota';
|
||||
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import {
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react';
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiInformationLine } from '@remixicon/react';
|
||||
import type { UsageWindows, QuotaProviderId } from '@/types';
|
||||
import { getAllModelFamilies, getDisplayModelName, sortModelFamilies, groupModelsByFamilyWithGetter } from '@/lib/quota/model-families';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
const formatTime = (timestamp: number | null) => {
|
||||
if (!timestamp) return '-';
|
||||
@@ -83,7 +84,6 @@ export const UsagePage: React.FC = () => {
|
||||
void updateDesktopSettings({ usageDropdownProviders: next });
|
||||
}, [dropdownProviderIds, selectedProviderId, setDropdownProviderIds]);
|
||||
|
||||
// Get models for the selected provider
|
||||
const providerModels = React.useMemo((): ModelInfo[] => {
|
||||
if (!usage?.models) return [];
|
||||
return Object.entries(usage.models)
|
||||
@@ -91,14 +91,12 @@ export const UsagePage: React.FC = () => {
|
||||
.filter((model) => Object.keys(model.windows.windows).length > 0);
|
||||
}, [usage?.models]);
|
||||
|
||||
// Apply default selections on mount if no prior selections exist
|
||||
React.useEffect(() => {
|
||||
if (selectedProviderId && providerModels.length > 0) {
|
||||
applyDefaultSelections(selectedProviderId, providerModels.map((m) => m.name));
|
||||
}
|
||||
}, [selectedProviderId, providerModels, applyDefaultSelections]);
|
||||
|
||||
// Group models by family
|
||||
const modelsByFamily = React.useMemo(() => {
|
||||
if (!selectedProviderId || providerModels.length === 0) {
|
||||
return new Map<string | null, ModelInfo[]>();
|
||||
@@ -110,16 +108,13 @@ export const UsagePage: React.FC = () => {
|
||||
);
|
||||
}, [providerModels, selectedProviderId]);
|
||||
|
||||
// Get sorted families
|
||||
const sortedFamilies = React.useMemo(() => {
|
||||
if (!selectedProviderId) return [];
|
||||
const families = getAllModelFamilies(selectedProviderId as QuotaProviderId);
|
||||
return sortModelFamilies(families);
|
||||
}, [selectedProviderId]);
|
||||
|
||||
// Collapsible state for family sections (persist per provider)
|
||||
const [collapsedFamilies, setCollapsedFamilies] = React.useState<Record<string, boolean>>(() => {
|
||||
// Default: all families start expanded (not collapsed)
|
||||
return {};
|
||||
});
|
||||
|
||||
@@ -133,7 +128,6 @@ export const UsagePage: React.FC = () => {
|
||||
const handleModelToggle = React.useCallback((modelName: string) => {
|
||||
if (!selectedProviderId) return;
|
||||
toggleModelSelected(selectedProviderId, modelName);
|
||||
// Also update settings to persist
|
||||
const currentSelected = selectedModels[selectedProviderId] ?? [];
|
||||
const isSelected = currentSelected.includes(modelName);
|
||||
const nextSelected = isSelected
|
||||
@@ -143,7 +137,6 @@ export const UsagePage: React.FC = () => {
|
||||
void updateDesktopSettings({ usageSelectedModels: nextSettings });
|
||||
}, [selectedProviderId, selectedModels, toggleModelSelected]);
|
||||
|
||||
// Get selected models for this provider
|
||||
const providerSelectedModels = selectedProviderId ? (selectedModels[selectedProviderId] ?? []) : [];
|
||||
|
||||
if (!selectedProviderId) {
|
||||
@@ -156,177 +149,220 @@ export const UsagePage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<ProviderLogo providerId={selectedProviderId} className="h-5 w-5" />
|
||||
<h1 className="typography-ui-header font-semibold text-lg">{providerName} Usage</h1>
|
||||
</div>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{isLoading ? 'Refreshing usage...' : `Last updated ${formatTime(lastUpdated)}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="typography-micro text-muted-foreground">Show in dropdown</span>
|
||||
<Switch
|
||||
checked={showInDropdown}
|
||||
onCheckedChange={handleDropdownToggle}
|
||||
aria-label={`Show ${providerName} in usage dropdown`}
|
||||
className="data-[state=checked]:bg-[var(--status-info)]"
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<ProviderLogo providerId={selectedProviderId} className="h-5 w-5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground truncate">
|
||||
{providerName} Usage
|
||||
</h2>
|
||||
<p className="typography-meta text-muted-foreground truncate">
|
||||
{isLoading ? (
|
||||
<span className="animate-pulse">Refreshing usage...</span>
|
||||
) : (
|
||||
`Last updated: ${formatTime(lastUpdated)}`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="mb-8 px-2">
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={showInDropdown}
|
||||
onClick={() => handleDropdownToggle(!showInDropdown)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleDropdownToggle(!showInDropdown);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={showInDropdown}
|
||||
onChange={handleDropdownToggle}
|
||||
ariaLabel="Show in header menu"
|
||||
/>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="typography-ui-label text-foreground">Show in Header Menu</span>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
When enabled, this provider's usage will be visible in the quick access dropdown menu in the app header.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* State Messages */}
|
||||
{!selectedResult && (
|
||||
<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 className="mb-8 px-2">
|
||||
<p className="typography-ui-label text-foreground">No usage data available yet.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<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 className="mb-8 rounded-lg border border-[var(--status-error-border)] bg-[var(--status-error-background)] px-4 py-3">
|
||||
<p className="typography-ui-label font-medium text-[var(--status-error)]">Failed to refresh usage data</p>
|
||||
<p className="typography-meta text-[var(--status-error)]/80 mt-1">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedResult && !selectedResult.configured && (
|
||||
<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">
|
||||
<div className="mb-8 rounded-lg border border-[var(--status-warning-border)] bg-[var(--status-warning-background)] px-4 py-3">
|
||||
<p className="typography-ui-label font-medium text-[var(--status-warning)]">Provider not configured</p>
|
||||
<p className="typography-meta text-[var(--status-warning)]/80 mt-1">
|
||||
Add credentials in the Providers tab to enable usage tracking.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overall Usage Windows */}
|
||||
{usage?.windows && Object.keys(usage.windows).length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{Object.entries(usage.windows).map(([label, window]) => (
|
||||
<UsageCard key={label} title={label} window={window} />
|
||||
))}
|
||||
<div className="mb-8">
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<div className="divide-y divide-[var(--surface-subtle)]">
|
||||
{Object.entries(usage.windows).map(([label, window]) => (
|
||||
<UsageCard key={label} title={label} window={window} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Models Section - Grouped by Family */}
|
||||
{/* Models Section */}
|
||||
{providerModels.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">Models</h2>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Toggle on to show in header dropdown
|
||||
</p>
|
||||
<div className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">Model Quotas</h3>
|
||||
</div>
|
||||
|
||||
{/* Predefined families (Gemini, Claude) */}
|
||||
{sortedFamilies.map((family) => {
|
||||
const familyModels = modelsByFamily.get(family.id) ?? [];
|
||||
if (familyModels.length === 0) return null;
|
||||
<div className="space-y-3">
|
||||
{/* Predefined families */}
|
||||
{sortedFamilies.map((family) => {
|
||||
const familyModels = modelsByFamily.get(family.id) ?? [];
|
||||
if (familyModels.length === 0) return null;
|
||||
|
||||
const isCollapsed = collapsedFamilies[family.id] ?? false;
|
||||
const isCollapsed = collapsedFamilies[family.id] ?? false;
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={family.id}
|
||||
open={!isCollapsed}
|
||||
onOpenChange={() => toggleFamilyCollapsed(family.id)}
|
||||
>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between py-2 text-left group">
|
||||
<div className="space-y-0.5">
|
||||
<div className="typography-ui-label font-semibold text-foreground">{family.label}</div>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{familyModels.length} model{familyModels.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<RiArrowRightSLine className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<RiArrowDownSLine className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2 space-y-3">
|
||||
{familyModels.map((model) => {
|
||||
const entries = Object.entries(model.windows.windows);
|
||||
if (entries.length === 0) return null;
|
||||
const [label, window] = entries[0];
|
||||
const isSelected = providerSelectedModels.includes(model.name);
|
||||
return (
|
||||
<section key={family.id} className="p-2">
|
||||
<Collapsible
|
||||
open={!isCollapsed}
|
||||
onOpenChange={() => toggleFamilyCollapsed(family.id)}
|
||||
>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between py-0.5 group">
|
||||
<div className="flex items-center gap-1.5 text-left">
|
||||
<span className="typography-ui-label font-normal text-foreground">{family.label}</span>
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
({familyModels.length})
|
||||
</span>
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<RiArrowRightSLine className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<RiArrowDownSLine className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="divide-y divide-[var(--surface-subtle)] mt-1">
|
||||
{familyModels.map((model) => {
|
||||
const entries = Object.entries(model.windows.windows);
|
||||
if (entries.length === 0) return null;
|
||||
const [label, window] = entries[0];
|
||||
const isSelected = providerSelectedModels.includes(model.name);
|
||||
|
||||
return (
|
||||
<UsageCard
|
||||
key={model.name}
|
||||
title={label}
|
||||
subtitle={getDisplayModelName(model.name)}
|
||||
window={window}
|
||||
showToggle
|
||||
toggleEnabled={isSelected}
|
||||
onToggle={() => handleModelToggle(model.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<UsageCard
|
||||
key={model.name}
|
||||
title={label}
|
||||
subtitle={getDisplayModelName(model.name)}
|
||||
window={window}
|
||||
showToggle
|
||||
toggleEnabled={isSelected}
|
||||
onToggle={() => handleModelToggle(model.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Other family */}
|
||||
{(() => {
|
||||
const otherModels = modelsByFamily.get(null) ?? [];
|
||||
if (otherModels.length === 0) return null;
|
||||
{/* Other family */}
|
||||
{(() => {
|
||||
const otherModels = modelsByFamily.get(null) ?? [];
|
||||
if (otherModels.length === 0) return null;
|
||||
|
||||
const isCollapsed = collapsedFamilies['other'] ?? false;
|
||||
const isCollapsed = collapsedFamilies['other'] ?? false;
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={!isCollapsed}
|
||||
onOpenChange={() => toggleFamilyCollapsed('other')}
|
||||
>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between py-2 text-left group">
|
||||
<div className="space-y-0.5">
|
||||
<div className="typography-ui-label font-semibold text-foreground">Other</div>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
{otherModels.length} model{otherModels.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<RiArrowRightSLine className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<RiArrowDownSLine className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2 space-y-3">
|
||||
{otherModels.map((model) => {
|
||||
const entries = Object.entries(model.windows.windows);
|
||||
if (entries.length === 0) return null;
|
||||
const [label, window] = entries[0];
|
||||
const isSelected = providerSelectedModels.includes(model.name);
|
||||
return (
|
||||
<section className="p-2">
|
||||
<Collapsible
|
||||
open={!isCollapsed}
|
||||
onOpenChange={() => toggleFamilyCollapsed('other')}
|
||||
>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between py-0.5 group">
|
||||
<div className="flex items-center gap-1.5 text-left">
|
||||
<span className="typography-ui-label font-normal text-foreground">Other Models</span>
|
||||
<span className="typography-micro text-muted-foreground">
|
||||
({otherModels.length})
|
||||
</span>
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<RiArrowRightSLine className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<RiArrowDownSLine className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="divide-y divide-[var(--surface-subtle)] mt-1">
|
||||
{otherModels.map((model) => {
|
||||
const entries = Object.entries(model.windows.windows);
|
||||
if (entries.length === 0) return null;
|
||||
const [label, window] = entries[0];
|
||||
const isSelected = providerSelectedModels.includes(model.name);
|
||||
|
||||
return (
|
||||
<UsageCard
|
||||
key={model.name}
|
||||
title={label}
|
||||
subtitle={getDisplayModelName(model.name)}
|
||||
window={window}
|
||||
showToggle
|
||||
toggleEnabled={isSelected}
|
||||
onToggle={() => handleModelToggle(model.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
})()}
|
||||
return (
|
||||
<UsageCard
|
||||
key={model.name}
|
||||
title={label}
|
||||
subtitle={getDisplayModelName(model.name)}
|
||||
window={window}
|
||||
showToggle
|
||||
toggleEnabled={isSelected}
|
||||
onToggle={() => handleModelToggle(model.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</section>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedResult?.configured && usage && Object.keys(usage.windows ?? {}).length === 0 &&
|
||||
providerModels.length === 0 && (
|
||||
<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 className="mb-8 px-2">
|
||||
<p className="typography-ui-label text-foreground">No quota windows reported</p>
|
||||
<p className="typography-meta text-muted-foreground mt-1">This provider does not currently report any rate limits or usage quotas.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
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 { ButtonSmall } from '@/components/ui/button-small';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
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';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { QUOTA_PROVIDERS, resolveUsageTone } from '@/lib/quota';
|
||||
import { useQuotaStore } from '@/stores/useQuotaStore';
|
||||
@@ -41,9 +39,6 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
const setUsageRefreshInterval = useQuotaStore((state) => state.setRefreshInterval);
|
||||
const setUsageDisplayMode = useQuotaStore((state) => state.setDisplayMode);
|
||||
const loadUsageSettings = useQuotaStore((state) => state.loadSettings);
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
void loadUsageSettings();
|
||||
@@ -79,22 +74,22 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
void persistUsageSettings({ usageDisplayMode: value });
|
||||
}, [persistUsageSettings, setUsageDisplayMode]);
|
||||
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
const bgClass = 'bg-background';
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className="border-b px-3 pt-4 pb-3">
|
||||
<h2 className="text-base font-semibold text-foreground mb-3">Usage</h2>
|
||||
<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">
|
||||
<Tooltip delayDuration={700}>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<Switch
|
||||
<Checkbox
|
||||
checked={usageAutoRefresh}
|
||||
onCheckedChange={handleUsageAutoRefreshChange}
|
||||
aria-label="Toggle auto refresh"
|
||||
className="data-[state=checked]:bg-[var(--status-info)]"
|
||||
onChange={handleUsageAutoRefreshChange}
|
||||
ariaLabel="Toggle auto refresh"
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -107,42 +102,36 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
onValueChange={handleUsageRefreshIntervalChange}
|
||||
disabled={!usageAutoRefresh}
|
||||
>
|
||||
<SelectTrigger size="sm" className="min-w-[72px]">
|
||||
<SelectTrigger className="w-fit">
|
||||
<SelectValue placeholder="Interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30000" className="pr-2 [&>span:first-child]:hidden">30s</SelectItem>
|
||||
<SelectItem value="60000" className="pr-2 [&>span:first-child]:hidden">1m</SelectItem>
|
||||
<SelectItem value="300000" className="pr-2 [&>span:first-child]:hidden">5m</SelectItem>
|
||||
<SelectItem value="30000">30s</SelectItem>
|
||||
<SelectItem value="60000">1m</SelectItem>
|
||||
<SelectItem value="300000">5m</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
<ButtonSmall
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 -my-1 text-muted-foreground overflow-hidden"
|
||||
className="h-7 w-7 px-0 text-muted-foreground"
|
||||
onClick={() => fetchAllQuotas()}
|
||||
aria-label="Refresh usage"
|
||||
title="Refresh usage"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RiRefreshLine className={cn('size-4', isLoading && 'animate-spin')} />
|
||||
</Button>
|
||||
<RiRefreshLine className={cn('h-3.5 w-3.5', isLoading && 'animate-spin')} />
|
||||
</ButtonSmall>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<span className="typography-micro text-muted-foreground">Display</span>
|
||||
<Select value={usageDisplayMode} onValueChange={handleUsageDisplayModeChange}>
|
||||
<SelectTrigger size="sm" className="min-w-[140px]">
|
||||
<SelectTrigger className="w-fit">
|
||||
<SelectValue placeholder="Display mode" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="usage" className="pr-2 [&>span:first-child]:hidden">
|
||||
Usage
|
||||
</SelectItem>
|
||||
<SelectItem value="remaining" className="pr-2 [&>span:first-child]:hidden">
|
||||
Quota remaining
|
||||
</SelectItem>
|
||||
<SelectItem value="usage">Usage</SelectItem>
|
||||
<SelectItem value="remaining">Quota remaining</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user