Commit 2eaa943d by QuentinHsu

perf(web): improve dialog sizing and footer layout

- migrate frontend dialogs to the shared footer API so actions stay separated from scrollable body content.
- tune dialog dimensions for model analytics, prefill groups, billing history, channel model sync, and related workflows.
- update channel terminology and dialog action translations across supported locales.
parent 7a5348ca
......@@ -54,7 +54,7 @@ export function Dialog({
children,
trigger,
footer,
contentHeight = 'min(58vh, 520px)',
contentHeight = 'auto',
contentClassName,
headerClassName,
titleClassName,
......
......@@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications'
import { useSystemConfig } from '@/hooks/use-system-config'
import { useTopNavLinks } from '@/hooks/use-top-nav-links'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton'
import { Dialog } from '@/components/dialog'
import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown'
......@@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) {
closeAuthPrompt()
}
}}
>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Sign in required')}</DialogTitle>
<DialogDescription>
{t('Please sign in to view {{module}}.', {
module: authPromptTarget?.title || '',
})}
</DialogDescription>
</DialogHeader>
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
{t('Redirecting to sign in in {{seconds}} seconds.', {
seconds: authPromptSecondsLeft,
})}
</div>
<DialogFooter>
title={t('Sign in required')}
description={t('Please sign in to view {{module}}.', {
module: authPromptTarget?.title || '',
})}
contentClassName='sm:max-w-md'
contentHeight='auto'
footer={
<>
<Button variant='outline' onClick={closeAuthPrompt}>
{t('Cancel')}
</Button>
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
{t('Redirecting to sign in in {{seconds}} seconds.', {
seconds: authPromptSecondsLeft,
})}
</div>
</Dialog>
</>
)
......
......@@ -20,16 +20,9 @@ import { useMemo } from 'react'
import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog } from '@/components/dialog'
import type {
SecureVerificationState,
VerificationMethod,
......@@ -91,122 +84,118 @@ export function SecureVerificationDialog({
(activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6))
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 gap-0 overflow-hidden border-none p-0 shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl'
showCloseButton={!state.loading}
>
<div className='bg-background flex max-h-[calc(100dvh-2rem)] flex-col'>
<DialogHeader className='border-b px-6 py-5 text-left'>
<DialogTitle className='flex items-center gap-2 text-lg font-semibold'>
<ShieldCheck className='text-primary h-5 w-5' />
{title}
</DialogTitle>
<DialogDescription className='text-left'>
{description}
</DialogDescription>
</DialogHeader>
<div className='flex-1 overflow-y-auto px-6 py-5'>
{availableTabs.length === 0 ? (
<div className='grid place-items-center gap-4 text-center'>
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
<ShieldCheck className='text-muted-foreground h-8 w-8' />
</div>
<p className='text-muted-foreground text-sm'>
{t(
'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.'
)}
</p>
</div>
) : (
<Tabs
value={activeMethod ?? availableTabs[0]}
onValueChange={(value) =>
onMethodChange(value as VerificationMethod)
<Dialog
open={open}
onOpenChange={onOpenChange}
title={
<>
<ShieldCheck className='text-primary h-5 w-5' />
{title}
</>
}
description={description}
contentClassName='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 overflow-hidden border-none shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl'
headerClassName='border-b pb-4 text-left'
titleClassName='flex items-center gap-2 text-lg font-semibold'
descriptionClassName='text-left'
contentHeight='auto'
bodyClassName='px-1 py-1'
showCloseButton={!state.loading}
footerClassName='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'
footer={
<>
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</>
}
>
{availableTabs.length === 0 ? (
<div className='grid place-items-center gap-4 text-center'>
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
<ShieldCheck className='text-muted-foreground h-8 w-8' />
</div>
<p className='text-muted-foreground text-sm'>
{t(
'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.'
)}
</p>
</div>
) : (
<Tabs
value={activeMethod ?? availableTabs[0]}
onValueChange={(value) => onMethodChange(value as VerificationMethod)}
className='gap-4'
>
<TabsList>
{methods.has2FA && (
<TabsTrigger value='2fa'>{t('Authenticator code')}</TabsTrigger>
)}
{methods.hasPasskey && methods.passkeySupported && (
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
)}
</TabsList>
<TabsContent value='2fa' className='space-y-3'>
<p className='text-muted-foreground text-sm'>
{t(
'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.'
)}
</p>
<Input
inputMode='numeric'
maxLength={8}
value={state.code}
onChange={(event) => onCodeChange(event.target.value)}
placeholder={t('Enter verification code')}
disabled={state.loading}
autoFocus={activeMethod === '2fa'}
onKeyDown={(event) => {
if (event.key === 'Enter' && !verifyDisabled) {
event.preventDefault()
handleVerify()
}
className='gap-4'
>
<TabsList>
{methods.has2FA && (
<TabsTrigger value='2fa'>
{t('Authenticator code')}
</TabsTrigger>
)}
{methods.hasPasskey && methods.passkeySupported && (
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
)}
</TabsList>
<TabsContent value='2fa' className='space-y-3'>
<p className='text-muted-foreground text-sm'>
}}
/>
</TabsContent>
<TabsContent value='passkey' className='space-y-4'>
<div className='bg-muted/50 flex items-center justify-center rounded-lg p-4'>
<div className='text-muted-foreground flex items-center gap-3'>
<KeyRound className='text-primary h-6 w-6' />
<div className='text-left text-sm'>
<p className='text-foreground font-medium'>
{t('Use your Passkey')}
</p>
<p>
{t(
'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.'
'We will prompt your device to confirm using biometrics or your hardware key.'
)}
</p>
<Input
inputMode='numeric'
maxLength={8}
value={state.code}
onChange={(event) => onCodeChange(event.target.value)}
placeholder={t('Enter verification code')}
disabled={state.loading}
autoFocus={activeMethod === '2fa'}
onKeyDown={(event) => {
if (event.key === 'Enter' && !verifyDisabled) {
event.preventDefault()
handleVerify()
}
}}
/>
</TabsContent>
<TabsContent value='passkey' className='space-y-4'>
<div className='bg-muted/50 flex items-center justify-center rounded-lg p-4'>
<div className='text-muted-foreground flex items-center gap-3'>
<KeyRound className='text-primary h-6 w-6' />
<div className='text-left text-sm'>
<p className='text-foreground font-medium'>
{t('Use your Passkey')}
</p>
<p>
{t(
'We will prompt your device to confirm using biometrics or your hardware key.'
)}
</p>
</div>
</div>
</div>
{!methods.passkeySupported && (
<p className='text-destructive text-sm'>
{t('This device does not support Passkey verification.')}
</p>
)}
</TabsContent>
</Tabs>
</div>
</div>
</div>
{!methods.passkeySupported && (
<p className='text-destructive text-sm'>
{t('This device does not support Passkey verification.')}
</p>
)}
</div>
<DialogFooter className='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'>
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</DialogFooter>
</div>
</DialogContent>
</TabsContent>
</Tabs>
)}
</Dialog>
)
}
......@@ -33,14 +33,6 @@ import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormField,
......@@ -50,6 +42,7 @@ import {
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile'
import { login, wechatLoginByCode } from '@/features/auth/api'
......@@ -414,43 +407,16 @@ export function UserAuthForm({
<Dialog
open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange}
>
<DialogContent className='max-w-sm'>
<DialogHeader className='text-left'>
<DialogTitle>{t('WeChat sign in')}</DialogTitle>
<DialogDescription>
{t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
</DialogDescription>
</DialogHeader>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
title={t('WeChat sign in')}
description={t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
contentClassName='max-w-sm'
headerClassName='text-left'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
......@@ -474,8 +440,32 @@ export function UserAuthForm({
) : null}
{t('Confirm')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog>
)}
</Form>
......
......@@ -27,14 +27,6 @@ import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormField,
......@@ -44,6 +36,7 @@ import {
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile'
import { register, wechatLoginByCode } from '@/features/auth/api'
......@@ -387,43 +380,16 @@ export function SignUpForm({
<Dialog
open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange}
>
<DialogContent className='max-w-sm'>
<DialogHeader className='text-left'>
<DialogTitle>{t('WeChat sign in')}</DialogTitle>
<DialogDescription>
{t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
</DialogDescription>
</DialogHeader>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
title={t('WeChat sign in')}
description={t(
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
)}
contentClassName='max-w-sm'
headerClassName='text-left'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
......@@ -447,8 +413,32 @@ export function SignUpForm({
) : null}
{t('Confirm')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog>
)}
</Form>
......
......@@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table'
import { Power, PowerOff, Tag, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
......@@ -38,6 +30,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import {
handleBatchDelete,
handleBatchDisable,
......@@ -188,29 +181,21 @@ export function DataTableBulkActions<TData>({
</BulkActionsToolbar>
{/* Set Tag Dialog */}
<Dialog open={showTagDialog} onOpenChange={setShowTagDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Set Tag')}</DialogTitle>
<DialogDescription>
{t('Set a tag for')} {selectedIds.length}{' '}
{t('selected channel(s). Leave empty to remove tag.')}
</DialogDescription>
</DialogHeader>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label htmlFor='tag'>{t('Tag')}</Label>
<Input
id='tag'
placeholder={t('Enter tag name (optional)')}
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Dialog
open={showTagDialog}
onOpenChange={setShowTagDialog}
title={t('Set Tag')}
description={
<>
{t('Set a tag for')}
{selectedIds.length}{' '}
{t('selected channel(s). Leave empty to remove tag.')}
</>
}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => {
......@@ -221,22 +206,37 @@ export function DataTableBulkActions<TData>({
{t('Cancel')}
</Button>
<Button onClick={handleSetTag}>{t('Set Tag')}</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label htmlFor='tag'>{t('Tag')}</Label>
<Input
id='tag'
placeholder={t('Enter tag name (optional)')}
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</div>
</div>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Delete Channels?')}</DialogTitle>
<DialogDescription>
{t('Are you sure you want to delete')} {selectedIds.length}{' '}
{t('channel(s)? This action cannot be undone.')}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Dialog
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
title={t('Delete Channels?')}
description={
<>
{t('Are you sure you want to delete')}
{selectedIds.length}{' '}
{t('channel(s)? This action cannot be undone.')}
</>
}
contentHeight='auto'
footer={
<>
<Button
variant='outline'
onClick={() => setShowDeleteConfirm(false)}
......@@ -246,8 +246,10 @@ export function DataTableBulkActions<TData>({
<Button variant='destructive' onClick={handleDeleteAll}>
{t('Delete')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{' '}
</Dialog>
</>
)
......
......@@ -24,14 +24,7 @@ import { toast } from 'sonner'
import { formatCurrencyFromUSD } from '@/lib/currency'
import { formatTimestampToDate } from '@/lib/format'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog } from '@/components/dialog'
import { getCodexUsage, updateChannelBalance } from '../../api'
import { channelsQueryKeys } from '../../lib'
import { useChannels } from '../channels-provider'
......@@ -161,53 +154,55 @@ export function BalanceQueryDialog({
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Query Balance')}</DialogTitle>
<DialogDescription>
{t('Update balance for:')} <strong>{currentRow.name}</strong>
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
{/* Current Balance Display */}
<div className='bg-muted/50 rounded-lg border p-4'>
<div className='text-muted-foreground mb-2 flex items-center gap-2 text-sm'>
<DollarSign className='h-4 w-4' />
<span>{t('Current Balance')}</span>
</div>
<div className='text-2xl font-bold'>
{balance !== null
? formatBalance(balance)
: formatBalance(currentRow.balance)}
</div>
<div className='text-muted-foreground mt-2 text-xs'>
{t('Last updated:')}{' '}
{formatDate(
balanceUpdatedTime ?? currentRow.balance_updated_time
)}
</div>
</div>
{/* Balance Update Button */}
<Button
className='w-full'
onClick={handleQueryBalance}
disabled={isQuerying}
>
{isQuerying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{!isQuerying && <RefreshCw className='mr-2 h-4 w-4' />}
{isQuerying ? t('Querying...') : t('Update Balance')}
</Button>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleClose}
title={t('Query Balance')}
description={
<>
{t('Update balance for:')}
<strong>{currentRow.name}</strong>
</>
}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleClose} disabled={isQuerying}>
{t('Close')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
{/* Current Balance Display */}
<div className='bg-muted/50 rounded-lg border p-4'>
<div className='text-muted-foreground mb-2 flex items-center gap-2 text-sm'>
<DollarSign className='h-4 w-4' />
<span>{t('Current Balance')}</span>
</div>
<div className='text-2xl font-bold'>
{balance !== null
? formatBalance(balance)
: formatBalance(currentRow.balance)}
</div>
<div className='text-muted-foreground mt-2 text-xs'>
{t('Last updated:')}{' '}
{formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)}
</div>
</div>
{/* Balance Update Button */}
<Button
className='w-full'
onClick={handleQueryBalance}
disabled={isQuerying}
>
{isQuerying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{!isQuerying && <RefreshCw className='mr-2 h-4 w-4' />}
{isQuerying ? t('Querying...') : t('Update Balance')}
</Button>
</div>
</Dialog>
)
}
......@@ -24,15 +24,8 @@ import { tryPrettyJson } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { completeCodexOAuth, startCodexOAuth } from '../../api'
type CodexOAuthDialogProps = {
......@@ -129,78 +122,18 @@ export function CodexOAuthDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-2xl'>
<DialogHeader>
<DialogTitle>{t('Codex Authorization')}</DialogTitle>
<DialogDescription>
{t(
'Generate a Codex OAuth credential and paste it into the channel key field.'
)}
</DialogDescription>
</DialogHeader>
<div className='space-y-4'>
<Alert>
<AlertDescription>
{t(
'1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".'
)}
</AlertDescription>
</Alert>
<div className='flex flex-wrap gap-2'>
<Button onClick={handleStart} disabled={state.isStarting}>
{state.isStarting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : (
<ExternalLink className='mr-2 h-4 w-4' />
)}
{t('Open authorization page')}
</Button>
<Button
type='button'
variant='outline'
disabled={!canCopyAuthorizeUrl}
onClick={async () => {
if (!state.authorizeUrl) return
await copyToClipboard(state.authorizeUrl)
}}
aria-label={t('Copy authorization link')}
title={t('Copy authorization link')}
>
{copiedText === state.authorizeUrl ? (
<Check className='mr-2 h-4 w-4 text-green-600' />
) : (
<Copy className='mr-2 h-4 w-4' />
)}
{t('Copy authorization link')}
</Button>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Callback URL')}</div>
<Input
value={state.callbackUrl}
onChange={(e) =>
setState((prev) => ({ ...prev, callbackUrl: e.target.value }))
}
placeholder={t(
'Paste the full callback URL (includes code & state)'
)}
autoComplete='off'
spellCheck={false}
/>
<div className='text-muted-foreground text-xs'>
{t(
'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.'
)}
</div>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Codex Authorization')}
description={t(
'Generate a Codex OAuth credential and paste it into the channel key field.'
)}
contentClassName='sm:max-w-2xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
......@@ -215,8 +148,68 @@ export function CodexOAuthDialog({
)}
{state.isCompleting ? t('Generating...') : t('Generate credential')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4'>
<Alert>
<AlertDescription>
{t(
'1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".'
)}
</AlertDescription>
</Alert>
<div className='flex flex-wrap gap-2'>
<Button onClick={handleStart} disabled={state.isStarting}>
{state.isStarting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : (
<ExternalLink className='mr-2 h-4 w-4' />
)}
{t('Open authorization page')}
</Button>
<Button
type='button'
variant='outline'
disabled={!canCopyAuthorizeUrl}
onClick={async () => {
if (!state.authorizeUrl) return
await copyToClipboard(state.authorizeUrl)
}}
aria-label={t('Copy authorization link')}
title={t('Copy authorization link')}
>
{copiedText === state.authorizeUrl ? (
<Check className='mr-2 h-4 w-4 text-green-600' />
) : (
<Copy className='mr-2 h-4 w-4' />
)}
{t('Copy authorization link')}
</Button>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Callback URL')}</div>
<Input
value={state.callbackUrl}
onChange={(e) =>
setState((prev) => ({ ...prev, callbackUrl: e.target.value }))
}
placeholder={t(
'Paste the full callback URL (includes code & state)'
)}
autoComplete='off'
spellCheck={false}
/>
<div className='text-muted-foreground text-xs'>
{t(
'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.'
)}
</div>
</div>
</div>
</Dialog>
)
}
......@@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { handleCopyChannel } from '../../lib'
import { useChannels } from '../channels-provider'
......@@ -74,45 +67,20 @@ export function CopyChannelDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Copy Channel')}</DialogTitle>
<DialogDescription>
{t('Create a copy of:')} <strong>{currentRow.name}</strong>
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='suffix'>{t('Name Suffix')}</Label>
<Input
id='suffix'
placeholder={t('_copy')}
value={suffix}
onChange={(e) => setSuffix(e.target.value)}
disabled={isCopying}
/>
<p className='text-muted-foreground text-xs'>
{t('New name will be:')} {currentRow.name}
{suffix}
</p>
</div>
<div className='flex items-center space-x-2'>
<Checkbox
id='reset-balance'
checked={resetBalance}
onCheckedChange={(checked) => setResetBalance(!!checked)}
disabled={isCopying}
/>
<Label htmlFor='reset-balance' className='text-sm font-normal'>
{t('Reset balance and used quota')}
</Label>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Copy Channel')}
description={
<>
{t('Create a copy of:')}
<strong>{currentRow.name}</strong>
</>
}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
......@@ -122,10 +90,39 @@ export function CopyChannelDialog({
</Button>
<Button onClick={handleCopy} disabled={isCopying}>
{isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isCopying ? 'Copying...' : 'Copy Channel'}
{isCopying ? t('Copying...') : t('Copy Channel')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='suffix'>{t('Name Suffix')}</Label>
<Input
id='suffix'
placeholder={t('_copy')}
value={suffix}
onChange={(e) => setSuffix(e.target.value)}
disabled={isCopying}
/>
<p className='text-muted-foreground text-xs'>
{t('New name will be:')} {currentRow.name}
{suffix}
</p>
</div>
<div className='flex items-center space-x-2'>
<Checkbox
id='reset-balance'
checked={resetBalance}
onCheckedChange={(checked) => setResetBalance(!!checked)}
disabled={isCopying}
/>
<Label htmlFor='reset-balance' className='text-sm font-normal'>
{t('Reset balance and used quota')}
</Label>
</div>
</div>
</Dialog>
)
}
......@@ -21,16 +21,9 @@ import { AlertTriangle } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
interface StatusCodeRiskDialogProps {
open: boolean
......@@ -84,73 +77,22 @@ export function StatusCodeRiskDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='max-w-lg'>
<DialogHeader>
<DialogTitle className='text-destructive flex items-center gap-2'>
<AlertTriangle className='h-5 w-5' />
{t('High-risk operation confirmation')}
</DialogTitle>
<DialogDescription>
{t('High-risk status code retry risk disclaimer')}
</DialogDescription>
</DialogHeader>
<div className='space-y-4'>
{detailItems.length > 0 && (
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'>
<p className='mb-2 text-sm font-medium'>
{t('Detected high-risk status code redirect rules')}
</p>
<ul className='list-inside list-disc text-sm'>
{detailItems.map((item) => (
<li key={item} className='font-mono text-xs'>
{item}
</li>
))}
</ul>
</div>
)}
<div className='space-y-2'>
{CHECKLIST_KEYS.map((key, idx) => (
<div key={key} className='flex items-start gap-2'>
<Checkbox
id={`risk-check-${idx}`}
checked={checkedItems.has(idx)}
onCheckedChange={() => toggleCheck(idx)}
/>
<Label
htmlFor={`risk-check-${idx}`}
className='text-sm leading-tight'
>
{t(key)}
</Label>
</div>
))}
</div>
<div className='space-y-1.5'>
<Label className='text-sm'>
{t('Action confirmation')}:{' '}
<code className='bg-muted rounded px-1 text-xs'>
{requiredText}
</code>
</Label>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={t('High-risk status code retry input placeholder')}
/>
{confirmText && !textMatches && (
<p className='text-destructive text-xs'>
{t('High-risk status code retry input mismatch')}
</p>
)}
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={
<>
<AlertTriangle className='h-5 w-5' />
{t('High-risk operation confirmation')}
</>
}
description={t('High-risk status code retry risk disclaimer')}
contentClassName='max-w-lg'
titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleCancel}>
{t('Cancel')}
</Button>
......@@ -161,8 +103,62 @@ export function StatusCodeRiskDialog({
>
{t('I confirm enabling high-risk retry')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4'>
{detailItems.length > 0 && (
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'>
<p className='mb-2 text-sm font-medium'>
{t('Detected high-risk status code redirect rules')}
</p>
<ul className='list-inside list-disc text-sm'>
{detailItems.map((item) => (
<li key={item} className='font-mono text-xs'>
{item}
</li>
))}
</ul>
</div>
)}
<div className='space-y-2'>
{CHECKLIST_KEYS.map((key, idx) => (
<div key={key} className='flex items-start gap-2'>
<Checkbox
id={`risk-check-${idx}`}
checked={checkedItems.has(idx)}
onCheckedChange={() => toggleCheck(idx)}
/>
<Label
htmlFor={`risk-check-${idx}`}
className='text-sm leading-tight'
>
{t(key)}
</Label>
</div>
))}
</div>
<div className='space-y-1.5'>
<Label className='text-sm'>
{t('Action confirmation')}:{' '}
<code className='bg-muted rounded px-1 text-xs'>
{requiredText}
</code>
</Label>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={t('High-risk status code retry input placeholder')}
/>
{confirmText && !textMatches && (
<p className='text-destructive text-xs'>
{t('High-risk status code retry input mismatch')}
</p>
)}
</div>
</div>
</Dialog>
)
}
......@@ -23,18 +23,11 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { MultiSelect } from '@/components/multi-select'
import {
getTagModels,
......@@ -190,115 +183,118 @@ export function TagBatchEditDialog({
if (!currentTag) return null
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className='max-h-[90vh] max-w-2xl overflow-y-auto'>
<DialogHeader>
<DialogTitle>{t('Batch Edit by Tag')}</DialogTitle>
<DialogDescription>
{t('Edit all channels with tag:')} <strong>{currentTag}</strong>
</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className='flex items-center justify-center py-12'>
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
</div>
) : (
<Dialog
open={open}
onOpenChange={handleClose}
title={t('Batch Edit by Tag')}
description={
<>
{t('Edit all channels with tag:')}
<strong>{currentTag}</strong>
</>
}
contentClassName='max-w-2xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
!isLoading ? (
<>
<div className='space-y-4 py-4'>
<Alert>
<AlertCircle className='h-4 w-4' />
<AlertDescription>
{t(
'All edits are overwrite operations. Leave fields empty to keep current values unchanged.'
)}
</AlertDescription>
</Alert>
{/* Tag Name */}
<div className='space-y-2'>
<Label htmlFor='new-tag'>{t('Tag Name')}</Label>
<Input
id='new-tag'
placeholder={t(
'Enter new tag name (leave empty to disband tag)'
)}
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
disabled={isSaving}
/>
<p className='text-muted-foreground text-xs'>
{t('Leave empty to disband the tag')}
</p>
</div>
{/* Models */}
<div className='space-y-2'>
<Label htmlFor='models'>{t('Models')}</Label>
<Textarea
id='models'
placeholder={t(
'Comma-separated model names (leave empty to keep current)'
)}
value={models}
onChange={(e) => setModels(e.target.value)}
disabled={isSaving}
rows={3}
/>
<p className='text-muted-foreground text-xs'>
{t(
'Current models for the longest channel in this tag. May not include all models from all channels.'
)}
</p>
</div>
{/* Model Mapping */}
<div className='space-y-2'>
<Label htmlFor='model-mapping'>{t('Model Mapping')}</Label>
<ModelMappingEditor
value={modelMapping}
onChange={setModelMapping}
disabled={isSaving}
/>
</div>
<Button variant='outline' onClick={handleClose} disabled={isSaving}>
{t('Cancel')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{isSaving ? t('Saving...') : t('Save Changes')}
</Button>
</>
) : null
}
>
{isLoading ? (
<div className='flex items-center justify-center py-12'>
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
</div>
) : (
<>
<div className='space-y-4 py-4'>
<Alert>
<AlertCircle className='h-4 w-4' />
<AlertDescription>
{t(
'All edits are overwrite operations. Leave fields empty to keep current values unchanged.'
)}
</AlertDescription>
</Alert>
{/* Tag Name */}
<div className='space-y-2'>
<Label htmlFor='new-tag'>{t('Tag Name')}</Label>
<Input
id='new-tag'
placeholder={t(
'Enter new tag name (leave empty to disband tag)'
)}
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
disabled={isSaving}
/>
<p className='text-muted-foreground text-xs'>
{t('Leave empty to disband the tag')}
</p>
</div>
{/* Groups */}
<div className='space-y-2'>
<Label htmlFor='groups'>{t('Groups')}</Label>
{isLoadingGroups ? (
<Skeleton className='h-10 w-full' />
) : (
<MultiSelect
options={groupOptions}
selected={groups}
onChange={setGroups}
placeholder={t(
'Select groups (leave empty to keep current)'
)}
/>
{/* Models */}
<div className='space-y-2'>
<Label htmlFor='models'>{t('Models')}</Label>
<Textarea
id='models'
placeholder={t(
'Comma-separated model names (leave empty to keep current)'
)}
value={models}
onChange={(e) => setModels(e.target.value)}
disabled={isSaving}
rows={3}
/>
<p className='text-muted-foreground text-xs'>
{t(
'Current models for the longest channel in this tag. May not include all models from all channels.'
)}
<p className='text-muted-foreground text-xs'>
{t('User groups that can access channels with this tag')}
</p>
</div>
</p>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={handleClose}
{/* Model Mapping */}
<div className='space-y-2'>
<Label htmlFor='model-mapping'>{t('Model Mapping')}</Label>
<ModelMappingEditor
value={modelMapping}
onChange={setModelMapping}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? t('Saving...') : t('Save Changes')}
</Button>
</DialogFooter>
</>
)}
</DialogContent>
/>
</div>
{/* Groups */}
<div className='space-y-2'>
<Label htmlFor='groups'>{t('Groups')}</Label>
{isLoadingGroups ? (
<Skeleton className='h-10 w-full' />
) : (
<MultiSelect
options={groupOptions}
selected={groups}
onChange={setGroups}
placeholder={t('Select groups (leave empty to keep current)')}
/>
)}
<p className='text-muted-foreground text-xs'>
{t('User groups that can access channels with this tag')}
</p>
</div>
</div>
</>
)}
</Dialog>
)
}
......@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'
import { formatDateTimeObject } from '@/lib/time'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Markdown } from '@/components/ui/markdown'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
interface AnnouncementDetailModalProps {
open: boolean
......@@ -47,38 +41,39 @@ export function AnnouncementDetailModal({
}: AnnouncementDetailModalProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Announcement Details')}</DialogTitle>
{announcement?.publishDate && (
<DialogDescription>
{t('Published:')}{' '}
{formatDateTimeObject(new Date(announcement.publishDate))}
</DialogDescription>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Announcement Details')}
description={
announcement?.publishDate
? `${t('Published:')} ${formatDateTimeObject(new Date(announcement.publishDate))}`
: undefined
}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-[min(58vh,520px)] pr-4'>
<div className='space-y-4'>
{announcement?.content && (
<div>
<h4 className='mb-2 font-medium'>{t('Content')}</h4>
<Markdown>{announcement.content}</Markdown>
</div>
)}
</DialogHeader>
<ScrollArea className='max-h-[60vh] pr-4'>
<div className='space-y-4'>
{announcement?.content && (
<div>
<h4 className='mb-2 font-medium'>{t('Content')}</h4>
<Markdown>{announcement.content}</Markdown>
</div>
)}
{announcement?.extra && (
<div>
<h4 className='mb-2 font-medium'>
{t('Additional Information')}
</h4>
<Markdown className='text-muted-foreground'>
{announcement.extra}
</Markdown>
</div>
)}
</div>
</ScrollArea>
</DialogContent>
{announcement?.extra && (
<div>
<h4 className='mb-2 font-medium'>
{t('Additional Information')}
</h4>
<Markdown className='text-muted-foreground'>
{announcement.extra}
</Markdown>
</div>
)}
</div>
</ScrollArea>
</Dialog>
)
}
......@@ -23,15 +23,9 @@ import { toast } from 'sonner'
import { getUserModels } from '@/lib/api'
import { Button } from '@/components/ui/button'
import { ComboboxInput } from '@/components/ui/combobox-input'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Dialog } from '@/components/dialog'
const APP_CONFIGS = {
claude: {
......@@ -151,76 +145,78 @@ export function CCSwitchDialog(props: Props) {
}
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Import to CC Switch')}</DialogTitle>
</DialogHeader>
<div className='space-y-4'>
<div className='space-y-2'>
<Label>{t('Application')}</Label>
<RadioGroup
value={app}
onValueChange={handleAppChange}
className='flex gap-4'
>
{(
Object.entries(APP_CONFIGS) as [
AppType,
(typeof APP_CONFIGS)[AppType],
][]
).map(([key, cfg]) => (
<div key={key} className='flex items-center gap-2'>
<RadioGroupItem value={key} id={`app-${key}`} />
<Label htmlFor={`app-${key}`} className='cursor-pointer'>
{cfg.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label>{t('Name')}</Label>
<ComboboxInput
options={[]}
value={name}
onValueChange={setName}
placeholder={currentConfig.defaultName}
emptyText=''
allowCustomValue={true}
/>
</div>
{currentConfig.modelFields.map((field) => (
<div key={field.key} className='space-y-2'>
<Label>
{t(field.labelKey)}
{field.required && (
<span className='text-destructive ml-0.5'>*</span>
)}
</Label>
<ComboboxInput
options={modelOptions}
value={models[field.key] || ''}
onValueChange={(v) =>
setModels((prev) => ({ ...prev, [field.key]: v }))
}
placeholder={t('Select or enter model name')}
emptyText={t('No models found')}
/>
</div>
))}
</div>
<DialogFooter>
<Dialog
open={props.open}
onOpenChange={props.onOpenChange}
title={t('Import to CC Switch')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={() => props.onOpenChange(false)}>
{t('Cancel')}
</Button>
<Button onClick={handleSubmit}>{t('Open CC Switch')}</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4'>
<div className='space-y-2'>
<Label>{t('Application')}</Label>
<RadioGroup
value={app}
onValueChange={handleAppChange}
className='flex gap-4'
>
{(
Object.entries(APP_CONFIGS) as [
AppType,
(typeof APP_CONFIGS)[AppType],
][]
).map(([key, cfg]) => (
<div key={key} className='flex items-center gap-2'>
<RadioGroupItem value={key} id={`app-${key}`} />
<Label htmlFor={`app-${key}`} className='cursor-pointer'>
{cfg.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label>{t('Name')}</Label>
<ComboboxInput
options={[]}
value={name}
onValueChange={setName}
placeholder={currentConfig.defaultName}
emptyText=''
allowCustomValue={true}
/>
</div>
{currentConfig.modelFields.map((field) => (
<div key={field.key} className='space-y-2'>
<Label>
{t(field.labelKey)}
{field.required && (
<span className='text-destructive ml-0.5'>*</span>
)}
</Label>
<ComboboxInput
options={modelOptions}
value={models[field.key] || ''}
onValueChange={(v) =>
setModels((prev) => ({ ...prev, [field.key]: v }))
}
placeholder={t('Select or enter model name')}
emptyText={t('No models found')}
/>
</div>
))}
</div>
</Dialog>
)
}
......@@ -25,19 +25,12 @@ import { toast } from 'sonner'
import { copyToClipboard } from '@/lib/copy-to-clipboard'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import {
handleBatchEnableModels,
handleBatchDisableModels,
......@@ -187,19 +180,17 @@ export function DataTableBulkActions<TData>({
</BulkActionsToolbar>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Delete Models?')}</DialogTitle>
<DialogDescription>
{t(
'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.',
{ count: selectedIds.length }
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Dialog
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
title={t('Delete Models?')}
description={t(
'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.',
{ count: selectedIds.length }
)}
contentHeight='auto'
footer={
<>
<Button
variant='outline'
onClick={() => setShowDeleteConfirm(false)}
......@@ -209,8 +200,10 @@ export function DataTableBulkActions<TData>({
<Button variant='destructive' onClick={handleDeleteAll}>
{t('Delete')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{' '}
</Dialog>
</>
)
......
......@@ -17,14 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
type DescriptionDialogProps = {
open: boolean
......@@ -41,21 +35,22 @@ export function DescriptionDialog({
}: DescriptionDialogProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='max-w-2xl'>
<DialogHeader>
<DialogTitle>{modelName}</DialogTitle>
<DialogDescription>{t('Model Description')}</DialogDescription>
</DialogHeader>
<ScrollArea className='max-h-96'>
<div className='space-y-2 pr-4'>
<p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'>
{description}
</p>
</div>
</ScrollArea>
</DialogContent>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={modelName}
description={t('Model Description')}
contentClassName='max-w-2xl'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-96'>
<div className='space-y-2 pr-4'>
<p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'>
{description}
</p>
</div>
</ScrollArea>
</Dialog>
)
}
......@@ -22,15 +22,9 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { Dialog } from '@/components/dialog'
import { estimatePrice, extendDeployment, getDeployment } from '../../api'
import { deploymentsQueryKeys } from '../../lib'
......@@ -164,62 +158,16 @@ export function ExtendDeploymentDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Extend deployment')}</DialogTitle>
</DialogHeader>
{isLoadingDetails ? (
<div className='flex items-center justify-center py-10'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
</div>
) : (
<div className='space-y-4'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Duration (hours)')}</div>
<Input
type='number'
min={1}
value={hours}
onChange={(e) => setHours(toInt(e.target.value, 1))}
/>
<div className='text-muted-foreground text-xs'>
{t('This will extend the deployment by the specified hours.')}
</div>
</div>
<Separator />
<div className='space-y-1'>
<div className='text-sm font-medium'>{t('Estimated cost')}</div>
<div className='text-muted-foreground text-sm'>
{isLoadingPrice || isFetchingPrice ? (
<span className='inline-flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
{t('Calculating...')}
</span>
) : priceParams ? (
priceSummary || t('Not available')
) : (
t('Not available')
)}
</div>
{!priceParams ? (
<div className='text-muted-foreground text-xs'>
{t('Unable to estimate price for this deployment.')}
</div>
) : null}
</div>
</div>
)}
<DialogFooter className='mt-4'>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Extend deployment')}
contentClassName='sm:max-w-lg'
footerClassName='mt-4'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')}
</Button>
......@@ -229,8 +177,57 @@ export function ExtendDeploymentDialog({
) : null}
{t('Extend')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
{isLoadingDetails ? (
<div className='flex items-center justify-center py-10'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
</div>
) : (
<div className='space-y-4'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Duration (hours)')}</div>
<Input
type='number'
min={1}
value={hours}
onChange={(e) => setHours(toInt(e.target.value, 1))}
/>
<div className='text-muted-foreground text-xs'>
{t('This will extend the deployment by the specified hours.')}
</div>
</div>
<Separator />
<div className='space-y-1'>
<div className='text-sm font-medium'>{t('Estimated cost')}</div>
<div className='text-muted-foreground text-sm'>
{isLoadingPrice || isFetchingPrice ? (
<span className='inline-flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
{t('Calculating...')}
</span>
) : priceParams ? (
priceSummary || t('Not available')
) : (
t('Not available')
)}
</div>
{!priceParams ? (
<div className='text-muted-foreground text-xs'>
{t('Unable to estimate price for this deployment.')}
</div>
) : null}
</div>
</div>
)}
</Dialog>
)
}
......@@ -22,14 +22,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { checkClusterNameAvailability, updateDeploymentName } from '../../api'
import { deploymentsQueryKeys } from '../../lib'
......@@ -111,27 +105,16 @@ export function RenameDeploymentDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Rename deployment')}</DialogTitle>
</DialogHeader>
<div className='space-y-2'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<Input
placeholder={t('Enter a new name')}
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete='off'
/>
<div className='text-muted-foreground text-xs'>{helper}</div>
</div>
<DialogFooter className='mt-4'>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Rename deployment')}
contentClassName='sm:max-w-lg'
footerClassName='mt-4'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')}
</Button>
......@@ -141,8 +124,22 @@ export function RenameDeploymentDialog({
) : null}
{t('Rename')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-2'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<Input
placeholder={t('Enter a new name')}
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete='off'
/>
<div className='text-muted-foreground text-xs'>{helper}</div>
</div>
</Dialog>
)
}
......@@ -24,16 +24,9 @@ import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { useIsMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge'
import { syncUpstream, previewUpstreamDiff } from '../../api'
import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants'
......@@ -125,117 +118,16 @@ export function SyncWizardDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className='flex max-h-[90vh] w-full flex-col gap-4 p-4 sm:max-w-2xl sm:p-6'
initialFocus={!isMobile}
>
<DialogHeader className='flex-shrink-0 text-start'>
<DialogTitle>{t('Sync Upstream Models')}</DialogTitle>
<DialogDescription>
{t('Synchronize models and vendors from an upstream source')}
</DialogDescription>
</DialogHeader>
<div className='flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto'>
<div className='space-y-3'>
<div>
<Label className='text-base'>{t('Select Sync Source')}</Label>
<p className='text-muted-foreground text-sm'>
{t('Choose where to fetch upstream metadata.')}
</p>
</div>
<RadioGroup
value={source}
onValueChange={(value) => {
const selected = SYNC_SOURCE_OPTIONS.find(
(option) => option.value === value
)
if (!selected || selected.disabled) return
setSource(selected.value)
}}
className='grid gap-3 md:grid-cols-2'
>
{SYNC_SOURCE_OPTIONS.map((option) => {
const isActive = source === option.value
const isDisabled = option.disabled
return (
<Label
key={option.value}
htmlFor={`sync-source-${option.value}`}
className={cn(
'flex-col items-start gap-0 rounded-lg border p-4 font-normal transition-all',
isActive && 'border-primary ring-primary ring-1',
isDisabled
? 'cursor-not-allowed opacity-60'
: 'hover:border-primary/60 cursor-pointer'
)}
>
<div className='flex items-start gap-3'>
<RadioGroupItem
value={option.value}
id={`sync-source-${option.value}`}
disabled={isDisabled}
/>
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<span className='font-medium'>{option.label}</span>
{option.value === 'official' && (
<StatusBadge
label='Default'
variant='neutral'
copyable={false}
/>
)}
</div>
<p className='text-muted-foreground text-sm'>
{option.description}
</p>
</div>
</div>
</Label>
)
})}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label className='text-base'>{t('Select Language')}</Label>
<RadioGroup
value={locale}
onValueChange={(v) => setLocale(v as SyncLocale)}
className='grid gap-3 sm:grid-cols-3'
>
{SYNC_LOCALE_OPTIONS.map((option) => (
<div
key={option.value}
className='flex items-center space-x-2 rounded-lg border p-3'
>
<RadioGroupItem
value={option.value}
id={`locale-${option.value}`}
/>
<Label
htmlFor={`locale-${option.value}`}
className='cursor-pointer font-normal'
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='bg-muted/50 rounded-lg border p-4'>
<p className='text-muted-foreground text-sm'>
{t(
'The sync will fetch missing models and vendors from the selected source. Existing records are updated only when you approve conflicts.'
)}
</p>
</div>
</div>
<DialogFooter className='flex-shrink-0 gap-2 sm:justify-end'>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Sync Upstream Models')}
description={t('Synchronize models and vendors from an upstream source')}
initialFocus={!isMobile}
contentHeight='auto'
bodyClassName='flex flex-col gap-6'
footer={
<>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
......@@ -246,10 +138,106 @@ export function SyncWizardDialog({
<Button onClick={handleSync} disabled={isSyncing}>
{isSyncing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
<RefreshCw className='mr-2 h-4 w-4' />
{isSyncing ? 'Syncing...' : 'Sync Now'}
{isSyncing ? t('Syncing...') : t('Sync Now')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-3'>
<div>
<Label className='text-base'>{t('Select Sync Source')}</Label>
<p className='text-muted-foreground text-sm'>
{t('Choose where to fetch upstream metadata.')}
</p>
</div>
<RadioGroup
value={source}
onValueChange={(value) => {
const selected = SYNC_SOURCE_OPTIONS.find(
(option) => option.value === value
)
if (!selected || selected.disabled) return
setSource(selected.value)
}}
className='grid gap-3 md:grid-cols-2'
>
{SYNC_SOURCE_OPTIONS.map((option) => {
const isActive = source === option.value
const isDisabled = option.disabled
return (
<Label
key={option.value}
htmlFor={`sync-source-${option.value}`}
className={cn(
'flex-col items-start gap-0 rounded-lg border p-4 font-normal transition-all',
isActive && 'border-primary ring-primary ring-1',
isDisabled
? 'cursor-not-allowed opacity-60'
: 'hover:border-primary/60 cursor-pointer'
)}
>
<div className='flex items-start gap-3'>
<RadioGroupItem
value={option.value}
id={`sync-source-${option.value}`}
disabled={isDisabled}
/>
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<span className='font-medium'>{option.label}</span>
{option.value === 'official' && (
<StatusBadge
label='Default'
variant='neutral'
copyable={false}
/>
)}
</div>
<p className='text-muted-foreground text-sm'>
{option.description}
</p>
</div>
</div>
</Label>
)
})}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label className='text-base'>{t('Select Language')}</Label>
<RadioGroup
value={locale}
onValueChange={(v) => setLocale(v as SyncLocale)}
className='grid gap-3 sm:grid-cols-3'
>
{SYNC_LOCALE_OPTIONS.map((option) => (
<div
key={option.value}
className='flex items-center space-x-2 rounded-lg border p-3'
>
<RadioGroupItem
value={option.value}
id={`locale-${option.value}`}
/>
<Label
htmlFor={`locale-${option.value}`}
className='cursor-pointer font-normal'
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='bg-muted/50 rounded-lg border p-4'>
<p className='text-muted-foreground text-sm'>
{t(
'The sync will fetch missing models and vendors from the selected source. Existing records are updated only when you approve conflicts.'
)}
</p>
</div>
</Dialog>
)
}
......@@ -25,14 +25,6 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormDescription,
......@@ -43,6 +35,7 @@ import {
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { createVendor, updateVendor } from '../../api'
import { vendorsQueryKeys, modelsQueryKeys } from '../../lib'
import { vendorFormSchema, type Vendor } from '../../types'
......@@ -53,6 +46,8 @@ type VendorMutateDialogProps = {
currentVendor?: Vendor | null
}
const VENDOR_MUTATE_FORM_ID = 'vendor-mutate-form'
export function VendorMutateDialog({
open,
onOpenChange,
......@@ -118,98 +113,107 @@ export function VendorMutateDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{isEdit ? t('Edit Vendor') : t('Create Vendor')}
</DialogTitle>
<DialogDescription>
{isEdit
? t('Update vendor information for {{name}}', {
name: currentVendor?.name,
})
: t('Add a new vendor to the system')}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Vendor Name *')}</FormLabel>
<FormControl>
<Input
placeholder={t('OpenAI, Anthropic, etc.')}
{...field}
/>
</FormControl>
<FormDescription>
{t('The unique name for this vendor')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Description')}</FormLabel>
<FormControl>
<Textarea
placeholder={t('Describe this vendor...')}
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={isEdit ? t('Edit Vendor') : t('Create Vendor')}
description={
isEdit
? t('Update vendor information for {{name}}', {
name: currentVendor?.name,
})
: t('Add a new vendor to the system')
}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button
type='submit'
form={VENDOR_MUTATE_FORM_ID}
disabled={isSaving}
>
{isSaving ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : null}
{isSaving ? t('Saving...') : isEdit ? t('Update') : t('Create')}
</Button>
</>
}
>
<Form {...form}>
<form
id={VENDOR_MUTATE_FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-4'
>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Vendor Name *')}</FormLabel>
<FormControl>
<Input
placeholder={t('OpenAI, Anthropic, etc.')}
{...field}
/>
</FormControl>
<FormDescription>
{t('The unique name for this vendor')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='icon'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Icon')}</FormLabel>
<FormControl>
<Input
placeholder={t('OpenAI, Anthropic, Google, etc.')}
{...field}
/>
</FormControl>
<FormDescription>
{t('@lobehub/icons key name')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Description')}</FormLabel>
<FormControl>
<Textarea
placeholder={t('Describe this vendor...')}
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={isSaving}>
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isSaving ? 'Saving...' : isEdit ? 'Update' : 'Create'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
<FormField
control={form.control}
name='icon'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Icon')}</FormLabel>
<FormControl>
<Input
placeholder={t('OpenAI, Anthropic, Google, etc.')}
{...field}
/>
</FormControl>
<FormDescription>
{t('@lobehub/icons key name')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</Dialog>
)
}
......@@ -32,12 +32,6 @@ import { formatQuotaWithCurrency } from '@/lib/currency'
import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton'
import {
Tooltip,
......@@ -45,6 +39,7 @@ import {
TooltipTrigger,
TooltipProvider,
} from '@/components/ui/tooltip'
import { Dialog } from '@/components/dialog'
import { Turnstile } from '@/components/turnstile'
import { getCheckinStatus, performCheckin } from '../api'
import type { CheckinRecord } from '../types'
......@@ -253,27 +248,26 @@ export function CheckinCalendarCard({
setTurnstileWidgetKey((v) => v + 1)
}
}}
title={t('Security Check')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Security Check')}</DialogTitle>
</DialogHeader>
<div className='text-muted-foreground text-sm'>
{t('Please complete the security check to continue.')}
</div>
<div className='flex justify-center py-4'>
<Turnstile
key={turnstileWidgetKey}
siteKey={turnstileSiteKey}
onVerify={(token) => {
doCheckin(token)
}}
onExpire={() => {
setTurnstileWidgetKey((v) => v + 1)
}}
/>
</div>
</DialogContent>
<div className='text-muted-foreground text-sm'>
{t('Please complete the security check to continue.')}
</div>
<div className='flex justify-center py-4'>
<Turnstile
key={turnstileWidgetKey}
siteKey={turnstileSiteKey}
onVerify={(token) => {
doCheckin(token)
}}
onExpire={() => {
setTurnstileWidgetKey((v) => v + 1)
}}
/>
</div>
</Dialog>
<div className='bg-card overflow-hidden rounded-2xl border'>
......
......@@ -20,17 +20,10 @@ import { useEffect } from 'react'
import { RefreshCw, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import { useAccessToken } from '../../hooks'
// ============================================================================
......@@ -57,45 +50,18 @@ export function AccessTokenDialog({
}, [open, token, generate])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Access Token')}</DialogTitle>
<DialogDescription>
{t(
"Your system access token for API authentication. Keep it secure and don't share it with others."
)}
</DialogDescription>
</DialogHeader>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label>
<div className='flex gap-2'>
<Input
id='token'
type='text'
value={token}
readOnly
className='font-mono text-xs'
placeholder={t('Click "Generate" to create a token')}
/>
<CopyButton
value={token}
variant='outline'
className='size-9'
iconClassName='size-4'
tooltip={t('Copy token')}
aria-label={t('Copy token')}
/>
</div>
<p className='text-muted-foreground text-xs'>
{t('Use this token for API authentication')}
</p>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Access Token')}
description={t(
"Your system access token for API authentication. Keep it secure and don't share it with others."
)}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
......@@ -116,8 +82,35 @@ export function AccessTokenDialog({
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label>
<div className='flex gap-2'>
<Input
id='token'
type='text'
value={token}
readOnly
className='font-mono text-xs'
placeholder={t('Click "Generate" to create a token')}
/>
<CopyButton
value={token}
variant='outline'
className='size-9'
iconClassName='size-4'
tooltip={t('Copy token')}
aria-label={t('Copy token')}
/>
</div>
<p className='text-muted-foreground text-xs'>
{t('Use this token for API authentication')}
</p>
</div>
</div>
</Dialog>
)
}
......@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input'
import { updateUserProfile } from '../../api'
......@@ -114,82 +107,79 @@ export function ChangePasswordDialog({
}
}
const formId = 'change-password-form'
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>{t('Change Password')}</DialogTitle>
<DialogDescription>
{t('Update your password for account:')}{' '}
<strong>{username}</strong>
</DialogDescription>
</DialogHeader>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='currentPassword'>{t('Current Password')}</Label>
<PasswordInput
id='currentPassword'
value={formData.originalPassword}
onChange={(e) =>
handleChange('originalPassword', e.target.value)
}
disabled={loading}
required
autoComplete='current-password'
/>
</div>
<div className='space-y-2'>
<Label htmlFor='newPassword'>{t('New Password')}</Label>
<PasswordInput
id='newPassword'
value={formData.newPassword}
onChange={(e) => handleChange('newPassword', e.target.value)}
disabled={loading}
required
minLength={8}
autoComplete='new-password'
/>
<p className='text-muted-foreground text-xs'>
{t('Must be at least 8 characters')}
</p>
</div>
<div className='space-y-2'>
<Label htmlFor='confirmPassword'>
{t('Confirm New Password')}
</Label>
<PasswordInput
id='confirmPassword'
value={formData.confirmPassword}
onChange={(e) =>
handleChange('confirmPassword', e.target.value)
}
disabled={loading}
required
autoComplete='new-password'
/>
</div>
</div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={loading}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Changing...') : t('Change Password')}
</Button>
</DialogFooter>
</form>
</DialogContent>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Change Password')}
description={
<>
{t('Update your password for account:')} <strong>{username}</strong>
</>
}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button type='submit' form={formId} disabled={loading}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Changing...') : t('Change Password')}
</Button>
</>
}
>
<form id={formId} onSubmit={handleSubmit} className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='currentPassword'>{t('Current Password')}</Label>
<PasswordInput
id='currentPassword'
value={formData.originalPassword}
onChange={(e) => handleChange('originalPassword', e.target.value)}
disabled={loading}
required
autoComplete='current-password'
/>
</div>
<div className='space-y-2'>
<Label htmlFor='newPassword'>{t('New Password')}</Label>
<PasswordInput
id='newPassword'
value={formData.newPassword}
onChange={(e) => handleChange('newPassword', e.target.value)}
disabled={loading}
required
minLength={8}
autoComplete='new-password'
/>
<p className='text-muted-foreground text-xs'>
{t('Must be at least 8 characters')}
</p>
</div>
<div className='space-y-2'>
<Label htmlFor='confirmPassword'>{t('Confirm New Password')}</Label>
<PasswordInput
id='confirmPassword'
value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
disabled={loading}
required
autoComplete='new-password'
/>
</div>
</form>
</Dialog>
)
}
......@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store'
import { api } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { deleteUserAccount } from '../../api'
// ============================================================================
......@@ -101,45 +94,24 @@ export function DeleteAccountDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle className='text-destructive flex items-center gap-2'>
<AlertTriangle className='h-5 w-5' />
{t('Delete Account')}
</DialogTitle>
<DialogDescription>
{t(
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
)}
</DialogDescription>
</DialogHeader>
<div className='my-6 space-y-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: This action is permanent and irreversible!')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='confirmation'>
{t('Type')} <strong>{username}</strong> {t('to confirm')}
</Label>
<Input
id='confirmation'
type='text'
value={confirmation}
onChange={(e) => setConfirmation(e.target.value)}
disabled={loading}
placeholder={username}
autoComplete='off'
/>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={
<>
<AlertTriangle className='h-5 w-5' />
{t('Delete Account')}
</>
}
description={t(
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
)}
contentClassName='sm:max-w-md'
titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
......@@ -157,8 +129,32 @@ export function DeleteAccountDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Deleting...') : t('Delete Account')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='my-6 space-y-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: This action is permanent and irreversible!')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='confirmation'>
{t('Type')} <strong>{username}</strong> {t('to confirm')}
</Label>
<Input
id='confirmation'
type='text'
value={confirmation}
onChange={(e) => setConfirmation(e.target.value)}
disabled={loading}
placeholder={username}
autoComplete='off'
/>
</div>
</div>
</Dialog>
)
}
......@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useCountdown } from '@/hooks/use-countdown'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { sendEmailVerification, bindEmail } from '../../api'
// ============================================================================
......@@ -129,60 +122,22 @@ export function EmailBindDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Bind Email')}</DialogTitle>
<DialogDescription>
{currentEmail
? t('Current email: {{email}}. Enter a new email to change.', {
email: currentEmail,
})
: t('Bind an email address to your account.')}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='email'>{t('Email Address')}</Label>
<Input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('Enter your email')}
disabled={loading}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<div className='flex gap-2'>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code')}
disabled={loading}
maxLength={6}
/>
<Button
type='button'
variant='outline'
onClick={handleSendCode}
disabled={sendingCode || isActive || !email}
>
{isActive
? `${secondsLeft}s`
: sendingCode
? t('Sending...')
: t('Send')}
</Button>
</div>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={t('Bind Email')}
description={
currentEmail
? t('Current email: {{email}}. Enter a new email to change.', {
email: currentEmail,
})
: t('Bind an email address to your account.')
}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
......@@ -199,8 +154,48 @@ export function EmailBindDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Binding...') : t('Bind Email')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='email'>{t('Email Address')}</Label>
<Input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('Enter your email')}
disabled={loading}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<div className='flex gap-2'>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code')}
disabled={loading}
maxLength={6}
/>
<Button
type='button'
variant='outline'
onClick={handleSendCode}
disabled={sendingCode || isActive || !email}
>
{isActive
? `${secondsLeft}s`
: sendingCode
? t('Sending...')
: t('Send')}
</Button>
</div>
</div>
</div>
</Dialog>
)
}
......@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { Send } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog } from '@/components/dialog'
// ============================================================================
// Telegram Bind Dialog Component
......@@ -45,56 +39,55 @@ export function TelegramBindDialog({
}: TelegramBindDialogProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Bind Telegram Account')}</DialogTitle>
<DialogDescription>
{t('Click the button below to bind your Telegram account')}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Bind Telegram Account')}
description={t('Click the button below to bind your Telegram account')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
>
<div className='space-y-4 py-4'>
<Alert>
<Send className='h-4 w-4' />
<AlertDescription>
{t(
'You will be redirected to Telegram to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='space-y-4 py-4'>
<Alert>
<Send className='h-4 w-4' />
<AlertDescription>
<div className='flex flex-col items-center justify-center gap-4 rounded-lg border p-6'>
<div className='flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 dark:bg-blue-900'>
<Send className='h-6 w-6 text-blue-600 dark:text-blue-400' />
</div>
<div className='text-center'>
<p className='text-muted-foreground text-sm'>
{t('Bot:')}{' '}
<span className='font-mono font-semibold'>@{botName}</span>
</p>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
'You will be redirected to Telegram to complete the binding process.'
"After clicking the button, you'll be asked to authorize the bot"
)}
</AlertDescription>
</Alert>
<div className='flex flex-col items-center justify-center gap-4 rounded-lg border p-6'>
<div className='flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 dark:bg-blue-900'>
<Send className='h-6 w-6 text-blue-600 dark:text-blue-400' />
</div>
<div className='text-center'>
<p className='text-muted-foreground text-sm'>
{t('Bot:')}{' '}
<span className='font-mono font-semibold'>@{botName}</span>
</p>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
"After clicking the button, you'll be asked to authorize the bot"
)}
</p>
</div>
</p>
</div>
{/* Telegram Login Widget will be injected here by react-telegram-login */}
<div id='telegram-login-widget' className='flex justify-center'>
{/* This would require the react-telegram-login library */}
<div className='text-muted-foreground rounded-lg border border-dashed px-6 py-3 text-sm'>
{t('Telegram Login Widget')}
</div>
{/* Telegram Login Widget will be injected here by react-telegram-login */}
<div id='telegram-login-widget' className='flex justify-center'>
{/* This would require the react-telegram-login library */}
<div className='text-muted-foreground rounded-lg border border-dashed px-6 py-3 text-sm'>
{t('Telegram Login Widget')}
</div>
</div>
<p className='text-muted-foreground text-center text-xs'>
{t('The binding will complete automatically after authorization')}
</p>
</div>
</DialogContent>
<p className='text-muted-foreground text-center text-xs'>
{t('The binding will complete automatically after authorization')}
</p>
</div>
</Dialog>
)
}
......@@ -23,17 +23,10 @@ import { toast } from 'sonner'
import { regenerate2FABackupCodes } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
// ============================================================================
// Two-FA Backup Codes Dialog Component
......@@ -94,82 +87,26 @@ export function TwoFABackupDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle className='flex items-center gap-2'>
<RefreshCw className='h-5 w-5' />
{t('Regenerate Backup Codes')}
</DialogTitle>
<DialogDescription>
{backupCodes.length > 0
? t('Your new backup codes are ready')
: t('Generate new backup codes for account recovery')}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
{backupCodes.length === 0 ? (
<>
<Alert>
<AlertDescription>
{t(
'Generating new codes will invalidate all existing backup codes.'
)}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter authenticator code')}
maxLength={6}
disabled={loading}
/>
</div>
</>
) : (
<>
<Alert>
<AlertDescription>
{t(
'Save these codes in a safe place. Each code can only be used once.'
)}
</AlertDescription>
</Alert>
<div className='rounded-lg border p-4'>
<div className='grid grid-cols-2 gap-2'>
{backupCodes.map((code, index) => (
<div
key={index}
className='bg-muted rounded-md p-2 text-center font-mono text-sm'
>
{code}
</div>
))}
</div>
</div>
<CopyButton
value={backupCodes.join('\n')}
variant='outline'
size='default'
className='w-full'
iconClassName='mr-2 size-4'
tooltip={t('Copy all backup codes')}
aria-label={t('Copy all backup codes')}
>
{t('Copy All Codes')}
</CopyButton>
</>
)}
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={
<>
<RefreshCw className='h-5 w-5' />
{t('Regenerate Backup Codes')}
</>
}
description={
backupCodes.length > 0
? t('Your new backup codes are ready')
: t('Generate new backup codes for account recovery')
}
contentClassName='sm:max-w-md'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
{backupCodes.length === 0 ? (
<>
<Button
......@@ -187,8 +124,69 @@ export function TwoFABackupDialog({
) : (
<Button onClick={handleDone}>{t('Done')}</Button>
)}
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
{backupCodes.length === 0 ? (
<>
<Alert>
<AlertDescription>
{t(
'Generating new codes will invalidate all existing backup codes.'
)}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter authenticator code')}
maxLength={6}
disabled={loading}
/>
</div>
</>
) : (
<>
<Alert>
<AlertDescription>
{t(
'Save these codes in a safe place. Each code can only be used once.'
)}
</AlertDescription>
</Alert>
<div className='rounded-lg border p-4'>
<div className='grid grid-cols-2 gap-2'>
{backupCodes.map((code, index) => (
<div
key={index}
className='bg-muted rounded-md p-2 text-center font-mono text-sm'
>
{code}
</div>
))}
</div>
</div>
<CopyButton
value={backupCodes.join('\n')}
variant='outline'
size='default'
className='w-full'
iconClassName='mr-2 size-4'
tooltip={t('Copy all backup codes')}
aria-label={t('Copy all backup codes')}
>
{t('Copy All Codes')}
</CopyButton>
</>
)}
</div>
</Dialog>
)
}
......@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
// ============================================================================
// Two-FA Disable Dialog Component
......@@ -98,60 +91,24 @@ export function TwoFADisableDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle className='text-destructive flex items-center gap-2'>
<AlertTriangle className='h-5 w-5' />
{t('Disable Two-Factor Authentication')}
</DialogTitle>
<DialogDescription>
{t(
'This action will permanently remove 2FA protection from your account.'
)}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: Disabling 2FA will make your account less secure.')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code or backup code')}
disabled={loading}
/>
<p className='text-muted-foreground text-xs'>
{t('Enter your authenticator code or a backup code')}
</p>
</div>
<div className='flex items-start space-x-2'>
<Checkbox
id='confirm'
checked={confirmed}
onCheckedChange={(checked) => setConfirmed(checked as boolean)}
/>
<Label
htmlFor='confirm'
className='text-sm leading-tight font-normal'
>
{t(
'I understand that disabling 2FA will remove all protection and backup codes'
)}
</Label>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={
<>
<AlertTriangle className='h-5 w-5' />
{t('Disable Two-Factor Authentication')}
</>
}
description={t(
'This action will permanently remove 2FA protection from your account.'
)}
contentClassName='sm:max-w-md'
titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => handleOpenChange(false)}
......@@ -167,8 +124,47 @@ export function TwoFADisableDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Disabling...') : t('Disable 2FA')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: Disabling 2FA will make your account less secure.')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code or backup code')}
disabled={loading}
/>
<p className='text-muted-foreground text-xs'>
{t('Enter your authenticator code or a backup code')}
</p>
</div>
<div className='flex items-start space-x-2'>
<Checkbox
id='confirm'
checked={confirmed}
onCheckedChange={(checked) => setConfirmed(checked as boolean)}
/>
<Label
htmlFor='confirm'
className='text-sm leading-tight font-normal'
>
{t(
'I understand that disabling 2FA will remove all protection and backup codes'
)}
</Label>
</div>
</div>
</Dialog>
)
}
......@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { QrCode } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog } from '@/components/dialog'
// ============================================================================
// WeChat Bind Dialog Component
......@@ -43,40 +37,39 @@ export function WeChatBindDialog({
}: WeChatBindDialogProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Bind WeChat Account')}</DialogTitle>
<DialogDescription>
{t('Scan the QR code with WeChat to bind your account')}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Bind WeChat Account')}
description={t('Scan the QR code with WeChat to bind your account')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
>
<div className='space-y-4 py-4'>
<Alert>
<QrCode className='h-4 w-4' />
<AlertDescription>
{t(
'Please use WeChat\'s "Scan QR Code" feature to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='space-y-4 py-4'>
<Alert>
<QrCode className='h-4 w-4' />
<AlertDescription>
{t(
'Please use WeChat\'s "Scan QR Code" feature to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='flex flex-col items-center justify-center rounded-lg border border-dashed p-8'>
<QrCode className='text-muted-foreground mb-3 h-16 w-16' />
<p className='text-muted-foreground text-sm'>
{t('WeChat QR code will be displayed here')}
</p>
<p className='text-muted-foreground mt-2 text-xs'>
{t('This feature requires server-side WeChat configuration')}
</p>
</div>
<p className='text-muted-foreground text-center text-xs'>
{t('After scanning, the binding will complete automatically')}
<div className='flex flex-col items-center justify-center rounded-lg border border-dashed p-8'>
<QrCode className='text-muted-foreground mb-3 h-16 w-16' />
<p className='text-muted-foreground text-sm'>
{t('WeChat QR code will be displayed here')}
</p>
<p className='text-muted-foreground mt-2 text-xs'>
{t('This feature requires server-side WeChat configuration')}
</p>
</div>
</DialogContent>
<p className='text-muted-foreground text-center text-xs'>
{t('After scanning, the binding will complete automatically')}
</p>
</div>
</Dialog>
)
}
......@@ -23,14 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormDescription,
......@@ -40,6 +32,7 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
const createChatDialogSchema = (t: (key: string) => string) =>
z.object({
......@@ -49,6 +42,8 @@ const createChatDialogSchema = (t: (key: string) => string) =>
type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>>
const CHAT_DIALOG_FORM_ID = 'chat-dialog-form'
export type ChatEntryData = {
name: string
url: string
......@@ -97,74 +92,73 @@ export function ChatDialog({
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-[500px]'>
<DialogHeader>
<DialogTitle>
{isEditMode ? t('Edit chat preset') : t('Add chat preset')}
</DialogTitle>
<DialogDescription>
{t('Configure a predefined chat link for end users.')}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-4'
<Dialog
open={open}
onOpenChange={onOpenChange}
title={isEditMode ? t('Edit chat preset') : t('Add chat preset')}
description={t('Configure a predefined chat link for end users.')}
contentClassName='sm:max-w-[500px]'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Chat Client Name')}</FormLabel>
<FormControl>
<Input
placeholder={t('Please enter chat client name')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Display name for this chat client.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('URL')}</FormLabel>
<FormControl>
<Input placeholder={t('Please enter the URL')} {...field} />
</FormControl>
<FormDescription>
{t('The URL for this chat client.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{isEditMode ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
{t('Cancel')}
</Button>
<Button type='submit' form={CHAT_DIALOG_FORM_ID}>
{isEditMode ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}>
<form
id={CHAT_DIALOG_FORM_ID}
onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-4'
>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Chat Client Name')}</FormLabel>
<FormControl>
<Input
placeholder={t('Please enter chat client name')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Display name for this chat client.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('URL')}</FormLabel>
<FormControl>
<Input placeholder={t('Please enter the URL')} {...field} />
</FormControl>
<FormDescription>
{t('The URL for this chat client.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</Dialog>
)
}
......@@ -36,14 +36,6 @@ import {
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormDescription,
......@@ -62,6 +54,7 @@ import {
TableRow,
} from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
......@@ -90,6 +83,8 @@ const faqSchema = z.object({
type FAQFormValues = z.infer<typeof faqSchema>
const FAQ_FORM_ID = 'faq-form'
export function FAQSection({ enabled, data }: FAQSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
......@@ -348,79 +343,78 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
</div>
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent className='max-w-2xl'>
<DialogHeader>
<DialogTitle>
{editingFaq ? t('Edit FAQ') : t('Add FAQ')}
</DialogTitle>
<DialogDescription>
{t('Create or update frequently asked questions for users')}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4'
<Dialog
open={showDialog}
onOpenChange={setShowDialog}
title={editingFaq ? t('Edit FAQ') : t('Add FAQ')}
description={t('Create or update frequently asked questions for users')}
contentClassName='max-w-2xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
<FormField
control={form.control}
name='question'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Question')}</FormLabel>
<FormControl>
<Input
placeholder={t('How to reset my quota?')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Maximum 200 characters')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='answer'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Answer')}</FormLabel>
<FormControl>
<Textarea
placeholder={t(
'Visit Settings → General and adjust quota options...'
)}
rows={8}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Maximum 1000 characters. Supports Markdown and HTML.'
{t('Cancel')}
</Button>
<Button type='submit' form={FAQ_FORM_ID}>
{editingFaq ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}>
<form
id={FAQ_FORM_ID}
onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4'
>
<FormField
control={form.control}
name='question'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Question')}</FormLabel>
<FormControl>
<Input
placeholder={t('How to reset my quota?')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Maximum 200 characters')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='answer'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Answer')}</FormLabel>
<FormControl>
<Textarea
placeholder={t(
'Visit Settings → General and adjust quota options...'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{editingFaq ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
rows={8}
{...field}
/>
</FormControl>
<FormDescription>
{t('Maximum 1000 characters. Supports Markdown and HTML.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
......@@ -36,14 +36,6 @@ import {
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormDescription,
......@@ -61,6 +53,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Dialog } from '@/components/dialog'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
......@@ -97,6 +90,8 @@ const createUptimeKumaSchema = (t: (key: string) => string) =>
type UptimeKumaFormValues = z.infer<ReturnType<typeof createUptimeKumaSchema>>
const UPTIME_KUMA_FORM_ID = 'uptime-kuma-form'
export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
......@@ -359,96 +354,100 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
</div>
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingGroup
? t('Edit Uptime Kuma Group')
: t('Add Uptime Kuma Group')}
</DialogTitle>
<DialogDescription>
{t('Configure monitoring status page groups for the dashboard')}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4'
<Dialog
open={showDialog}
onOpenChange={setShowDialog}
title={
editingGroup
? t('Edit Uptime Kuma Group')
: t('Add Uptime Kuma Group')
}
description={t(
'Configure monitoring status page groups for the dashboard'
)}
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
<FormField
control={form.control}
name='categoryName'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Category Name')}</FormLabel>
<FormControl>
<Input
placeholder={t('e.g., Core APIs, OpenAI, Claude')}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Display name for this monitoring group (max 50 characters)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Uptime Kuma URL')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://status.example.com')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Base URL of your Uptime Kuma instance')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='slug'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Status Page Slug')}</FormLabel>
<FormControl>
<Input placeholder={t('my-status')} {...field} />
</FormControl>
<FormDescription>
{t('The slug is appended to the URL:')} {'{url}'}
{t('/status/')}
{'{slug}'}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
>
{t('Cancel')}
</Button>
<Button type='submit'>
{editingGroup ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
{t('Cancel')}
</Button>
<Button type='submit' form={UPTIME_KUMA_FORM_ID}>
{editingGroup ? t('Update') : t('Add')}
</Button>
</>
}
>
<Form {...form}>
<form
id={UPTIME_KUMA_FORM_ID}
onSubmit={form.handleSubmit(handleSubmitForm)}
className='space-y-4'
>
<FormField
control={form.control}
name='categoryName'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Category Name')}</FormLabel>
<FormControl>
<Input
placeholder={t('e.g., Core APIs, OpenAI, Claude')}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Display name for this monitoring group (max 50 characters)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Uptime Kuma URL')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://status.example.com')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Base URL of your Uptime Kuma instance')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='slug'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Status Page Slug')}</FormLabel>
<FormControl>
<Input placeholder={t('my-status')} {...field} />
</FormControl>
<FormDescription>
{t('The slug is appended to the URL:')} {'{url}'}
{t('/status/')}
{'{slug}'}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
......@@ -20,12 +20,7 @@ import { useEffect, useMemo, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { formatTimestampToDate } from '@/lib/format'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog } from '@/components/dialog'
import { getAffinityUsageCache } from './api'
function formatRate(hit: number, total: number): string {
......@@ -135,38 +130,42 @@ export function CacheStatsDialog(props: Props) {
}, [stats, props.target, t])
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Channel Affinity: Upstream Cache Hit')}</DialogTitle>
</DialogHeader>
<p className='text-muted-foreground text-xs'>
{t(
'Hit criteria: If cached tokens exist in usage, it counts as a hit.'
)}
</p>
{loading ? (
<div className='text-muted-foreground py-8 text-center text-sm'>
{t('Loading...')}
</div>
) : rows.length > 0 ? (
<div className='space-y-2'>
{rows.map((row) => (
<div
key={row.key}
className='flex justify-between border-b pb-1 text-sm'
>
<span className='text-muted-foreground'>{row.key}</span>
<span className='font-medium'>{row.value}</span>
</div>
))}
</div>
) : (
<div className='text-muted-foreground py-8 text-center text-sm'>
{t('No data available')}
</div>
<Dialog
open={props.open}
onOpenChange={props.onOpenChange}
title={t('Channel Affinity: Upstream Cache Hit')}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
>
<p className='text-muted-foreground text-xs'>
{t(
'Hit criteria: If cached tokens exist in usage, it counts as a hit.'
)}
</DialogContent>
</p>
{loading ? (
<div className='text-muted-foreground py-8 text-center text-sm'>
{t('Loading...')}
</div>
) : rows.length > 0 ? (
<div className='space-y-2'>
{rows.map((row) => (
<div
key={row.key}
className='flex justify-between gap-4 border-b pb-1 text-sm'
>
<span className='text-muted-foreground'>{row.key}</span>
<span className='text-right font-medium break-all'>
{row.value}
</span>
</div>
))}
</div>
) : (
<div className='text-muted-foreground py-8 text-center text-sm'>
{t('No data available')}
</div>
)}
</Dialog>
)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment