feat(ui): add pause/resume toggle and tier badges

- add pause/resume dropdown menu option in account item

- display tier badges (ultra/pro/free) with color coding

- show paused badge when account is paused

- wire onPauseToggle through component tree

Refs #282
This commit is contained in:
kaitranntt
2026-01-06 12:18:12 -05:00
parent b92a35d09b
commit 4ad7292700
6 changed files with 70 additions and 0 deletions
@@ -22,6 +22,8 @@ import {
Loader2,
CheckCircle2,
HelpCircle,
Pause,
Play,
} from 'lucide-react';
import {
cn,
@@ -86,6 +88,7 @@ export function AccountItem({
account,
onSetDefault,
onRemove,
onPauseToggle,
isRemoving,
privacyMode,
showQuota,
@@ -139,6 +142,28 @@ export function AccountItem({
Default
</Badge>
)}
{account.tier && account.tier !== 'unknown' && (
<Badge
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 uppercase',
account.tier === 'ultra' && 'border-purple-500 text-purple-600',
account.tier === 'pro' && 'border-blue-500 text-blue-600',
account.tier === 'free' && 'border-gray-400 text-gray-500'
)}
>
{account.tier}
</Badge>
)}
{account.paused && (
<Badge
variant="outline"
className="text-[10px] h-4 px-1.5 border-yellow-500 text-yellow-600"
>
<Pause className="w-2 h-2 mr-0.5" />
Paused
</Badge>
)}
</div>
{account.lastUsedAt && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
@@ -162,6 +187,21 @@ export function AccountItem({
Set as default
</DropdownMenuItem>
)}
{onPauseToggle && (
<DropdownMenuItem onClick={() => onPauseToggle(!account.paused)}>
{account.paused ? (
<>
<Play className="w-4 h-4 mr-2" />
Resume account
</>
) : (
<>
<Pause className="w-4 h-4 mr-2" />
Pause account
</>
)}
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={onRemove}
@@ -15,6 +15,7 @@ interface AccountsSectionProps {
onAddAccount: () => void;
onSetDefault: (accountId: string) => void;
onRemoveAccount: (accountId: string) => void;
onPauseToggle?: (accountId: string, paused: boolean) => void;
isRemovingAccount?: boolean;
privacyMode?: boolean;
/** Show quota bars for accounts (only applicable for 'agy' provider) */
@@ -31,6 +32,7 @@ export function AccountsSection({
onAddAccount,
onSetDefault,
onRemoveAccount,
onPauseToggle,
isRemovingAccount,
privacyMode,
showQuota,
@@ -65,6 +67,9 @@ export function AccountsSection({
account={account}
onSetDefault={() => onSetDefault(account.id)}
onRemove={() => onRemoveAccount(account.id)}
onPauseToggle={
onPauseToggle ? (paused) => onPauseToggle(account.id, paused) : undefined
}
isRemoving={isRemovingAccount}
privacyMode={privacyMode}
showQuota={showQuota}
@@ -38,6 +38,7 @@ export function ProviderEditor({
onAddAccount,
onSetDefault,
onRemoveAccount,
onPauseToggle,
isRemovingAccount,
}: ProviderEditorProps) {
const [customPresetOpen, setCustomPresetOpen] = useState(false);
@@ -200,6 +201,7 @@ export function ProviderEditor({
onAddAccount={onAddAccount}
onSetDefault={onSetDefault}
onRemoveAccount={onRemoveAccount}
onPauseToggle={onPauseToggle}
isRemovingAccount={isRemovingAccount}
privacyMode={privacyMode}
isRemoteMode={isRemoteMode}
@@ -36,6 +36,7 @@ interface ModelConfigTabProps {
onAddAccount: () => void;
onSetDefault: (accountId: string) => void;
onRemoveAccount: (accountId: string) => void;
onPauseToggle?: (accountId: string, paused: boolean) => void;
isRemovingAccount?: boolean;
privacyMode?: boolean;
/** True if connected to remote CLIProxy (quota not available) */
@@ -60,6 +61,7 @@ export function ModelConfigTab({
onAddAccount,
onSetDefault,
onRemoveAccount,
onPauseToggle,
isRemovingAccount,
privacyMode,
isRemoteMode,
@@ -134,6 +136,7 @@ export function ModelConfigTab({
onAddAccount={onAddAccount}
onSetDefault={onSetDefault}
onRemoveAccount={onRemoveAccount}
onPauseToggle={onPauseToggle}
isRemovingAccount={isRemovingAccount}
privacyMode={privacyMode}
showQuota={provider === 'agy' && !isRemoteMode}
@@ -30,6 +30,7 @@ export interface ProviderEditorProps {
onAddAccount: () => void;
onSetDefault: (accountId: string) => void;
onRemoveAccount: (accountId: string) => void;
onPauseToggle?: (accountId: string, paused: boolean) => void;
isRemovingAccount?: boolean;
}
@@ -37,6 +38,7 @@ export interface AccountItemProps {
account: OAuthAccount;
onSetDefault: () => void;
onRemove: () => void;
onPauseToggle?: (paused: boolean) => void;
isRemoving?: boolean;
privacyMode?: boolean;
/** Show quota bar (only for 'agy' provider) */
+18
View File
@@ -21,6 +21,8 @@ import {
useCliproxyAuth,
useSetDefaultAccount,
useRemoveAccount,
usePauseAccount,
useResumeAccount,
useDeleteVariant,
} from '@/hooks/use-cliproxy';
import type { AuthStatus, Variant } from '@/lib/api-client';
@@ -179,6 +181,8 @@ export function CliproxyPage() {
const { data: variantsData, isFetching } = useCliproxy();
const setDefaultMutation = useSetDefaultAccount();
const removeMutation = useRemoveAccount();
const pauseMutation = usePauseAccount();
const resumeMutation = useResumeAccount();
const deleteMutation = useDeleteVariant();
// Selection state: either a provider or a variant
@@ -216,6 +220,14 @@ export function CliproxyPage() {
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
};
const handlePauseToggle = (provider: string, accountId: string, paused: boolean) => {
if (paused) {
pauseMutation.mutate({ provider, accountId });
} else {
resumeMutation.mutate({ provider, accountId });
}
};
const handleSelectProvider = (provider: string) => {
setSelectedProvider(provider);
setSelectedVariant(null);
@@ -361,6 +373,9 @@ export function CliproxyPage() {
accountId,
})
}
onPauseToggle={(accountId, paused) =>
handlePauseToggle(selectedVariantData.provider, accountId, paused)
}
isRemovingAccount={removeMutation.isPending}
/>
) : selectedStatus ? (
@@ -389,6 +404,9 @@ export function CliproxyPage() {
accountId,
})
}
onPauseToggle={(accountId, paused) =>
handlePauseToggle(selectedStatus.provider, accountId, paused)
}
isRemovingAccount={removeMutation.isPending}
/>
) : (