Resolve verified V1 frontend feedback by improving channel workflows, auth behavior, API key interactions, user filtering, layout persistence, subscription quota handling, i18n text, pricing metadata, and stale frontend cache recovery. - Add a global frontend cache version cleanup to prevent old frontend localStorage from causing page errors after upgrades. - Fix channel copy refresh, model mapping input focus loss, create-channel fetch-model title state, upstream model update confirmation, and batch test toast behavior. - Respect password login settings and improve Turnstile, forgot-password, registration, and invite-link flows. - Make user role/status filtering server-side and preserve table page size in URLs. - Improve API key edit validation feedback and prefetch real keys for reliable copy actions. - Fix rankings access fail-open behavior, double scrollbars, subscription received amount conversion/display, token i18n wording, model deletion confirmation grammar, and Claude pricing context inference. - Add clearer Playground model/group loading errors. Validation: - bun run typecheck - bun run i18n:sync - gofmt on modified Go files - go test ./controller ./model -run '^$'
242 lines
7.5 KiB
TypeScript
Vendored
242 lines
7.5 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { useState } from 'react'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import {
|
|
Plus,
|
|
MoreHorizontal,
|
|
Settings2,
|
|
Trash2,
|
|
Tags,
|
|
TestTube,
|
|
DollarSign,
|
|
SortAsc,
|
|
RefreshCw,
|
|
ArrowUpFromLine,
|
|
} from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
|
import {
|
|
handleDeleteAllDisabled,
|
|
handleFixAbilities,
|
|
handleTestAllChannels,
|
|
handleUpdateAllBalances,
|
|
} from '../lib'
|
|
import { useChannels } from './channels-provider'
|
|
|
|
export function ChannelsPrimaryButtons() {
|
|
const { t } = useTranslation()
|
|
const {
|
|
setOpen,
|
|
setCurrentRow,
|
|
enableTagMode,
|
|
setEnableTagMode,
|
|
idSort,
|
|
setIdSort,
|
|
upstream,
|
|
} = useChannels()
|
|
const queryClient = useQueryClient()
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
|
|
|
const handleTagModeToggle = (checked: boolean) => {
|
|
localStorage.setItem('enable-tag-mode', String(checked))
|
|
setEnableTagMode(checked)
|
|
}
|
|
|
|
const handleIdSortToggle = (checked: boolean) => {
|
|
localStorage.setItem('channels-id-sort', String(checked))
|
|
setIdSort(checked)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className='flex items-center gap-2'>
|
|
{/* Desktop: Toggle switches visible */}
|
|
<div className='hidden items-center gap-2 rounded-md border px-3 py-1.5 sm:flex'>
|
|
<Tags className='text-muted-foreground h-4 w-4' />
|
|
<Label htmlFor='tag-mode' className='cursor-pointer text-sm'>
|
|
{t('Tag Mode')}
|
|
</Label>
|
|
<Switch
|
|
id='tag-mode'
|
|
checked={enableTagMode}
|
|
onCheckedChange={handleTagModeToggle}
|
|
/>
|
|
</div>
|
|
|
|
<div className='hidden items-center gap-2 rounded-md border px-3 py-1.5 sm:flex'>
|
|
<SortAsc className='text-muted-foreground h-4 w-4' />
|
|
<Label htmlFor='id-sort' className='cursor-pointer text-sm'>
|
|
{t('Sort by ID')}
|
|
</Label>
|
|
<Switch
|
|
id='id-sort'
|
|
checked={idSort}
|
|
onCheckedChange={handleIdSortToggle}
|
|
/>
|
|
</div>
|
|
|
|
{/* Create Channel */}
|
|
<Button
|
|
onClick={() => {
|
|
setCurrentRow(null)
|
|
setOpen('create-channel')
|
|
}}
|
|
size='sm'
|
|
>
|
|
<Plus className='h-4 w-4' />
|
|
<span className='max-sm:hidden'>{t('Create Channel')}</span>
|
|
<span className='sm:hidden'>{t('Create')}</span>
|
|
</Button>
|
|
|
|
{/* More Actions */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger render={<Button variant='outline' size='sm' />}>
|
|
<MoreHorizontal className='h-4 w-4' />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align='end' className='w-56'>
|
|
{/* Mobile-only: toggle switches */}
|
|
<DropdownMenuCheckboxItem
|
|
className='sm:hidden'
|
|
checked={enableTagMode}
|
|
onCheckedChange={handleTagModeToggle}
|
|
>
|
|
<Tags className='mr-2 h-4 w-4' />
|
|
{t('Tag Mode')}
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
<DropdownMenuCheckboxItem
|
|
className='sm:hidden'
|
|
checked={idSort}
|
|
onCheckedChange={handleIdSortToggle}
|
|
>
|
|
<SortAsc className='mr-2 h-4 w-4' />
|
|
{t('Sort by ID')}
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
<DropdownMenuSeparator className='sm:hidden' />
|
|
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
handleTestAllChannels(queryClient)
|
|
}}
|
|
>
|
|
{t('Test All Channels')}
|
|
<DropdownMenuShortcut>
|
|
<TestTube className='h-4 w-4' />
|
|
</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
handleUpdateAllBalances(queryClient)
|
|
}}
|
|
>
|
|
{t('Update All Balances')}
|
|
<DropdownMenuShortcut>
|
|
<DollarSign className='h-4 w-4' />
|
|
</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem
|
|
onClick={() => upstream.detectAllUpdates()}
|
|
disabled={upstream.detectAllLoading}
|
|
>
|
|
{t('Detect All Upstream Updates')}
|
|
<DropdownMenuShortcut>
|
|
<RefreshCw className='h-4 w-4' />
|
|
</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
onClick={() => upstream.applyAllUpdates()}
|
|
disabled={upstream.applyAllLoading}
|
|
>
|
|
{t('Apply All Upstream Updates')}
|
|
<DropdownMenuShortcut>
|
|
<ArrowUpFromLine className='h-4 w-4' />
|
|
</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
handleFixAbilities(queryClient, (_result) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log('Fix abilities result:', _result)
|
|
})
|
|
}}
|
|
>
|
|
{t('Fix Abilities')}
|
|
<DropdownMenuShortcut>
|
|
<Settings2 className='h-4 w-4' />
|
|
</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem
|
|
onSelect={(e) => {
|
|
e.preventDefault()
|
|
setShowDeleteDialog(true)
|
|
}}
|
|
className='text-destructive focus:text-destructive'
|
|
>
|
|
{t('Delete All Disabled')}
|
|
<DropdownMenuShortcut>
|
|
<Trash2 className='h-4 w-4' />
|
|
</DropdownMenuShortcut>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<ConfirmDialog
|
|
open={showDeleteDialog}
|
|
onOpenChange={setShowDeleteDialog}
|
|
title={t('Delete All Disabled Channels?')}
|
|
desc='This will permanently delete all manually and automatically disabled channels. This action cannot be undone.'
|
|
destructive
|
|
handleConfirm={() => {
|
|
handleDeleteAllDisabled(queryClient, (_count) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Deleted ${_count} channels`)
|
|
})
|
|
setShowDeleteDialog(false)
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
}
|