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.
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ export function Dialog({
|
|||||||
children,
|
children,
|
||||||
trigger,
|
trigger,
|
||||||
footer,
|
footer,
|
||||||
contentHeight = 'min(58vh, 520px)',
|
contentHeight = 'auto',
|
||||||
contentClassName,
|
contentClassName,
|
||||||
headerClassName,
|
headerClassName,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
|
|||||||
+14
-23
@@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications'
|
|||||||
import { useSystemConfig } from '@/hooks/use-system-config'
|
import { useSystemConfig } from '@/hooks/use-system-config'
|
||||||
import { useTopNavLinks } from '@/hooks/use-top-nav-links'
|
import { useTopNavLinks } from '@/hooks/use-top-nav-links'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { LanguageSwitcher } from '@/components/language-switcher'
|
import { LanguageSwitcher } from '@/components/language-switcher'
|
||||||
import { NotificationPopover } from '@/components/notification-popover'
|
import { NotificationPopover } from '@/components/notification-popover'
|
||||||
import { ProfileDropdown } from '@/components/profile-dropdown'
|
import { ProfileDropdown } from '@/components/profile-dropdown'
|
||||||
@@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) {
|
|||||||
closeAuthPrompt()
|
closeAuthPrompt()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
title={t('Sign in required')}
|
||||||
<DialogContent className='sm:max-w-md'>
|
description={t('Please sign in to view {{module}}.', {
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('Sign in required')}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t('Please sign in to view {{module}}.', {
|
|
||||||
module: authPromptTarget?.title || '',
|
module: authPromptTarget?.title || '',
|
||||||
})}
|
})}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-md'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={closeAuthPrompt}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
|
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
|
||||||
{t('Redirecting to sign in in {{seconds}} seconds.', {
|
{t('Redirecting to sign in in {{seconds}} seconds.', {
|
||||||
seconds: authPromptSecondsLeft,
|
seconds: authPromptSecondsLeft,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={closeAuthPrompt}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+40
-51
@@ -20,16 +20,9 @@ import { useMemo } from 'react'
|
|||||||
import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
|
import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import type {
|
import type {
|
||||||
SecureVerificationState,
|
SecureVerificationState,
|
||||||
VerificationMethod,
|
VerificationMethod,
|
||||||
@@ -91,23 +84,45 @@ export function SecureVerificationDialog({
|
|||||||
(activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6))
|
(activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent
|
open={open}
|
||||||
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'
|
onOpenChange={onOpenChange}
|
||||||
showCloseButton={!state.loading}
|
title={
|
||||||
>
|
<>
|
||||||
<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' />
|
<ShieldCheck className='text-primary h-5 w-5' />
|
||||||
{title}
|
{title}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription className='text-left'>
|
}
|
||||||
{description}
|
description={description}
|
||||||
</DialogDescription>
|
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'
|
||||||
</DialogHeader>
|
headerClassName='border-b pb-4 text-left'
|
||||||
|
titleClassName='flex items-center gap-2 text-lg font-semibold'
|
||||||
<div className='flex-1 overflow-y-auto px-6 py-5'>
|
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 ? (
|
{availableTabs.length === 0 ? (
|
||||||
<div className='grid place-items-center gap-4 text-center'>
|
<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'>
|
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
|
||||||
@@ -122,16 +137,12 @@ export function SecureVerificationDialog({
|
|||||||
) : (
|
) : (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeMethod ?? availableTabs[0]}
|
value={activeMethod ?? availableTabs[0]}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => onMethodChange(value as VerificationMethod)}
|
||||||
onMethodChange(value as VerificationMethod)
|
|
||||||
}
|
|
||||||
className='gap-4'
|
className='gap-4'
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{methods.has2FA && (
|
{methods.has2FA && (
|
||||||
<TabsTrigger value='2fa'>
|
<TabsTrigger value='2fa'>{t('Authenticator code')}</TabsTrigger>
|
||||||
{t('Authenticator code')}
|
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
)}
|
||||||
{methods.hasPasskey && methods.passkeySupported && (
|
{methods.hasPasskey && methods.passkeySupported && (
|
||||||
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
|
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
|
||||||
@@ -185,28 +196,6 @@ export function SecureVerificationDialog({
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</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>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,6 @@ import {
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useStatus } from '@/hooks/use-status'
|
import { useStatus } from '@/hooks/use-status'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -50,6 +42,7 @@ import {
|
|||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { PasswordInput } from '@/components/password-input'
|
import { PasswordInput } from '@/components/password-input'
|
||||||
import { Turnstile } from '@/components/turnstile'
|
import { Turnstile } from '@/components/turnstile'
|
||||||
import { login, wechatLoginByCode } from '@/features/auth/api'
|
import { login, wechatLoginByCode } from '@/features/auth/api'
|
||||||
@@ -414,43 +407,16 @@ export function UserAuthForm({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={isWeChatDialogOpen}
|
open={isWeChatDialogOpen}
|
||||||
onOpenChange={handleWeChatDialogChange}
|
onOpenChange={handleWeChatDialogChange}
|
||||||
>
|
title={t('WeChat sign in')}
|
||||||
<DialogContent className='max-w-sm'>
|
description={t(
|
||||||
<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.'
|
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='max-w-sm'
|
||||||
</DialogHeader>
|
headerClassName='text-left'
|
||||||
|
contentHeight='auto'
|
||||||
{wechatQrCodeUrl ? (
|
bodyClassName='space-y-4'
|
||||||
<div className='flex justify-center'>
|
footer={
|
||||||
<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>
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -474,8 +440,32 @@ export function UserAuthForm({
|
|||||||
) : null}
|
) : null}
|
||||||
{t('Confirm')}
|
{t('Confirm')}
|
||||||
</Button>
|
</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>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -26,14 +26,6 @@ import { toast } from 'sonner'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useStatus } from '@/hooks/use-status'
|
import { useStatus } from '@/hooks/use-status'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -44,6 +36,7 @@ import {
|
|||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { PasswordInput } from '@/components/password-input'
|
import { PasswordInput } from '@/components/password-input'
|
||||||
import { Turnstile } from '@/components/turnstile'
|
import { Turnstile } from '@/components/turnstile'
|
||||||
import { register, wechatLoginByCode } from '@/features/auth/api'
|
import { register, wechatLoginByCode } from '@/features/auth/api'
|
||||||
@@ -387,43 +380,16 @@ export function SignUpForm({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={isWeChatDialogOpen}
|
open={isWeChatDialogOpen}
|
||||||
onOpenChange={handleWeChatDialogChange}
|
onOpenChange={handleWeChatDialogChange}
|
||||||
>
|
title={t('WeChat sign in')}
|
||||||
<DialogContent className='max-w-sm'>
|
description={t(
|
||||||
<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.'
|
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='max-w-sm'
|
||||||
</DialogHeader>
|
headerClassName='text-left'
|
||||||
|
contentHeight='auto'
|
||||||
{wechatQrCodeUrl ? (
|
bodyClassName='space-y-4'
|
||||||
<div className='flex justify-center'>
|
footer={
|
||||||
<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>
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -447,8 +413,32 @@ export function SignUpForm({
|
|||||||
) : null}
|
) : null}
|
||||||
{t('Confirm')}
|
{t('Confirm')}
|
||||||
</Button>
|
</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>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table'
|
|||||||
import { Power, PowerOff, Tag, Trash2 } from 'lucide-react'
|
import { Power, PowerOff, Tag, Trash2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +30,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
handleBatchDelete,
|
handleBatchDelete,
|
||||||
handleBatchDisable,
|
handleBatchDisable,
|
||||||
@@ -188,16 +181,34 @@ export function DataTableBulkActions<TData>({
|
|||||||
</BulkActionsToolbar>
|
</BulkActionsToolbar>
|
||||||
|
|
||||||
{/* Set Tag Dialog */}
|
{/* Set Tag Dialog */}
|
||||||
<Dialog open={showTagDialog} onOpenChange={setShowTagDialog}>
|
<Dialog
|
||||||
<DialogContent>
|
open={showTagDialog}
|
||||||
<DialogHeader>
|
onOpenChange={setShowTagDialog}
|
||||||
<DialogTitle>{t('Set Tag')}</DialogTitle>
|
title={t('Set Tag')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Set a tag for')} {selectedIds.length}{' '}
|
<>
|
||||||
|
{t('Set a tag for')}
|
||||||
|
{selectedIds.length}{' '}
|
||||||
{t('selected channel(s). Leave empty to remove tag.')}
|
{t('selected channel(s). Leave empty to remove tag.')}
|
||||||
</DialogDescription>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => {
|
||||||
|
setShowTagDialog(false)
|
||||||
|
setTagValue('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSetTag}>{t('Set Tag')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='grid gap-4 py-4'>
|
<div className='grid gap-4 py-4'>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label htmlFor='tag'>{t('Tag')}</Label>
|
<Label htmlFor='tag'>{t('Tag')}</Label>
|
||||||
@@ -209,34 +220,23 @@ export function DataTableBulkActions<TData>({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => {
|
|
||||||
setShowTagDialog(false)
|
|
||||||
setTagValue('')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSetTag}>{t('Set Tag')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
<Dialog
|
||||||
<DialogContent>
|
open={showDeleteConfirm}
|
||||||
<DialogHeader>
|
onOpenChange={setShowDeleteConfirm}
|
||||||
<DialogTitle>{t('Delete Channels?')}</DialogTitle>
|
title={t('Delete Channels?')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Are you sure you want to delete')} {selectedIds.length}{' '}
|
<>
|
||||||
|
{t('Are you sure you want to delete')}
|
||||||
|
{selectedIds.length}{' '}
|
||||||
{t('channel(s)? This action cannot be undone.')}
|
{t('channel(s)? This action cannot be undone.')}
|
||||||
</DialogDescription>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentHeight='auto'
|
||||||
<DialogFooter>
|
footer={
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
@@ -246,8 +246,10 @@ export function DataTableBulkActions<TData>({
|
|||||||
<Button variant='destructive' onClick={handleDeleteAll}>
|
<Button variant='destructive' onClick={handleDeleteAll}>
|
||||||
{t('Delete')}
|
{t('Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</>
|
||||||
</DialogContent>
|
}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+22
-27
@@ -24,14 +24,7 @@ import { toast } from 'sonner'
|
|||||||
import { formatCurrencyFromUSD } from '@/lib/currency'
|
import { formatCurrencyFromUSD } from '@/lib/currency'
|
||||||
import { formatTimestampToDate } from '@/lib/format'
|
import { formatTimestampToDate } from '@/lib/format'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import { Dialog } from '@/components/dialog'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { getCodexUsage, updateChannelBalance } from '../../api'
|
import { getCodexUsage, updateChannelBalance } from '../../api'
|
||||||
import { channelsQueryKeys } from '../../lib'
|
import { channelsQueryKeys } from '../../lib'
|
||||||
import { useChannels } from '../channels-provider'
|
import { useChannels } from '../channels-provider'
|
||||||
@@ -161,15 +154,26 @@ export function BalanceQueryDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog
|
||||||
<DialogContent>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleClose}
|
||||||
<DialogTitle>{t('Query Balance')}</DialogTitle>
|
title={t('Query Balance')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Update balance for:')} <strong>{currentRow.name}</strong>
|
<>
|
||||||
</DialogDescription>
|
{t('Update balance for:')}
|
||||||
</DialogHeader>
|
<strong>{currentRow.name}</strong>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={handleClose} disabled={isQuerying}>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
{/* Current Balance Display */}
|
{/* Current Balance Display */}
|
||||||
<div className='bg-muted/50 rounded-lg border p-4'>
|
<div className='bg-muted/50 rounded-lg border p-4'>
|
||||||
@@ -184,9 +188,7 @@ export function BalanceQueryDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground mt-2 text-xs'>
|
<div className='text-muted-foreground mt-2 text-xs'>
|
||||||
{t('Last updated:')}{' '}
|
{t('Last updated:')}{' '}
|
||||||
{formatDate(
|
{formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)}
|
||||||
balanceUpdatedTime ?? currentRow.balance_updated_time
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -201,13 +203,6 @@ export function BalanceQueryDialog({
|
|||||||
{isQuerying ? t('Querying...') : t('Update Balance')}
|
{isQuerying ? t('Querying...') : t('Update Balance')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={handleClose} disabled={isQuerying}>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-24
@@ -33,14 +33,6 @@ import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
|||||||
import { useIsMobile } from '@/hooks/use-mobile'
|
import { useIsMobile } from '@/hooks/use-mobile'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -75,6 +67,7 @@ import {
|
|||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||||
import { DataTablePagination } from '@/components/data-table/pagination'
|
import { DataTablePagination } from '@/components/data-table/pagination'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
sideDrawerContentClassName,
|
sideDrawerContentClassName,
|
||||||
sideDrawerFooterClassName,
|
sideDrawerFooterClassName,
|
||||||
@@ -529,15 +522,27 @@ export function ChannelTestDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleClose}
|
||||||
<DialogTitle>{t('Test Channel Connection')}</DialogTitle>
|
title={t('Test Channel Connection')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Test connectivity for:')} <strong>{currentRow.name}</strong>
|
<>
|
||||||
</DialogDescription>
|
{t('Test connectivity for:')}
|
||||||
</DialogHeader>
|
<strong>{currentRow.name}</strong>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-3xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={handleClose}>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-4 pr-1'>
|
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-4 pr-1'>
|
||||||
<div className='grid gap-4 md:grid-cols-2'>
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
@@ -695,13 +700,6 @@ export function ChannelTestDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={handleClose}>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<FailureDetailsSheet
|
<FailureDetailsSheet
|
||||||
details={failureDetails}
|
details={failureDetails}
|
||||||
|
|||||||
+28
-35
@@ -24,15 +24,8 @@ import { tryPrettyJson } from '@/lib/utils'
|
|||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { completeCodexOAuth, startCodexOAuth } from '../../api'
|
import { completeCodexOAuth, startCodexOAuth } from '../../api'
|
||||||
|
|
||||||
type CodexOAuthDialogProps = {
|
type CodexOAuthDialogProps = {
|
||||||
@@ -129,17 +122,35 @@ export function CodexOAuthDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-2xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Codex Authorization')}</DialogTitle>
|
title={t('Codex Authorization')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t(
|
|
||||||
'Generate a Codex OAuth credential and paste it into the channel key field.'
|
'Generate a Codex OAuth credential and paste it into the channel key field.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-2xl'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={state.isStarting || state.isCompleting}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleComplete} disabled={!canComplete}>
|
||||||
|
{state.isCompleting && (
|
||||||
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||||
|
)}
|
||||||
|
{state.isCompleting ? t('Generating...') : t('Generate credential')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -199,24 +210,6 @@ export function CodexOAuthDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={state.isStarting || state.isCompleting}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleComplete} disabled={!canComplete}>
|
|
||||||
{state.isCompleting && (
|
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
||||||
)}
|
|
||||||
{state.isCompleting ? t('Generating...') : t('Generate credential')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-30
@@ -31,16 +31,9 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import dayjs from '@/lib/dayjs'
|
import dayjs from '@/lib/dayjs'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
||||||
|
|
||||||
type CodexRateLimitWindow = {
|
type CodexRateLimitWindow = {
|
||||||
@@ -414,18 +407,33 @@ export function CodexUsageDialog({
|
|||||||
}, [response])
|
}, [response])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={t('Codex Account & Usage')}
|
||||||
{t('Codex Account & Usage')}
|
description={
|
||||||
</DialogTitle>
|
<>
|
||||||
<DialogDescription>
|
{t('Channel:')}
|
||||||
{t('Channel:')} <strong>{channelName || '-'}</strong>{' '}
|
<strong>{channelName || '-'}</strong>{' '}
|
||||||
{channelId ? `(#${channelId})` : ''}
|
{channelId ? `(#${channelId})` : ''}
|
||||||
</DialogDescription>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentClassName='sm:max-w-3xl'
|
||||||
|
titleClassName='flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<div className='rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400'>
|
<div className='rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400'>
|
||||||
@@ -583,17 +591,6 @@ export function CodexUsageDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-32
@@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { handleCopyChannel } from '../../lib'
|
import { handleCopyChannel } from '../../lib'
|
||||||
import { useChannels } from '../channels-provider'
|
import { useChannels } from '../channels-provider'
|
||||||
|
|
||||||
@@ -74,15 +67,34 @@ export function CopyChannelDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Copy Channel')}</DialogTitle>
|
title={t('Copy Channel')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Create a copy of:')} <strong>{currentRow.name}</strong>
|
<>
|
||||||
</DialogDescription>
|
{t('Create a copy of:')}
|
||||||
</DialogHeader>
|
<strong>{currentRow.name}</strong>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isCopying}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCopy} disabled={isCopying}>
|
||||||
|
{isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{isCopying ? t('Copying...') : t('Copy Channel')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='suffix'>{t('Name Suffix')}</Label>
|
<Label htmlFor='suffix'>{t('Name Suffix')}</Label>
|
||||||
@@ -111,21 +123,6 @@ export function CopyChannelDialog({
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={isCopying}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleCopy} disabled={isCopying}>
|
|
||||||
{isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{isCopying ? 'Copying...' : 'Copy Channel'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
@@ -43,6 +35,7 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { GroupBadge } from '@/components/group-badge'
|
import { GroupBadge } from '@/components/group-badge'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import {
|
import {
|
||||||
@@ -222,19 +215,33 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
|||||||
if (!currentTag) return null
|
if (!currentTag) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[90vh] max-w-2xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleClose}
|
||||||
<DialogTitle>
|
title={
|
||||||
{t('Edit Tag:')} {currentTag}
|
<>
|
||||||
</DialogTitle>
|
{t('Edit Tag:')}
|
||||||
<DialogDescription>
|
{currentTag}
|
||||||
{t(
|
</>
|
||||||
|
}
|
||||||
|
description={t(
|
||||||
'Batch edit all channels with this tag. Leave fields empty to keep current values.'
|
'Batch edit all channels with this tag. Leave fields empty to keep current values.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='max-h-[90vh] max-w-2xl'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={handleClose}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||||
|
{isSubmitting && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{t('Save Changes')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ScrollArea className='max-h-[60vh] pr-4'>
|
<ScrollArea className='max-h-[60vh] pr-4'>
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
{/* Tag Name */}
|
{/* Tag Name */}
|
||||||
@@ -430,17 +437,6 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={handleClose}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{t('Save Changes')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-41
@@ -28,14 +28,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
@@ -44,6 +36,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { fetchUpstreamModels, updateChannel } from '../../api'
|
import { fetchUpstreamModels, updateChannel } from '../../api'
|
||||||
import {
|
import {
|
||||||
channelsQueryKeys,
|
channelsQueryKeys,
|
||||||
@@ -365,28 +358,46 @@ export function FetchModelsDialog({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showFooterActions =
|
||||||
|
!!(activeChannel || customFetcher) &&
|
||||||
|
!isFetching &&
|
||||||
|
(fetchedModels.length > 0 || removedModels.length > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog
|
||||||
<DialogContent className='max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleClose}
|
||||||
<DialogTitle>{t('Fetch Models')}</DialogTitle>
|
title={t('Fetch Models')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{activeChannel ? (
|
activeChannel ? (
|
||||||
<>
|
<>
|
||||||
{t('Fetch available models for:')}{' '}
|
{t('Channel:')} <strong>{activeChannel.name}</strong>
|
||||||
<strong>{activeChannel.name}</strong>
|
|
||||||
</>
|
</>
|
||||||
) : channelName ? (
|
) : channelName ? (
|
||||||
<>
|
<>
|
||||||
{t('Fetch available models for:')}{' '}
|
{t('Channel:')} <strong>{channelName}</strong>
|
||||||
<strong>{channelName}</strong>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
t('Fetch available models from upstream')
|
t('Fetch available models from upstream')
|
||||||
)}
|
)
|
||||||
</DialogDescription>
|
}
|
||||||
</DialogHeader>
|
contentClassName='max-w-3xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
showFooterActions ? (
|
||||||
|
<>
|
||||||
|
<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' />}
|
||||||
|
{isSaving ? t('Saving...') : t('Save Models')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
{!activeChannel && !customFetcher ? (
|
{!activeChannel && !customFetcher ? (
|
||||||
<div className='text-muted-foreground py-8 text-center'>
|
<div className='text-muted-foreground py-8 text-center'>
|
||||||
{t('No channel selected')}
|
{t('No channel selected')}
|
||||||
@@ -459,8 +470,7 @@ export function FetchModelsDialog({
|
|||||||
className='max-h-96 space-y-2 overflow-y-auto'
|
className='max-h-96 space-y-2 overflow-y-auto'
|
||||||
>
|
>
|
||||||
{getSortedCategoryEntries(newModelsByCategory).map(
|
{getSortedCategoryEntries(newModelsByCategory).map(
|
||||||
([category, models]) =>
|
([category, models]) => renderModelCategory(category, models)
|
||||||
renderModelCategory(category, models)
|
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -469,8 +479,7 @@ export function FetchModelsDialog({
|
|||||||
className='max-h-96 space-y-2 overflow-y-auto'
|
className='max-h-96 space-y-2 overflow-y-auto'
|
||||||
>
|
>
|
||||||
{getSortedCategoryEntries(existingModelsByCategory).map(
|
{getSortedCategoryEntries(existingModelsByCategory).map(
|
||||||
([category, models]) =>
|
([category, models]) => renderModelCategory(category, models)
|
||||||
renderModelCategory(category, models)
|
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -494,23 +503,8 @@ export function FetchModelsDialog({
|
|||||||
{t('{{n}} model(s) selected', { n: selectedModels.length })}
|
{t('{{n}} model(s) selected', { n: selectedModels.length })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<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' />}
|
|
||||||
{isSaving ? t('Saving...') : t('Save Models')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-21
@@ -22,13 +22,6 @@ import { Loader2, RefreshCw, Trash2, Power, PowerOff } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -47,6 +40,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import {
|
import {
|
||||||
getMultiKeyStatus,
|
getMultiKeyStatus,
|
||||||
@@ -228,10 +222,11 @@ export function MultiKeyManageDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='flex max-h-[90vh] max-w-5xl flex-col'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
{t('Multi-Key Management')}
|
{t('Multi-Key Management')}
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={currentRow.name}
|
label={currentRow.name}
|
||||||
@@ -249,12 +244,16 @@ export function MultiKeyManageDialog({
|
|||||||
copyable={false}
|
copyable={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription>
|
}
|
||||||
{t('Manage multi-key status and configuration for this channel')}
|
description={t(
|
||||||
</DialogDescription>
|
'Manage multi-key status and configuration for this channel'
|
||||||
</DialogHeader>
|
)}
|
||||||
|
contentClassName='flex max-h-[90vh] max-w-5xl flex-col'
|
||||||
|
titleClassName='flex items-center gap-2'
|
||||||
|
contentHeight='min(72vh, 720px)'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<div className='flex min-h-0 flex-1 flex-col space-y-4 overflow-hidden'>
|
<div className='flex min-h-0 flex-1 flex-col space-y-4 overflow-hidden'>
|
||||||
{/* Statistics */}
|
{/* Statistics */}
|
||||||
<div className='grid shrink-0 grid-cols-3 gap-3'>
|
<div className='grid shrink-0 grid-cols-3 gap-3'>
|
||||||
@@ -339,9 +338,7 @@ export function MultiKeyManageDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant='destructive'
|
variant='destructive'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() =>
|
onClick={() => setConfirmAction({ type: 'delete-disabled' })}
|
||||||
setConfirmAction({ type: 'delete-disabled' })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Trash2 className='mr-2 h-4 w-4' />
|
<Trash2 className='mr-2 h-4 w-4' />
|
||||||
{t('Delete Auto-Disabled')}
|
{t('Delete Auto-Disabled')}
|
||||||
@@ -436,7 +433,6 @@ export function MultiKeyManageDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
{/* Confirmation Dialog */}
|
||||||
|
|||||||
+20
-27
@@ -34,18 +34,11 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
deleteOllamaModel,
|
deleteOllamaModel,
|
||||||
fetchModels as fetchModelsFromEndpoint,
|
fetchModels as fetchModelsFromEndpoint,
|
||||||
@@ -375,21 +368,30 @@ export function OllamaModelsDialog({
|
|||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={close}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={close}
|
||||||
<DialogTitle>{t('Ollama Models')}</DialogTitle>
|
title={t('Ollama Models')}
|
||||||
<DialogDescription>
|
description={
|
||||||
|
<>
|
||||||
{t('Manage local models for:')} <strong>{currentRow?.name}</strong>
|
{t('Manage local models for:')} <strong>{currentRow?.name}</strong>
|
||||||
</DialogDescription>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentClassName='sm:max-w-3xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<Button variant='outline' onClick={close}>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
{!isOllamaChannel ? (
|
{!isOllamaChannel ? (
|
||||||
<div className='text-muted-foreground py-8 text-center'>
|
<div className='text-muted-foreground py-8 text-center'>
|
||||||
{t('This channel is not an Ollama channel.')}
|
{t('This channel is not an Ollama channel.')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-2 pr-1'>
|
<div className='space-y-4 py-2 pr-1'>
|
||||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between'>
|
<div className='flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between'>
|
||||||
<div className='flex-1 space-y-2'>
|
<div className='flex-1 space-y-2'>
|
||||||
<Label htmlFor='ollama-pull'>{t('Pull model')}</Label>
|
<Label htmlFor='ollama-pull'>{t('Pull model')}</Label>
|
||||||
@@ -521,9 +523,7 @@ export function OllamaModelsDialog({
|
|||||||
<div className='flex min-w-0 items-start gap-3'>
|
<div className='flex min-w-0 items-start gap-3'>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheckedChange={(v) =>
|
onCheckedChange={(v) => toggleSelected(m.id, !!v)}
|
||||||
toggleSelected(m.id, !!v)
|
|
||||||
}
|
|
||||||
aria-label={`Select model ${m.id}`}
|
aria-label={`Select model ${m.id}`}
|
||||||
/>
|
/>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
@@ -566,13 +566,6 @@ export function OllamaModelsDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={close}>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
onOpenChange={(v) => {
|
onOpenChange={(v) => {
|
||||||
|
|||||||
+30
-44
@@ -43,14 +43,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import {
|
import {
|
||||||
@@ -63,6 +55,7 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -1701,17 +1694,33 @@ export function ParamOverrideEditorDialog(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='flex max-h-[90vh] flex-col gap-0 p-0 sm:max-w-5xl'>
|
open={props.open}
|
||||||
<DialogHeader className='border-b px-6 py-4'>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle>{t('Parameter Override')}</DialogTitle>
|
title={t('Parameter Override')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t(
|
|
||||||
'Create request parameter override rules with a visual editor or raw JSON.'
|
'Create request parameter override rules with a visual editor or raw JSON.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='flex max-h-[90vh] flex-col gap-0 p-0 sm:max-w-5xl'
|
||||||
</DialogHeader>
|
headerClassName='border-b px-6 py-4'
|
||||||
|
footerClassName='border-t px-6 py-4'
|
||||||
|
contentHeight='min(72vh, 720px)'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => props.onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='button' onClick={handleSave}>
|
||||||
|
{t('Save')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className='bg-muted/30 border-b px-4 py-3'>
|
<div className='bg-muted/30 border-b px-4 py-3'>
|
||||||
<div className='flex flex-wrap items-center gap-2'>
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
@@ -1791,7 +1800,6 @@ export function ParamOverrideEditorDialog(
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
<div className='min-h-0 flex-1 overflow-hidden'>
|
||||||
{editMode === 'visual' ? (
|
{editMode === 'visual' ? (
|
||||||
@@ -1885,15 +1893,11 @@ export function ParamOverrideEditorDialog(
|
|||||||
role='button'
|
role='button'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
draggable={operations.length > 1}
|
draggable={operations.length > 1}
|
||||||
onClick={() =>
|
onClick={() => setSelectedOperationId(operation.id)}
|
||||||
setSelectedOperationId(operation.id)
|
|
||||||
}
|
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) =>
|
||||||
handleDragStart(e, operation.id)
|
handleDragStart(e, operation.id)
|
||||||
}
|
}
|
||||||
onDragOver={(e) =>
|
onDragOver={(e) => handleDragOver(e, operation.id)}
|
||||||
handleDragOver(e, operation.id)
|
|
||||||
}
|
|
||||||
onDrop={(e) => handleDrop(e, operation.id)}
|
onDrop={(e) => handleDrop(e, operation.id)}
|
||||||
onDragEnd={resetDragState}
|
onDragEnd={resetDragState}
|
||||||
onKeyDown={(e: KeyboardEvent) => {
|
onKeyDown={(e: KeyboardEvent) => {
|
||||||
@@ -1948,9 +1952,7 @@ export function ParamOverrideEditorDialog(
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
|
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
getModeTagTailwind(
|
getModeTagTailwind(operation.mode || 'set')
|
||||||
operation.mode || 'set'
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
@@ -2038,9 +2040,7 @@ export function ParamOverrideEditorDialog(
|
|||||||
className='font-mono text-xs'
|
className='font-mono text-xs'
|
||||||
/>
|
/>
|
||||||
<p className='text-muted-foreground mt-2 text-xs'>
|
<p className='text-muted-foreground mt-2 text-xs'>
|
||||||
{t(
|
{t('Edit JSON text directly. Format will be validated on save.')}
|
||||||
'Edit JSON text directly. Format will be validated on save.'
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
{jsonError && (
|
{jsonError && (
|
||||||
<p className='text-destructive mt-1 text-xs'>{jsonError}</p>
|
<p className='text-destructive mt-1 text-xs'>{jsonError}</p>
|
||||||
@@ -2048,21 +2048,7 @@ export function ParamOverrideEditorDialog(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<DialogFooter className='border-t px-6 py-4'>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => props.onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='button' onClick={handleSave}>
|
|
||||||
{t('Save')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-32
@@ -21,16 +21,9 @@ import { AlertTriangle } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
interface StatusCodeRiskDialogProps {
|
interface StatusCodeRiskDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -84,18 +77,35 @@ export function StatusCodeRiskDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className='text-destructive flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<AlertTriangle className='h-5 w-5' />
|
<AlertTriangle className='h-5 w-5' />
|
||||||
{t('High-risk operation confirmation')}
|
{t('High-risk operation confirmation')}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription>
|
}
|
||||||
{t('High-risk status code retry risk disclaimer')}
|
description={t('High-risk status code retry risk disclaimer')}
|
||||||
</DialogDescription>
|
contentClassName='max-w-lg'
|
||||||
</DialogHeader>
|
titleClassName='text-destructive flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={handleCancel}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='destructive'
|
||||||
|
disabled={!canConfirm}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
{t('I confirm enabling high-risk retry')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
{detailItems.length > 0 && (
|
{detailItems.length > 0 && (
|
||||||
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'>
|
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'>
|
||||||
@@ -149,20 +159,6 @@ export function StatusCodeRiskDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={handleCancel}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='destructive'
|
|
||||||
disabled={!canConfirm}
|
|
||||||
onClick={handleConfirm}
|
|
||||||
>
|
|
||||||
{t('I confirm enabling high-risk retry')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-35
@@ -23,18 +23,11 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { MultiSelect } from '@/components/multi-select'
|
import { MultiSelect } from '@/components/multi-select'
|
||||||
import {
|
import {
|
||||||
getTagModels,
|
getTagModels,
|
||||||
@@ -190,15 +183,35 @@ export function TagBatchEditDialog({
|
|||||||
if (!currentTag) return null
|
if (!currentTag) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[90vh] max-w-2xl overflow-y-auto'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleClose}
|
||||||
<DialogTitle>{t('Batch Edit by Tag')}</DialogTitle>
|
title={t('Batch Edit by Tag')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Edit all channels with tag:')} <strong>{currentTag}</strong>
|
<>
|
||||||
</DialogDescription>
|
{t('Edit all channels with tag:')}
|
||||||
</DialogHeader>
|
<strong>{currentTag}</strong>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
contentClassName='max-w-2xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
!isLoading ? (
|
||||||
|
<>
|
||||||
|
<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 ? (
|
{isLoading ? (
|
||||||
<div className='flex items-center justify-center py-12'>
|
<div className='flex items-center justify-center py-12'>
|
||||||
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
|
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
|
||||||
@@ -272,9 +285,7 @@ export function TagBatchEditDialog({
|
|||||||
options={groupOptions}
|
options={groupOptions}
|
||||||
selected={groups}
|
selected={groups}
|
||||||
onChange={setGroups}
|
onChange={setGroups}
|
||||||
placeholder={t(
|
placeholder={t('Select groups (leave empty to keep current)')}
|
||||||
'Select groups (leave empty to keep current)'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
@@ -282,23 +293,8 @@ export function TagBatchEditDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<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' />}
|
|
||||||
{isSaving ? t('Saving...') : t('Save Changes')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-46
@@ -21,17 +21,11 @@ import { Search } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
|
|
||||||
interface UpstreamUpdateDialogProps {
|
interface UpstreamUpdateDialogProps {
|
||||||
@@ -120,18 +114,36 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={props.open} onOpenChange={(v) => !v && props.onCancel()}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={(v) => !v && props.onCancel()}
|
||||||
<DialogTitle>{t('Upstream Model Updates')}</DialogTitle>
|
title={t('Upstream Model Updates')}
|
||||||
</DialogHeader>
|
contentClassName='sm:max-w-lg'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={props.onCancel}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={
|
||||||
|
props.confirmLoading ||
|
||||||
|
(props.addModels.length === 0 &&
|
||||||
|
props.removeModels.length === 0)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<p className='text-muted-foreground text-sm'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
{t(
|
{t(
|
||||||
'Select models to process. Unselected "add" models will be ignored.'
|
'Select models to process. Unselected "add" models will be ignored.'
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={(v) => setActiveTab(v as 'add' | 'remove')}
|
onValueChange={(v) => setActiveTab(v as 'add' | 'remove')}
|
||||||
@@ -139,21 +151,13 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
|||||||
<TabsList className='grid w-full grid-cols-2'>
|
<TabsList className='grid w-full grid-cols-2'>
|
||||||
<TabsTrigger value='add' className='gap-1'>
|
<TabsTrigger value='add' className='gap-1'>
|
||||||
{t('Add Models')}
|
{t('Add Models')}
|
||||||
<StatusBadge
|
<StatusBadge variant='neutral' className='ml-1' copyable={false}>
|
||||||
variant='neutral'
|
|
||||||
className='ml-1'
|
|
||||||
copyable={false}
|
|
||||||
>
|
|
||||||
{selectedAdd.size}/{props.addModels.length}
|
{selectedAdd.size}/{props.addModels.length}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value='remove' className='gap-1'>
|
<TabsTrigger value='remove' className='gap-1'>
|
||||||
{t('Remove Models')}
|
{t('Remove Models')}
|
||||||
<StatusBadge
|
<StatusBadge variant='neutral' className='ml-1' copyable={false}>
|
||||||
variant='neutral'
|
|
||||||
className='ml-1'
|
|
||||||
copyable={false}
|
|
||||||
>
|
|
||||||
{selectedRemove.size}/{props.removeModels.length}
|
{selectedRemove.size}/{props.removeModels.length}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -248,11 +252,7 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedRemove.has(model)}
|
checked={selectedRemove.has(model)}
|
||||||
onCheckedChange={() =>
|
onCheckedChange={() =>
|
||||||
toggleModel(
|
toggleModel(model, selectedRemove, setSelectedRemove)
|
||||||
model,
|
|
||||||
selectedRemove,
|
|
||||||
setSelectedRemove
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className='truncate text-sm'>{model}</span>
|
<span className='truncate text-sm'>{model}</span>
|
||||||
@@ -269,23 +269,6 @@ export function UpstreamUpdateDialog(props: UpstreamUpdateDialogProps) {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={props.onCancel}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={
|
|
||||||
props.confirmLoading ||
|
|
||||||
(props.addModels.length === 0 &&
|
|
||||||
props.removeModels.length === 0)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('Confirm')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
+24
-39
@@ -21,15 +21,6 @@ import { Save, Settings2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { TimeGranularity } from '@/lib/time'
|
import type { TimeGranularity } from '@/lib/time'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -39,6 +30,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
|
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
|
||||||
MODEL_ANALYTICS_CHART_OPTIONS,
|
MODEL_ANALYTICS_CHART_OPTIONS,
|
||||||
@@ -74,23 +66,28 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogTrigger render={<Button variant='outline' size='sm' />}>
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
trigger={
|
||||||
|
<Button variant='outline' size='sm'>
|
||||||
<Settings2 className='mr-2 h-4 w-4' />
|
<Settings2 className='mr-2 h-4 w-4' />
|
||||||
{t('Preferences')}
|
{t('Preferences')}
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
<DialogContent className='sm:max-w-md'>
|
}
|
||||||
<DialogHeader>
|
title={t('Model Analytics Defaults')}
|
||||||
<DialogTitle>{t('Dashboard Preferences')}</DialogTitle>
|
description={t('Set default ranges and charts for model analytics.')}
|
||||||
<DialogDescription>
|
contentClassName='sm:max-w-md'
|
||||||
{t(
|
contentHeight='auto'
|
||||||
'Choose the default charts, range, and time granularity for model analytics.'
|
bodyClassName='grid gap-3'
|
||||||
)}
|
footer={
|
||||||
</DialogDescription>
|
<Button onClick={handleSave} type='button'>
|
||||||
</DialogHeader>
|
<Save className='mr-2 h-4 w-4' />
|
||||||
|
{t('Save Preferences')}
|
||||||
<div className='grid gap-4 py-2'>
|
</Button>
|
||||||
<div className='grid gap-2'>
|
}
|
||||||
|
>
|
||||||
|
<div className='grid gap-1.5'>
|
||||||
<Label htmlFor='default-time-range'>{t('Default range')}</Label>
|
<Label htmlFor='default-time-range'>{t('Default range')}</Label>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
@@ -121,8 +118,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='grid gap-1.5'>
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='default-time-granularity'>
|
<Label htmlFor='default-time-granularity'>
|
||||||
{t('Default time granularity')}
|
{t('Default time granularity')}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -155,8 +151,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='grid gap-1.5'>
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='consumption-distribution-chart'>
|
<Label htmlFor='consumption-distribution-chart'>
|
||||||
{t('Default consumption chart')}
|
{t('Default consumption chart')}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -190,8 +185,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='grid gap-1.5'>
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='model-analytics-chart'>
|
<Label htmlFor='model-analytics-chart'>
|
||||||
{t('Default model call chart')}
|
{t('Default model call chart')}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -224,15 +218,6 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={handleSave} type='button'>
|
|
||||||
<Save className='mr-2 h-4 w-4' />
|
|
||||||
{t('Save Preferences')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-40
@@ -23,15 +23,6 @@ import { useAuthStore } from '@/stores/auth-store'
|
|||||||
import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
|
import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
@@ -44,6 +35,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { DateTimePicker } from '@/components/datetime-picker'
|
import { DateTimePicker } from '@/components/datetime-picker'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
TIME_GRANULARITY_OPTIONS,
|
TIME_GRANULARITY_OPTIONS,
|
||||||
TIME_RANGE_PRESETS,
|
TIME_RANGE_PRESETS,
|
||||||
@@ -144,23 +136,35 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogTrigger render={<Button variant='outline' size='sm' />}>
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
trigger={
|
||||||
|
<Button variant='outline' size='sm'>
|
||||||
<Filter className='mr-2 h-4 w-4' />
|
<Filter className='mr-2 h-4 w-4' />
|
||||||
{t('Filter')}
|
{t('Filter')}
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
<DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'>
|
}
|
||||||
<DialogHeader>
|
title={t('Model Analytics Filters')}
|
||||||
<DialogTitle>{t('Filter Dashboard Models')}</DialogTitle>
|
description={t('Filter the model analytics view by time range and user.')}
|
||||||
<DialogDescription>
|
contentClassName='max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'
|
||||||
{t(
|
contentHeight='min(48vh, 460px)'
|
||||||
'Set filters to customize your dashboard statistics and charts.'
|
footerClassName='grid grid-cols-2 gap-2 sm:flex'
|
||||||
)}
|
footer={
|
||||||
</DialogDescription>
|
<>
|
||||||
</DialogHeader>
|
<Button onClick={handleReset} variant='outline' type='button'>
|
||||||
|
<RotateCcw className='mr-2 h-4 w-4' />
|
||||||
<ScrollArea className='flex-1 pr-3 sm:pr-4'>
|
{t('Reset')}
|
||||||
<div className='grid gap-3 py-3 sm:gap-4 sm:py-4'>
|
</Button>
|
||||||
|
<Button onClick={handleApply} type='submit'>
|
||||||
|
<Search className='mr-2 h-4 w-4' />
|
||||||
|
{t('Apply Filters')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ScrollArea className='h-full pr-3 sm:pr-4'>
|
||||||
|
<div className='grid gap-2.5 py-2'>
|
||||||
{/* Quick time range selection */}
|
{/* Quick time range selection */}
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label className='flex items-center gap-2'>
|
<Label className='flex items-center gap-2'>
|
||||||
@@ -173,9 +177,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
|||||||
key={range.days}
|
key={range.days}
|
||||||
type='button'
|
type='button'
|
||||||
size='sm'
|
size='sm'
|
||||||
variant={
|
variant={selectedRange === range.days ? 'default' : 'outline'}
|
||||||
selectedRange === range.days ? 'default' : 'outline'
|
|
||||||
}
|
|
||||||
onClick={() => handleQuickRange(range.days)}
|
onClick={() => handleQuickRange(range.days)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1',
|
'flex-1',
|
||||||
@@ -192,7 +194,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
|||||||
<SectionDivider label={t('Custom Time Range')} />
|
<SectionDivider label={t('Custom Time Range')} />
|
||||||
|
|
||||||
{/* Custom time range */}
|
{/* Custom time range */}
|
||||||
<div className='grid gap-3 sm:gap-4'>
|
<div className='grid gap-2.5'>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label htmlFor='start_timestamp'>{t('Start Time')}</Label>
|
<Label htmlFor='start_timestamp'>{t('Start Time')}</Label>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
@@ -265,18 +267,6 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
|
|
||||||
<Button onClick={handleReset} variant='outline' type='button'>
|
|
||||||
<RotateCcw className='mr-2 h-4 w-4' />
|
|
||||||
{t('Reset')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleApply} type='submit'>
|
|
||||||
<Search className='mr-2 h-4 w-4' />
|
|
||||||
{t('Apply Filters')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-20
@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { formatDateTimeObject } from '@/lib/time'
|
import { formatDateTimeObject } from '@/lib/time'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Markdown } from '@/components/ui/markdown'
|
import { Markdown } from '@/components/ui/markdown'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
interface AnnouncementDetailModalProps {
|
interface AnnouncementDetailModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -47,18 +41,20 @@ export function AnnouncementDetailModal({
|
|||||||
}: AnnouncementDetailModalProps) {
|
}: AnnouncementDetailModalProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Announcement Details')}</DialogTitle>
|
title={t('Announcement Details')}
|
||||||
{announcement?.publishDate && (
|
description={
|
||||||
<DialogDescription>
|
announcement?.publishDate
|
||||||
{t('Published:')}{' '}
|
? `${t('Published:')} ${formatDateTimeObject(new Date(announcement.publishDate))}`
|
||||||
{formatDateTimeObject(new Date(announcement.publishDate))}
|
: undefined
|
||||||
</DialogDescription>
|
}
|
||||||
)}
|
contentClassName='sm:max-w-lg'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
<ScrollArea className='max-h-[60vh] pr-4'>
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
|
<ScrollArea className='max-h-[min(58vh,520px)] pr-4'>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
{announcement?.content && (
|
{announcement?.content && (
|
||||||
<div>
|
<div>
|
||||||
@@ -78,7 +74,6 @@ export function AnnouncementDetailModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,9 @@ import { toast } from 'sonner'
|
|||||||
import { getUserModels } from '@/lib/api'
|
import { getUserModels } from '@/lib/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ComboboxInput } from '@/components/ui/combobox-input'
|
import { ComboboxInput } from '@/components/ui/combobox-input'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
const APP_CONFIGS = {
|
const APP_CONFIGS = {
|
||||||
claude: {
|
claude: {
|
||||||
@@ -151,12 +145,22 @@ export function CCSwitchDialog(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle>{t('Import to CC Switch')}</DialogTitle>
|
title={t('Import to CC Switch')}
|
||||||
</DialogHeader>
|
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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('Application')}</Label>
|
<Label>{t('Application')}</Label>
|
||||||
@@ -213,14 +217,6 @@ export function CCSwitchDialog(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={() => props.onOpenChange(false)}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit}>{t('Open CC Switch')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,20 +24,13 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { copyToClipboard } from '@/lib/copy-to-clipboard'
|
import { copyToClipboard } from '@/lib/copy-to-clipboard'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
handleBatchEnableModels,
|
handleBatchEnableModels,
|
||||||
handleBatchDisableModels,
|
handleBatchDisableModels,
|
||||||
@@ -187,19 +180,17 @@ export function DataTableBulkActions<TData>({
|
|||||||
</BulkActionsToolbar>
|
</BulkActionsToolbar>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
<Dialog
|
||||||
<DialogContent>
|
open={showDeleteConfirm}
|
||||||
<DialogHeader>
|
onOpenChange={setShowDeleteConfirm}
|
||||||
<DialogTitle>{t('Delete Models?')}</DialogTitle>
|
title={t('Delete Models?')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t(
|
|
||||||
'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.',
|
'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.',
|
||||||
{ count: selectedIds.length }
|
{ count: selectedIds.length }
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
footer={
|
||||||
|
<>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
onClick={() => setShowDeleteConfirm(false)}
|
onClick={() => setShowDeleteConfirm(false)}
|
||||||
@@ -209,8 +200,10 @@ export function DataTableBulkActions<TData>({
|
|||||||
<Button variant='destructive' onClick={handleDeleteAll}>
|
<Button variant='destructive' onClick={handleDeleteAll}>
|
||||||
{t('Delete')}
|
{t('Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</>
|
||||||
</DialogContent>
|
}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-15
@@ -17,14 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
type DescriptionDialogProps = {
|
type DescriptionDialogProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -41,13 +35,15 @@ export function DescriptionDialog({
|
|||||||
}: DescriptionDialogProps) {
|
}: DescriptionDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-w-2xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{modelName}</DialogTitle>
|
title={modelName}
|
||||||
<DialogDescription>{t('Model Description')}</DialogDescription>
|
description={t('Model Description')}
|
||||||
</DialogHeader>
|
contentClassName='max-w-2xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<ScrollArea className='max-h-96'>
|
<ScrollArea className='max-h-96'>
|
||||||
<div className='space-y-2 pr-4'>
|
<div className='space-y-2 pr-4'>
|
||||||
<p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'>
|
<p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'>
|
||||||
@@ -55,7 +51,6 @@ export function DescriptionDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-26
@@ -22,15 +22,9 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { estimatePrice, extendDeployment, getDeployment } from '../../api'
|
import { estimatePrice, extendDeployment, getDeployment } from '../../api'
|
||||||
import { deploymentsQueryKeys } from '../../lib'
|
import { deploymentsQueryKeys } from '../../lib'
|
||||||
|
|
||||||
@@ -164,12 +158,28 @@ export function ExtendDeploymentDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Extend deployment')}</DialogTitle>
|
title={t('Extend deployment')}
|
||||||
</DialogHeader>
|
contentClassName='sm:max-w-lg'
|
||||||
|
footerClassName='mt-4'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||||
|
) : null}
|
||||||
|
{t('Extend')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
{isLoadingDetails ? (
|
{isLoadingDetails ? (
|
||||||
<div className='flex items-center justify-center py-10'>
|
<div className='flex items-center justify-center py-10'>
|
||||||
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
||||||
@@ -218,19 +228,6 @@ export function ExtendDeploymentDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter className='mt-4'>
|
|
||||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
||||||
) : null}
|
|
||||||
{t('Extend')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-23
@@ -22,13 +22,6 @@ import { ChevronLeft, ChevronRight, Loader2, Plus, Search } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useIsMobile } from '@/hooks/use-mobile'
|
import { useIsMobile } from '@/hooks/use-mobile'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
@@ -37,6 +30,7 @@ import {
|
|||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/components/ui/empty'
|
} from '@/components/ui/empty'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { getMissingModels } from '../../api'
|
import { getMissingModels } from '../../api'
|
||||||
import { DEFAULT_PAGE_SIZE } from '../../constants'
|
import { DEFAULT_PAGE_SIZE } from '../../constants'
|
||||||
@@ -115,18 +109,19 @@ export function MissingModelsDialog({
|
|||||||
const showPagination = totalItems > pageSize
|
const showPagination = totalItems > pageSize
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent
|
open={open}
|
||||||
className='flex max-h-[85vh] max-w-2xl flex-col gap-3 p-4'
|
onOpenChange={onOpenChange}
|
||||||
|
title={t('Missing Models')}
|
||||||
|
description={t(
|
||||||
|
'Models that are being used but not configured in the system'
|
||||||
|
)}
|
||||||
|
contentClassName='flex max-h-[85vh] max-w-2xl flex-col gap-3 p-4'
|
||||||
|
headerClassName='flex-shrink-0 text-start'
|
||||||
|
contentHeight='min(74vh, 760px)'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
initialFocus={!isMobile}
|
initialFocus={!isMobile}
|
||||||
>
|
>
|
||||||
<DialogHeader className='flex-shrink-0 text-start'>
|
|
||||||
<DialogTitle>{t('Missing Models')}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t('Models that are being used but not configured in the system')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='flex items-center justify-center py-12'>
|
<div className='flex items-center justify-center py-12'>
|
||||||
<Loader2 className='h-8 w-8 animate-spin' />
|
<Loader2 className='h-8 w-8 animate-spin' />
|
||||||
@@ -142,8 +137,7 @@ export function MissingModelsDialog({
|
|||||||
<div className='flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto'>
|
<div className='flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto'>
|
||||||
<div className='flex flex-shrink-0 items-center justify-between gap-3'>
|
<div className='flex flex-shrink-0 items-center justify-between gap-3'>
|
||||||
<div className='text-muted-foreground text-sm whitespace-nowrap'>
|
<div className='text-muted-foreground text-sm whitespace-nowrap'>
|
||||||
{t('Showing')} {displayStart}-{displayEnd} {t('of')}{' '}
|
{t('Showing')} {displayStart}-{displayEnd} {t('of')} {totalItems}
|
||||||
{totalItems}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='relative w-48'>
|
<div className='relative w-48'>
|
||||||
<Search className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
|
<Search className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
|
||||||
@@ -225,9 +219,7 @@ export function MissingModelsDialog({
|
|||||||
size='icon'
|
size='icon'
|
||||||
className='h-8 w-8'
|
className='h-8 w-8'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((prev) =>
|
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
|
||||||
Math.min(totalPages, prev + 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
aria-label={t('Next page')}
|
aria-label={t('Next page')}
|
||||||
@@ -241,7 +233,6 @@ export function MissingModelsDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-85
@@ -25,7 +25,6 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -40,14 +39,6 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
@@ -64,6 +55,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { TableId } from '@/components/table-id'
|
import { TableId } from '@/components/table-id'
|
||||||
import { deletePrefillGroup, getPrefillGroups } from '../../api'
|
import { deletePrefillGroup, getPrefillGroups } from '../../api'
|
||||||
@@ -172,51 +164,31 @@ export function PrefillGroupManagementDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent
|
open={open}
|
||||||
showCloseButton={false}
|
onOpenChange={onOpenChange}
|
||||||
className='prefill-dialog-content !top-4 !flex !-translate-y-0 !flex-col !gap-0 !border-none !bg-transparent !p-0 !shadow-none sm:!top-1/2 sm:!-translate-y-1/2'
|
title={
|
||||||
style={{ maxWidth: 'min(100vw, 64rem)' }}
|
<>
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'prefill-dialog-panel border-border/70 bg-background flex max-h-[calc(100dvh-1.5rem)] flex-col overflow-hidden border shadow-2xl',
|
|
||||||
isMobile ? 'rounded-none' : 'rounded-2xl'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative flex flex-col gap-3 border-b px-4 py-4 sm:px-6 sm:py-5',
|
|
||||||
isMobile && 'pt-[calc(env(safe-area-inset-top,0px)+1rem)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogHeader className='max-w-3xl gap-3 pr-12 text-start sm:pr-0'>
|
|
||||||
<DialogTitle className='flex flex-wrap items-center gap-2 text-xl'>
|
|
||||||
<Layers3 className='text-foreground/80 h-5 w-5' />
|
<Layers3 className='text-foreground/80 h-5 w-5' />
|
||||||
{t('Prefill Group Management')}
|
{t('Prefill Group Management')}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription className='text-base leading-relaxed sm:text-sm'>
|
}
|
||||||
{t(
|
description={t(
|
||||||
'Create reusable bundles of models, tags, endpoints, and user groups to speed up configuration elsewhere in the console.'
|
'Create reusable bundles of models, tags, endpoints, and user groups to speed up configuration elsewhere in the console.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName={cn(
|
||||||
</DialogHeader>
|
'w-[calc(100vw-2rem)] sm:max-w-[52rem]',
|
||||||
|
isMobile && 'max-w-none rounded-none'
|
||||||
<DialogClose
|
)}
|
||||||
render={
|
titleClassName='flex flex-wrap items-center gap-2 text-lg'
|
||||||
<Button
|
descriptionClassName='text-sm leading-relaxed'
|
||||||
variant='ghost'
|
contentHeight='auto'
|
||||||
size='icon'
|
bodyClassName={cn(
|
||||||
className='text-muted-foreground hover:text-foreground absolute top-4 right-4 border border-transparent sm:top-5 sm:right-6'
|
'space-y-3',
|
||||||
/>
|
isMobile && 'pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]'
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<span className='sr-only'>{t('Close dialog')}</span>
|
<div className='bg-muted/30 flex flex-wrap items-center justify-between gap-3 rounded-md border p-2 text-sm'>
|
||||||
<X className='h-4 w-4' />
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-wrap items-center gap-3 border-b px-4 py-3 text-sm sm:px-6'>
|
|
||||||
<div className='flex flex-wrap items-center gap-2'>
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<Button size='sm' onClick={onCreateGroup}>
|
<Button size='sm' onClick={onCreateGroup}>
|
||||||
<Plus className='mr-2 h-4 w-4' />
|
<Plus className='mr-2 h-4 w-4' />
|
||||||
@@ -243,14 +215,7 @@ export function PrefillGroupManagementDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className='flex flex-col gap-3'>
|
||||||
className={cn(
|
|
||||||
'flex flex-1 flex-col overflow-hidden px-4 py-4 sm:px-6 sm:py-6',
|
|
||||||
isMobile && 'pb-[calc(env(safe-area-inset-bottom,0px)+1.5rem)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className='flex-1 overflow-y-auto'>
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert variant='destructive'>
|
<Alert variant='destructive'>
|
||||||
<AlertTitle>{t('Unable to load groups')}</AlertTitle>
|
<AlertTitle>{t('Unable to load groups')}</AlertTitle>
|
||||||
@@ -262,14 +227,14 @@ export function PrefillGroupManagementDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='flex flex-col items-center justify-center gap-2 py-16 text-center'>
|
<div className='flex flex-col items-center justify-center gap-2 py-12 text-center'>
|
||||||
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
||||||
<p className='text-muted-foreground text-sm'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
{t('Fetching prefill groups...')}
|
{t('Fetching prefill groups...')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : normalizedGroups.length === 0 ? (
|
) : normalizedGroups.length === 0 ? (
|
||||||
<Empty className='border border-dashed'>
|
<Empty className='border border-dashed py-10'>
|
||||||
<EmptyMedia variant='icon'>
|
<EmptyMedia variant='icon'>
|
||||||
<Layers3 className='h-6 w-6' />
|
<Layers3 className='h-6 w-6' />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
@@ -288,7 +253,7 @@ export function PrefillGroupManagementDialog({
|
|||||||
</EmptyDescription>
|
</EmptyDescription>
|
||||||
</Empty>
|
</Empty>
|
||||||
) : isMobile ? (
|
) : isMobile ? (
|
||||||
<div className='space-y-4'>
|
<div className='space-y-3'>
|
||||||
{normalizedGroups.map(({ group, meta, parsedItems }) => (
|
{normalizedGroups.map(({ group, meta, parsedItems }) => (
|
||||||
<Card key={group.id} className='border-border/60'>
|
<Card key={group.id} className='border-border/60'>
|
||||||
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
||||||
@@ -301,9 +266,7 @@ export function PrefillGroupManagementDialog({
|
|||||||
copyable={false}
|
copyable={false}
|
||||||
>
|
>
|
||||||
{meta.label}
|
{meta.label}
|
||||||
<span className='text-muted-foreground/30'>
|
<span className='text-muted-foreground/30'>·</span>
|
||||||
·
|
|
||||||
</span>
|
|
||||||
<span className='text-muted-foreground font-mono'>
|
<span className='text-muted-foreground font-mono'>
|
||||||
#{group.id}
|
#{group.id}
|
||||||
</span>
|
</span>
|
||||||
@@ -383,12 +346,12 @@ export function PrefillGroupManagementDialog({
|
|||||||
) : (
|
) : (
|
||||||
<div className='rounded-md border'>
|
<div className='rounded-md border'>
|
||||||
<div className='w-full overflow-x-auto'>
|
<div className='w-full overflow-x-auto'>
|
||||||
<Table className='min-w-[720px]'>
|
<Table className='min-w-[680px]'>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t('Group')}</TableHead>
|
<TableHead>{t('Group')}</TableHead>
|
||||||
<TableHead>{t('Type')}</TableHead>
|
<TableHead>{t('Type')}</TableHead>
|
||||||
<TableHead className='min-w-[280px]'>
|
<TableHead className='min-w-[240px]'>
|
||||||
{t('Items')}
|
{t('Items')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className='w-[120px] text-right'>
|
<TableHead className='w-[120px] text-right'>
|
||||||
@@ -397,15 +360,12 @@ export function PrefillGroupManagementDialog({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{normalizedGroups.map(
|
{normalizedGroups.map(({ group, meta, parsedItems }) => (
|
||||||
({ group, meta, parsedItems }) => (
|
|
||||||
<TableRow key={group.id}>
|
<TableRow key={group.id}>
|
||||||
<TableCell className='align-top whitespace-normal'>
|
<TableCell className='align-top whitespace-normal'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='flex flex-wrap items-center gap-2'>
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<span className='font-medium'>
|
<span className='font-medium'>{group.name}</span>
|
||||||
{group.name}
|
|
||||||
</span>
|
|
||||||
<TableId value={group.id} />
|
<TableId value={group.id} />
|
||||||
</div>
|
</div>
|
||||||
{group.description ? (
|
{group.description ? (
|
||||||
@@ -431,9 +391,7 @@ export function PrefillGroupManagementDialog({
|
|||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{parsedItems.length > 0 ? (
|
{parsedItems.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{parsedItems
|
{parsedItems.slice(0, 6).map((item) => (
|
||||||
.slice(0, 6)
|
|
||||||
.map((item) => (
|
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
key={item}
|
key={item}
|
||||||
label={item}
|
label={item}
|
||||||
@@ -471,9 +429,7 @@ export function PrefillGroupManagementDialog({
|
|||||||
onClick={() => onEditGroup(group)}
|
onClick={() => onEditGroup(group)}
|
||||||
>
|
>
|
||||||
<Pencil className='h-4 w-4' />
|
<Pencil className='h-4 w-4' />
|
||||||
<span className='sr-only'>
|
<span className='sr-only'>Edit group</span>
|
||||||
Edit group
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='icon'
|
size='icon'
|
||||||
@@ -482,25 +438,18 @@ export function PrefillGroupManagementDialog({
|
|||||||
onClick={() => handleDeleteClick(group)}
|
onClick={() => handleDeleteClick(group)}
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4' />
|
<Trash2 className='h-4 w-4' />
|
||||||
<span className='sr-only'>
|
<span className='sr-only'>Delete group</span>
|
||||||
Delete group
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
+23
-26
@@ -22,14 +22,8 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { checkClusterNameAvailability, updateDeploymentName } from '../../api'
|
import { checkClusterNameAvailability, updateDeploymentName } from '../../api'
|
||||||
import { deploymentsQueryKeys } from '../../lib'
|
import { deploymentsQueryKeys } from '../../lib'
|
||||||
|
|
||||||
@@ -111,12 +105,28 @@ export function RenameDeploymentDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Rename deployment')}</DialogTitle>
|
title={t('Rename deployment')}
|
||||||
</DialogHeader>
|
contentClassName='sm:max-w-lg'
|
||||||
|
footerClassName='mt-4'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||||
|
) : null}
|
||||||
|
{t('Rename')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{t('Deployment ID')}:{' '}
|
{t('Deployment ID')}:{' '}
|
||||||
@@ -130,19 +140,6 @@ export function RenameDeploymentDialog({
|
|||||||
/>
|
/>
|
||||||
<div className='text-muted-foreground text-xs'>{helper}</div>
|
<div className='text-muted-foreground text-xs'>{helper}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className='mt-4'>
|
|
||||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => void onSubmit()} disabled={!canSubmit}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
||||||
) : null}
|
|
||||||
{t('Rename')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-36
@@ -24,16 +24,9 @@ import { toast } from 'sonner'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useIsMobile } from '@/hooks/use-mobile'
|
import { useIsMobile } from '@/hooks/use-mobile'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { syncUpstream, previewUpstreamDiff } from '../../api'
|
import { syncUpstream, previewUpstreamDiff } from '../../api'
|
||||||
import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants'
|
import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants'
|
||||||
@@ -125,19 +118,31 @@ export function SyncWizardDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent
|
open={open}
|
||||||
className='flex max-h-[90vh] w-full flex-col gap-4 p-4 sm:max-w-2xl sm:p-6'
|
onOpenChange={onOpenChange}
|
||||||
|
title={t('Sync Upstream Models')}
|
||||||
|
description={t('Synchronize models and vendors from an upstream source')}
|
||||||
initialFocus={!isMobile}
|
initialFocus={!isMobile}
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='flex flex-col gap-6'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSyncing}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<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 ? t('Syncing...') : t('Sync Now')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<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 className='space-y-3'>
|
||||||
<div>
|
<div>
|
||||||
<Label className='text-base'>{t('Select Sync Source')}</Label>
|
<Label className='text-base'>{t('Select Sync Source')}</Label>
|
||||||
@@ -233,23 +238,6 @@ export function SyncWizardDialog({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className='flex-shrink-0 gap-2 sm:justify-end'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={isSyncing}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<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'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-40
@@ -30,13 +30,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -47,6 +40,7 @@ import {
|
|||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { getDeployment, updateDeployment } from '../../api'
|
import { getDeployment, updateDeployment } from '../../api'
|
||||||
import { deploymentsQueryKeys } from '../../lib'
|
import { deploymentsQueryKeys } from '../../lib'
|
||||||
|
|
||||||
@@ -64,6 +58,8 @@ const schema = z.object({
|
|||||||
|
|
||||||
type Values = z.input<typeof schema>
|
type Values = z.input<typeof schema>
|
||||||
|
|
||||||
|
const UPDATE_CONFIG_FORM_ID = 'update-config-form'
|
||||||
|
|
||||||
function normalizeJsonObject(input?: string) {
|
function normalizeJsonObject(input?: string) {
|
||||||
if (!input || !input.trim()) return undefined
|
if (!input || !input.trim()) return undefined
|
||||||
const parsed = JSON.parse(input)
|
const parsed = JSON.parse(input)
|
||||||
@@ -212,12 +208,37 @@ export function UpdateConfigDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{title}</DialogTitle>
|
title={title}
|
||||||
</DialogHeader>
|
contentClassName='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
isLoading ? null : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
form={UPDATE_CONFIG_FORM_ID}
|
||||||
|
disabled={form.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
{form.formState.isSubmitting ? (
|
||||||
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||||
|
) : null}
|
||||||
|
{t('Update')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='flex items-center justify-center py-10'>
|
<div className='flex items-center justify-center py-10'>
|
||||||
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
||||||
@@ -226,6 +247,7 @@ export function UpdateConfigDialog({
|
|||||||
<div className='max-h-[calc(100dvh-8.5rem)] overflow-y-auto py-2 pr-1 sm:max-h-[72vh]'>
|
<div className='max-h-[calc(100dvh-8.5rem)] overflow-y-auto py-2 pr-1 sm:max-h-[72vh]'>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={UPDATE_CONFIG_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
@@ -238,10 +260,7 @@ export function UpdateConfigDialog({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('Image')}</FormLabel>
|
<FormLabel>{t('Image')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='ollama/ollama:latest' {...field} />
|
||||||
placeholder='ollama/ollama:latest'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -286,9 +305,7 @@ export function UpdateConfigDialog({
|
|||||||
name='entrypoint'
|
name='entrypoint'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>{t('Entrypoint (space separated)')}</FormLabel>
|
||||||
{t('Entrypoint (space separated)')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='bash -lc' {...field} />
|
<Input placeholder='bash -lc' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -394,9 +411,7 @@ export function UpdateConfigDialog({
|
|||||||
name='secret_env_json'
|
name='secret_env_json'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>{t('Secret env (JSON object)')}</FormLabel>
|
||||||
{t('Secret env (JSON object)')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
className='min-h-40 font-mono text-xs'
|
className='min-h-40 font-mono text-xs'
|
||||||
@@ -411,27 +426,10 @@ export function UpdateConfigDialog({
|
|||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<DialogFooter className='grid grid-cols-2 gap-2 pt-2 sm:flex'>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='submit' disabled={form.formState.isSubmitting}>
|
|
||||||
{form.formState.isSubmitting ? (
|
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
||||||
) : null}
|
|
||||||
{t('Update')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-51
@@ -37,14 +37,6 @@ import { toast } from 'sonner'
|
|||||||
import { useIsMobile } from '@/hooks/use-mobile'
|
import { useIsMobile } from '@/hooks/use-mobile'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -67,6 +59,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { applyUpstreamOverwrite } from '../../api'
|
import { applyUpstreamOverwrite } from '../../api'
|
||||||
import { modelsQueryKeys, vendorsQueryKeys } from '../../lib'
|
import { modelsQueryKeys, vendorsQueryKeys } from '../../lib'
|
||||||
@@ -453,21 +446,46 @@ export function UpstreamConflictDialog({
|
|||||||
}
|
}
|
||||||
onOpenChange(nextOpen)
|
onOpenChange(nextOpen)
|
||||||
}}
|
}}
|
||||||
>
|
title={t('Resolve Conflicts')}
|
||||||
<DialogContent
|
description={t(
|
||||||
className='flex max-h-[90vh] w-full flex-col gap-4 p-4 sm:max-w-5xl sm:p-6'
|
|
||||||
initialFocus={!isMobile}
|
|
||||||
>
|
|
||||||
<div className='flex min-h-0 flex-1 flex-col gap-4 overflow-hidden'>
|
|
||||||
<DialogHeader className='flex-shrink-0 text-start'>
|
|
||||||
<DialogTitle>{t('Resolve Conflicts')}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t(
|
|
||||||
'Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.'
|
'Select the fields you want to overwrite with upstream data. Unselected fields keep their local values.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='w-full sm:max-w-5xl'
|
||||||
</DialogHeader>
|
contentHeight='min(72vh, 720px)'
|
||||||
|
bodyClassName='flex flex-col gap-4'
|
||||||
|
initialFocus={!isMobile}
|
||||||
|
footerClassName='sm:justify-between'
|
||||||
|
footer={
|
||||||
|
<div className='flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
|
<div className='text-muted-foreground flex flex-1 items-start gap-2 text-xs'>
|
||||||
|
<Info className='h-4 w-4 flex-shrink-0' />
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
'Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-2 sm:flex-row sm:justify-end'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => {
|
||||||
|
setUpstreamConflicts([])
|
||||||
|
onOpenChange(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleApplyOverwrite}
|
||||||
|
disabled={isSubmitting || !hasSelection}
|
||||||
|
>
|
||||||
|
{isSubmitting ? t('Applying...') : t('Apply Overwrite')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='flex min-h-0 flex-1 flex-col gap-4'>
|
||||||
{!hasConflicts ? (
|
{!hasConflicts ? (
|
||||||
<div className='text-muted-foreground flex flex-1 items-center justify-center rounded-md border border-dashed p-8 text-center text-sm'>
|
<div className='text-muted-foreground flex flex-1 items-center justify-center rounded-md border border-dashed p-8 text-center text-sm'>
|
||||||
{t('No conflict entries available.')}
|
{t('No conflict entries available.')}
|
||||||
@@ -639,36 +657,6 @@ export function UpstreamConflictDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className='flex-shrink-0'>
|
|
||||||
<div className='flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
|
||||||
<div className='text-muted-foreground flex flex-1 items-start gap-2 text-xs'>
|
|
||||||
<Info className='h-4 w-4 flex-shrink-0' />
|
|
||||||
<span>
|
|
||||||
{t(
|
|
||||||
'Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2 sm:flex-row sm:justify-end'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => {
|
|
||||||
setUpstreamConflicts([])
|
|
||||||
onOpenChange(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleApplyOverwrite}
|
|
||||||
disabled={isSubmitting || !hasSelection}
|
|
||||||
>
|
|
||||||
{isSubmitting ? t('Applying...') : t('Apply Overwrite')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-37
@@ -24,14 +24,6 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -43,6 +35,7 @@ import {
|
|||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { createVendor, updateVendor } from '../../api'
|
import { createVendor, updateVendor } from '../../api'
|
||||||
import { vendorsQueryKeys, modelsQueryKeys } from '../../lib'
|
import { vendorsQueryKeys, modelsQueryKeys } from '../../lib'
|
||||||
import { vendorFormSchema, type Vendor } from '../../types'
|
import { vendorFormSchema, type Vendor } from '../../types'
|
||||||
@@ -53,6 +46,8 @@ type VendorMutateDialogProps = {
|
|||||||
currentVendor?: Vendor | null
|
currentVendor?: Vendor | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VENDOR_MUTATE_FORM_ID = 'vendor-mutate-form'
|
||||||
|
|
||||||
export function VendorMutateDialog({
|
export function VendorMutateDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@@ -118,23 +113,48 @@ export function VendorMutateDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={isEdit ? t('Edit Vendor') : t('Create Vendor')}
|
||||||
{isEdit ? t('Edit Vendor') : t('Create Vendor')}
|
description={
|
||||||
</DialogTitle>
|
isEdit
|
||||||
<DialogDescription>
|
|
||||||
{isEdit
|
|
||||||
? t('Update vendor information for {{name}}', {
|
? t('Update vendor information for {{name}}', {
|
||||||
name: currentVendor?.name,
|
name: currentVendor?.name,
|
||||||
})
|
})
|
||||||
: t('Add a new vendor to the system')}
|
: t('Add a new vendor to the system')
|
||||||
</DialogDescription>
|
}
|
||||||
</DialogHeader>
|
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 {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
<form
|
||||||
|
id={VENDOR_MUTATE_FORM_ID}
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className='space-y-4'
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='name'
|
name='name'
|
||||||
@@ -192,24 +212,8 @@ export function VendorMutateDialog({
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-30
@@ -27,14 +27,8 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { getDeployment, listDeploymentContainers } from '../../api'
|
import { getDeployment, listDeploymentContainers } from '../../api'
|
||||||
|
|
||||||
export function ViewDetailsDialog({
|
export function ViewDetailsDialog({
|
||||||
@@ -116,12 +110,25 @@ export function ViewDetailsDialog({
|
|||||||
}, [details])
|
}, [details])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Deployment details')}</DialogTitle>
|
title={t('Deployment details')}
|
||||||
</DialogHeader>
|
contentClassName='max-h-[calc(100dvh-2rem)] overflow-hidden max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-3xl'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className='w-full sm:w-auto'
|
||||||
|
>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='max-h-[calc(100dvh-8.5rem)] space-y-3 overflow-y-auto py-2 pr-1 sm:max-h-[72vh] sm:space-y-4'>
|
<div className='max-h-[calc(100dvh-8.5rem)] space-y-3 overflow-y-auto py-2 pr-1 sm:max-h-[72vh] sm:space-y-4'>
|
||||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
@@ -184,9 +191,7 @@ export function ViewDetailsDialog({
|
|||||||
{t('Total GPUs')}
|
{t('Total GPUs')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-1 font-medium'>
|
<div className='mt-1 font-medium'>
|
||||||
{String(
|
{String(details?.total_gpus ?? details?.hardware_qty ?? '-')}
|
||||||
details?.total_gpus ?? details?.hardware_qty ?? '-'
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded-lg border p-3'>
|
<div className='rounded-lg border p-3'>
|
||||||
@@ -231,9 +236,7 @@ export function ViewDetailsDialog({
|
|||||||
className='flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2'
|
className='flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2'
|
||||||
>
|
>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<div className='truncate font-mono text-sm'>
|
<div className='truncate font-mono text-sm'>{id}</div>
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>
|
||||||
{status ? `${t('Status')}: ${status}` : ''}
|
{status ? `${t('Status')}: ${status}` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -268,17 +271,6 @@ export function ViewDetailsDialog({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
className='w-full sm:w-auto'
|
|
||||||
>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { Download, Loader2, RefreshCcw, Terminal } from 'lucide-react'
|
import { Download, Loader2, RefreshCcw, Terminal } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -36,6 +30,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { getDeploymentLogs, listDeploymentContainers } from '../../api'
|
import { getDeploymentLogs, listDeploymentContainers } from '../../api'
|
||||||
|
|
||||||
interface ViewLogsDialogProps {
|
interface ViewLogsDialogProps {
|
||||||
@@ -142,15 +137,20 @@ export function ViewLogsDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='flex h-[calc(100dvh-2rem)] flex-col max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:h-[80vh] sm:max-w-4xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<Terminal className='h-5 w-5' />
|
<Terminal className='h-5 w-5' />
|
||||||
{t('Deployment logs')}
|
{t('Deployment logs')}
|
||||||
</DialogTitle>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentClassName='flex h-[calc(100dvh-2rem)] flex-col max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:h-[80vh] sm:max-w-4xl'
|
||||||
|
titleClassName='flex items-center gap-2'
|
||||||
|
contentHeight='min(72vh, 720px)'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<div className='mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3'>
|
<div className='mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3'>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{t('Deployment ID')}: {deploymentId}
|
{t('Deployment ID')}: {deploymentId}
|
||||||
@@ -187,12 +187,9 @@ export function ViewLogsDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mb-3 grid gap-2 sm:grid-cols-2 sm:gap-3'>
|
<div className='mb-3 grid gap-2 sm:grid-cols-2 sm:gap-3'>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<div className='text-muted-foreground text-xs'>
|
<div className='text-muted-foreground text-xs'>{t('Container')}</div>
|
||||||
{t('Container')}
|
|
||||||
</div>
|
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
...containers.flatMap((c) => {
|
...containers.flatMap((c) => {
|
||||||
@@ -280,7 +277,6 @@ export function ViewLogsDialog({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className='flex-1 overflow-auto rounded-md border bg-black p-3 sm:p-4'
|
className='flex-1 overflow-auto rounded-md border bg-black p-3 sm:p-4'
|
||||||
@@ -315,7 +311,6 @@ export function ViewLogsDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ import { formatQuotaWithCurrency } from '@/lib/currency'
|
|||||||
import dayjs from '@/lib/dayjs'
|
import dayjs from '@/lib/dayjs'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -45,6 +39,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { Turnstile } from '@/components/turnstile'
|
import { Turnstile } from '@/components/turnstile'
|
||||||
import { getCheckinStatus, performCheckin } from '../api'
|
import { getCheckinStatus, performCheckin } from '../api'
|
||||||
import type { CheckinRecord } from '../types'
|
import type { CheckinRecord } from '../types'
|
||||||
@@ -253,11 +248,11 @@ export function CheckinCalendarCard({
|
|||||||
setTurnstileWidgetKey((v) => v + 1)
|
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'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{t('Please complete the security check to continue.')}
|
{t('Please complete the security check to continue.')}
|
||||||
</div>
|
</div>
|
||||||
@@ -273,7 +268,6 @@ export function CheckinCalendarCard({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div className='bg-card overflow-hidden rounded-2xl border'>
|
<div className='bg-card overflow-hidden rounded-2xl border'>
|
||||||
|
|||||||
+34
-41
@@ -20,17 +20,10 @@ import { useEffect } from 'react'
|
|||||||
import { RefreshCw, Loader2 } from 'lucide-react'
|
import { RefreshCw, Loader2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { CopyButton } from '@/components/copy-button'
|
import { CopyButton } from '@/components/copy-button'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { useAccessToken } from '../../hooks'
|
import { useAccessToken } from '../../hooks'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -57,17 +50,41 @@ export function AccessTokenDialog({
|
|||||||
}, [open, token, generate])
|
}, [open, token, generate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Access Token')}</DialogTitle>
|
title={t('Access Token')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t(
|
|
||||||
"Your system access token for API authentication. Keep it secure and don't share it with others."
|
"Your system access token for API authentication. Keep it secure and don't share it with others."
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-md'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={generate}
|
||||||
|
disabled={generating}
|
||||||
|
className='gap-2'
|
||||||
|
>
|
||||||
|
{generating ? (
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin' />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className='h-4 w-4' />
|
||||||
|
)}
|
||||||
|
{generating ? t('Generating...') : t('Regenerate')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='my-6 space-y-4'>
|
<div className='my-6 space-y-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='token'>{t('Token')}</Label>
|
<Label htmlFor='token'>{t('Token')}</Label>
|
||||||
@@ -94,30 +111,6 @@ export function AccessTokenDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={generate}
|
|
||||||
disabled={generating}
|
|
||||||
className='gap-2'
|
|
||||||
>
|
|
||||||
{generating ? (
|
|
||||||
<Loader2 className='h-4 w-4 animate-spin' />
|
|
||||||
) : (
|
|
||||||
<RefreshCw className='h-4 w-4' />
|
|
||||||
)}
|
|
||||||
{generating ? t('Generating...') : t('Regenerate')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-46
@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { PasswordInput } from '@/components/password-input'
|
import { PasswordInput } from '@/components/password-input'
|
||||||
import { updateUserProfile } from '../../api'
|
import { updateUserProfile } from '../../api'
|
||||||
|
|
||||||
@@ -114,27 +107,45 @@ export function ChangePasswordDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const formId = 'change-password-form'
|
||||||
<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'>
|
return (
|
||||||
|
<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'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='currentPassword'>{t('Current Password')}</Label>
|
<Label htmlFor='currentPassword'>{t('Current Password')}</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id='currentPassword'
|
id='currentPassword'
|
||||||
value={formData.originalPassword}
|
value={formData.originalPassword}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleChange('originalPassword', e.target.value)}
|
||||||
handleChange('originalPassword', e.target.value)
|
|
||||||
}
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
required
|
required
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
@@ -158,38 +169,17 @@ export function ChangePasswordDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='confirmPassword'>
|
<Label htmlFor='confirmPassword'>{t('Confirm New Password')}</Label>
|
||||||
{t('Confirm New Password')}
|
|
||||||
</Label>
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id='confirmPassword'
|
id='confirmPassword'
|
||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={(e) =>
|
onChange={(e) => handleChange('confirmPassword', e.target.value)}
|
||||||
handleChange('confirmPassword', e.target.value)
|
|
||||||
}
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
required
|
required
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-39
@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { deleteUserAccount } from '../../api'
|
import { deleteUserAccount } from '../../api'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -101,20 +94,44 @@ export function DeleteAccountDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle className='text-destructive flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<AlertTriangle className='h-5 w-5' />
|
<AlertTriangle className='h-5 w-5' />
|
||||||
{t('Delete Account')}
|
{t('Delete Account')}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription>
|
}
|
||||||
{t(
|
description={t(
|
||||||
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
|
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-md'
|
||||||
</DialogHeader>
|
titleClassName='text-destructive flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => handleOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={loading || confirmation !== username}
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{loading ? t('Deleting...') : t('Delete Account')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='my-6 space-y-4'>
|
<div className='my-6 space-y-4'>
|
||||||
<Alert variant='destructive'>
|
<Alert variant='destructive'>
|
||||||
<AlertTriangle className='h-4 w-4' />
|
<AlertTriangle className='h-4 w-4' />
|
||||||
@@ -138,27 +155,6 @@ export function DeleteAccountDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => handleOpenChange(false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='destructive'
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={loading || confirmation !== username}
|
|
||||||
>
|
|
||||||
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{loading ? t('Deleting...') : t('Delete Account')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-38
@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useCountdown } from '@/hooks/use-countdown'
|
import { useCountdown } from '@/hooks/use-countdown'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { sendEmailVerification, bindEmail } from '../../api'
|
import { sendEmailVerification, bindEmail } from '../../api'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -129,19 +122,41 @@ export function EmailBindDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle>{t('Bind Email')}</DialogTitle>
|
title={t('Bind Email')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{currentEmail
|
currentEmail
|
||||||
? t('Current email: {{email}}. Enter a new email to change.', {
|
? t('Current email: {{email}}. Enter a new email to change.', {
|
||||||
email: currentEmail,
|
email: currentEmail,
|
||||||
})
|
})
|
||||||
: t('Bind an email address to your account.')}
|
: t('Bind an email address to your account.')
|
||||||
</DialogDescription>
|
}
|
||||||
</DialogHeader>
|
contentClassName='sm:max-w-md'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => handleOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={handleBind}
|
||||||
|
disabled={loading || !email || !code}
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{loading ? t('Binding...') : t('Bind Email')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='email'>{t('Email Address')}</Label>
|
<Label htmlFor='email'>{t('Email Address')}</Label>
|
||||||
@@ -181,26 +196,6 @@ export function EmailBindDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => handleOpenChange(false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={handleBind}
|
|
||||||
disabled={loading || !email || !code}
|
|
||||||
>
|
|
||||||
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{loading ? t('Binding...') : t('Bind Email')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-17
@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { Send } from 'lucide-react'
|
import { Send } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import { Dialog } from '@/components/dialog'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Telegram Bind Dialog Component
|
// Telegram Bind Dialog Component
|
||||||
@@ -45,15 +39,15 @@ export function TelegramBindDialog({
|
|||||||
}: TelegramBindDialogProps) {
|
}: TelegramBindDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Bind Telegram Account')}</DialogTitle>
|
title={t('Bind Telegram Account')}
|
||||||
<DialogDescription>
|
description={t('Click the button below to bind your Telegram account')}
|
||||||
{t('Click the button below to bind your Telegram account')}
|
contentClassName='sm:max-w-md'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<Alert>
|
<Alert>
|
||||||
<Send className='h-4 w-4' />
|
<Send className='h-4 w-4' />
|
||||||
@@ -94,7 +88,6 @@ export function TelegramBindDialog({
|
|||||||
{t('The binding will complete automatically after authorization')}
|
{t('The binding will complete automatically after authorization')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-40
@@ -23,17 +23,10 @@ import { toast } from 'sonner'
|
|||||||
import { regenerate2FABackupCodes } from '@/lib/api'
|
import { regenerate2FABackupCodes } from '@/lib/api'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { CopyButton } from '@/components/copy-button'
|
import { CopyButton } from '@/components/copy-button'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Two-FA Backup Codes Dialog Component
|
// Two-FA Backup Codes Dialog Component
|
||||||
@@ -94,20 +87,46 @@ export function TwoFABackupDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<RefreshCw className='h-5 w-5' />
|
<RefreshCw className='h-5 w-5' />
|
||||||
{t('Regenerate Backup Codes')}
|
{t('Regenerate Backup Codes')}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription>
|
}
|
||||||
{backupCodes.length > 0
|
description={
|
||||||
|
backupCodes.length > 0
|
||||||
? t('Your new backup codes are ready')
|
? t('Your new backup codes are ready')
|
||||||
: t('Generate new backup codes for account recovery')}
|
: t('Generate new backup codes for account recovery')
|
||||||
</DialogDescription>
|
}
|
||||||
</DialogHeader>
|
contentClassName='sm:max-w-md'
|
||||||
|
titleClassName='flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
{backupCodes.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => handleOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRegenerate} disabled={loading || !code}>
|
||||||
|
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{loading ? t('Generating...') : t('Generate New Codes')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleDone}>{t('Done')}</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
{backupCodes.length === 0 ? (
|
{backupCodes.length === 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -168,27 +187,6 @@ export function TwoFABackupDialog({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
{backupCodes.length === 0 ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => handleOpenChange(false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleRegenerate} disabled={loading || !code}>
|
|
||||||
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{loading ? t('Generating...') : t('Generate New Codes')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button onClick={handleDone}>{t('Done')}</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-37
@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api'
|
|||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Two-FA Disable Dialog Component
|
// Two-FA Disable Dialog Component
|
||||||
@@ -98,20 +91,42 @@ export function TwoFADisableDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle className='text-destructive flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<AlertTriangle className='h-5 w-5' />
|
<AlertTriangle className='h-5 w-5' />
|
||||||
{t('Disable Two-Factor Authentication')}
|
{t('Disable Two-Factor Authentication')}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription>
|
}
|
||||||
{t(
|
description={t(
|
||||||
'This action will permanently remove 2FA protection from your account.'
|
'This action will permanently remove 2FA protection from your account.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-md'
|
||||||
</DialogHeader>
|
titleClassName='text-destructive flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => handleOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='destructive'
|
||||||
|
onClick={handleDisable}
|
||||||
|
disabled={loading || !code || !confirmed}
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{loading ? t('Disabling...') : t('Disable 2FA')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<Alert variant='destructive'>
|
<Alert variant='destructive'>
|
||||||
<AlertTriangle className='h-4 w-4' />
|
<AlertTriangle className='h-4 w-4' />
|
||||||
@@ -150,25 +165,6 @@ export function TwoFADisableDialog({
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => handleOpenChange(false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='destructive'
|
|
||||||
onClick={handleDisable}
|
|
||||||
disabled={loading || !code || !confirmed}
|
|
||||||
>
|
|
||||||
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{loading ? t('Disabling...') : t('Disable 2FA')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-46
@@ -24,17 +24,10 @@ import { toast } from 'sonner'
|
|||||||
import { setup2FA, enable2FA } from '@/lib/api'
|
import { setup2FA, enable2FA } from '@/lib/api'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { CopyButton } from '@/components/copy-button'
|
import { CopyButton } from '@/components/copy-button'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import type { TwoFASetupData } from '../../types'
|
import type { TwoFASetupData } from '../../types'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -136,15 +129,51 @@ export function TwoFASetupDialog({
|
|||||||
}, [open, setupData, initializing, handleSetup])
|
}, [open, setupData, initializing, handleSetup])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle>{t('Setup Two-Factor Authentication')}</DialogTitle>
|
title={t('Setup Two-Factor Authentication')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{t('Step')} {step + 1} {t('of 3:')} {stepLabels[step]}
|
<>
|
||||||
</DialogDescription>
|
{t('Step')}
|
||||||
</DialogHeader>
|
{step + 1}
|
||||||
|
{t('of 3:')}
|
||||||
|
{stepLabels[step]}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
contentClassName='sm:max-w-lg'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
{step > 0 && (
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setStep(step - 1)}
|
||||||
|
disabled={initializing || loading}
|
||||||
|
>
|
||||||
|
{t('Back')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step < 2 ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => setStep(step + 1)}
|
||||||
|
disabled={initializing || !setupData}
|
||||||
|
>
|
||||||
|
{t('Next')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleEnable}
|
||||||
|
disabled={initializing || loading || !code}
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{loading ? t('Enabling...') : t('Enable 2FA')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
{initializing ? (
|
{initializing ? (
|
||||||
<div className='flex flex-col items-center justify-center gap-3 py-8'>
|
<div className='flex flex-col items-center justify-center gap-3 py-8'>
|
||||||
@@ -251,35 +280,6 @@ export function TwoFASetupDialog({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
{step > 0 && (
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setStep(step - 1)}
|
|
||||||
disabled={initializing || loading}
|
|
||||||
>
|
|
||||||
{t('Back')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step < 2 ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => setStep(step + 1)}
|
|
||||||
disabled={initializing || !setupData}
|
|
||||||
>
|
|
||||||
{t('Next')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={handleEnable}
|
|
||||||
disabled={initializing || loading || !code}
|
|
||||||
>
|
|
||||||
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{loading ? t('Enabling...') : t('Enable 2FA')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-17
@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { QrCode } from 'lucide-react'
|
import { QrCode } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import { Dialog } from '@/components/dialog'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// WeChat Bind Dialog Component
|
// WeChat Bind Dialog Component
|
||||||
@@ -43,15 +37,15 @@ export function WeChatBindDialog({
|
|||||||
}: WeChatBindDialogProps) {
|
}: WeChatBindDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Bind WeChat Account')}</DialogTitle>
|
title={t('Bind WeChat Account')}
|
||||||
<DialogDescription>
|
description={t('Scan the QR code with WeChat to bind your account')}
|
||||||
{t('Scan the QR code with WeChat to bind your account')}
|
contentClassName='sm:max-w-md'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<Alert>
|
<Alert>
|
||||||
<QrCode className='h-4 w-4' />
|
<QrCode className='h-4 w-4' />
|
||||||
@@ -76,7 +70,6 @@ export function WeChatBindDialog({
|
|||||||
{t('After scanning, the binding will complete automatically')}
|
{t('After scanning, the binding will complete automatically')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-20
@@ -25,12 +25,6 @@ import { formatQuota } from '@/lib/format'
|
|||||||
import { useSystemConfig } from '@/hooks/use-system-config'
|
import { useSystemConfig } from '@/hooks/use-system-config'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -40,6 +34,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { GroupBadge } from '@/components/group-badge'
|
import { GroupBadge } from '@/components/group-badge'
|
||||||
import {
|
import {
|
||||||
paySubscriptionStripe,
|
paySubscriptionStripe,
|
||||||
@@ -259,15 +254,20 @@ export function SubscriptionPurchaseDialog(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<Crown className='h-5 w-5' />
|
<Crown className='h-5 w-5' />
|
||||||
{t('Purchase Subscription')}
|
{t('Purchase Subscription')}
|
||||||
</DialogTitle>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentClassName='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'
|
||||||
|
titleClassName='flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<div className='space-y-3 sm:space-y-4'>
|
<div className='space-y-3 sm:space-y-4'>
|
||||||
<div className='bg-muted/50 space-y-2.5 rounded-lg border p-3 sm:space-y-3 sm:p-4'>
|
<div className='bg-muted/50 space-y-2.5 rounded-lg border p-3 sm:space-y-3 sm:p-4'>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
@@ -346,9 +346,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
|
|||||||
) : (
|
) : (
|
||||||
insufficientBalance && (
|
insufficientBalance && (
|
||||||
<Alert variant='destructive'>
|
<Alert variant='destructive'>
|
||||||
<AlertDescription>
|
<AlertDescription>{t('Insufficient balance')}</AlertDescription>
|
||||||
{t('Insufficient balance')}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -412,9 +410,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
|
|||||||
})),
|
})),
|
||||||
]}
|
]}
|
||||||
value={selectedEpayMethod}
|
value={selectedEpayMethod}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) => v !== null && setSelectedEpayMethod(v)}
|
||||||
v !== null && setSelectedEpayMethod(v)
|
|
||||||
}
|
|
||||||
disabled={limitReached}
|
disabled={limitReached}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='flex-1'>
|
<SelectTrigger className='flex-1'>
|
||||||
@@ -441,7 +437,6 @@ export function SubscriptionPurchaseDialog(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+39
-45
@@ -21,14 +21,6 @@ import { type Resolver, useForm } from 'react-hook-form'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -50,6 +42,7 @@ import {
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import {
|
import {
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
SettingsSwitchContent,
|
SettingsSwitchContent,
|
||||||
@@ -74,6 +67,8 @@ type ProviderFormDialogProps = {
|
|||||||
provider?: CustomOAuthProvider | null
|
provider?: CustomOAuthProvider | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PROVIDER_FORM_ID = 'custom-oauth-provider-form'
|
||||||
|
|
||||||
export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isEditing = !!props.provider
|
const isEditing = !!props.provider
|
||||||
@@ -174,23 +169,43 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
|||||||
const isPending = createProvider.isPending || updateProvider.isPending
|
const isPending = createProvider.isPending || updateProvider.isPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[85vh] overflow-y-auto sm:max-w-2xl'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle>
|
title={isEditing ? t('Edit OAuth Provider') : t('Add OAuth Provider')}
|
||||||
{isEditing ? t('Edit OAuth Provider') : t('Add OAuth Provider')}
|
description={
|
||||||
</DialogTitle>
|
isEditing
|
||||||
<DialogDescription>
|
|
||||||
{isEditing
|
|
||||||
? t('Update the configuration for this custom OAuth provider.')
|
? t('Update the configuration for this custom OAuth provider.')
|
||||||
: t(
|
: t('Configure a new custom OAuth provider for user authentication.')
|
||||||
'Configure a new custom OAuth provider for user authentication.'
|
}
|
||||||
)}
|
contentClassName='max-h-[85vh] overflow-y-auto sm:max-w-2xl'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => props.onOpenChange(false)}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={PROVIDER_FORM_ID} disabled={isPending}>
|
||||||
|
{isPending
|
||||||
|
? t('Saving...')
|
||||||
|
: isEditing
|
||||||
|
? t('Update Provider')
|
||||||
|
: t('Create Provider')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
|
<SettingsForm
|
||||||
|
id={PROVIDER_FORM_ID}
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
{/* Preset Selector (only for creating) */}
|
{/* Preset Selector (only for creating) */}
|
||||||
{!isEditing && <PresetSelector form={form} />}
|
{!isEditing && <PresetSelector form={form} />}
|
||||||
|
|
||||||
@@ -352,9 +367,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t('How client credentials are sent to the token endpoint')}
|
||||||
'How client credentials are sent to the token endpoint'
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -587,27 +600,8 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => props.onOpenChange(false)}
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='submit' disabled={isPending}>
|
|
||||||
{isPending
|
|
||||||
? t('Saving...')
|
|
||||||
: isEditing
|
|
||||||
? t('Update Provider')
|
|
||||||
: t('Create Provider')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</SettingsForm>
|
</SettingsForm>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-33
@@ -36,14 +36,6 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -72,6 +64,7 @@ import {
|
|||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { DateTimePicker } from '@/components/datetime-picker'
|
import { DateTimePicker } from '@/components/datetime-picker'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { SettingsSwitchField } from '../components/settings-form-layout'
|
import { SettingsSwitchField } from '../components/settings-form-layout'
|
||||||
import { SettingsSection } from '../components/settings-section'
|
import { SettingsSection } from '../components/settings-section'
|
||||||
@@ -105,6 +98,8 @@ const announcementSchema = z.object({
|
|||||||
|
|
||||||
type AnnouncementFormValues = z.infer<typeof announcementSchema>
|
type AnnouncementFormValues = z.infer<typeof announcementSchema>
|
||||||
|
|
||||||
|
const ANNOUNCEMENT_FORM_ID = 'announcement-form'
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{
|
{
|
||||||
value: 'default',
|
value: 'default',
|
||||||
@@ -460,20 +455,36 @@ export function AnnouncementsSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
<Dialog
|
||||||
<DialogContent className='max-w-2xl'>
|
open={showDialog}
|
||||||
<DialogHeader>
|
onOpenChange={setShowDialog}
|
||||||
<DialogTitle>
|
title={
|
||||||
{editingAnnouncement
|
editingAnnouncement ? t('Edit Announcement') : t('Add Announcement')
|
||||||
? t('Edit Announcement')
|
}
|
||||||
: t('Add Announcement')}
|
description={t(
|
||||||
</DialogTitle>
|
'Create or update system announcements for the dashboard'
|
||||||
<DialogDescription>
|
)}
|
||||||
{t('Create or update system announcements for the dashboard')}
|
contentClassName='max-w-2xl'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowDialog(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={ANNOUNCEMENT_FORM_ID}>
|
||||||
|
{editingAnnouncement ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={ANNOUNCEMENT_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmitForm)}
|
onSubmit={form.handleSubmit(handleSubmitForm)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -593,21 +604,8 @@ export function AnnouncementsSection({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setShowDialog(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='submit'>
|
|
||||||
{editingAnnouncement ? t('Update') : t('Add')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -70,6 +62,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { SettingsSwitchField } from '../components/settings-form-layout'
|
import { SettingsSwitchField } from '../components/settings-form-layout'
|
||||||
import { SettingsSection } from '../components/settings-section'
|
import { SettingsSection } from '../components/settings-section'
|
||||||
@@ -98,6 +91,8 @@ const createApiInfoSchema = (t: (key: string) => string) =>
|
|||||||
|
|
||||||
type ApiInfoFormValues = z.infer<ReturnType<typeof createApiInfoSchema>>
|
type ApiInfoFormValues = z.infer<ReturnType<typeof createApiInfoSchema>>
|
||||||
|
|
||||||
|
const API_INFO_FORM_ID = 'api-info-form'
|
||||||
|
|
||||||
const colorOptions = [
|
const colorOptions = [
|
||||||
{ value: 'blue', label: 'Blue' },
|
{ value: 'blue', label: 'Blue' },
|
||||||
{ value: 'green', label: 'Green' },
|
{ value: 'green', label: 'Green' },
|
||||||
@@ -408,18 +403,31 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
<Dialog
|
||||||
<DialogContent>
|
open={showDialog}
|
||||||
<DialogHeader>
|
onOpenChange={setShowDialog}
|
||||||
<DialogTitle>
|
title={editingApiInfo ? t('Edit API Shortcut') : t('Add API Shortcut')}
|
||||||
{editingApiInfo ? t('Edit API Shortcut') : t('Add API Shortcut')}
|
description={t('Configure API documentation links for the dashboard')}
|
||||||
</DialogTitle>
|
contentHeight='auto'
|
||||||
<DialogDescription>
|
bodyClassName='space-y-4'
|
||||||
{t('Configure API documentation links for the dashboard')}
|
footer={
|
||||||
</DialogDescription>
|
<>
|
||||||
</DialogHeader>
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowDialog(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={API_INFO_FORM_ID}>
|
||||||
|
{editingApiInfo ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={API_INFO_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmitForm)}
|
onSubmit={form.handleSubmit(handleSubmitForm)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -520,21 +528,8 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setShowDialog(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='submit'>
|
|
||||||
{editingApiInfo ? t('Update') : t('Add')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ import { useForm } from 'react-hook-form'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -40,6 +32,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
const createChatDialogSchema = (t: (key: string) => string) =>
|
const createChatDialogSchema = (t: (key: string) => string) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -49,6 +42,8 @@ const createChatDialogSchema = (t: (key: string) => string) =>
|
|||||||
|
|
||||||
type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>>
|
type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>>
|
||||||
|
|
||||||
|
const CHAT_DIALOG_FORM_ID = 'chat-dialog-form'
|
||||||
|
|
||||||
export type ChatEntryData = {
|
export type ChatEntryData = {
|
||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
@@ -97,19 +92,32 @@ export function ChatDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-[500px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={isEditMode ? t('Edit chat preset') : t('Add chat preset')}
|
||||||
{isEditMode ? t('Edit chat preset') : t('Add chat preset')}
|
description={t('Configure a predefined chat link for end users.')}
|
||||||
</DialogTitle>
|
contentClassName='sm:max-w-[500px]'
|
||||||
<DialogDescription>
|
contentHeight='auto'
|
||||||
{t('Configure a predefined chat link for end users.')}
|
bodyClassName='space-y-4'
|
||||||
</DialogDescription>
|
footer={
|
||||||
</DialogHeader>
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={CHAT_DIALOG_FORM_ID}>
|
||||||
|
{isEditMode ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={CHAT_DIALOG_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -149,22 +157,8 @@ export function ChatDialog({
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,6 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -62,6 +54,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { SettingsSwitchField } from '../components/settings-form-layout'
|
import { SettingsSwitchField } from '../components/settings-form-layout'
|
||||||
import { SettingsSection } from '../components/settings-section'
|
import { SettingsSection } from '../components/settings-section'
|
||||||
import { useUpdateOption } from '../hooks/use-update-option'
|
import { useUpdateOption } from '../hooks/use-update-option'
|
||||||
@@ -90,6 +83,8 @@ const faqSchema = z.object({
|
|||||||
|
|
||||||
type FAQFormValues = z.infer<typeof faqSchema>
|
type FAQFormValues = z.infer<typeof faqSchema>
|
||||||
|
|
||||||
|
const FAQ_FORM_ID = 'faq-form'
|
||||||
|
|
||||||
export function FAQSection({ enabled, data }: FAQSectionProps) {
|
export function FAQSection({ enabled, data }: FAQSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateOption = useUpdateOption()
|
const updateOption = useUpdateOption()
|
||||||
@@ -348,18 +343,32 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
<Dialog
|
||||||
<DialogContent className='max-w-2xl'>
|
open={showDialog}
|
||||||
<DialogHeader>
|
onOpenChange={setShowDialog}
|
||||||
<DialogTitle>
|
title={editingFaq ? t('Edit FAQ') : t('Add FAQ')}
|
||||||
{editingFaq ? t('Edit FAQ') : t('Add FAQ')}
|
description={t('Create or update frequently asked questions for users')}
|
||||||
</DialogTitle>
|
contentClassName='max-w-2xl'
|
||||||
<DialogDescription>
|
contentHeight='auto'
|
||||||
{t('Create or update frequently asked questions for users')}
|
bodyClassName='space-y-4'
|
||||||
</DialogDescription>
|
footer={
|
||||||
</DialogHeader>
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowDialog(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={FAQ_FORM_ID}>
|
||||||
|
{editingFaq ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={FAQ_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmitForm)}
|
onSubmit={form.handleSubmit(handleSubmitForm)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -398,29 +407,14 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t('Maximum 1000 characters. Supports Markdown and HTML.')}
|
||||||
'Maximum 1000 characters. Supports Markdown and HTML.'
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|||||||
@@ -35,14 +35,6 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -61,6 +53,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { SettingsSwitchField } from '../components/settings-form-layout'
|
import { SettingsSwitchField } from '../components/settings-form-layout'
|
||||||
import { SettingsSection } from '../components/settings-section'
|
import { SettingsSection } from '../components/settings-section'
|
||||||
import { useUpdateOption } from '../hooks/use-update-option'
|
import { useUpdateOption } from '../hooks/use-update-option'
|
||||||
@@ -97,6 +90,8 @@ const createUptimeKumaSchema = (t: (key: string) => string) =>
|
|||||||
|
|
||||||
type UptimeKumaFormValues = z.infer<ReturnType<typeof createUptimeKumaSchema>>
|
type UptimeKumaFormValues = z.infer<ReturnType<typeof createUptimeKumaSchema>>
|
||||||
|
|
||||||
|
const UPTIME_KUMA_FORM_ID = 'uptime-kuma-form'
|
||||||
|
|
||||||
export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
|
export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateOption = useUpdateOption()
|
const updateOption = useUpdateOption()
|
||||||
@@ -359,20 +354,37 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
<Dialog
|
||||||
<DialogContent>
|
open={showDialog}
|
||||||
<DialogHeader>
|
onOpenChange={setShowDialog}
|
||||||
<DialogTitle>
|
title={
|
||||||
{editingGroup
|
editingGroup
|
||||||
? t('Edit Uptime Kuma Group')
|
? t('Edit Uptime Kuma Group')
|
||||||
: t('Add Uptime Kuma Group')}
|
: t('Add Uptime Kuma Group')
|
||||||
</DialogTitle>
|
}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t('Configure monitoring status page groups for the dashboard')}
|
'Configure monitoring status page groups for the dashboard'
|
||||||
</DialogDescription>
|
)}
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowDialog(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={UPTIME_KUMA_FORM_ID}>
|
||||||
|
{editingGroup ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={UPTIME_KUMA_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmitForm)}
|
onSubmit={form.handleSubmit(handleSubmitForm)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -434,21 +446,8 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|||||||
+13
-14
@@ -20,12 +20,7 @@ import { useEffect, useMemo, useState, useRef } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { formatTimestampToDate } from '@/lib/format'
|
import { formatTimestampToDate } from '@/lib/format'
|
||||||
import {
|
import { Dialog } from '@/components/dialog'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { getAffinityUsageCache } from './api'
|
import { getAffinityUsageCache } from './api'
|
||||||
|
|
||||||
function formatRate(hit: number, total: number): string {
|
function formatRate(hit: number, total: number): string {
|
||||||
@@ -135,11 +130,14 @@ export function CacheStatsDialog(props: Props) {
|
|||||||
}, [stats, props.target, t])
|
}, [stats, props.target, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle>{t('Channel Affinity: Upstream Cache Hit')}</DialogTitle>
|
title={t('Channel Affinity: Upstream Cache Hit')}
|
||||||
</DialogHeader>
|
contentClassName='sm:max-w-lg'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
{t(
|
{t(
|
||||||
'Hit criteria: If cached tokens exist in usage, it counts as a hit.'
|
'Hit criteria: If cached tokens exist in usage, it counts as a hit.'
|
||||||
@@ -154,10 +152,12 @@ export function CacheStatsDialog(props: Props) {
|
|||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
<div
|
<div
|
||||||
key={row.key}
|
key={row.key}
|
||||||
className='flex justify-between border-b pb-1 text-sm'
|
className='flex justify-between gap-4 border-b pb-1 text-sm'
|
||||||
>
|
>
|
||||||
<span className='text-muted-foreground'>{row.key}</span>
|
<span className='text-muted-foreground'>{row.key}</span>
|
||||||
<span className='font-medium'>{row.value}</span>
|
<span className='text-right font-medium break-all'>
|
||||||
|
{row.value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +166,6 @@ export function CacheStatsDialog(props: Props) {
|
|||||||
{t('No data available')}
|
{t('No data available')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-5
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
||||||
import { Edit, FileText, Plus, RefreshCw, Trash2, X } from 'lucide-react'
|
import { Edit, FileText, Plus, RefreshCw, Trash2, X } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
|
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
|
||||||
import { SettingsSwitchField } from '../../components/settings-form-layout'
|
import { SettingsSwitchField } from '../../components/settings-form-layout'
|
||||||
import { SettingsPageActionsPortal } from '../../components/settings-page-context'
|
import { SettingsPageActionsPortal } from '../../components/settings-page-context'
|
||||||
@@ -82,6 +82,43 @@ function RuleBadgeList(props: { items: string[] }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChannelAffinityConfirmDialog(props: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
title: ReactNode
|
||||||
|
desc: ReactNode
|
||||||
|
handleConfirm: () => void
|
||||||
|
destructive?: boolean
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={props.open}
|
||||||
|
onOpenChange={props.onOpenChange}
|
||||||
|
title={props.title}
|
||||||
|
contentClassName='sm:max-w-md'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='flex items-start'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={() => props.onOpenChange(false)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={props.destructive ? 'destructive' : 'default'}
|
||||||
|
onClick={props.handleConfirm}
|
||||||
|
>
|
||||||
|
{t('Continue')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='text-muted-foreground text-sm'>{props.desc}</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function serializeRules(rules: AffinityRule[]): string {
|
function serializeRules(rules: AffinityRule[]): string {
|
||||||
return JSON.stringify(rules.map(({ id: _, ...rest }) => rest))
|
return JSON.stringify(rules.map(({ id: _, ...rest }) => rest))
|
||||||
}
|
}
|
||||||
@@ -641,7 +678,7 @@ export function ChannelAffinitySection(props: Props) {
|
|||||||
templateKey={ruleTemplateKey}
|
templateKey={ruleTemplateKey}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ChannelAffinityConfirmDialog
|
||||||
open={clearAllDialogOpen}
|
open={clearAllDialogOpen}
|
||||||
onOpenChange={setClearAllDialogOpen}
|
onOpenChange={setClearAllDialogOpen}
|
||||||
title={t('Confirm clearing all channel affinity cache')}
|
title={t('Confirm clearing all channel affinity cache')}
|
||||||
@@ -653,7 +690,7 @@ export function ChannelAffinitySection(props: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{clearRuleName !== null && (
|
{clearRuleName !== null && (
|
||||||
<ConfirmDialog
|
<ChannelAffinityConfirmDialog
|
||||||
open
|
open
|
||||||
onOpenChange={(v) => !v && setClearRuleName(null)}
|
onOpenChange={(v) => !v && setClearRuleName(null)}
|
||||||
title={t('Confirm clearing cache for this rule')}
|
title={t('Confirm clearing cache for this rule')}
|
||||||
@@ -663,7 +700,7 @@ export function ChannelAffinitySection(props: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConfirmDialog
|
<ChannelAffinityConfirmDialog
|
||||||
open={fillTemplateDialogOpen}
|
open={fillTemplateDialogOpen}
|
||||||
onOpenChange={setFillTemplateDialogOpen}
|
onOpenChange={setFillTemplateDialogOpen}
|
||||||
title={t('Fill Codex CLI / Claude CLI Templates')}
|
title={t('Fill Codex CLI / Claude CLI Templates')}
|
||||||
|
|||||||
+42
-44
@@ -27,13 +27,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +39,7 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { SettingsSwitchField } from '../../components/settings-form-layout'
|
import { SettingsSwitchField } from '../../components/settings-form-layout'
|
||||||
import { RULE_TEMPLATES } from './constants'
|
import { RULE_TEMPLATES } from './constants'
|
||||||
import type { AffinityRule, KeySource } from './types'
|
import type { AffinityRule, KeySource } from './types'
|
||||||
@@ -69,6 +63,8 @@ const CONTEXT_KEY_PRESETS = [
|
|||||||
'specific_channel_id',
|
'specific_channel_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const RULE_FORM_ID = 'channel-affinity-rule-form'
|
||||||
|
|
||||||
interface RuleFormValues {
|
interface RuleFormValues {
|
||||||
name: string
|
name: string
|
||||||
model_regex_text: string
|
model_regex_text: string
|
||||||
@@ -230,13 +226,33 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[85vh] max-w-2xl overflow-y-auto'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle>{isEdit ? t('Edit Rule') : t('Add Rule')}</DialogTitle>
|
title={isEdit ? t('Edit Rule') : t('Add Rule')}
|
||||||
</DialogHeader>
|
contentClassName='max-w-2xl'
|
||||||
|
contentHeight='auto'
|
||||||
<form onSubmit={form.handleSubmit(handleSave)} className='space-y-4'>
|
bodyClassName='pr-2'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => props.onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={RULE_FORM_ID}>
|
||||||
|
{t('Save')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
id={RULE_FORM_ID}
|
||||||
|
onSubmit={form.handleSubmit(handleSave)}
|
||||||
|
className='min-w-0 space-y-4 overflow-x-clip'
|
||||||
|
>
|
||||||
<div className='grid gap-1.5'>
|
<div className='grid gap-1.5'>
|
||||||
<Label>{t('Name')} *</Label>
|
<Label>{t('Name')} *</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -245,7 +261,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-3'>
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
<div className='grid gap-1.5'>
|
<div className='grid gap-1.5'>
|
||||||
<Label>{t('Model Regex (one per line)')} *</Label>
|
<Label>{t('Model Regex (one per line)')} *</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -281,10 +297,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setKeySources((prev) => [
|
setKeySources((prev) => [...prev, { type: 'gjson', path: '' }])
|
||||||
...prev,
|
|
||||||
{ type: 'gjson', path: '' },
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Plus className='mr-1 h-3 w-3' />
|
<Plus className='mr-1 h-3 w-3' />
|
||||||
@@ -296,7 +309,10 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
</p>
|
</p>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
{keySources.map((src, idx) => (
|
{keySources.map((src, idx) => (
|
||||||
<div key={idx} className='flex items-center gap-2'>
|
<div
|
||||||
|
key={idx}
|
||||||
|
className='flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center'
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
...KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t })),
|
...KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t })),
|
||||||
@@ -312,7 +328,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
setKeySources(next)
|
setKeySources(next)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='w-[160px]'>
|
<SelectTrigger className='w-full sm:w-[160px]'>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent alignItemWithTrigger={false}>
|
<SelectContent alignItemWithTrigger={false}>
|
||||||
@@ -326,15 +342,13 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
className='flex-1'
|
className='min-w-0 flex-1'
|
||||||
placeholder={
|
placeholder={
|
||||||
src.type === 'gjson'
|
src.type === 'gjson'
|
||||||
? 'metadata.conversation_id'
|
? 'metadata.conversation_id'
|
||||||
: 'user_id'
|
: 'user_id'
|
||||||
}
|
}
|
||||||
value={
|
value={src.type === 'gjson' ? src.path || '' : src.key || ''}
|
||||||
src.type === 'gjson' ? src.path || '' : src.key || ''
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = [...keySources]
|
const next = [...keySources]
|
||||||
if (src.type === 'gjson') {
|
if (src.type === 'gjson') {
|
||||||
@@ -385,7 +399,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-3'>
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
<div className='grid gap-1.5'>
|
<div className='grid gap-1.5'>
|
||||||
<Label>{t('Value Regex')}</Label>
|
<Label>{t('Value Regex')}</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -416,17 +430,13 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
<div className='grid gap-3 sm:grid-cols-3'>
|
<div className='grid gap-3 sm:grid-cols-3'>
|
||||||
<SettingsSwitchField
|
<SettingsSwitchField
|
||||||
checked={form.watch('include_using_group')}
|
checked={form.watch('include_using_group')}
|
||||||
onCheckedChange={(v) =>
|
onCheckedChange={(v) => form.setValue('include_using_group', v)}
|
||||||
form.setValue('include_using_group', v)
|
|
||||||
}
|
|
||||||
label={t('Include Group')}
|
label={t('Include Group')}
|
||||||
className='border-b-0 py-0'
|
className='border-b-0 py-0'
|
||||||
/>
|
/>
|
||||||
<SettingsSwitchField
|
<SettingsSwitchField
|
||||||
checked={form.watch('include_model_name')}
|
checked={form.watch('include_model_name')}
|
||||||
onCheckedChange={(v) =>
|
onCheckedChange={(v) => form.setValue('include_model_name', v)}
|
||||||
form.setValue('include_model_name', v)
|
|
||||||
}
|
|
||||||
label={t('Include Model')}
|
label={t('Include Model')}
|
||||||
className='border-b-0 py-0'
|
className='border-b-0 py-0'
|
||||||
/>
|
/>
|
||||||
@@ -439,19 +449,7 @@ export function RuleEditorDialog(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => props.onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='submit'>{t('Save')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-33
@@ -22,14 +22,6 @@ import { useForm } from 'react-hook-form'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -40,6 +32,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
const createAmountDiscountDialogSchema = (t: (key: string) => string) =>
|
const createAmountDiscountDialogSchema = (t: (key: string) => string) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -57,6 +50,8 @@ type AmountDiscountDialogFormValues = z.infer<
|
|||||||
ReturnType<typeof createAmountDiscountDialogSchema>
|
ReturnType<typeof createAmountDiscountDialogSchema>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
const AMOUNT_DISCOUNT_FORM_ID = 'amount-discount-form'
|
||||||
|
|
||||||
export type AmountDiscountData = {
|
export type AmountDiscountData = {
|
||||||
amount: number
|
amount: number
|
||||||
discountRate: number
|
discountRate: number
|
||||||
@@ -115,19 +110,34 @@ export function AmountDiscountDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-[500px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={isEditMode ? t('Edit discount tier') : t('Add discount tier')}
|
||||||
{isEditMode ? t('Edit discount tier') : t('Add discount tier')}
|
description={t(
|
||||||
</DialogTitle>
|
'Set a discount rate for a specific recharge amount threshold.'
|
||||||
<DialogDescription>
|
)}
|
||||||
{t('Set a discount rate for a specific recharge amount threshold.')}
|
contentClassName='sm:max-w-[500px]'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={AMOUNT_DISCOUNT_FORM_ID}>
|
||||||
|
{isEditMode ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={AMOUNT_DISCOUNT_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -195,22 +205,8 @@ export function AmountDiscountDialog({
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-33
@@ -22,14 +22,6 @@ import { useForm } from 'react-hook-form'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -48,6 +40,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import type { CreemProduct } from '@/features/wallet/types'
|
import type { CreemProduct } from '@/features/wallet/types'
|
||||||
import { safeNumberFieldProps } from '../utils/numeric-field'
|
import { safeNumberFieldProps } from '../utils/numeric-field'
|
||||||
|
|
||||||
@@ -61,6 +54,8 @@ const creemProductDialogSchema = z.object({
|
|||||||
|
|
||||||
type CreemProductDialogFormValues = z.infer<typeof creemProductDialogSchema>
|
type CreemProductDialogFormValues = z.infer<typeof creemProductDialogSchema>
|
||||||
|
|
||||||
|
const CREEM_PRODUCT_FORM_ID = 'creem-product-form'
|
||||||
|
|
||||||
// Re-export for backwards compatibility
|
// Re-export for backwards compatibility
|
||||||
export type CreemProductData = CreemProduct
|
export type CreemProductData = CreemProduct
|
||||||
|
|
||||||
@@ -119,19 +114,32 @@ export function CreemProductDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-[500px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={isEditMode ? t('Edit product') : t('Add product')}
|
||||||
{isEditMode ? t('Edit product') : t('Add product')}
|
description={t('Configure a Creem product for user recharge options.')}
|
||||||
</DialogTitle>
|
contentClassName='sm:max-w-[500px]'
|
||||||
<DialogDescription>
|
contentHeight='auto'
|
||||||
{t('Configure a Creem product for user recharge options.')}
|
bodyClassName='space-y-4'
|
||||||
</DialogDescription>
|
footer={
|
||||||
</DialogHeader>
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={CREEM_PRODUCT_FORM_ID}>
|
||||||
|
{isEditMode ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={CREEM_PRODUCT_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -247,22 +255,8 @@ export function CreemProductDialog({
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-33
@@ -23,14 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Combobox } from '@/components/ui/combobox'
|
import { Combobox } from '@/components/ui/combobox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -41,6 +33,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
const createPaymentMethodDialogSchema = (t: (key: string) => string) =>
|
const createPaymentMethodDialogSchema = (t: (key: string) => string) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -54,6 +47,8 @@ type PaymentMethodDialogFormValues = z.infer<
|
|||||||
ReturnType<typeof createPaymentMethodDialogSchema>
|
ReturnType<typeof createPaymentMethodDialogSchema>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
const PAYMENT_METHOD_FORM_ID = 'payment-method-form'
|
||||||
|
|
||||||
export type PaymentMethodData = {
|
export type PaymentMethodData = {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
@@ -169,19 +164,32 @@ export function PaymentMethodDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-[500px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={isEditMode ? t('Edit payment method') : t('Add payment method')}
|
||||||
{isEditMode ? t('Edit payment method') : t('Add payment method')}
|
description={t('Configure a payment method for user recharge options.')}
|
||||||
</DialogTitle>
|
contentClassName='sm:max-w-[500px]'
|
||||||
<DialogDescription>
|
contentHeight='auto'
|
||||||
{t('Configure a payment method for user recharge options.')}
|
bodyClassName='space-y-4'
|
||||||
</DialogDescription>
|
footer={
|
||||||
</DialogHeader>
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={PAYMENT_METHOD_FORM_ID}>
|
||||||
|
{isEditMode ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={PAYMENT_METHOD_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -281,22 +289,8 @@ export function PaymentMethodDialog({
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-29
@@ -22,13 +22,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
@@ -41,6 +34,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { SettingsSwitchField } from '../components/settings-form-layout'
|
import { SettingsSwitchField } from '../components/settings-form-layout'
|
||||||
|
|
||||||
export interface WaffoSettingsValues {
|
export interface WaffoSettingsValues {
|
||||||
@@ -411,15 +405,29 @@ export function WaffoSettingsSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={methodDialogOpen} onOpenChange={setMethodDialogOpen}>
|
<Dialog
|
||||||
<DialogContent>
|
open={methodDialogOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setMethodDialogOpen}
|
||||||
<DialogTitle>
|
title={
|
||||||
{editingIdx === -1
|
editingIdx === -1 ? t('Add payment method') : t('Edit payment method')
|
||||||
? t('Add payment method')
|
}
|
||||||
: t('Edit payment method')}
|
contentHeight='auto'
|
||||||
</DialogTitle>
|
bodyClassName='space-y-4'
|
||||||
</DialogHeader>
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setMethodDialogOpen(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='button' onClick={saveMethod}>
|
||||||
|
{t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div className='grid gap-1.5'>
|
<div className='grid gap-1.5'>
|
||||||
<Label>{t('Display name')} *</Label>
|
<Label>{t('Display name')} *</Label>
|
||||||
@@ -505,19 +513,6 @@ export function WaffoSettingsSection({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setMethodDialogOpen(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='button' onClick={saveMethod}>
|
|
||||||
{t('Confirm')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+31
-37
@@ -22,15 +22,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { formatTimestamp, formatTimestampToDate } from '@/lib/format'
|
import { formatTimestamp, formatTimestampToDate } from '@/lib/format'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Markdown } from '@/components/ui/markdown'
|
import { Markdown } from '@/components/ui/markdown'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { SettingsSection } from '../components/settings-section'
|
import { SettingsSection } from '../components/settings-section'
|
||||||
|
|
||||||
type ReleaseInfo = {
|
type ReleaseInfo = {
|
||||||
@@ -140,38 +133,29 @@ export function UpdateCheckerSection({
|
|||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog
|
||||||
<DialogContent className='max-h-[80vh] overflow-y-auto'>
|
open={dialogOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setDialogOpen}
|
||||||
<DialogTitle>
|
title={
|
||||||
{release?.tag_name
|
release?.tag_name
|
||||||
? t('New version available: {{version}}', {
|
? t('New version available: {{version}}', {
|
||||||
version: release.tag_name,
|
version: release.tag_name,
|
||||||
})
|
})
|
||||||
: t('Release details')}
|
: t('Release details')
|
||||||
</DialogTitle>
|
}
|
||||||
{release?.published_at && (
|
description={
|
||||||
<DialogDescription>
|
release?.published_at
|
||||||
{t('Published')}{' '}
|
? `${t('Published')} ${formatTimestampToDate(
|
||||||
{formatTimestampToDate(
|
|
||||||
new Date(release.published_at).getTime(),
|
new Date(release.published_at).getTime(),
|
||||||
'milliseconds'
|
'milliseconds'
|
||||||
)}
|
)}`
|
||||||
</DialogDescription>
|
: undefined
|
||||||
)}
|
}
|
||||||
</DialogHeader>
|
contentClassName='max-h-[80vh] overflow-y-auto'
|
||||||
|
contentHeight='auto'
|
||||||
<div className='space-y-4'>
|
bodyClassName='space-y-4'
|
||||||
{release?.body ? (
|
footer={
|
||||||
<Markdown>{release.body}</Markdown>
|
<>
|
||||||
) : (
|
|
||||||
<p className='text-muted-foreground text-sm'>
|
|
||||||
{t('No release notes provided.')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
@@ -185,8 +169,18 @@ export function UpdateCheckerSection({
|
|||||||
{t('Open release')}
|
{t('Open release')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</>
|
||||||
</DialogContent>
|
}
|
||||||
|
>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{release?.body ? (
|
||||||
|
<Markdown>{release.body}</Markdown>
|
||||||
|
) : (
|
||||||
|
<p className='text-muted-foreground text-sm'>
|
||||||
|
{t('No release notes provided.')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+20
-25
@@ -30,14 +30,6 @@ import { Search } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -56,6 +48,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { DataTablePagination } from '@/components/data-table/pagination'
|
import { DataTablePagination } from '@/components/data-table/pagination'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import type { UpstreamChannel } from '../types'
|
import type { UpstreamChannel } from '../types'
|
||||||
import {
|
import {
|
||||||
@@ -330,15 +323,25 @@ export function ChannelSelectorDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='flex max-h-[90vh] max-w-[calc(100%-2rem)] flex-col sm:max-w-[90vw] xl:max-w-[1400px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Select Sync Channels')}</DialogTitle>
|
title={t('Select Sync Channels')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t('Choose channels to sync upstream ratio configurations from')}
|
'Choose channels to sync upstream ratio configurations from'
|
||||||
</DialogDescription>
|
)}
|
||||||
</DialogHeader>
|
contentClassName='flex max-h-[90vh] max-w-[calc(100%-2rem)] flex-col sm:max-w-[90vw] xl:max-w-[1400px]'
|
||||||
|
contentHeight='min(72vh, 720px)'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirm}>{t('Confirm Selection')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='flex flex-1 flex-col gap-4 overflow-hidden'>
|
<div className='flex flex-1 flex-col gap-4 overflow-hidden'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='relative flex-1'>
|
<div className='relative flex-1'>
|
||||||
@@ -403,14 +406,6 @@ export function ChannelSelectorDialog({
|
|||||||
|
|
||||||
<DataTablePagination table={table} />
|
<DataTablePagination table={table} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConfirm}>{t('Confirm Selection')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-84
@@ -33,14 +33,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -51,6 +43,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { safeJsonParse } from '../utils/json-parser'
|
import { safeJsonParse } from '../utils/json-parser'
|
||||||
|
|
||||||
type GroupRatioVisualEditorProps = {
|
type GroupRatioVisualEditorProps = {
|
||||||
@@ -677,14 +670,25 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Auto Group Dialog */}
|
{/* Auto Group Dialog */}
|
||||||
<Dialog open={autoGroupDialogOpen} onOpenChange={setAutoGroupDialogOpen}>
|
<Dialog
|
||||||
<DialogContent>
|
open={autoGroupDialogOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setAutoGroupDialogOpen}
|
||||||
<DialogTitle>{t('Add auto group')}</DialogTitle>
|
title={t('Add auto group')}
|
||||||
<DialogDescription>
|
description={t('Add a group identifier to the auto assignment list.')}
|
||||||
{t('Add a group identifier to the auto assignment list.')}
|
contentHeight='auto'
|
||||||
</DialogDescription>
|
bodyClassName='space-y-4'
|
||||||
</DialogHeader>
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setAutoGroupDialogOpen(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAutoGroupSave}>{t('Add')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('Group identifier')}</Label>
|
<Label>{t('Group identifier')}</Label>
|
||||||
@@ -695,27 +699,30 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setAutoGroupDialogOpen(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleAutoGroupSave}>{t('Add')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* User Group Dialog */}
|
{/* User Group Dialog */}
|
||||||
<Dialog open={userGroupDialogOpen} onOpenChange={setUserGroupDialogOpen}>
|
<Dialog
|
||||||
<DialogContent>
|
open={userGroupDialogOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setUserGroupDialogOpen}
|
||||||
<DialogTitle>{t('Add user group')}</DialogTitle>
|
title={t('Add user group')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t('Create a new user group to configure ratio overrides for.')}
|
'Create a new user group to configure ratio overrides for.'
|
||||||
</DialogDescription>
|
)}
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setUserGroupDialogOpen(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleUserGroupSave}>{t('Add')}</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('User group name')}</Label>
|
<Label>{t('User group name')}</Label>
|
||||||
@@ -726,16 +733,6 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setUserGroupDialogOpen(false)}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleUserGroupSave}>{t('Add')}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Group Override Dialog */}
|
{/* Group Override Dialog */}
|
||||||
@@ -1016,18 +1013,28 @@ function SimpleGroupDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={
|
||||||
{editData
|
editData
|
||||||
? t('Edit {{title}}', { title })
|
? t('Edit {{title}}', { title })
|
||||||
: t('Add {{title}}', { title })}
|
: t('Add {{title}}', { title })
|
||||||
</DialogTitle>
|
}
|
||||||
<DialogDescription>
|
description={t('Configure the ratio for this group.')}
|
||||||
{t('Configure the ratio for this group.')}
|
contentHeight='auto'
|
||||||
</DialogDescription>
|
bodyClassName='space-y-4'
|
||||||
</DialogHeader>
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
{editData ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('Group name')}</Label>
|
<Label>{t('Group name')}</Label>
|
||||||
@@ -1052,15 +1059,6 @@ function SimpleGroupDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSave}>
|
|
||||||
{editData ? t('Update') : t('Add')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1107,23 +1105,33 @@ function GroupOverrideDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={editData ? t('Edit ratio override') : t('Add ratio override')}
|
||||||
{editData ? t('Edit ratio override') : t('Add ratio override')}
|
description={
|
||||||
</DialogTitle>
|
userGroup
|
||||||
<DialogDescription>
|
|
||||||
{userGroup
|
|
||||||
? t(
|
? t(
|
||||||
'Configure a custom ratio for "{{userGroup}}" users when using a specific token group.',
|
'Configure a custom ratio for "{{userGroup}}" users when using a specific token group.',
|
||||||
{ userGroup }
|
{ userGroup }
|
||||||
)
|
)
|
||||||
: t(
|
: t(
|
||||||
'Configure a custom ratio for when users use a specific token group.'
|
'Configure a custom ratio for when users use a specific token group.'
|
||||||
)}
|
)
|
||||||
</DialogDescription>
|
}
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
{editData ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('Target group')}</Label>
|
<Label>{t('Target group')}</Label>
|
||||||
@@ -1157,15 +1165,6 @@ function GroupOverrideDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSave}>
|
|
||||||
{editData ? t('Update') : t('Add')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-38
@@ -22,14 +22,6 @@ import { useForm } from 'react-hook-form'
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -40,6 +32,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
const rateLimitDialogSchema = z.object({
|
const rateLimitDialogSchema = z.object({
|
||||||
groupName: z.string().min(1, 'Group name is required'),
|
groupName: z.string().min(1, 'Group name is required'),
|
||||||
@@ -55,6 +48,8 @@ const rateLimitDialogSchema = z.object({
|
|||||||
|
|
||||||
type RateLimitDialogFormValues = z.infer<typeof rateLimitDialogSchema>
|
type RateLimitDialogFormValues = z.infer<typeof rateLimitDialogSchema>
|
||||||
|
|
||||||
|
const RATE_LIMIT_FORM_ID = 'rate-limit-form'
|
||||||
|
|
||||||
export type RateLimitEntryData = {
|
export type RateLimitEntryData = {
|
||||||
groupName: string
|
groupName: string
|
||||||
maxRequests: number
|
maxRequests: number
|
||||||
@@ -105,21 +100,36 @@ export function RateLimitDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-[500px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>
|
title={
|
||||||
{isEditMode
|
isEditMode ? t('Edit group rate limit') : t('Add group rate limit')
|
||||||
? t('Edit group rate limit')
|
}
|
||||||
: t('Add group rate limit')}
|
description={t(
|
||||||
</DialogTitle>
|
'Configure rate limiting rules for a specific user group.'
|
||||||
<DialogDescription>
|
)}
|
||||||
{t('Configure rate limiting rules for a specific user group.')}
|
contentClassName='sm:max-w-[500px]'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' form={RATE_LIMIT_FORM_ID}>
|
||||||
|
{isEditMode ? t('Update') : t('Add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
id={RATE_LIMIT_FORM_ID}
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
className='space-y-4'
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
@@ -151,9 +161,7 @@ export function RateLimitDialog({
|
|||||||
name='maxRequests'
|
name='maxRequests'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>{t('Max Requests (including failures)')}</FormLabel>
|
||||||
{t('Max Requests (including failures)')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Input
|
<Input
|
||||||
@@ -209,22 +217,8 @@ export function RateLimitDialog({
|
|||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-18
@@ -21,13 +21,8 @@ import { ExternalLink, Copy, Music } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
|
|
||||||
export interface AudioClip {
|
export interface AudioClip {
|
||||||
@@ -152,15 +147,20 @@ export function AudioPreviewDialog(props: AudioPreviewDialogProps) {
|
|||||||
const clips = Array.isArray(props.clips) ? props.clips : []
|
const clips = Array.isArray(props.clips) ? props.clips : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<Music className='h-5 w-5' />
|
<Music className='h-5 w-5' />
|
||||||
{t('Audio Preview')}
|
{t('Audio Preview')}
|
||||||
</DialogTitle>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
|
contentClassName='sm:max-w-lg'
|
||||||
|
titleClassName='flex items-center gap-2'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
{clips.length === 0 ? (
|
{clips.length === 0 ? (
|
||||||
<p className='text-muted-foreground py-4 text-center text-sm'>
|
<p className='text-muted-foreground py-4 text-center text-sm'>
|
||||||
{t('None')}
|
{t('None')}
|
||||||
@@ -169,15 +169,11 @@ export function AudioPreviewDialog(props: AudioPreviewDialogProps) {
|
|||||||
<ScrollArea className='max-h-[60vh]'>
|
<ScrollArea className='max-h-[60vh]'>
|
||||||
<div className='space-y-3 pr-2'>
|
<div className='space-y-3 pr-2'>
|
||||||
{clips.map((clip, idx) => (
|
{clips.map((clip, idx) => (
|
||||||
<AudioClipCard
|
<AudioClipCard key={clip.clip_id || clip.id || idx} clip={clip} />
|
||||||
key={clip.clip_id || clip.id || idx}
|
|
||||||
clip={clip}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-29
@@ -36,15 +36,9 @@ import { formatLogQuota, formatTokens, formatUseTime } from '@/lib/format'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
||||||
import { DynamicPricingBreakdown } from '@/features/pricing/components/dynamic-pricing-breakdown'
|
import { DynamicPricingBreakdown } from '@/features/pricing/components/dynamic-pricing-breakdown'
|
||||||
import type { UsageLog } from '../../data/schema'
|
import type { UsageLog } from '../../data/schema'
|
||||||
@@ -485,16 +479,11 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
useChannel && useChannel.length > 0 ? useChannel.join(' → ') : undefined
|
useChannel && useChannel.length > 0 ? useChannel.join(' → ') : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent
|
open={props.open}
|
||||||
className={cn(
|
onOpenChange={props.onOpenChange}
|
||||||
'min-w-0 overflow-hidden',
|
title={
|
||||||
'max-sm:max-h-[calc(100dvh-1.5rem)] max-sm:w-[calc(100vw-1.5rem)] max-sm:max-w-[calc(100vw-1.5rem)] max-sm:p-4',
|
<>
|
||||||
isTieredBilling ? 'sm:max-w-4xl lg:max-w-5xl' : 'sm:max-w-lg'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogHeader className='max-sm:gap-1'>
|
|
||||||
<DialogTitle className='flex items-center gap-2 text-base'>
|
|
||||||
{t('Log Details')}
|
{t('Log Details')}
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={t(typeConfig.label)}
|
label={t(typeConfig.label)}
|
||||||
@@ -502,12 +491,20 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
/>
|
/>
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription className='sr-only'>
|
}
|
||||||
{t('View the complete details for this log entry')}
|
description={t('View the complete details for this log entry')}
|
||||||
</DialogDescription>
|
contentClassName={cn(
|
||||||
</DialogHeader>
|
'min-w-0 overflow-hidden',
|
||||||
|
'max-sm:max-h-[calc(100dvh-1.5rem)] max-sm:w-[calc(100vw-1.5rem)] max-sm:max-w-[calc(100vw-1.5rem)] max-sm:p-4',
|
||||||
|
isTieredBilling ? 'sm:max-w-4xl lg:max-w-5xl' : 'sm:max-w-lg'
|
||||||
|
)}
|
||||||
|
headerClassName='max-sm:gap-1'
|
||||||
|
titleClassName='flex items-center gap-2 text-base'
|
||||||
|
descriptionClassName='sr-only'
|
||||||
|
contentHeight='min(72vh, 720px)'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<ScrollArea className='max-h-[70vh] min-w-0 overflow-hidden pr-2 max-sm:max-h-[calc(100dvh-7rem)] sm:pr-4'>
|
<ScrollArea className='max-h-[70vh] min-w-0 overflow-hidden pr-2 max-sm:max-h-[calc(100dvh-7rem)] sm:pr-4'>
|
||||||
<div className='w-full max-w-full min-w-0 space-y-2.5 overflow-hidden py-1 sm:space-y-3'>
|
<div className='w-full max-w-full min-w-0 space-y-2.5 overflow-hidden py-1 sm:space-y-3'>
|
||||||
{/* Overview section - key identifiers */}
|
{/* Overview section - key identifiers */}
|
||||||
@@ -550,11 +547,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{props.log.token_name && (
|
{props.log.token_name && (
|
||||||
<DetailRow
|
<DetailRow label={t('Token')} value={props.log.token_name} mono />
|
||||||
label={t('Token')}
|
|
||||||
value={props.log.token_name}
|
|
||||||
mono
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(props.log.group || other?.group) && (
|
{(props.log.group || other?.group) && (
|
||||||
@@ -1041,7 +1034,6 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-17
@@ -20,15 +20,9 @@ import { Copy, Check } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
interface FailReasonDialogProps {
|
interface FailReasonDialogProps {
|
||||||
failReason: string
|
failReason: string
|
||||||
@@ -45,15 +39,15 @@ export function FailReasonDialog({
|
|||||||
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
|
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Fail Reason Details')}</DialogTitle>
|
title={t('Fail Reason Details')}
|
||||||
<DialogDescription>
|
description={t('View the complete error message and details')}
|
||||||
{t('View the complete error message and details')}
|
contentClassName='sm:max-w-lg'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<ScrollArea className='max-h-[500px] pr-4'>
|
<ScrollArea className='max-h-[500px] pr-4'>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
@@ -81,7 +75,6 @@ export function FailReasonDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
interface ImageDialogProps {
|
interface ImageDialogProps {
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
@@ -65,17 +59,17 @@ export function ImageDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-3xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle>{t('Image Preview')}</DialogTitle>
|
title={t('Image Preview')}
|
||||||
<DialogDescription>
|
description={
|
||||||
{taskId
|
taskId ? `${t('Task ID:')} ${taskId}` : t('View the generated image')
|
||||||
? `${t('Task ID:')} ${taskId}`
|
}
|
||||||
: t('View the generated image')}
|
contentClassName='sm:max-w-3xl'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<ScrollArea className='max-h-[600px]'>
|
<ScrollArea className='max-h-[600px]'>
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
<div className='bg-muted/50 relative flex min-h-[300px] items-center justify-center rounded-lg border'>
|
<div className='bg-muted/50 relative flex min-h-[300px] items-center justify-center rounded-lg border'>
|
||||||
@@ -114,7 +108,6 @@ export function ImageDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,9 @@ import { Copy, Check } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
|
|
||||||
interface PromptDialogProps {
|
interface PromptDialogProps {
|
||||||
prompt: string
|
prompt: string
|
||||||
@@ -47,15 +41,15 @@ export function PromptDialog({
|
|||||||
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
|
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Prompt Details')}</DialogTitle>
|
title={t('Prompt Details')}
|
||||||
<DialogDescription>
|
description={t('View the complete prompt and its English translation')}
|
||||||
{t('View the complete prompt and its English translation')}
|
contentClassName='sm:max-w-lg'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
<ScrollArea className='max-h-[500px] pr-4'>
|
<ScrollArea className='max-h-[500px] pr-4'>
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
{/* Original Prompt */}
|
{/* Original Prompt */}
|
||||||
@@ -109,7 +103,6 @@ export function PromptDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-17
@@ -21,14 +21,8 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { formatQuota, formatCompactNumber } from '@/lib/format'
|
import { formatQuota, formatCompactNumber } from '@/lib/format'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { getUserInfo } from '../../api'
|
import { getUserInfo } from '../../api'
|
||||||
import type { UserInfo } from '../../types'
|
import type { UserInfo } from '../../types'
|
||||||
|
|
||||||
@@ -88,17 +82,17 @@ export function UserInfoDialog({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('User Information')}</DialogTitle>
|
title={t('User Information')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t(
|
|
||||||
'View detailed information about this user including balance, usage statistics, and invitation details.'
|
'View detailed information about this user including balance, usage statistics, and invitation details.'
|
||||||
)}
|
)}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-lg'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='flex items-center justify-center py-8'>
|
<div className='flex items-center justify-center py-8'>
|
||||||
<Loader2 className='text-muted-foreground size-6 animate-spin' />
|
<Loader2 className='text-muted-foreground size-6 animate-spin' />
|
||||||
@@ -185,7 +179,6 @@ export function UserInfoDialog({
|
|||||||
{t('No user information available')}
|
{t('No user information available')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-18
@@ -33,13 +33,6 @@ import { SiGithub, SiDiscord } from 'react-icons/si'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +42,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import {
|
import {
|
||||||
getUser,
|
getUser,
|
||||||
@@ -318,18 +312,22 @@ export function UserBindingDialog(props: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='sm:max-w-lg'>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle className='flex items-center gap-2'>
|
title={
|
||||||
|
<>
|
||||||
<Link2 className='h-5 w-5' />
|
<Link2 className='h-5 w-5' />
|
||||||
{t('Account Binding Management')}
|
{t('Account Binding Management')}
|
||||||
</DialogTitle>
|
</>
|
||||||
<DialogDescription className='sr-only'>
|
}
|
||||||
{t('Manage account bindings for this user')}
|
description={t('Manage account bindings for this user')}
|
||||||
</DialogDescription>
|
contentClassName='sm:max-w-lg'
|
||||||
</DialogHeader>
|
titleClassName='flex items-center gap-2'
|
||||||
|
descriptionClassName='sr-only'
|
||||||
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className='flex items-center justify-center py-8'>
|
<div className='flex items-center justify-center py-8'>
|
||||||
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
||||||
@@ -432,7 +430,6 @@ export function UserBindingDialog(props: Props) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
+20
-28
@@ -23,16 +23,9 @@ import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency'
|
|||||||
import { formatQuota, parseQuotaFromDollars } from '@/lib/format'
|
import { formatQuota, parseQuotaFromDollars } from '@/lib/format'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { adjustUserQuota } from '../api'
|
import { adjustUserQuota } from '../api'
|
||||||
import type { QuotaAdjustMode } from '../types'
|
import type { QuotaAdjustMode } from '../types'
|
||||||
|
|
||||||
@@ -115,18 +108,26 @@ export function UserQuotaDialog(props: UserQuotaDialogProps) {
|
|||||||
: t('Enter amount in {{currency}}', { currency: currencyLabel })
|
: t('Enter amount in {{currency}}', { currency: currencyLabel })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
<Dialog
|
||||||
<DialogContent>
|
open={props.open}
|
||||||
<DialogHeader>
|
onOpenChange={props.onOpenChange}
|
||||||
<DialogTitle>{t('Adjust Quota')}</DialogTitle>
|
title={t('Adjust Quota')}
|
||||||
<DialogDescription>
|
description={t('Select an operation mode and enter the amount')}
|
||||||
{t('Select an operation mode and enter the amount')}
|
contentHeight='auto'
|
||||||
</DialogDescription>
|
bodyClassName='space-y-4'
|
||||||
</DialogHeader>
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant='outline' onClick={handleCancel}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirm} disabled={loading}>
|
||||||
|
{loading ? t('Processing...') : t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>{getPreviewText()}</div>
|
||||||
{getPreviewText()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('Mode')}</Label>
|
<Label>{t('Mode')}</Label>
|
||||||
@@ -173,15 +174,6 @@ export function UserQuotaDialog(props: UserQuotaDialogProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant='outline' onClick={handleCancel}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConfirm} disabled={loading}>
|
|
||||||
{loading ? t('Processing...') : t('Confirm')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-20
@@ -33,13 +33,6 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
@@ -52,6 +45,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { useBillingHistory } from '../../hooks/use-billing-history'
|
import { useBillingHistory } from '../../hooks/use-billing-history'
|
||||||
import {
|
import {
|
||||||
@@ -101,16 +95,18 @@ export function BillingHistoryDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-4xl'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Billing History')}</DialogTitle>
|
title={t('Billing History')}
|
||||||
<DialogDescription>
|
description={t(
|
||||||
{t('View your topup transaction records and payment history')}
|
'View your topup transaction records and payment history'
|
||||||
</DialogDescription>
|
)}
|
||||||
</DialogHeader>
|
contentClassName='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-4xl'
|
||||||
|
contentHeight='auto'
|
||||||
<div className='min-h-0 flex-1 space-y-3 sm:space-y-4'>
|
bodyClassName='space-y-3'
|
||||||
|
>
|
||||||
|
<div className='min-h-0 space-y-3'>
|
||||||
{/* Search and Filter Bar */}
|
{/* Search and Filter Bar */}
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='relative flex-1'>
|
<div className='relative flex-1'>
|
||||||
@@ -149,7 +145,7 @@ export function BillingHistoryDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Records List */}
|
{/* Records List */}
|
||||||
<ScrollArea className='h-[calc(100dvh-15rem)] pr-3 sm:h-[500px] sm:pr-4'>
|
<ScrollArea className='max-h-[min(54vh,520px)] pr-3 sm:pr-4'>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
@@ -170,7 +166,7 @@ export function BillingHistoryDialog({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : records.length === 0 ? (
|
) : records.length === 0 ? (
|
||||||
<div className='text-muted-foreground flex h-[320px] flex-col items-center justify-center text-center sm:h-[400px]'>
|
<div className='text-muted-foreground flex min-h-40 flex-col items-center justify-center py-10 text-center'>
|
||||||
<p className='text-sm font-medium'>
|
<p className='text-sm font-medium'>
|
||||||
{t('No billing records found')}
|
{t('No billing records found')}
|
||||||
</p>
|
</p>
|
||||||
@@ -316,7 +312,6 @@ export function BillingHistoryDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Confirm Complete Order Dialog */}
|
{/* Confirm Complete Order Dialog */}
|
||||||
|
|||||||
+26
-32
@@ -20,14 +20,7 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { formatNumber } from '@/lib/format'
|
import { formatNumber } from '@/lib/format'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import { Dialog } from '@/components/dialog'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { formatCreemPrice } from '../../lib/format'
|
import { formatCreemPrice } from '../../lib/format'
|
||||||
import type { CreemProduct } from '../../types'
|
import type { CreemProduct } from '../../types'
|
||||||
|
|
||||||
@@ -51,15 +44,31 @@ export function CreemConfirmDialog({
|
|||||||
if (!product) return null
|
if (!product) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-[425px]'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{t('Confirm Creem Purchase')}</DialogTitle>
|
title={t('Confirm Creem Purchase')}
|
||||||
<DialogDescription>
|
description={t('Review your purchase details before proceeding.')}
|
||||||
{t('Review your purchase details before proceeding.')}
|
contentClassName='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-[425px]'
|
||||||
</DialogDescription>
|
footerClassName='grid grid-cols-2 gap-2 sm:flex'
|
||||||
</DialogHeader>
|
contentHeight='auto'
|
||||||
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onConfirm} disabled={processing}>
|
||||||
|
{processing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{t('Confirm Payment')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-3 py-3 sm:space-y-4 sm:py-4'>
|
<div className='space-y-3 py-3 sm:space-y-4 sm:py-4'>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground'>{t('Product')}</span>
|
<span className='text-muted-foreground'>{t('Product')}</span>
|
||||||
@@ -76,21 +85,6 @@ export function CreemConfirmDialog({
|
|||||||
<span className='font-medium'>{formatNumber(product.quota)}</span>
|
<span className='font-medium'>{formatNumber(product.quota)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={processing}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onConfirm} disabled={processing}>
|
|
||||||
{processing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{t('Confirm Payment')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,9 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { formatQuota } from '@/lib/format'
|
import { formatQuota } from '@/lib/format'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Dialog } from '@/components/dialog'
|
||||||
import { QUOTA_PER_DOLLAR } from '../../constants'
|
import { QUOTA_PER_DOLLAR } from '../../constants'
|
||||||
|
|
||||||
interface TransferDialogProps {
|
interface TransferDialogProps {
|
||||||
@@ -66,17 +59,32 @@ export function TransferDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog
|
||||||
<DialogContent className='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'>
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className='text-xl font-semibold'>
|
title={t('Transfer Rewards')}
|
||||||
{t('Transfer Rewards')}
|
description={t('Move affiliate rewards to your main balance')}
|
||||||
</DialogTitle>
|
contentClassName='max-sm:w-[calc(100vw-1.5rem)] sm:max-w-md'
|
||||||
<DialogDescription>
|
titleClassName='text-xl font-semibold'
|
||||||
{t('Move affiliate rewards to your main balance')}
|
footerClassName='grid grid-cols-2 gap-2 sm:flex'
|
||||||
</DialogDescription>
|
contentHeight='auto'
|
||||||
</DialogHeader>
|
bodyClassName='space-y-4'
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={transferring}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirm} disabled={transferring}>
|
||||||
|
{transferring && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||||
|
{t('Transfer')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className='space-y-4 py-3 sm:space-y-6 sm:py-4'>
|
<div className='space-y-4 py-3 sm:space-y-6 sm:py-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
<Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
|
||||||
@@ -109,21 +117,6 @@ export function TransferDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={transferring}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConfirm} disabled={transferring}>
|
|
||||||
{transferring && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
|
||||||
{t('Transfer')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+9
-2
@@ -645,7 +645,6 @@
|
|||||||
"Channel Affinity": "Channel Affinity",
|
"Channel Affinity": "Channel Affinity",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
|
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
|
||||||
"Keep affinity when channel is disabled": "Keep affinity when channel is disabled",
|
|
||||||
"Channel copied successfully": "Channel copied successfully",
|
"Channel copied successfully": "Channel copied successfully",
|
||||||
"Channel created successfully": "Channel created successfully",
|
"Channel created successfully": "Channel created successfully",
|
||||||
"Channel deleted successfully": "Channel deleted successfully",
|
"Channel deleted successfully": "Channel deleted successfully",
|
||||||
@@ -964,6 +963,7 @@
|
|||||||
"Copy to clipboard": "Copy to clipboard",
|
"Copy to clipboard": "Copy to clipboard",
|
||||||
"Copy token": "Copy token",
|
"Copy token": "Copy token",
|
||||||
"Copy URL": "Copy URL",
|
"Copy URL": "Copy URL",
|
||||||
|
"Copying...": "Copying...",
|
||||||
"Copywriting, ad creative, SEO": "Copywriting, ad creative, SEO",
|
"Copywriting, ad creative, SEO": "Copywriting, ad creative, SEO",
|
||||||
"Core concepts": "Core concepts",
|
"Core concepts": "Core concepts",
|
||||||
"Core Configuration": "Core Configuration",
|
"Core Configuration": "Core Configuration",
|
||||||
@@ -1739,6 +1739,7 @@
|
|||||||
"Filter models by provider, group, type, endpoint, and tags.": "Filter models by provider, group, type, endpoint, and tags.",
|
"Filter models by provider, group, type, endpoint, and tags.": "Filter models by provider, group, type, endpoint, and tags.",
|
||||||
"Filter models by type, endpoint, vendor, group and tags": "Filter models by type, endpoint, vendor, group and tags",
|
"Filter models by type, endpoint, vendor, group and tags": "Filter models by type, endpoint, vendor, group and tags",
|
||||||
"Filter models...": "Filter models...",
|
"Filter models...": "Filter models...",
|
||||||
|
"Filter the model analytics view by time range and user.": "Filter the model analytics view by time range and user.",
|
||||||
"Filter...": "Filter...",
|
"Filter...": "Filter...",
|
||||||
"Filters": "Filters",
|
"Filters": "Filters",
|
||||||
"Filters active": "Filters active",
|
"Filters active": "Filters active",
|
||||||
@@ -1995,7 +1996,6 @@
|
|||||||
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing",
|
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing",
|
||||||
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "If default auto group is enabled, newly created tokens start with auto instead of an empty group.",
|
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "If default auto group is enabled, newly created tokens start with auto instead of an empty group.",
|
||||||
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.",
|
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.",
|
||||||
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.",
|
|
||||||
"If this keeps happening, please report it on GitHub Issues.": "If this keeps happening, please report it on GitHub Issues.",
|
"If this keeps happening, please report it on GitHub Issues.": "If this keeps happening, please report it on GitHub Issues.",
|
||||||
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.",
|
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.",
|
||||||
"Ignored upstream models": "Ignored upstream models",
|
"Ignored upstream models": "Ignored upstream models",
|
||||||
@@ -2108,6 +2108,7 @@
|
|||||||
"Just now": "Just now",
|
"Just now": "Just now",
|
||||||
"JustSong": "JustSong",
|
"JustSong": "JustSong",
|
||||||
"K": "K",
|
"K": "K",
|
||||||
|
"Keep affinity when channel is disabled": "Keep affinity when channel is disabled",
|
||||||
"Keep enabled if you need to proxy requests for different upstream accounts.": "Keep enabled if you need to proxy requests for different upstream accounts.",
|
"Keep enabled if you need to proxy requests for different upstream accounts.": "Keep enabled if you need to proxy requests for different upstream accounts.",
|
||||||
"Keep enough balance before production traffic": "Keep enough balance before production traffic",
|
"Keep enough balance before production traffic": "Keep enough balance before production traffic",
|
||||||
"Keep original value": "Keep original value",
|
"Keep original value": "Keep original value",
|
||||||
@@ -2326,6 +2327,8 @@
|
|||||||
"Model": "Model",
|
"Model": "Model",
|
||||||
"Model Access": "Model Access",
|
"Model Access": "Model Access",
|
||||||
"Model Analytics": "Model Analytics",
|
"Model Analytics": "Model Analytics",
|
||||||
|
"Model Analytics Defaults": "Model Analytics Defaults",
|
||||||
|
"Model Analytics Filters": "Model Analytics Filters",
|
||||||
"model billing support": "model billing support",
|
"model billing support": "model billing support",
|
||||||
"Model Call Analytics": "Model Call Analytics",
|
"Model Call Analytics": "Model Call Analytics",
|
||||||
"Model context usage": "Model context usage",
|
"Model context usage": "Model context usage",
|
||||||
@@ -3610,6 +3613,7 @@
|
|||||||
"Set a tag for": "Set a tag for",
|
"Set a tag for": "Set a tag for",
|
||||||
"Set API key access restrictions": "Set API key access restrictions",
|
"Set API key access restrictions": "Set API key access restrictions",
|
||||||
"Set API key basic information": "Set API key basic information",
|
"Set API key basic information": "Set API key basic information",
|
||||||
|
"Set default ranges and charts for model analytics.": "Set default ranges and charts for model analytics.",
|
||||||
"Set Field": "Set Field",
|
"Set Field": "Set Field",
|
||||||
"Set filters to customize your dashboard statistics and charts.": "Set filters to customize your dashboard statistics and charts.",
|
"Set filters to customize your dashboard statistics and charts.": "Set filters to customize your dashboard statistics and charts.",
|
||||||
"Set filters to narrow down your log search results.": "Set filters to narrow down your log search results.",
|
"Set filters to narrow down your log search results.": "Set filters to narrow down your log search results.",
|
||||||
@@ -3811,11 +3815,13 @@
|
|||||||
"Sync Endpoints": "Sync Endpoints",
|
"Sync Endpoints": "Sync Endpoints",
|
||||||
"Sync Fields": "Sync Fields",
|
"Sync Fields": "Sync Fields",
|
||||||
"Sync from the public upstream metadata repository.": "Sync from the public upstream metadata repository.",
|
"Sync from the public upstream metadata repository.": "Sync from the public upstream metadata repository.",
|
||||||
|
"Sync Now": "Sync Now",
|
||||||
"Sync this model with official upstream": "Sync this model with official upstream",
|
"Sync this model with official upstream": "Sync this model with official upstream",
|
||||||
"Sync Upstream": "Sync Upstream",
|
"Sync Upstream": "Sync Upstream",
|
||||||
"Sync Upstream Models": "Sync Upstream Models",
|
"Sync Upstream Models": "Sync Upstream Models",
|
||||||
"Synchronize models and vendors from an upstream source": "Synchronize models and vendors from an upstream source",
|
"Synchronize models and vendors from an upstream source": "Synchronize models and vendors from an upstream source",
|
||||||
"Syncing prices, please wait...": "Syncing prices, please wait...",
|
"Syncing prices, please wait...": "Syncing prices, please wait...",
|
||||||
|
"Syncing...": "Syncing...",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
"System Administration": "System Administration",
|
"System Administration": "System Administration",
|
||||||
"System Announcements": "System Announcements",
|
"System Announcements": "System Announcements",
|
||||||
@@ -4453,6 +4459,7 @@
|
|||||||
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.",
|
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.",
|
||||||
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.",
|
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.",
|
||||||
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "When enabled, if channels in the current group fail, it will try channels in the next group in order.",
|
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "When enabled, if channels in the current group fail, it will try channels in the next group in order.",
|
||||||
|
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.",
|
||||||
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.",
|
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.",
|
||||||
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "When enabled, Midjourney callbacks are accepted (reveals server IP).",
|
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "When enabled, Midjourney callbacks are accepted (reveals server IP).",
|
||||||
"When enabled, newly created tokens start in the first auto group.": "When enabled, newly created tokens start in the first auto group.",
|
"When enabled, newly created tokens start in the first auto group.": "When enabled, newly created tokens start in the first auto group.",
|
||||||
|
|||||||
Vendored
+9
-2
@@ -645,7 +645,6 @@
|
|||||||
"Channel Affinity": "Affinité de canal",
|
"Channel Affinity": "Affinité de canal",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
|
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
|
||||||
"Keep affinity when channel is disabled": "Conserver l'affinité lorsque le canal est désactivé",
|
|
||||||
"Channel copied successfully": "Canal copié avec succès",
|
"Channel copied successfully": "Canal copié avec succès",
|
||||||
"Channel created successfully": "Canal créé avec succès",
|
"Channel created successfully": "Canal créé avec succès",
|
||||||
"Channel deleted successfully": "Canal supprimé avec succès",
|
"Channel deleted successfully": "Canal supprimé avec succès",
|
||||||
@@ -964,6 +963,7 @@
|
|||||||
"Copy to clipboard": "Copier dans le presse-papiers",
|
"Copy to clipboard": "Copier dans le presse-papiers",
|
||||||
"Copy token": "Copier le Jeton",
|
"Copy token": "Copier le Jeton",
|
||||||
"Copy URL": "Copier l'URL",
|
"Copy URL": "Copier l'URL",
|
||||||
|
"Copying...": "Copie...",
|
||||||
"Copywriting, ad creative, SEO": "Rédaction publicitaire, créatif, SEO",
|
"Copywriting, ad creative, SEO": "Rédaction publicitaire, créatif, SEO",
|
||||||
"Core concepts": "Concepts clés",
|
"Core concepts": "Concepts clés",
|
||||||
"Core Configuration": "Configuration principale",
|
"Core Configuration": "Configuration principale",
|
||||||
@@ -1739,6 +1739,7 @@
|
|||||||
"Filter models by provider, group, type, endpoint, and tags.": "Filtrer les modèles par fournisseur, groupe, type, endpoint et tags.",
|
"Filter models by provider, group, type, endpoint, and tags.": "Filtrer les modèles par fournisseur, groupe, type, endpoint et tags.",
|
||||||
"Filter models by type, endpoint, vendor, group and tags": "Filtrer les modèles par type, point d'accès, fournisseur, groupe et tags",
|
"Filter models by type, endpoint, vendor, group and tags": "Filtrer les modèles par type, point d'accès, fournisseur, groupe et tags",
|
||||||
"Filter models...": "Filtrer les modèles...",
|
"Filter models...": "Filtrer les modèles...",
|
||||||
|
"Filter the model analytics view by time range and user.": "Filtrez la vue d’analyse des modèles par période et utilisateur.",
|
||||||
"Filter...": "Filtrer...",
|
"Filter...": "Filtrer...",
|
||||||
"Filters": "Filtres",
|
"Filters": "Filtres",
|
||||||
"Filters active": "Filtres actifs",
|
"Filters active": "Filtres actifs",
|
||||||
@@ -1995,7 +1996,6 @@
|
|||||||
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Si vous vous connectez à des projets de relais One API ou New API en amont, utilisez le type OpenAI à la place sauf si vous savez ce que vous faites",
|
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Si vous vous connectez à des projets de relais One API ou New API en amont, utilisez le type OpenAI à la place sauf si vous savez ce que vous faites",
|
||||||
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Si le groupe auto par défaut est activé, les nouveaux jetons commencent avec auto au lieu d’un groupe vide.",
|
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Si le groupe auto par défaut est activé, les nouveaux jetons commencent avec auto au lieu d’un groupe vide.",
|
||||||
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Si le canal affinitaire échoue et qu'une nouvelle tentative réussit sur un autre canal, mettre à jour l'affinité vers le canal ayant réussi.",
|
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Si le canal affinitaire échoue et qu'une nouvelle tentative réussit sur un autre canal, mettre à jour l'affinité vers le canal ayant réussi.",
|
||||||
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Lorsque cette option est activée, conserver l'entrée d'affinité même si le canal affinitaire est désactivé ou n'est plus utilisable pour le groupe/modèle actuel. Laissez-la désactivée pour supprimer l'entrée et sélectionner un autre canal.",
|
|
||||||
"If this keeps happening, please report it on GitHub Issues.": "Si cela continue, veuillez le signaler sur GitHub Issues.",
|
"If this keeps happening, please report it on GitHub Issues.": "Si cela continue, veuillez le signaler sur GitHub Issues.",
|
||||||
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Si vous fournissez des services d’IA générative au public en Chine continentale, vous remplirez les obligations légales applicables, notamment le dépôt, l’évaluation de sécurité, la sécurité du contenu, le traitement des plaintes, l’étiquetage du contenu généré, la conservation des journaux et la protection des informations personnelles.",
|
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Si vous fournissez des services d’IA générative au public en Chine continentale, vous remplirez les obligations légales applicables, notamment le dépôt, l’évaluation de sécurité, la sécurité du contenu, le traitement des plaintes, l’étiquetage du contenu généré, la conservation des journaux et la protection des informations personnelles.",
|
||||||
"Ignored upstream models": "Modèles amont ignorés",
|
"Ignored upstream models": "Modèles amont ignorés",
|
||||||
@@ -2108,6 +2108,7 @@
|
|||||||
"Just now": "À l'instant",
|
"Just now": "À l'instant",
|
||||||
"JustSong": "JustSong",
|
"JustSong": "JustSong",
|
||||||
"K": "K",
|
"K": "K",
|
||||||
|
"Keep affinity when channel is disabled": "Conserver l'affinité lorsque le canal est désactivé",
|
||||||
"Keep enabled if you need to proxy requests for different upstream accounts.": "Gardez activé si vous devez proxifier les requêtes pour différents comptes en amont.",
|
"Keep enabled if you need to proxy requests for different upstream accounts.": "Gardez activé si vous devez proxifier les requêtes pour différents comptes en amont.",
|
||||||
"Keep enough balance before production traffic": "Gardez un solde suffisant avant le trafic de production",
|
"Keep enough balance before production traffic": "Gardez un solde suffisant avant le trafic de production",
|
||||||
"Keep original value": "Conserver la valeur originale",
|
"Keep original value": "Conserver la valeur originale",
|
||||||
@@ -2326,6 +2327,8 @@
|
|||||||
"Model": "Modèle",
|
"Model": "Modèle",
|
||||||
"Model Access": "Accès au modèle",
|
"Model Access": "Accès au modèle",
|
||||||
"Model Analytics": "Analyse des modèles",
|
"Model Analytics": "Analyse des modèles",
|
||||||
|
"Model Analytics Defaults": "Paramètres par défaut de l’analyse des modèles",
|
||||||
|
"Model Analytics Filters": "Filtres d’analyse des modèles",
|
||||||
"model billing support": "prise en charge de la facturation des modèles",
|
"model billing support": "prise en charge de la facturation des modèles",
|
||||||
"Model Call Analytics": "Analyse des appels de modèles",
|
"Model Call Analytics": "Analyse des appels de modèles",
|
||||||
"Model context usage": "Utilisation du contexte du modèle",
|
"Model context usage": "Utilisation du contexte du modèle",
|
||||||
@@ -3610,6 +3613,7 @@
|
|||||||
"Set a tag for": "Définir un tag pour",
|
"Set a tag for": "Définir un tag pour",
|
||||||
"Set API key access restrictions": "Définir les restrictions d'accès de la clé API",
|
"Set API key access restrictions": "Définir les restrictions d'accès de la clé API",
|
||||||
"Set API key basic information": "Définir les informations de base de la clé API",
|
"Set API key basic information": "Définir les informations de base de la clé API",
|
||||||
|
"Set default ranges and charts for model analytics.": "Définissez les plages et graphiques par défaut pour l’analyse des modèles.",
|
||||||
"Set Field": "Définir le champ",
|
"Set Field": "Définir le champ",
|
||||||
"Set filters to customize your dashboard statistics and charts.": "Définir des filtres pour personnaliser les statistiques et les graphiques de votre tableau de bord.",
|
"Set filters to customize your dashboard statistics and charts.": "Définir des filtres pour personnaliser les statistiques et les graphiques de votre tableau de bord.",
|
||||||
"Set filters to narrow down your log search results.": "Définir des filtres pour affiner vos résultats de recherche de journaux.",
|
"Set filters to narrow down your log search results.": "Définir des filtres pour affiner vos résultats de recherche de journaux.",
|
||||||
@@ -3811,11 +3815,13 @@
|
|||||||
"Sync Endpoints": "Points de synchronisation",
|
"Sync Endpoints": "Points de synchronisation",
|
||||||
"Sync Fields": "Synchroniser les champs",
|
"Sync Fields": "Synchroniser les champs",
|
||||||
"Sync from the public upstream metadata repository.": "Synchroniser depuis le dépôt de métadonnées public en amont.",
|
"Sync from the public upstream metadata repository.": "Synchroniser depuis le dépôt de métadonnées public en amont.",
|
||||||
|
"Sync Now": "Synchroniser maintenant",
|
||||||
"Sync this model with official upstream": "Synchroniser ce modèle avec la source amont officielle",
|
"Sync this model with official upstream": "Synchroniser ce modèle avec la source amont officielle",
|
||||||
"Sync Upstream": "Synchroniser l'amont",
|
"Sync Upstream": "Synchroniser l'amont",
|
||||||
"Sync Upstream Models": "Synchroniser les modèles amont",
|
"Sync Upstream Models": "Synchroniser les modèles amont",
|
||||||
"Synchronize models and vendors from an upstream source": "Synchroniser les modèles et les fournisseurs à partir d'une source amont",
|
"Synchronize models and vendors from an upstream source": "Synchroniser les modèles et les fournisseurs à partir d'une source amont",
|
||||||
"Syncing prices, please wait...": "Synchronisation des prix, veuillez patienter...",
|
"Syncing prices, please wait...": "Synchronisation des prix, veuillez patienter...",
|
||||||
|
"Syncing...": "Synchronisation...",
|
||||||
"System": "Système",
|
"System": "Système",
|
||||||
"System Administration": "Administration du système",
|
"System Administration": "Administration du système",
|
||||||
"System Announcements": "Annonces système",
|
"System Announcements": "Annonces système",
|
||||||
@@ -4453,6 +4459,7 @@
|
|||||||
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Quand un jeton utilise le groupe auto, le système essaie les groupes de haut en bas jusqu’à trouver un groupe disponible.",
|
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Quand un jeton utilise le groupe auto, le système essaie les groupes de haut en bas jusqu’à trouver un groupe disponible.",
|
||||||
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "Si les conditions sont remplies, le prix final est multiplié par X. Plusieurs correspondances se multiplient ; les valeurs < 1 agissent comme des remises.",
|
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "Si les conditions sont remplies, le prix final est multiplié par X. Plusieurs correspondances se multiplient ; les valeurs < 1 agissent comme des remises.",
|
||||||
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Lorsqu'elle est activée, si les canaux du groupe actuel échouent, le système essaiera les canaux du groupe suivant dans l'ordre.",
|
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Lorsqu'elle est activée, si les canaux du groupe actuel échouent, le système essaiera les canaux du groupe suivant dans l'ordre.",
|
||||||
|
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Lorsque cette option est activée, conserver l'entrée d'affinité même si le canal affinitaire est désactivé ou n'est plus utilisable pour le groupe/modèle actuel. Laissez-la désactivée pour supprimer l'entrée et sélectionner un autre canal.",
|
||||||
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "Lorsqu'activé, les corps de requête volumineux sont temporairement stockés sur disque, réduisant considérablement l'utilisation mémoire. SSD recommandé.",
|
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "Lorsqu'activé, les corps de requête volumineux sont temporairement stockés sur disque, réduisant considérablement l'utilisation mémoire. SSD recommandé.",
|
||||||
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "Lorsque activé, les callbacks Midjourney sont acceptés (révèle l'IP du serveur).",
|
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "Lorsque activé, les callbacks Midjourney sont acceptés (révèle l'IP du serveur).",
|
||||||
"When enabled, newly created tokens start in the first auto group.": "Lorsqu'elle est activée, les jetons nouvellement créés commencent dans le premier groupe automatique.",
|
"When enabled, newly created tokens start in the first auto group.": "Lorsqu'elle est activée, les jetons nouvellement créés commencent dans le premier groupe automatique.",
|
||||||
|
|||||||
Vendored
+65
-58
@@ -16,8 +16,8 @@
|
|||||||
"({{total}} total, {{omit}} omitted)": "(合計 {{total}} 件、{{omit}} 件を省略)",
|
"({{total}} total, {{omit}} omitted)": "(合計 {{total}} 件、{{omit}} 件を省略)",
|
||||||
"(Leave empty to dissolve tag)": "(タグを解除するには空欄のままにしてください)",
|
"(Leave empty to dissolve tag)": "(タグを解除するには空欄のままにしてください)",
|
||||||
"(Optional: redirect model names)": "(オプション: モデル名をリダイレクト)",
|
"(Optional: redirect model names)": "(オプション: モデル名をリダイレクト)",
|
||||||
"(Override all channels' groups)": "(全チャンネルのグループを上書き)",
|
"(Override all channels' groups)": "(全チャネルのグループを上書き)",
|
||||||
"(Override all channels' models)": "(全チャンネルのモデルを上書き)",
|
"(Override all channels' models)": "(全チャネルのモデルを上書き)",
|
||||||
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
|
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
|
||||||
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
|
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
|
||||||
"{\"original-model\": \"replacement-model\"}": "{\" original - model \":\" replacement - model \"}",
|
"{\"original-model\": \"replacement-model\"}": "{\" original - model \":\" replacement - model \"}",
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
"Add {{title}}": "{{title}}を追加",
|
"Add {{title}}": "{{title}}を追加",
|
||||||
"Add a group identifier to the auto assignment list.": "自動割り当てリストにグループ識別子を追加します。",
|
"Add a group identifier to the auto assignment list.": "自動割り当てリストにグループ識別子を追加します。",
|
||||||
"Add a new API key by providing necessary info.": "必要な情報を提供して新しいAPIキーを追加。",
|
"Add a new API key by providing necessary info.": "必要な情報を提供して新しいAPIキーを追加。",
|
||||||
"Add a new channel by providing the necessary information.": "必要な情報を提供して新しいチャンネルを追加。",
|
"Add a new channel by providing the necessary information.": "必要な情報を提供して新しいチャネルを追加。",
|
||||||
"Add a new model to the system by providing the necessary information.": "必要な情報を提供してシステムに新しいモデルを追加してください。",
|
"Add a new model to the system by providing the necessary information.": "必要な情報を提供してシステムに新しいモデルを追加してください。",
|
||||||
"Add a new user by providing necessary info.": "必要な情報を提供して新しいユーザーを追加します。",
|
"Add a new user by providing necessary info.": "必要な情報を提供して新しいユーザーを追加します。",
|
||||||
"Add a new vendor to the system": "システムに新しいベンダーを追加",
|
"Add a new vendor to the system": "システムに新しいベンダーを追加",
|
||||||
@@ -365,7 +365,7 @@
|
|||||||
"Append": "末尾に追加",
|
"Append": "末尾に追加",
|
||||||
"Append mode: New keys will be added to the end of the existing key list": "追記モード: 新しいキーは既存のキー一覧の末尾に追加されます",
|
"Append mode: New keys will be added to the end of the existing key list": "追記モード: 新しいキーは既存のキー一覧の末尾に追加されます",
|
||||||
"Append Template": "テンプレートを追加",
|
"Append Template": "テンプレートを追加",
|
||||||
"Append to channel": "チャンネルに追加",
|
"Append to channel": "チャネルに追加",
|
||||||
"Append to End": "末尾に追加",
|
"Append to End": "末尾に追加",
|
||||||
"Append to existing keys": "既存のキーに追加",
|
"Append to existing keys": "既存のキーに追加",
|
||||||
"Append value to array / string / object end": "配列/文字列/オブジェクトの末尾に値を追加",
|
"Append value to array / string / object end": "配列/文字列/オブジェクトの末尾に値を追加",
|
||||||
@@ -452,7 +452,7 @@
|
|||||||
"Auto-discovers endpoints from the provider": "プロバイダーからエンドポイントを自動検出します",
|
"Auto-discovers endpoints from the provider": "プロバイダーからエンドポイントを自動検出します",
|
||||||
"Auto-fill when one field exists and another is missing": "一方のフィールドがあり他方が欠けている場合に自動補完",
|
"Auto-fill when one field exists and another is missing": "一方のフィールドがあり他方が欠けている場合に自動補完",
|
||||||
"Auto-retry status codes": "自動リトライするステータスコード",
|
"Auto-retry status codes": "自動リトライするステータスコード",
|
||||||
"Automatically disable channel on repeated failures": "繰り返しの失敗でチャンネルを自動的に無効にする",
|
"Automatically disable channel on repeated failures": "繰り返しの失敗でチャネルを自動的に無効にする",
|
||||||
"Automatically disable channels exceeding this response time": "この応答時間を超えるチャネルを自動的に無効にする",
|
"Automatically disable channels exceeding this response time": "この応答時間を超えるチャネルを自動的に無効にする",
|
||||||
"Automatically disable channels when tests fail": "テストが失敗したときにチャネルを自動的に無効にする",
|
"Automatically disable channels when tests fail": "テストが失敗したときにチャネルを自動的に無効にする",
|
||||||
"Automatically probe all channels in the background": "バックグラウンドですべてのチャネルを自動的にプローブする",
|
"Automatically probe all channels in the background": "バックグラウンドですべてのチャネルを自動的にプローブする",
|
||||||
@@ -510,7 +510,7 @@
|
|||||||
"Base Price": "基本価格",
|
"Base Price": "基本価格",
|
||||||
"Base rate limit windows for this account.": "このアカウント向けの基本レート制限ウィンドウ。",
|
"Base rate limit windows for this account.": "このアカウント向けの基本レート制限ウィンドウ。",
|
||||||
"Base URL": "ベースURL",
|
"Base URL": "ベースURL",
|
||||||
"Base URL is required for this channel type": "このチャンネルタイプには Base URL が必要です",
|
"Base URL is required for this channel type": "このチャネルタイプには Base URL が必要です",
|
||||||
"Base URL of your Uptime Kuma instance": "Uptime KumaインスタンスのベースURL",
|
"Base URL of your Uptime Kuma instance": "Uptime KumaインスタンスのベースURL",
|
||||||
"Basic Authentication": "基本認証",
|
"Basic Authentication": "基本認証",
|
||||||
"Basic Configuration": "基本設定",
|
"Basic Configuration": "基本設定",
|
||||||
@@ -645,26 +645,25 @@
|
|||||||
"Channel Affinity": "チャネルアフィニティ",
|
"Channel Affinity": "チャネルアフィニティ",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
|
||||||
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
|
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
|
||||||
"Keep affinity when channel is disabled": "チャネル無効時にアフィニティを保持",
|
"Channel copied successfully": "チャネルが正常にコピーされました",
|
||||||
"Channel copied successfully": "チャンネルが正常にコピーされました",
|
"Channel created successfully": "チャネルが正常に作成されました",
|
||||||
"Channel created successfully": "チャンネルが正常に作成されました",
|
"Channel deleted successfully": "チャネルが正常に削除されました",
|
||||||
"Channel deleted successfully": "チャンネルが正常に削除されました",
|
"Channel disabled successfully": "チャネルが正常に無効化されました",
|
||||||
"Channel disabled successfully": "チャンネルが正常に無効化されました",
|
"Channel enabled successfully": "チャネルが正常に有効化されました",
|
||||||
"Channel enabled successfully": "チャンネルが正常に有効化されました",
|
|
||||||
"Channel Extra Settings": "チャネル詳細設定",
|
"Channel Extra Settings": "チャネル詳細設定",
|
||||||
"Channel ID": "チャネルID",
|
"Channel ID": "チャネルID",
|
||||||
"Channel key": "チャネルキー",
|
"Channel key": "チャネルキー",
|
||||||
"Channel key unlocked": "チャンネルキーが解除されました",
|
"Channel key unlocked": "チャネルキーが解除されました",
|
||||||
"Channel models": "チャネルモデル",
|
"Channel models": "チャネルモデル",
|
||||||
"Channel name is required": "チャンネル名が必要です",
|
"Channel name is required": "チャネル名が必要です",
|
||||||
"Channel test completed": "チャンネルテストが完了しました",
|
"Channel test completed": "チャネルテストが完了しました",
|
||||||
"Channel type is required": "チャンネルタイプが必要です",
|
"Channel type is required": "チャネルタイプが必要です",
|
||||||
"Channel updated successfully": "チャンネルが正常に更新されました",
|
"Channel updated successfully": "チャネルが正常に更新されました",
|
||||||
"Channel-specific settings (JSON format)": "チャンネル固有の設定 (JSON 形式)",
|
"Channel-specific settings (JSON format)": "チャネル固有の設定 (JSON 形式)",
|
||||||
"Channel:": "チャンネル:",
|
"Channel:": "チャネル:",
|
||||||
"channel(s)? This action cannot be undone.": "チャネルを削除しますか?この操作は元に戻せません。",
|
"channel(s)? This action cannot be undone.": "チャネルを削除しますか?この操作は元に戻せません。",
|
||||||
"Channels": "チャネル",
|
"Channels": "チャネル",
|
||||||
"Channels deleted successfully": "チャンネルが正常に削除されました",
|
"Channels deleted successfully": "チャネルが正常に削除されました",
|
||||||
"Character chat, storytelling, persona": "キャラクター会話・ストーリーテリング・ペルソナ",
|
"Character chat, storytelling, persona": "キャラクター会話・ストーリーテリング・ペルソナ",
|
||||||
"Chart Preferences": "チャートの環境設定",
|
"Chart Preferences": "チャートの環境設定",
|
||||||
"Chart Settings": "チャート設定",
|
"Chart Settings": "チャート設定",
|
||||||
@@ -770,8 +769,8 @@
|
|||||||
"Codex": "Codex",
|
"Codex": "Codex",
|
||||||
"Codex Account & Usage": "Codex アカウントと使用量",
|
"Codex Account & Usage": "Codex アカウントと使用量",
|
||||||
"Codex Authorization": "Codex認証",
|
"Codex Authorization": "Codex認証",
|
||||||
"Codex channels do not support batch creation": "Codex チャンネルは一括作成をサポートしていません",
|
"Codex channels do not support batch creation": "Codex チャネルは一括作成をサポートしていません",
|
||||||
"Codex channels use an OAuth JSON credential as the key.": "CodexチャンネルはOAuth JSON認証情報をキーとして使用します。",
|
"Codex channels use an OAuth JSON credential as the key.": "CodexチャネルはOAuth JSON認証情報をキーとして使用します。",
|
||||||
"Codex CLI Header Passthrough": "Codex CLI ヘッダーパススルー",
|
"Codex CLI Header Passthrough": "Codex CLI ヘッダーパススルー",
|
||||||
"Codex credential must be a JSON object with access_token and account_id": "Codex 認証情報は access_token と account_id を含む JSON オブジェクトである必要があります",
|
"Codex credential must be a JSON object with access_token and account_id": "Codex 認証情報は access_token と account_id を含む JSON オブジェクトである必要があります",
|
||||||
"Cohere": "Cohere",
|
"Cohere": "Cohere",
|
||||||
@@ -964,6 +963,7 @@
|
|||||||
"Copy to clipboard": "クリップボードにコピー",
|
"Copy to clipboard": "クリップボードにコピー",
|
||||||
"Copy token": "トークンをコピー",
|
"Copy token": "トークンをコピー",
|
||||||
"Copy URL": "URLをコピー",
|
"Copy URL": "URLをコピー",
|
||||||
|
"Copying...": "コピー中...",
|
||||||
"Copywriting, ad creative, SEO": "コピーライティング・広告クリエイティブ・SEO",
|
"Copywriting, ad creative, SEO": "コピーライティング・広告クリエイティブ・SEO",
|
||||||
"Core concepts": "基本概念",
|
"Core concepts": "基本概念",
|
||||||
"Core Configuration": "コア設定",
|
"Core Configuration": "コア設定",
|
||||||
@@ -992,7 +992,7 @@
|
|||||||
"Create deployment": "デプロイを作成",
|
"Create deployment": "デプロイを作成",
|
||||||
"Create Model": "モデルを作成",
|
"Create Model": "モデルを作成",
|
||||||
"Create multiple API keys at once (random suffix will be added to names)": "一度に複数のAPIキーを一括作成(名前にランダムな接尾辞が追加されます)",
|
"Create multiple API keys at once (random suffix will be added to names)": "一度に複数のAPIキーを一括作成(名前にランダムな接尾辞が追加されます)",
|
||||||
"Create multiple channels from multiple keys": "複数のキーから複数のチャンネルを作成",
|
"Create multiple channels from multiple keys": "複数のキーから複数のチャネルを作成",
|
||||||
"Create multiple redemption codes at once (1-100)": "複数の引き換えコードを一度に作成します (1-100)",
|
"Create multiple redemption codes at once (1-100)": "複数の引き換えコードを一度に作成します (1-100)",
|
||||||
"Create new subscription plan": "新しいサブスクリプションプランを作成",
|
"Create new subscription plan": "新しいサブスクリプションプランを作成",
|
||||||
"Create or update frequently asked questions for users": "ユーザー向けのよくある質問を作成または更新します",
|
"Create or update frequently asked questions for users": "ユーザー向けのよくある質問を作成または更新します",
|
||||||
@@ -1046,7 +1046,7 @@
|
|||||||
"Custom": "カスタム",
|
"Custom": "カスタム",
|
||||||
"Custom (seconds)": "カスタム(秒)",
|
"Custom (seconds)": "カスタム(秒)",
|
||||||
"Custom Amount": "カスタム金額",
|
"Custom Amount": "カスタム金額",
|
||||||
"Custom API base URL. For official channels, New API has built-in addresses. Only fill this for third-party proxy sites or special endpoints. Do not add /v1 or trailing slash.": "カスタムAPIベースURL。公式チャンネルの場合、New APIには組み込みのアドレスがあります。これは、サードパーティのプロキシサイトまたは特別なエンドポイントに対してのみ入力してください。/v1 や末尾のスラッシュを追加しないでください。",
|
"Custom API base URL. For official channels, New API has built-in addresses. Only fill this for third-party proxy sites or special endpoints. Do not add /v1 or trailing slash.": "カスタムAPIベースURL。公式チャネルの場合、New APIには組み込みのアドレスがあります。これは、サードパーティのプロキシサイトまたは特別なエンドポイントに対してのみ入力してください。/v1 や末尾のスラッシュを追加しないでください。",
|
||||||
"Custom API base URL. Leave empty to use provider default.": "カスタム API ベース URL。プロバイダのデフォルトを使用する場合は空にしてください。",
|
"Custom API base URL. Leave empty to use provider default.": "カスタム API ベース URL。プロバイダのデフォルトを使用する場合は空にしてください。",
|
||||||
"Custom Currency": "カスタム通貨",
|
"Custom Currency": "カスタム通貨",
|
||||||
"Custom Currency Symbol": "カスタム通貨記号",
|
"Custom Currency Symbol": "カスタム通貨記号",
|
||||||
@@ -1096,14 +1096,14 @@
|
|||||||
"Default (New Frontend)": "デフォルト(新フロントエンド)",
|
"Default (New Frontend)": "デフォルト(新フロントエンド)",
|
||||||
"Default / range": "デフォルト / 範囲",
|
"Default / range": "デフォルト / 範囲",
|
||||||
"Default API Version *": "デフォルトのAPIバージョン *",
|
"Default API Version *": "デフォルトのAPIバージョン *",
|
||||||
"Default API version for this channel": "このチャンネルのデフォルトのAPIバージョン",
|
"Default API version for this channel": "このチャネルのデフォルトのAPIバージョン",
|
||||||
"Default Collapse Sidebar": "デフォルトのサイドバー折りたたみ",
|
"Default Collapse Sidebar": "デフォルトのサイドバー折りたたみ",
|
||||||
"Default consumption chart": "デフォルトの消費チャート",
|
"Default consumption chart": "デフォルトの消費チャート",
|
||||||
"Default Max Tokens": "デフォルトの最大トークン",
|
"Default Max Tokens": "デフォルトの最大トークン",
|
||||||
"Default model call chart": "デフォルトのモデル呼び出しチャート",
|
"Default model call chart": "デフォルトのモデル呼び出しチャート",
|
||||||
"Default range": "デフォルト範囲",
|
"Default range": "デフォルト範囲",
|
||||||
"Default Responses API version, if empty, will use the API version above": "デフォルトの応答APIバージョン。空の場合、上記のAPIバージョンが使用されます",
|
"Default Responses API version, if empty, will use the API version above": "デフォルトの応答APIバージョン。空の場合、上記のAPIバージョンが使用されます",
|
||||||
"Default system prompt for this channel": "このチャンネルのデフォルトのシステムプロンプト",
|
"Default system prompt for this channel": "このチャネルのデフォルトのシステムプロンプト",
|
||||||
"Default time granularity": "デフォルトの時間粒度",
|
"Default time granularity": "デフォルトの時間粒度",
|
||||||
"Default to auto groups": "デフォルトで自動グループ化",
|
"Default to auto groups": "デフォルトで自動グループ化",
|
||||||
"Default TTL (seconds)": "デフォルト TTL(秒)",
|
"Default TTL (seconds)": "デフォルト TTL(秒)",
|
||||||
@@ -1118,7 +1118,7 @@
|
|||||||
"Delete a runtime request header": "ランタイムリクエストヘッダーを削除",
|
"Delete a runtime request header": "ランタイムリクエストヘッダーを削除",
|
||||||
"Delete Account": "アカウント削除",
|
"Delete Account": "アカウント削除",
|
||||||
"Delete All Disabled": "すべての無効なものを削除",
|
"Delete All Disabled": "すべての無効なものを削除",
|
||||||
"Delete All Disabled Channels?": "すべての無効なチャンネルを削除しますか?",
|
"Delete All Disabled Channels?": "すべての無効なチャネルを削除しますか?",
|
||||||
"Delete Auto-Disabled": "自動無効化されたものを削除",
|
"Delete Auto-Disabled": "自動無効化されたものを削除",
|
||||||
"Delete Channel": "チャネルを削除",
|
"Delete Channel": "チャネルを削除",
|
||||||
"Delete Channels?": "チャネルを削除しますか?",
|
"Delete Channels?": "チャネルを削除しますか?",
|
||||||
@@ -1323,7 +1323,7 @@
|
|||||||
"Edit Announcement": "お知らせを編集",
|
"Edit Announcement": "お知らせを編集",
|
||||||
"Edit API Shortcut": "API ショートカットを編集",
|
"Edit API Shortcut": "API ショートカットを編集",
|
||||||
"Edit billing ratios and user-selectable groups in one table.": "課金倍率とユーザーが選択できるグループを1つの表で編集します。",
|
"Edit billing ratios and user-selectable groups in one table.": "課金倍率とユーザーが選択できるグループを1つの表で編集します。",
|
||||||
"Edit Channel": "チャンネルを編集",
|
"Edit Channel": "チャネルを編集",
|
||||||
"Edit chat preset": "チャットプリセットを編集",
|
"Edit chat preset": "チャットプリセットを編集",
|
||||||
"Edit discount tier": "割引ティアを編集",
|
"Edit discount tier": "割引ティアを編集",
|
||||||
"Edit FAQ": "FAQ を編集",
|
"Edit FAQ": "FAQ を編集",
|
||||||
@@ -1512,8 +1512,8 @@
|
|||||||
"Exact": "完全一致",
|
"Exact": "完全一致",
|
||||||
"Exact Match": "完全一致",
|
"Exact Match": "完全一致",
|
||||||
"Example": "サンプル",
|
"Example": "サンプル",
|
||||||
"Example (all channels):": "例(全チャンネル):",
|
"Example (all channels):": "例(全チャネル):",
|
||||||
"Example (specific channels):": "例(特定チャンネル):",
|
"Example (specific channels):": "例(特定チャネル):",
|
||||||
"Example:": "例:",
|
"Example:": "例:",
|
||||||
"example.com blocked-site.com": "example.com blocked-site.com",
|
"example.com blocked-site.com": "example.com blocked-site.com",
|
||||||
"example.com company.com": "example.com company.com",
|
"example.com company.com": "example.com company.com",
|
||||||
@@ -1575,7 +1575,7 @@
|
|||||||
"Failed to copy model names": "モデル名のコピーに失敗しました",
|
"Failed to copy model names": "モデル名のコピーに失敗しました",
|
||||||
"Failed to copy to clipboard": "クリップボードにコピーできませんでした",
|
"Failed to copy to clipboard": "クリップボードにコピーできませんでした",
|
||||||
"Failed to create API key": "APIキーの作成に失敗しました",
|
"Failed to create API key": "APIキーの作成に失敗しました",
|
||||||
"Failed to create channel": "チャンネルの作成に失敗しました",
|
"Failed to create channel": "チャネルの作成に失敗しました",
|
||||||
"Failed to create deployment": "デプロイの作成に失敗しました",
|
"Failed to create deployment": "デプロイの作成に失敗しました",
|
||||||
"Failed to create provider": "プロバイダーの作成に失敗しました",
|
"Failed to create provider": "プロバイダーの作成に失敗しました",
|
||||||
"Failed to create redemption code": "引き換えコードの作成に失敗しました",
|
"Failed to create redemption code": "引き換えコードの作成に失敗しました",
|
||||||
@@ -1584,7 +1584,7 @@
|
|||||||
"Failed to delete account": "アカウントの削除に失敗しました",
|
"Failed to delete account": "アカウントの削除に失敗しました",
|
||||||
"Failed to delete API key": "APIキーの削除に失敗しました",
|
"Failed to delete API key": "APIキーの削除に失敗しました",
|
||||||
"Failed to delete API keys": "APIキーの削除に失敗しました",
|
"Failed to delete API keys": "APIキーの削除に失敗しました",
|
||||||
"Failed to delete channel": "チャンネルの削除に失敗しました",
|
"Failed to delete channel": "チャネルの削除に失敗しました",
|
||||||
"Failed to delete disabled channels": "無効化されたチャネルの削除に失敗しました",
|
"Failed to delete disabled channels": "無効化されたチャネルの削除に失敗しました",
|
||||||
"Failed to delete invalid redemption codes": "無効な引き換えコードの削除に失敗しました",
|
"Failed to delete invalid redemption codes": "無効な引き換えコードの削除に失敗しました",
|
||||||
"Failed to delete model": "モデルの削除に失敗しました",
|
"Failed to delete model": "モデルの削除に失敗しました",
|
||||||
@@ -1600,9 +1600,9 @@
|
|||||||
"Failed to discover OIDC endpoints": "OIDCエンドポイントの検出に失敗しました",
|
"Failed to discover OIDC endpoints": "OIDCエンドポイントの検出に失敗しました",
|
||||||
"Failed to enable {{count}} model(s)": "{{count}} 個のモデルの有効化に失敗しました",
|
"Failed to enable {{count}} model(s)": "{{count}} 個のモデルの有効化に失敗しました",
|
||||||
"Failed to enable 2FA": "2FA の有効化に失敗しました",
|
"Failed to enable 2FA": "2FA の有効化に失敗しました",
|
||||||
"Failed to enable channels": "チャンネルの有効化に失敗しました",
|
"Failed to enable channels": "チャネルの有効化に失敗しました",
|
||||||
"Failed to enable model": "モデルの有効化に失敗しました",
|
"Failed to enable model": "モデルの有効化に失敗しました",
|
||||||
"Failed to enable tag channels": "タグチャンネルの有効化に失敗しました",
|
"Failed to enable tag channels": "タグチャネルの有効化に失敗しました",
|
||||||
"Failed to fetch channel key": "チャネルキーの取得に失敗しました",
|
"Failed to fetch channel key": "チャネルキーの取得に失敗しました",
|
||||||
"Failed to fetch checkin status": "チェックインステータスの取得に失敗しました",
|
"Failed to fetch checkin status": "チェックインステータスの取得に失敗しました",
|
||||||
"Failed to fetch deployment details": "デプロイメント詳細の取得に失敗しました",
|
"Failed to fetch deployment details": "デプロイメント詳細の取得に失敗しました",
|
||||||
@@ -1707,8 +1707,8 @@
|
|||||||
"Files to Retain": "保持ファイル数",
|
"Files to Retain": "保持ファイル数",
|
||||||
"Fill All Models": "すべてのモデルを埋める",
|
"Fill All Models": "すべてのモデルを埋める",
|
||||||
"Fill Codex CLI / Claude CLI Templates": "Codex CLI / Claude CLI テンプレートを入力",
|
"Fill Codex CLI / Claude CLI Templates": "Codex CLI / Claude CLI テンプレートを入力",
|
||||||
"Fill example (all channels)": "例を入力(全チャンネル)",
|
"Fill example (all channels)": "例を入力(全チャネル)",
|
||||||
"Fill example (specific channels)": "例を入力(特定チャンネル)",
|
"Fill example (specific channels)": "例を入力(特定チャネル)",
|
||||||
"Fill in both Merchant ID and API Private Key before creating.": "作成前に Merchant ID と API 秘密鍵の両方を入力してください。",
|
"Fill in both Merchant ID and API Private Key before creating.": "作成前に Merchant ID と API 秘密鍵の両方を入力してください。",
|
||||||
"Fill in the credentials above to begin.": "開始するには上記の認証情報を入力してください。",
|
"Fill in the credentials above to begin.": "開始するには上記の認証情報を入力してください。",
|
||||||
"Fill in the following info to create a new subscription plan": "以下の情報を入力して新しいサブスクリプションプランを作成",
|
"Fill in the following info to create a new subscription plan": "以下の情報を入力して新しいサブスクリプションプランを作成",
|
||||||
@@ -1720,7 +1720,7 @@
|
|||||||
"Filled {{count}} related model(s)": "{{count}} 個の関連モデルを補完しました",
|
"Filled {{count}} related model(s)": "{{count}} 個の関連モデルを補完しました",
|
||||||
"Filter": "フィルター",
|
"Filter": "フィルター",
|
||||||
"Filter by API key...": "APIキーでフィルター...",
|
"Filter by API key...": "APIキーでフィルター...",
|
||||||
"Filter by channel ID": "チャンネルIDでフィルター",
|
"Filter by channel ID": "チャネルIDでフィルター",
|
||||||
"Filter by group": "グループでフィルター",
|
"Filter by group": "グループでフィルター",
|
||||||
"Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター",
|
"Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター",
|
||||||
"Filter by model name...": "モデル名でフィルター...",
|
"Filter by model name...": "モデル名でフィルター...",
|
||||||
@@ -1739,6 +1739,7 @@
|
|||||||
"Filter models by provider, group, type, endpoint, and tags.": "プロバイダー、グループ、タイプ、エンドポイント、タグでモデルを絞り込みます。",
|
"Filter models by provider, group, type, endpoint, and tags.": "プロバイダー、グループ、タイプ、エンドポイント、タグでモデルを絞り込みます。",
|
||||||
"Filter models by type, endpoint, vendor, group and tags": "タイプ、エンドポイント、ベンダー、グループ、タグでモデルをフィルタリング",
|
"Filter models by type, endpoint, vendor, group and tags": "タイプ、エンドポイント、ベンダー、グループ、タグでモデルをフィルタリング",
|
||||||
"Filter models...": "モデルをフィルタリング...",
|
"Filter models...": "モデルをフィルタリング...",
|
||||||
|
"Filter the model analytics view by time range and user.": "時間範囲とユーザーでモデル分析ビューを絞り込みます。",
|
||||||
"Filter...": "フィルター…",
|
"Filter...": "フィルター…",
|
||||||
"Filters": "フィルター",
|
"Filters": "フィルター",
|
||||||
"Filters active": "フィルター有効",
|
"Filters active": "フィルター有効",
|
||||||
@@ -1780,7 +1781,7 @@
|
|||||||
"Force a syntactically valid JSON response": "構文的に有効な JSON 応答を強制",
|
"Force a syntactically valid JSON response": "構文的に有効な JSON 応答を強制",
|
||||||
"Force AUTH LOGIN": "AUTH LOGINを強制",
|
"Force AUTH LOGIN": "AUTH LOGINを強制",
|
||||||
"Force Format": "強制フォーマット",
|
"Force Format": "強制フォーマット",
|
||||||
"Force format response to OpenAI standard (OpenAI channel only)": "応答をOpenAI標準に強制フォーマット (OpenAIチャンネルのみ)",
|
"Force format response to OpenAI standard (OpenAI channel only)": "応答をOpenAI標準に強制フォーマット (OpenAIチャネルのみ)",
|
||||||
"Force JSON object or schema-conforming output": "JSON オブジェクトまたはスキーマ準拠の出力を強制します",
|
"Force JSON object or schema-conforming output": "JSON オブジェクトまたはスキーマ準拠の出力を強制します",
|
||||||
"Force SMTP authentication using AUTH LOGIN method": "AUTH LOGIN方式を使用してSMTP認証を強制する",
|
"Force SMTP authentication using AUTH LOGIN method": "AUTH LOGIN方式を使用してSMTP認証を強制する",
|
||||||
"Forest Whisper": "フォレストウィスパー",
|
"Forest Whisper": "フォレストウィスパー",
|
||||||
@@ -1821,7 +1822,7 @@
|
|||||||
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "アダプターが無効になっていても、Geminiは思考モードを自動検出します。価格設定と予算編成をより細かく制御する必要がある場合にのみ、これを有効にしてください。",
|
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "アダプターが無効になっていても、Geminiは思考モードを自動検出します。価格設定と予算編成をより細かく制御する必要がある場合にのみ、これを有効にしてください。",
|
||||||
"General": "一般",
|
"General": "一般",
|
||||||
"General Settings": "一般設定",
|
"General Settings": "一般設定",
|
||||||
"Generate a Codex OAuth credential and paste it into the channel key field.": "Codex OAuth認証情報を生成し、チャンネルキー欄に貼り付けてください。",
|
"Generate a Codex OAuth credential and paste it into the channel key field.": "Codex OAuth認証情報を生成し、チャネルキー欄に貼り付けてください。",
|
||||||
"Generate and manage your API access token": "API アクセス トークンを生成および管理",
|
"Generate and manage your API access token": "API アクセス トークンを生成および管理",
|
||||||
"Generate credential": "認証情報を生成",
|
"Generate credential": "認証情報を生成",
|
||||||
"Generate Lyrics": "歌詞を生成",
|
"Generate Lyrics": "歌詞を生成",
|
||||||
@@ -1991,11 +1992,10 @@
|
|||||||
"Icon identifier (e.g. github, gitlab)": "アイコン識別子 (例: github, gitlab)",
|
"Icon identifier (e.g. github, gitlab)": "アイコン識別子 (例: github, gitlab)",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "アップストリームエラーにこれらのキーワードのいずれかが含まれている場合 (大文字と小文字を区別しない)、チャネルは自動的に無効になります。",
|
"If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "アップストリームエラーにこれらのキーワードのいずれかが含まれている場合 (大文字と小文字を区別しない)、チャネルは自動的に無効になります。",
|
||||||
"If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "認証が成功すると、生成されたJSONがキー欄に挿入されます。保存するにはチャンネルを保存してください。",
|
"If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "認証が成功すると、生成されたJSONがキー欄に挿入されます。保存するにはチャネルを保存してください。",
|
||||||
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "上流の One API または New API リレープロジェクトに接続する場合、知っている場合を除き OpenAI タイプを使用してください",
|
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "上流の One API または New API リレープロジェクトに接続する場合、知っている場合を除き OpenAI タイプを使用してください",
|
||||||
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "デフォルト auto グループを有効にすると、新規トークンは空グループではなく auto で開始します。",
|
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "デフォルト auto グループを有効にすると、新規トークンは空グループではなく auto で開始します。",
|
||||||
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "アフィニティチャネルが失敗し、別のチャネルでリトライが成功した場合、アフィニティを成功したチャネルに更新します。",
|
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "アフィニティチャネルが失敗し、別のチャネルでリトライが成功した場合、アフィニティを成功したチャネルに更新します。",
|
||||||
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "有効にすると、アフィニティチャネルが無効化された、または現在のグループ/モデルで利用できなくなった場合でも、そのアフィニティエントリを保持します。無効のままにすると、エントリを削除して別のチャネルを選択します。",
|
|
||||||
"If this keeps happening, please report it on GitHub Issues.": "この問題が続く場合は、GitHub Issues で報告してください。",
|
"If this keeps happening, please report it on GitHub Issues.": "この問題が続く場合は、GitHub Issues で報告してください。",
|
||||||
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "中国本土で一般向けに生成 AI サービスを提供する場合、届出、セキュリティ評価、コンテンツ安全、苦情対応、生成コンテンツのラベル表示、ログ保存、個人情報保護などの法的義務を履行します。",
|
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "中国本土で一般向けに生成 AI サービスを提供する場合、届出、セキュリティ評価、コンテンツ安全、苦情対応、生成コンテンツのラベル表示、ログ保存、個人情報保護などの法的義務を履行します。",
|
||||||
"Ignored upstream models": "無視する上流モデル",
|
"Ignored upstream models": "無視する上流モデル",
|
||||||
@@ -2108,6 +2108,7 @@
|
|||||||
"Just now": "たった今",
|
"Just now": "たった今",
|
||||||
"JustSong": "JustSong",
|
"JustSong": "JustSong",
|
||||||
"K": "K",
|
"K": "K",
|
||||||
|
"Keep affinity when channel is disabled": "チャネル無効時にアフィニティを保持",
|
||||||
"Keep enabled if you need to proxy requests for different upstream accounts.": "異なる上流アカウントのリクエストをプロキシする必要がある場合は有効にしたままにしてください。",
|
"Keep enabled if you need to proxy requests for different upstream accounts.": "異なる上流アカウントのリクエストをプロキシする必要がある場合は有効にしたままにしてください。",
|
||||||
"Keep enough balance before production traffic": "本番トラフィック前に十分な残高を確保",
|
"Keep enough balance before production traffic": "本番トラフィック前に十分な残高を確保",
|
||||||
"Keep original value": "元の値を保持",
|
"Keep original value": "元の値を保持",
|
||||||
@@ -2196,7 +2197,7 @@
|
|||||||
"Load template...": "テンプレートをロード...",
|
"Load template...": "テンプレートをロード...",
|
||||||
"Loader": "ローダー",
|
"Loader": "ローダー",
|
||||||
"Loading": "読み込み中",
|
"Loading": "読み込み中",
|
||||||
"Loading channel details": "チャンネル詳細を読み込み中",
|
"Loading channel details": "チャネル詳細を読み込み中",
|
||||||
"Loading configuration": "設定を読み込んでいます",
|
"Loading configuration": "設定を読み込んでいます",
|
||||||
"Loading content settings...": "コンテンツ設定をロード中...",
|
"Loading content settings...": "コンテンツ設定をロード中...",
|
||||||
"Loading current models...": "現在のモデルをロード中...",
|
"Loading current models...": "現在のモデルをロード中...",
|
||||||
@@ -2326,6 +2327,8 @@
|
|||||||
"Model": "モデル",
|
"Model": "モデル",
|
||||||
"Model Access": "モデルアクセス",
|
"Model Access": "モデルアクセス",
|
||||||
"Model Analytics": "モデル分析",
|
"Model Analytics": "モデル分析",
|
||||||
|
"Model Analytics Defaults": "モデル分析のデフォルト設定",
|
||||||
|
"Model Analytics Filters": "モデル分析フィルター",
|
||||||
"model billing support": "モデル課金対応",
|
"model billing support": "モデル課金対応",
|
||||||
"Model Call Analytics": "モデル呼び出し分析",
|
"Model Call Analytics": "モデル呼び出し分析",
|
||||||
"Model context usage": "モデルのコンテキスト使用量",
|
"Model context usage": "モデルのコンテキスト使用量",
|
||||||
@@ -2458,8 +2461,8 @@
|
|||||||
"Name must be between {{min}} and {{max}} characters": "名前は{{min}}文字以上{{max}}文字以下である必要があります",
|
"Name must be between {{min}} and {{max}} characters": "名前は{{min}}文字以上{{max}}文字以下である必要があります",
|
||||||
"Name Rule": "名前ルール",
|
"Name Rule": "名前ルール",
|
||||||
"Name Suffix": "名前サフィックス",
|
"Name Suffix": "名前サフィックス",
|
||||||
"Name the channel and choose the upstream provider.": "チャンネル名を設定し、上流プロバイダーを選択します。",
|
"Name the channel and choose the upstream provider.": "チャネル名を設定し、上流プロバイダーを選択します。",
|
||||||
"Name the channel, choose the provider, configure API access, and set credentials.": "チャンネル名を設定し、プロバイダーを選択し、API アクセスと認証情報を設定します。",
|
"Name the channel, choose the provider, configure API access, and set credentials.": "チャネル名を設定し、プロバイダーを選択し、API アクセスと認証情報を設定します。",
|
||||||
"Name, provider type, and availability.": "名前、プロバイダー種別、利用可否。",
|
"Name, provider type, and availability.": "名前、プロバイダー種別、利用可否。",
|
||||||
"name@example.com": "name@example.com",
|
"name@example.com": "name@example.com",
|
||||||
"Native format": "ネイティブ形式",
|
"Native format": "ネイティブ形式",
|
||||||
@@ -2516,7 +2519,7 @@
|
|||||||
"No changes made": "変更はありません",
|
"No changes made": "変更はありません",
|
||||||
"No changes to save": "保存する変更がありません",
|
"No changes to save": "保存する変更がありません",
|
||||||
"No channel selected": "チャネルが選択されていません",
|
"No channel selected": "チャネルが選択されていません",
|
||||||
"No channel type found.": "チャンネルタイプが見つかりません。",
|
"No channel type found.": "チャネルタイプが見つかりません。",
|
||||||
"No channels available. Create your first channel to get started.": "利用可能なチャネルがありません。最初のチャネルを作成して開始してください。",
|
"No channels available. Create your first channel to get started.": "利用可能なチャネルがありません。最初のチャネルを作成して開始してください。",
|
||||||
"No channels found": "チャネルが見つかりません",
|
"No channels found": "チャネルが見つかりません",
|
||||||
"No Channels Found": "チャネルが見つかりません",
|
"No Channels Found": "チャネルが見つかりません",
|
||||||
@@ -2604,7 +2607,7 @@
|
|||||||
"No records found. Try adjusting your filters.": "記録が見つかりません。フィルターを調整してみてください。",
|
"No records found. Try adjusting your filters.": "記録が見つかりません。フィルターを調整してみてください。",
|
||||||
"No redemption codes available. Create your first redemption code to get started.": "利用可能な引き換えコードがありません。最初の引き換えコードを作成して開始してください。",
|
"No redemption codes available. Create your first redemption code to get started.": "利用可能な引き換えコードがありません。最初の引き換えコードを作成して開始してください。",
|
||||||
"No Redemption Codes Found": "引き換えコードが見つかりません",
|
"No Redemption Codes Found": "引き換えコードが見つかりません",
|
||||||
"No related models available for this channel type": "このチャンネルタイプに関連するモデルが利用できません",
|
"No related models available for this channel type": "このチャネルタイプに関連するモデルが利用できません",
|
||||||
"No release notes provided.": "リリースノートは提供されていません。",
|
"No release notes provided.": "リリースノートは提供されていません。",
|
||||||
"No Reset": "リセットなし",
|
"No Reset": "リセットなし",
|
||||||
"No restriction": "制限なし",
|
"No restriction": "制限なし",
|
||||||
@@ -2750,13 +2753,13 @@
|
|||||||
"Optional JSON policy to restrict access based on user info fields": "ユーザー情報フィールドに基づいてアクセスを制限するためのオプションのJSONポリシー",
|
"Optional JSON policy to restrict access based on user info fields": "ユーザー情報フィールドに基づいてアクセスを制限するためのオプションのJSONポリシー",
|
||||||
"Optional minimum recharge amount for this method.": "この方法のオプションの最小チャージ額。",
|
"Optional minimum recharge amount for this method.": "この方法のオプションの最小チャージ額。",
|
||||||
"Optional multiplier per user group used when calculating recharge pricing. Provide a JSON object such as": "チャージ料金を計算する際に使用されるユーザーグループごとのオプションの乗数。次のようなJSONオブジェクトを提供してください",
|
"Optional multiplier per user group used when calculating recharge pricing. Provide a JSON object such as": "チャージ料金を計算する際に使用されるユーザーグループごとのオプションの乗数。次のようなJSONオブジェクトを提供してください",
|
||||||
"Optional notes about this channel": "このチャンネルに関するオプションのノート",
|
"Optional notes about this channel": "このチャネルに関するオプションのノート",
|
||||||
"Optional notes about when to use this group": "このグループを使用する時期に関するオプションのメモ",
|
"Optional notes about when to use this group": "このグループを使用する時期に関するオプションのメモ",
|
||||||
"Optional ratio used when upstream cache hits occur.": "アップストリームキャッシュヒットが発生したときに使用されるオプションの比率。",
|
"Optional ratio used when upstream cache hits occur.": "アップストリームキャッシュヒットが発生したときに使用されるオプションの比率。",
|
||||||
"Optional rule description": "任意のルール説明",
|
"Optional rule description": "任意のルール説明",
|
||||||
"Optional settings for advanced container configuration.": "高度なコンテナ設定のためのオプション設定。",
|
"Optional settings for advanced container configuration.": "高度なコンテナ設定のためのオプション設定。",
|
||||||
"Optional supplementary information (max 100 characters)": "オプションの補足情報 (最大100文字)",
|
"Optional supplementary information (max 100 characters)": "オプションの補足情報 (最大100文字)",
|
||||||
"Optional tag for grouping channels": "チャンネルをグループ化するためのオプションのタグ",
|
"Optional tag for grouping channels": "チャネルをグループ化するためのオプションのタグ",
|
||||||
"Opus Model": "Opus モデル",
|
"Opus Model": "Opus モデル",
|
||||||
"Or continue with": "または、以下で続行",
|
"Or continue with": "または、以下で続行",
|
||||||
"Or enter this key manually:": "または、このキーを手動で入力してください:",
|
"Or enter this key manually:": "または、このキーを手動で入力してください:",
|
||||||
@@ -2967,7 +2970,7 @@
|
|||||||
"Please select a payment method": "お支払い方法を選択してください",
|
"Please select a payment method": "お支払い方法を選択してください",
|
||||||
"Please select a primary model": "プライマリモデルを選択してください",
|
"Please select a primary model": "プライマリモデルを選択してください",
|
||||||
"Please select a subscription plan": "サブスクリプションプランを選択してください",
|
"Please select a subscription plan": "サブスクリプションプランを選択してください",
|
||||||
"Please select at least one channel": "少なくとも1つのチャンネルを選択してください",
|
"Please select at least one channel": "少なくとも1つのチャネルを選択してください",
|
||||||
"Please select at least one model": "少なくとも1つのモデルを選択してください",
|
"Please select at least one model": "少なくとも1つのモデルを選択してください",
|
||||||
"Please select items to delete": "削除する項目を選択してください",
|
"Please select items to delete": "削除する項目を選択してください",
|
||||||
"Please Select user groups that can access this channel.": "このチャネルにアクセスできるユーザーグループを選択してください。",
|
"Please Select user groups that can access this channel.": "このチャネルにアクセスできるユーザーグループを選択してください。",
|
||||||
@@ -3476,7 +3479,7 @@
|
|||||||
"Search": "検索",
|
"Search": "検索",
|
||||||
"Search by name or URL...": "名前またはURLで検索...",
|
"Search by name or URL...": "名前またはURLで検索...",
|
||||||
"Search by order number...": "注文番号で検索...",
|
"Search by order number...": "注文番号で検索...",
|
||||||
"Search channel type...": "チャンネルタイプを検索...",
|
"Search channel type...": "チャネルタイプを検索...",
|
||||||
"Search chat presets...": "チャットプリセットを検索...",
|
"Search chat presets...": "チャットプリセットを検索...",
|
||||||
"Search colors...": "色を検索...",
|
"Search colors...": "色を検索...",
|
||||||
"Search conflicting models or fields": "競合するモデルまたはフィールドを検索",
|
"Search conflicting models or fields": "競合するモデルまたはフィールドを検索",
|
||||||
@@ -3554,7 +3557,7 @@
|
|||||||
"Select Model": "モデルを選択",
|
"Select Model": "モデルを選択",
|
||||||
"Select model {{model}}": "モデル {{model}} を選択",
|
"Select model {{model}}": "モデル {{model}} を選択",
|
||||||
"Select models (empty for allow all)": "モデルを選択 (すべて許可する場合は空)",
|
"Select models (empty for allow all)": "モデルを選択 (すべて許可する場合は空)",
|
||||||
"Select models and apply to channel models list.": "モデルを選択し、チャンネルモデルリストに適用します。",
|
"Select models and apply to channel models list.": "モデルを選択し、チャネルモデルリストに適用します。",
|
||||||
"Select models or add custom ones": "モデルを選択するか、カスタムモデルを追加",
|
"Select models or add custom ones": "モデルを選択するか、カスタムモデルを追加",
|
||||||
"Select models to process. Unselected \"add\" models will be ignored.": "処理するモデルを選択してください。未選択の「追加」モデルは無視されます。",
|
"Select models to process. Unselected \"add\" models will be ignored.": "処理するモデルを選択してください。未選択の「追加」モデルは無視されます。",
|
||||||
"Select models to run batch tests.": "バッチテストを実行するモデルを選択してください。",
|
"Select models to run batch tests.": "バッチテストを実行するモデルを選択してください。",
|
||||||
@@ -3571,7 +3574,7 @@
|
|||||||
"Select start time": "開始時間を選択",
|
"Select start time": "開始時間を選択",
|
||||||
"Select subscription plan": "サブスクリプションプランを選択",
|
"Select subscription plan": "サブスクリプションプランを選択",
|
||||||
"Select Sync Channels": "同期チャネルを選択",
|
"Select Sync Channels": "同期チャネルを選択",
|
||||||
"Select sync channels to compare prices": "価格比較のために同期チャンネルを選択してください",
|
"Select sync channels to compare prices": "価格比較のために同期チャネルを選択してください",
|
||||||
"Select sync channels to compare ratios": "比率を比較するために同期チャネルを選択",
|
"Select sync channels to compare ratios": "比率を比較するために同期チャネルを選択",
|
||||||
"Select Sync Source": "同期元を選択",
|
"Select Sync Source": "同期元を選択",
|
||||||
"Select the API endpoint region": "APIエンドポイントのリージョンを選択",
|
"Select the API endpoint region": "APIエンドポイントのリージョンを選択",
|
||||||
@@ -3583,7 +3586,7 @@
|
|||||||
"Selectable groups": "選択可能なグループ",
|
"Selectable groups": "選択可能なグループ",
|
||||||
"selected": "選択済み",
|
"selected": "選択済み",
|
||||||
"Selected {{count}}": "{{count}} 件選択済み",
|
"Selected {{count}}": "{{count}} 件選択済み",
|
||||||
"selected channel(s). Leave empty to remove tag.": "選択されたチャンネル。タグを削除するには空のままにしてください。",
|
"selected channel(s). Leave empty to remove tag.": "選択されたチャネル。タグを削除するには空のままにしてください。",
|
||||||
"Selected conflicts were overwritten successfully.": "選択した競合が正常に上書きされました。",
|
"Selected conflicts were overwritten successfully.": "選択した競合が正常に上書きされました。",
|
||||||
"Selected when creating a token and used as the default billing group for API calls.": "トークン作成時に選択され、API 呼び出しのデフォルト課金グループとして使われます。",
|
"Selected when creating a token and used as the default billing group for API calls.": "トークン作成時に選択され、API 呼び出しのデフォルト課金グループとして使われます。",
|
||||||
"Self-Use Mode": "セルフユースモード",
|
"Self-Use Mode": "セルフユースモード",
|
||||||
@@ -3610,6 +3613,7 @@
|
|||||||
"Set a tag for": "のタグを設定",
|
"Set a tag for": "のタグを設定",
|
||||||
"Set API key access restrictions": "API キーのアクセス制限を設定",
|
"Set API key access restrictions": "API キーのアクセス制限を設定",
|
||||||
"Set API key basic information": "API キーの基本情報を設定",
|
"Set API key basic information": "API キーの基本情報を設定",
|
||||||
|
"Set default ranges and charts for model analytics.": "モデル分析の既定の範囲とチャートを設定します。",
|
||||||
"Set Field": "フィールドを設定",
|
"Set Field": "フィールドを設定",
|
||||||
"Set filters to customize your dashboard statistics and charts.": "ダッシュボードの統計とグラフをカスタマイズするためにフィルターを設定します。",
|
"Set filters to customize your dashboard statistics and charts.": "ダッシュボードの統計とグラフをカスタマイズするためにフィルターを設定します。",
|
||||||
"Set filters to narrow down your log search results.": "ログ検索結果を絞り込むためにフィルターを設定します。",
|
"Set filters to narrow down your log search results.": "ログ検索結果を絞り込むためにフィルターを設定します。",
|
||||||
@@ -3811,11 +3815,13 @@
|
|||||||
"Sync Endpoints": "同期エンドポイント",
|
"Sync Endpoints": "同期エンドポイント",
|
||||||
"Sync Fields": "フィールド同期",
|
"Sync Fields": "フィールド同期",
|
||||||
"Sync from the public upstream metadata repository.": "公開上流メタデータリポジトリから同期します。",
|
"Sync from the public upstream metadata repository.": "公開上流メタデータリポジトリから同期します。",
|
||||||
|
"Sync Now": "今すぐ同期",
|
||||||
"Sync this model with official upstream": "このモデルを公式アップストリームと同期",
|
"Sync this model with official upstream": "このモデルを公式アップストリームと同期",
|
||||||
"Sync Upstream": "アップストリームを同期",
|
"Sync Upstream": "アップストリームを同期",
|
||||||
"Sync Upstream Models": "アップストリームモデルを同期",
|
"Sync Upstream Models": "アップストリームモデルを同期",
|
||||||
"Synchronize models and vendors from an upstream source": "アップストリームソースからモデルとベンダーを同期",
|
"Synchronize models and vendors from an upstream source": "アップストリームソースからモデルとベンダーを同期",
|
||||||
"Syncing prices, please wait...": "価格を同期中、しばらくお待ちください...",
|
"Syncing prices, please wait...": "価格を同期中、しばらくお待ちください...",
|
||||||
|
"Syncing...": "同期中...",
|
||||||
"System": "システム",
|
"System": "システム",
|
||||||
"System Administration": "システム管理",
|
"System Administration": "システム管理",
|
||||||
"System Announcements": "システムのお知らせ",
|
"System Announcements": "システムのお知らせ",
|
||||||
@@ -3936,9 +3942,9 @@
|
|||||||
"This action cannot be undone.": "この操作は元に戻せません。",
|
"This action cannot be undone.": "この操作は元に戻せません。",
|
||||||
"This action cannot be undone. This will permanently delete your account and remove all your data from our servers.": "この操作は元に戻せません。これにより、あなたのアカウントは完全に削除され、すべてのデータがサーバーから削除されます。",
|
"This action cannot be undone. This will permanently delete your account and remove all your data from our servers.": "この操作は元に戻せません。これにより、あなたのアカウントは完全に削除され、すべてのデータがサーバーから削除されます。",
|
||||||
"This action will permanently remove 2FA protection from your account.": "この操作により、アカウントから2FA保護が完全に削除されます。",
|
"This action will permanently remove 2FA protection from your account.": "この操作により、アカウントから2FA保護が完全に削除されます。",
|
||||||
"This channel is not an Ollama channel.": "このチャンネルはOllamaチャンネルではありません。",
|
"This channel is not an Ollama channel.": "このチャネルはOllamaチャネルではありません。",
|
||||||
"This channel type does not support fetching models": "このチャンネルタイプはモデルの取得をサポートしていません",
|
"This channel type does not support fetching models": "このチャネルタイプはモデルの取得をサポートしていません",
|
||||||
"This channel type requires additional configuration": "このチャンネルタイプには追加設定が必要です",
|
"This channel type requires additional configuration": "このチャネルタイプには追加設定が必要です",
|
||||||
"This confirmation unlocks payment, redemption code, subscription plan, and invitation reward features. Please read the statements carefully.": "この確認により、支払い、引換コード、サブスクリプションプラン、招待報酬の機能が解除されます。各項目をよく読んでください。",
|
"This confirmation unlocks payment, redemption code, subscription plan, and invitation reward features. Please read the statements carefully.": "この確認により、支払い、引換コード、サブスクリプションプラン、招待報酬の機能が解除されます。各項目をよく読んでください。",
|
||||||
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "これはモデルリクエストのレート制限を制御します。Web/API ルートのスロットリングは環境変数で設定され、引き続き 429 を返す場合があります。",
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "これはモデルリクエストのレート制限を制御します。Web/API ルートのスロットリングは環境変数で設定され、引き続き 429 を返す場合があります。",
|
||||||
"This data may be unreliable, use with caution": "このデータは信頼できない可能性があります。注意して使用してください",
|
"This data may be unreliable, use with caution": "このデータは信頼できない可能性があります。注意して使用してください",
|
||||||
@@ -4287,7 +4293,7 @@
|
|||||||
"User Group": "ユーザーグループ",
|
"User Group": "ユーザーグループ",
|
||||||
"User group name": "ユーザーグループ名",
|
"User group name": "ユーザーグループ名",
|
||||||
"User Group: {{ratio}}x": "ユーザーグループ:{{ratio}}x",
|
"User Group: {{ratio}}x": "ユーザーグループ:{{ratio}}x",
|
||||||
"User groups that can access channels with this tag": "このタグを持つチャンネルにアクセスできるユーザーグループ",
|
"User groups that can access channels with this tag": "このタグを持つチャネルにアクセスできるユーザーグループ",
|
||||||
"User groups that can access this channel. ": "このチャネルにアクセスできるユーザグループ。",
|
"User groups that can access this channel. ": "このチャネルにアクセスできるユーザグループ。",
|
||||||
"User ID": "ユーザー ID",
|
"User ID": "ユーザー ID",
|
||||||
"User ID Field": "ユーザーIDフィールド",
|
"User ID Field": "ユーザーIDフィールド",
|
||||||
@@ -4452,7 +4458,8 @@
|
|||||||
"What would you like to know?": "何を知りたいですか?",
|
"What would you like to know?": "何を知りたいですか?",
|
||||||
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "トークンが auto グループを使用すると、システムは上から順に利用可能なグループを探します。",
|
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "トークンが auto グループを使用すると、システムは上から順に利用可能なグループを探します。",
|
||||||
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件に一致したとき、最終価格に X を掛けます。複数一致は掛け合わさり、1 未満は割引として効きます。",
|
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件に一致したとき、最終価格に X を掛けます。複数一致は掛け合わさり、1 未満は割引として効きます。",
|
||||||
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "有効にすると、現在のグループのチャンネルが失敗した場合、次のグループのチャンネルを順番に試します。",
|
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "有効にすると、現在のグループのチャネルが失敗した場合、次のグループのチャネルを順番に試します。",
|
||||||
|
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "有効にすると、アフィニティチャネルが無効化された、または現在のグループ/モデルで利用できなくなった場合でも、そのアフィニティエントリを保持します。無効のままにすると、エントリを削除して別のチャネルを選択します。",
|
||||||
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "有効にすると、大きなリクエストボディはメモリではなくディスクに一時保存され、メモリ使用量が大幅に削減されます。SSD環境での使用を推奨します。",
|
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "有効にすると、大きなリクエストボディはメモリではなくディスクに一時保存され、メモリ使用量が大幅に削減されます。SSD環境での使用を推奨します。",
|
||||||
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "有効にすると、Midjourney のコールバックを受け入れます (サーバーの IP を公開します)。",
|
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "有効にすると、Midjourney のコールバックを受け入れます (サーバーの IP を公開します)。",
|
||||||
"When enabled, newly created tokens start in the first auto group.": "有効にすると、新しく作成されたトークンは最初の自動グループで開始されます。",
|
"When enabled, newly created tokens start in the first auto group.": "有効にすると、新しく作成されたトークンは最初の自動グループで開始されます。",
|
||||||
|
|||||||
Vendored
+9
-2
@@ -645,7 +645,6 @@
|
|||||||
"Channel Affinity": "Привязка к каналу",
|
"Channel Affinity": "Привязка к каналу",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
|
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
|
||||||
"Keep affinity when channel is disabled": "Сохранять привязку при отключении канала",
|
|
||||||
"Channel copied successfully": "Канал успешно скопирован",
|
"Channel copied successfully": "Канал успешно скопирован",
|
||||||
"Channel created successfully": "Канал успешно создан",
|
"Channel created successfully": "Канал успешно создан",
|
||||||
"Channel deleted successfully": "Канал успешно удалён",
|
"Channel deleted successfully": "Канал успешно удалён",
|
||||||
@@ -964,6 +963,7 @@
|
|||||||
"Copy to clipboard": "Копировать в буфер обмена",
|
"Copy to clipboard": "Копировать в буфер обмена",
|
||||||
"Copy token": "Копировать токен",
|
"Copy token": "Копировать токен",
|
||||||
"Copy URL": "Скопировать URL",
|
"Copy URL": "Скопировать URL",
|
||||||
|
"Copying...": "Копирование...",
|
||||||
"Copywriting, ad creative, SEO": "Копирайтинг, рекламные креативы, SEO",
|
"Copywriting, ad creative, SEO": "Копирайтинг, рекламные креативы, SEO",
|
||||||
"Core concepts": "Основные понятия",
|
"Core concepts": "Основные понятия",
|
||||||
"Core Configuration": "Основная конфигурация",
|
"Core Configuration": "Основная конфигурация",
|
||||||
@@ -1739,6 +1739,7 @@
|
|||||||
"Filter models by provider, group, type, endpoint, and tags.": "Фильтруйте модели по поставщику, группе, типу, endpoint и тегам.",
|
"Filter models by provider, group, type, endpoint, and tags.": "Фильтруйте модели по поставщику, группе, типу, endpoint и тегам.",
|
||||||
"Filter models by type, endpoint, vendor, group and tags": "Фильтровать модели по типу, точке доступа, поставщику, группе и тегам",
|
"Filter models by type, endpoint, vendor, group and tags": "Фильтровать модели по типу, точке доступа, поставщику, группе и тегам",
|
||||||
"Filter models...": "Фильтровать модели...",
|
"Filter models...": "Фильтровать модели...",
|
||||||
|
"Filter the model analytics view by time range and user.": "Фильтруйте представление аналитики моделей по периоду и пользователю.",
|
||||||
"Filter...": "Фильтр...",
|
"Filter...": "Фильтр...",
|
||||||
"Filters": "Фильтры",
|
"Filters": "Фильтры",
|
||||||
"Filters active": "Фильтры активны",
|
"Filters active": "Фильтры активны",
|
||||||
@@ -1995,7 +1996,6 @@
|
|||||||
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "При подключении к upstream One API или проектам-ретрансляторам New API используйте тип OpenAI, если только вы точно знаете, что делаете",
|
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "При подключении к upstream One API или проектам-ретрансляторам New API используйте тип OpenAI, если только вы точно знаете, что делаете",
|
||||||
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Если группа auto включена по умолчанию, новые токены создаются с auto вместо пустой группы.",
|
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Если группа auto включена по умолчанию, новые токены создаются с auto вместо пустой группы.",
|
||||||
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Если привязанный канал не работает и повторная попытка удалась через другой канал, привязка обновляется на успешный канал.",
|
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Если привязанный канал не работает и повторная попытка удалась через другой канал, привязка обновляется на успешный канал.",
|
||||||
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Если включено, запись привязки сохраняется, даже когда привязанный канал отключён или больше не подходит для текущей группы/модели. Оставьте выключенным, чтобы удалять запись и выбирать другой канал.",
|
|
||||||
"If this keeps happening, please report it on GitHub Issues.": "Если проблема повторяется, сообщите о ней в GitHub Issues.",
|
"If this keeps happening, please report it on GitHub Issues.": "Если проблема повторяется, сообщите о ней в GitHub Issues.",
|
||||||
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Если вы предоставляете услуги генеративного ИИ населению материкового Китая, вы будете выполнять юридические обязанности, включая регистрацию, оценку безопасности, безопасность контента, обработку жалоб, маркировку сгенерированного контента, хранение журналов и защиту персональных данных.",
|
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Если вы предоставляете услуги генеративного ИИ населению материкового Китая, вы будете выполнять юридические обязанности, включая регистрацию, оценку безопасности, безопасность контента, обработку жалоб, маркировку сгенерированного контента, хранение журналов и защиту персональных данных.",
|
||||||
"Ignored upstream models": "Игнорируемые upstream-модели",
|
"Ignored upstream models": "Игнорируемые upstream-модели",
|
||||||
@@ -2108,6 +2108,7 @@
|
|||||||
"Just now": "Только что",
|
"Just now": "Только что",
|
||||||
"JustSong": "JustSong",
|
"JustSong": "JustSong",
|
||||||
"K": "K",
|
"K": "K",
|
||||||
|
"Keep affinity when channel is disabled": "Сохранять привязку при отключении канала",
|
||||||
"Keep enabled if you need to proxy requests for different upstream accounts.": "Оставьте включённым, если нужно проксировать запросы для разных upstream-аккаунтов.",
|
"Keep enabled if you need to proxy requests for different upstream accounts.": "Оставьте включённым, если нужно проксировать запросы для разных upstream-аккаунтов.",
|
||||||
"Keep enough balance before production traffic": "Поддерживайте достаточный баланс перед рабочим трафиком",
|
"Keep enough balance before production traffic": "Поддерживайте достаточный баланс перед рабочим трафиком",
|
||||||
"Keep original value": "Сохранить исходное значение",
|
"Keep original value": "Сохранить исходное значение",
|
||||||
@@ -2326,6 +2327,8 @@
|
|||||||
"Model": "Модель",
|
"Model": "Модель",
|
||||||
"Model Access": "Доступ к моделям",
|
"Model Access": "Доступ к моделям",
|
||||||
"Model Analytics": "Аналитика моделей",
|
"Model Analytics": "Аналитика моделей",
|
||||||
|
"Model Analytics Defaults": "Настройки аналитики моделей по умолчанию",
|
||||||
|
"Model Analytics Filters": "Фильтры аналитики моделей",
|
||||||
"model billing support": "поддержка биллинга моделей",
|
"model billing support": "поддержка биллинга моделей",
|
||||||
"Model Call Analytics": "Аналитика вызовов моделей",
|
"Model Call Analytics": "Аналитика вызовов моделей",
|
||||||
"Model context usage": "Использование контекста модели",
|
"Model context usage": "Использование контекста модели",
|
||||||
@@ -3610,6 +3613,7 @@
|
|||||||
"Set a tag for": "Установить тег для",
|
"Set a tag for": "Установить тег для",
|
||||||
"Set API key access restrictions": "Настройте ограничения доступа API-ключа",
|
"Set API key access restrictions": "Настройте ограничения доступа API-ключа",
|
||||||
"Set API key basic information": "Настройте основные сведения API-ключа",
|
"Set API key basic information": "Настройте основные сведения API-ключа",
|
||||||
|
"Set default ranges and charts for model analytics.": "Настройте диапазоны и графики по умолчанию для аналитики моделей.",
|
||||||
"Set Field": "Установить поле",
|
"Set Field": "Установить поле",
|
||||||
"Set filters to customize your dashboard statistics and charts.": "Установите фильтры, чтобы настроить статистику и диаграммы вашей панели управления.",
|
"Set filters to customize your dashboard statistics and charts.": "Установите фильтры, чтобы настроить статистику и диаграммы вашей панели управления.",
|
||||||
"Set filters to narrow down your log search results.": "Установите фильтры, чтобы сузить результаты поиска по журналам.",
|
"Set filters to narrow down your log search results.": "Установите фильтры, чтобы сузить результаты поиска по журналам.",
|
||||||
@@ -3811,11 +3815,13 @@
|
|||||||
"Sync Endpoints": "Точки синхронизации",
|
"Sync Endpoints": "Точки синхронизации",
|
||||||
"Sync Fields": "Синхронизировать поля",
|
"Sync Fields": "Синхронизировать поля",
|
||||||
"Sync from the public upstream metadata repository.": "Синхронизировать из публичного репозитория метаданных верхнего уровня.",
|
"Sync from the public upstream metadata repository.": "Синхронизировать из публичного репозитория метаданных верхнего уровня.",
|
||||||
|
"Sync Now": "Синхронизировать сейчас",
|
||||||
"Sync this model with official upstream": "Синхронизировать эту модель с официальным upstream",
|
"Sync this model with official upstream": "Синхронизировать эту модель с официальным upstream",
|
||||||
"Sync Upstream": "Синхронизировать Upstream",
|
"Sync Upstream": "Синхронизировать Upstream",
|
||||||
"Sync Upstream Models": "Синхронизировать модели Upstream",
|
"Sync Upstream Models": "Синхронизировать модели Upstream",
|
||||||
"Synchronize models and vendors from an upstream source": "Синхронизировать модели и поставщиков из upstream источника",
|
"Synchronize models and vendors from an upstream source": "Синхронизировать модели и поставщиков из upstream источника",
|
||||||
"Syncing prices, please wait...": "Синхронизация цен, подождите...",
|
"Syncing prices, please wait...": "Синхронизация цен, подождите...",
|
||||||
|
"Syncing...": "Синхронизация...",
|
||||||
"System": "Система",
|
"System": "Система",
|
||||||
"System Administration": "Администрирование системы",
|
"System Administration": "Администрирование системы",
|
||||||
"System Announcements": "Системные объявления",
|
"System Announcements": "Системные объявления",
|
||||||
@@ -4453,6 +4459,7 @@
|
|||||||
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Когда токен использует группу auto, система перебирает группы сверху вниз, пока не найдёт доступную.",
|
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Когда токен использует группу auto, система перебирает группы сверху вниз, пока не найдёт доступную.",
|
||||||
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "При совпадении условий итоговая цена умножается на X. Несколько совпадений умножаются вместе; значения < 1 действуют как скидки.",
|
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "При совпадении условий итоговая цена умножается на X. Несколько совпадений умножаются вместе; значения < 1 действуют как скидки.",
|
||||||
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Если включено, при сбое каналов в текущей группе система попробует каналы следующей группы по порядку.",
|
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Если включено, при сбое каналов в текущей группе система попробует каналы следующей группы по порядку.",
|
||||||
|
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Если включено, запись привязки сохраняется, даже когда привязанный канал отключён или больше не подходит для текущей группы/модели. Оставьте выключенным, чтобы удалять запись и выбирать другой канал.",
|
||||||
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "При включении большие тела запросов временно сохраняются на диске, что значительно снижает использование памяти. Рекомендуется SSD.",
|
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "При включении большие тела запросов временно сохраняются на диске, что значительно снижает использование памяти. Рекомендуется SSD.",
|
||||||
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "При включении принимаются обратные вызовы Midjourney (раскрывает IP сервера).",
|
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "При включении принимаются обратные вызовы Midjourney (раскрывает IP сервера).",
|
||||||
"When enabled, newly created tokens start in the first auto group.": "При включении вновь созданные токены начинаются в первой автогруппе.",
|
"When enabled, newly created tokens start in the first auto group.": "При включении вновь созданные токены начинаются в первой автогруппе.",
|
||||||
|
|||||||
Vendored
+11
-4
@@ -645,7 +645,6 @@
|
|||||||
"Channel Affinity": "Ưu tiên kênh",
|
"Channel Affinity": "Ưu tiên kênh",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
|
||||||
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
|
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
|
||||||
"Keep affinity when channel is disabled": "Giữ ưu tiên khi kênh bị tắt",
|
|
||||||
"Channel copied successfully": "Sao chép kênh thành công",
|
"Channel copied successfully": "Sao chép kênh thành công",
|
||||||
"Channel created successfully": "Tạo kênh thành công",
|
"Channel created successfully": "Tạo kênh thành công",
|
||||||
"Channel deleted successfully": "Xóa kênh thành công",
|
"Channel deleted successfully": "Xóa kênh thành công",
|
||||||
@@ -655,7 +654,7 @@
|
|||||||
"Channel ID": "Mã kênh",
|
"Channel ID": "Mã kênh",
|
||||||
"Channel key": "Khóa kênh",
|
"Channel key": "Khóa kênh",
|
||||||
"Channel key unlocked": "Khóa kênh đã được mở khóa",
|
"Channel key unlocked": "Khóa kênh đã được mở khóa",
|
||||||
"Channel models": "Channel model",
|
"Channel models": "Mô hình kênh",
|
||||||
"Channel name is required": "Tên kênh là bắt buộc",
|
"Channel name is required": "Tên kênh là bắt buộc",
|
||||||
"Channel test completed": "Kiểm tra kênh hoàn tất",
|
"Channel test completed": "Kiểm tra kênh hoàn tất",
|
||||||
"Channel type is required": "Loại kênh là bắt buộc",
|
"Channel type is required": "Loại kênh là bắt buộc",
|
||||||
@@ -964,6 +963,7 @@
|
|||||||
"Copy to clipboard": "Sao chép vào bảng tạm",
|
"Copy to clipboard": "Sao chép vào bảng tạm",
|
||||||
"Copy token": "Sao chép mã thông báo",
|
"Copy token": "Sao chép mã thông báo",
|
||||||
"Copy URL": "Sao chép URL",
|
"Copy URL": "Sao chép URL",
|
||||||
|
"Copying...": "Đang sao chép...",
|
||||||
"Copywriting, ad creative, SEO": "Viết copy, sáng tạo quảng cáo, SEO",
|
"Copywriting, ad creative, SEO": "Viết copy, sáng tạo quảng cáo, SEO",
|
||||||
"Core concepts": "Khái niệm chính",
|
"Core concepts": "Khái niệm chính",
|
||||||
"Core Configuration": "Cấu hình chính",
|
"Core Configuration": "Cấu hình chính",
|
||||||
@@ -1739,6 +1739,7 @@
|
|||||||
"Filter models by provider, group, type, endpoint, and tags.": "Lọc mô hình theo nhà cung cấp, nhóm, loại, endpoint và thẻ.",
|
"Filter models by provider, group, type, endpoint, and tags.": "Lọc mô hình theo nhà cung cấp, nhóm, loại, endpoint và thẻ.",
|
||||||
"Filter models by type, endpoint, vendor, group and tags": "Lọc mô hình theo loại, endpoint, nhà cung cấp, nhóm và thẻ",
|
"Filter models by type, endpoint, vendor, group and tags": "Lọc mô hình theo loại, endpoint, nhà cung cấp, nhóm và thẻ",
|
||||||
"Filter models...": "Lọc mô hình...",
|
"Filter models...": "Lọc mô hình...",
|
||||||
|
"Filter the model analytics view by time range and user.": "Lọc chế độ xem phân tích mô hình theo khoảng thời gian và người dùng.",
|
||||||
"Filter...": "Lọc...",
|
"Filter...": "Lọc...",
|
||||||
"Filters": "Bộ lọc",
|
"Filters": "Bộ lọc",
|
||||||
"Filters active": "Bộ lọc đang bật",
|
"Filters active": "Bộ lọc đang bật",
|
||||||
@@ -1995,7 +1996,6 @@
|
|||||||
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Nếu kết nối với dự án relay One API hoặc New API upstream, hãy sử dụng loại OpenAI thay thế trừ khi bạn biết mình đang làm gì",
|
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Nếu kết nối với dự án relay One API hoặc New API upstream, hãy sử dụng loại OpenAI thay thế trừ khi bạn biết mình đang làm gì",
|
||||||
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Nếu bật nhóm auto mặc định, token mới sẽ bắt đầu với auto thay vì nhóm trống.",
|
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Nếu bật nhóm auto mặc định, token mới sẽ bắt đầu với auto thay vì nhóm trống.",
|
||||||
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Nếu kênh ưu tiên thất bại và thử lại thành công trên kênh khác, cập nhật ưu tiên sang kênh thành công.",
|
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Nếu kênh ưu tiên thất bại và thử lại thành công trên kênh khác, cập nhật ưu tiên sang kênh thành công.",
|
||||||
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Khi bật, giữ mục ưu tiên ngay cả khi kênh ưu tiên bị tắt hoặc không còn dùng được cho nhóm/mô hình hiện tại. Để tắt để xóa mục đó và chọn kênh khác.",
|
|
||||||
"If this keeps happening, please report it on GitHub Issues.": "Nếu sự cố tiếp tục xảy ra, vui lòng báo cáo trên GitHub Issues.",
|
"If this keeps happening, please report it on GitHub Issues.": "Nếu sự cố tiếp tục xảy ra, vui lòng báo cáo trên GitHub Issues.",
|
||||||
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Nếu bạn cung cấp dịch vụ AI tạo sinh cho công chúng tại Trung Quốc đại lục, bạn sẽ thực hiện các nghĩa vụ pháp lý bao gồm đăng ký, đánh giá an toàn, an toàn nội dung, xử lý khiếu nại, gắn nhãn nội dung được tạo, lưu giữ nhật ký và bảo vệ thông tin cá nhân.",
|
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Nếu bạn cung cấp dịch vụ AI tạo sinh cho công chúng tại Trung Quốc đại lục, bạn sẽ thực hiện các nghĩa vụ pháp lý bao gồm đăng ký, đánh giá an toàn, an toàn nội dung, xử lý khiếu nại, gắn nhãn nội dung được tạo, lưu giữ nhật ký và bảo vệ thông tin cá nhân.",
|
||||||
"Ignored upstream models": "Mô hình upstream bị bỏ qua",
|
"Ignored upstream models": "Mô hình upstream bị bỏ qua",
|
||||||
@@ -2108,6 +2108,7 @@
|
|||||||
"Just now": "Vừa nãy",
|
"Just now": "Vừa nãy",
|
||||||
"JustSong": "JustSong",
|
"JustSong": "JustSong",
|
||||||
"K": "K",
|
"K": "K",
|
||||||
|
"Keep affinity when channel is disabled": "Giữ ưu tiên khi kênh bị tắt",
|
||||||
"Keep enabled if you need to proxy requests for different upstream accounts.": "Giữ bật nếu bạn cần proxy yêu cầu cho các tài khoản upstream khác nhau.",
|
"Keep enabled if you need to proxy requests for different upstream accounts.": "Giữ bật nếu bạn cần proxy yêu cầu cho các tài khoản upstream khác nhau.",
|
||||||
"Keep enough balance before production traffic": "Giữ đủ số dư trước khi chạy lưu lượng production",
|
"Keep enough balance before production traffic": "Giữ đủ số dư trước khi chạy lưu lượng production",
|
||||||
"Keep original value": "Giữ giá trị gốc",
|
"Keep original value": "Giữ giá trị gốc",
|
||||||
@@ -2326,6 +2327,8 @@
|
|||||||
"Model": "Mô hình",
|
"Model": "Mô hình",
|
||||||
"Model Access": "Truy cập mô hình",
|
"Model Access": "Truy cập mô hình",
|
||||||
"Model Analytics": "Phân tích mô hình",
|
"Model Analytics": "Phân tích mô hình",
|
||||||
|
"Model Analytics Defaults": "Mặc định phân tích mô hình",
|
||||||
|
"Model Analytics Filters": "Bộ lọc phân tích mô hình",
|
||||||
"model billing support": "hỗ trợ tính phí mô hình",
|
"model billing support": "hỗ trợ tính phí mô hình",
|
||||||
"Model Call Analytics": "Phân tích lượt gọi mô hình",
|
"Model Call Analytics": "Phân tích lượt gọi mô hình",
|
||||||
"Model context usage": "Sử dụng ngữ cảnh mô hình",
|
"Model context usage": "Sử dụng ngữ cảnh mô hình",
|
||||||
@@ -3610,6 +3613,7 @@
|
|||||||
"Set a tag for": "Gắn thẻ cho",
|
"Set a tag for": "Gắn thẻ cho",
|
||||||
"Set API key access restrictions": "Thiết lập hạn chế truy cập cho khóa API",
|
"Set API key access restrictions": "Thiết lập hạn chế truy cập cho khóa API",
|
||||||
"Set API key basic information": "Thiết lập thông tin cơ bản cho khóa API",
|
"Set API key basic information": "Thiết lập thông tin cơ bản cho khóa API",
|
||||||
|
"Set default ranges and charts for model analytics.": "Đặt khoảng thời gian và biểu đồ mặc định cho phân tích mô hình.",
|
||||||
"Set Field": "Đặt trường",
|
"Set Field": "Đặt trường",
|
||||||
"Set filters to customize your dashboard statistics and charts.": "Đặt bộ lọc để tùy chỉnh số liệu thống kê và biểu đồ trên bảng điều khiển của bạn.",
|
"Set filters to customize your dashboard statistics and charts.": "Đặt bộ lọc để tùy chỉnh số liệu thống kê và biểu đồ trên bảng điều khiển của bạn.",
|
||||||
"Set filters to narrow down your log search results.": "Đặt bộ lọc để thu hẹp kết quả tìm kiếm nhật ký của bạn.",
|
"Set filters to narrow down your log search results.": "Đặt bộ lọc để thu hẹp kết quả tìm kiếm nhật ký của bạn.",
|
||||||
@@ -3811,11 +3815,13 @@
|
|||||||
"Sync Endpoints": "Điểm đồng bộ",
|
"Sync Endpoints": "Điểm đồng bộ",
|
||||||
"Sync Fields": "Đồng bộ trường",
|
"Sync Fields": "Đồng bộ trường",
|
||||||
"Sync from the public upstream metadata repository.": "Đồng bộ từ kho lưu trữ siêu dữ liệu upstream công khai.",
|
"Sync from the public upstream metadata repository.": "Đồng bộ từ kho lưu trữ siêu dữ liệu upstream công khai.",
|
||||||
|
"Sync Now": "Đồng bộ ngay",
|
||||||
"Sync this model with official upstream": "Synchronize this model with the official source.",
|
"Sync this model with official upstream": "Synchronize this model with the official source.",
|
||||||
"Sync Upstream": "Đồng bộ nguồn",
|
"Sync Upstream": "Đồng bộ nguồn",
|
||||||
"Sync Upstream Models": "Đồng bộ các mô hình nguồn",
|
"Sync Upstream Models": "Đồng bộ các mô hình nguồn",
|
||||||
"Synchronize models and vendors from an upstream source": "Đồng bộ hóa các mô hình và nhà cung cấp từ một nguồn thượng nguồn",
|
"Synchronize models and vendors from an upstream source": "Đồng bộ hóa các mô hình và nhà cung cấp từ một nguồn thượng nguồn",
|
||||||
"Syncing prices, please wait...": "Đang đồng bộ giá, vui lòng đợi...",
|
"Syncing prices, please wait...": "Đang đồng bộ giá, vui lòng đợi...",
|
||||||
|
"Syncing...": "Đang đồng bộ...",
|
||||||
"System": "Hệ thống",
|
"System": "Hệ thống",
|
||||||
"System Administration": "Quản trị hệ thống",
|
"System Administration": "Quản trị hệ thống",
|
||||||
"System Announcements": "Thông báo hệ thống",
|
"System Announcements": "Thông báo hệ thống",
|
||||||
@@ -3877,7 +3883,7 @@
|
|||||||
"Test": "Kiểm tra",
|
"Test": "Kiểm tra",
|
||||||
"Test {{count}} selected": "Kiểm tra {{count}} mục đã chọn",
|
"Test {{count}} selected": "Kiểm tra {{count}} mục đã chọn",
|
||||||
"Test All Channels": "Kiểm tra tất cả các kênh",
|
"Test All Channels": "Kiểm tra tất cả các kênh",
|
||||||
"Test Channel Connection": "Check channel connection",
|
"Test Channel Connection": "Kiểm tra kết nối kênh",
|
||||||
"Test Connection": "Kiểm tra kết nối",
|
"Test Connection": "Kiểm tra kết nối",
|
||||||
"Test connectivity for:": "Kiểm tra kết nối cho:",
|
"Test connectivity for:": "Kiểm tra kết nối cho:",
|
||||||
"Test failed": "Kiểm tra thất bại",
|
"Test failed": "Kiểm tra thất bại",
|
||||||
@@ -4453,6 +4459,7 @@
|
|||||||
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Khi token dùng nhóm auto, hệ thống thử các nhóm từ trên xuống dưới cho đến khi tìm được nhóm khả dụng.",
|
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Khi token dùng nhóm auto, hệ thống thử các nhóm từ trên xuống dưới cho đến khi tìm được nhóm khả dụng.",
|
||||||
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "Khi thỏa điều kiện, giá cuối nhân với X. Nhiều điều kiện khớp nhân lại với nhau; giá trị < 1 hoạt động như giảm giá.",
|
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "Khi thỏa điều kiện, giá cuối nhân với X. Nhiều điều kiện khớp nhân lại với nhau; giá trị < 1 hoạt động như giảm giá.",
|
||||||
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Khi được bật, nếu các kênh trong nhóm hiện tại thất bại, hệ thống sẽ thử các kênh của nhóm tiếp theo theo thứ tự.",
|
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Khi được bật, nếu các kênh trong nhóm hiện tại thất bại, hệ thống sẽ thử các kênh của nhóm tiếp theo theo thứ tự.",
|
||||||
|
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Khi bật, giữ mục ưu tiên ngay cả khi kênh ưu tiên bị tắt hoặc không còn dùng được cho nhóm/mô hình hiện tại. Để tắt để xóa mục đó và chọn kênh khác.",
|
||||||
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "Khi bật, nội dung yêu cầu lớn sẽ được lưu tạm trên đĩa thay vì bộ nhớ, giảm đáng kể việc sử dụng bộ nhớ. Khuyến nghị dùng SSD.",
|
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "Khi bật, nội dung yêu cầu lớn sẽ được lưu tạm trên đĩa thay vì bộ nhớ, giảm đáng kể việc sử dụng bộ nhớ. Khuyến nghị dùng SSD.",
|
||||||
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "Khi được bật, các callback của Midjourney được chấp nhận (lộ IP máy chủ).",
|
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "Khi được bật, các callback của Midjourney được chấp nhận (lộ IP máy chủ).",
|
||||||
"When enabled, newly created tokens start in the first auto group.": "Khi được bật, các token mới được tạo sẽ bắt đầu trong nhóm tự động đầu tiên.",
|
"When enabled, newly created tokens start in the first auto group.": "Khi được bật, các token mới được tạo sẽ bắt đầu trong nhóm tự động đầu tiên.",
|
||||||
|
|||||||
Vendored
+26
-19
@@ -140,7 +140,7 @@
|
|||||||
"Add {{title}}": "添加{{title}}",
|
"Add {{title}}": "添加{{title}}",
|
||||||
"Add a group identifier to the auto assignment list.": "将分组标识符添加到自动分配列表。",
|
"Add a group identifier to the auto assignment list.": "将分组标识符添加到自动分配列表。",
|
||||||
"Add a new API key by providing necessary info.": "通过提供必要信息添加新的 API 密钥。",
|
"Add a new API key by providing necessary info.": "通过提供必要信息添加新的 API 密钥。",
|
||||||
"Add a new channel by providing the necessary information.": "通过提供必要信息添加新的通道。",
|
"Add a new channel by providing the necessary information.": "通过提供必要信息添加新的渠道。",
|
||||||
"Add a new model to the system by providing the necessary information.": "通过提供必要信息向系统添加新模型。",
|
"Add a new model to the system by providing the necessary information.": "通过提供必要信息向系统添加新模型。",
|
||||||
"Add a new user by providing necessary info.": "通过提供必要信息来添加新用户。",
|
"Add a new user by providing necessary info.": "通过提供必要信息来添加新用户。",
|
||||||
"Add a new vendor to the system": "向系统添加新供应商",
|
"Add a new vendor to the system": "向系统添加新供应商",
|
||||||
@@ -452,7 +452,7 @@
|
|||||||
"Auto-discovers endpoints from the provider": "自动从提供商发现端点",
|
"Auto-discovers endpoints from the provider": "自动从提供商发现端点",
|
||||||
"Auto-fill when one field exists and another is missing": "在一个字段有值、另一个缺失时自动补齐",
|
"Auto-fill when one field exists and another is missing": "在一个字段有值、另一个缺失时自动补齐",
|
||||||
"Auto-retry status codes": "自动重试状态码",
|
"Auto-retry status codes": "自动重试状态码",
|
||||||
"Automatically disable channel on repeated failures": "重复失败时自动禁用通道",
|
"Automatically disable channel on repeated failures": "重复失败时自动禁用渠道",
|
||||||
"Automatically disable channels exceeding this response time": "自动禁用超出此响应时间的渠道",
|
"Automatically disable channels exceeding this response time": "自动禁用超出此响应时间的渠道",
|
||||||
"Automatically disable channels when tests fail": "当测试失败时自动禁用渠道",
|
"Automatically disable channels when tests fail": "当测试失败时自动禁用渠道",
|
||||||
"Automatically probe all channels in the background": "在后台自动探测所有渠道",
|
"Automatically probe all channels in the background": "在后台自动探测所有渠道",
|
||||||
@@ -645,7 +645,6 @@
|
|||||||
"Channel Affinity": "渠道亲和性",
|
"Channel Affinity": "渠道亲和性",
|
||||||
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
|
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
|
||||||
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
|
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
|
||||||
"Keep affinity when channel is disabled": "渠道禁用后保留亲和",
|
|
||||||
"Channel copied successfully": "渠道复制成功",
|
"Channel copied successfully": "渠道复制成功",
|
||||||
"Channel created successfully": "渠道创建成功",
|
"Channel created successfully": "渠道创建成功",
|
||||||
"Channel deleted successfully": "渠道删除成功",
|
"Channel deleted successfully": "渠道删除成功",
|
||||||
@@ -661,7 +660,7 @@
|
|||||||
"Channel type is required": "渠道类型是必填的",
|
"Channel type is required": "渠道类型是必填的",
|
||||||
"Channel updated successfully": "渠道更新成功",
|
"Channel updated successfully": "渠道更新成功",
|
||||||
"Channel-specific settings (JSON format)": "渠道特定设置(JSON 格式)",
|
"Channel-specific settings (JSON format)": "渠道特定设置(JSON 格式)",
|
||||||
"Channel:": "频道:",
|
"Channel:": "渠道:",
|
||||||
"channel(s)? This action cannot be undone.": "渠道?此操作无法撤销。",
|
"channel(s)? This action cannot be undone.": "渠道?此操作无法撤销。",
|
||||||
"Channels": "渠道",
|
"Channels": "渠道",
|
||||||
"Channels deleted successfully": "渠道删除成功",
|
"Channels deleted successfully": "渠道删除成功",
|
||||||
@@ -771,7 +770,7 @@
|
|||||||
"Codex Account & Usage": "Codex 账户和用量",
|
"Codex Account & Usage": "Codex 账户和用量",
|
||||||
"Codex Authorization": "Codex 授权",
|
"Codex Authorization": "Codex 授权",
|
||||||
"Codex channels do not support batch creation": "Codex 渠道不支持批量创建",
|
"Codex channels do not support batch creation": "Codex 渠道不支持批量创建",
|
||||||
"Codex channels use an OAuth JSON credential as the key.": "Codex 频道使用 OAuth JSON 凭据作为密钥。",
|
"Codex channels use an OAuth JSON credential as the key.": "Codex 渠道使用 OAuth JSON 凭据作为密钥。",
|
||||||
"Codex CLI Header Passthrough": "Codex CLI 请求头透传",
|
"Codex CLI Header Passthrough": "Codex CLI 请求头透传",
|
||||||
"Codex credential must be a JSON object with access_token and account_id": "Codex 凭据必须是包含 access_token 和 account_id 的 JSON 对象",
|
"Codex credential must be a JSON object with access_token and account_id": "Codex 凭据必须是包含 access_token 和 account_id 的 JSON 对象",
|
||||||
"Cohere": "Cohere",
|
"Cohere": "Cohere",
|
||||||
@@ -964,6 +963,7 @@
|
|||||||
"Copy to clipboard": "复制到剪贴板",
|
"Copy to clipboard": "复制到剪贴板",
|
||||||
"Copy token": "复制令牌",
|
"Copy token": "复制令牌",
|
||||||
"Copy URL": "复制 URL",
|
"Copy URL": "复制 URL",
|
||||||
|
"Copying...": "正在复制...",
|
||||||
"Copywriting, ad creative, SEO": "文案、广告创意、SEO",
|
"Copywriting, ad creative, SEO": "文案、广告创意、SEO",
|
||||||
"Core concepts": "核心概念",
|
"Core concepts": "核心概念",
|
||||||
"Core Configuration": "核心配置",
|
"Core Configuration": "核心配置",
|
||||||
@@ -1512,8 +1512,8 @@
|
|||||||
"Exact": "精确",
|
"Exact": "精确",
|
||||||
"Exact Match": "完全匹配",
|
"Exact Match": "完全匹配",
|
||||||
"Example": "示例",
|
"Example": "示例",
|
||||||
"Example (all channels):": "示例(全部频道):",
|
"Example (all channels):": "示例(全部渠道):",
|
||||||
"Example (specific channels):": "示例(指定频道):",
|
"Example (specific channels):": "示例(指定渠道):",
|
||||||
"Example:": "示例:",
|
"Example:": "示例:",
|
||||||
"example.com blocked-site.com": "example.com blocked-site.com",
|
"example.com blocked-site.com": "example.com blocked-site.com",
|
||||||
"example.com company.com": "example.com company.com",
|
"example.com company.com": "example.com company.com",
|
||||||
@@ -1663,12 +1663,12 @@
|
|||||||
"Failed to sync prices": "同步价格失败",
|
"Failed to sync prices": "同步价格失败",
|
||||||
"Failed to sync ratios": "同步比率失败",
|
"Failed to sync ratios": "同步比率失败",
|
||||||
"Failed to test all channels": "无法测试所有渠道",
|
"Failed to test all channels": "无法测试所有渠道",
|
||||||
"Failed to test channel": "测试通道失败",
|
"Failed to test channel": "测试渠道失败",
|
||||||
"Failed to update all balances": "无法更新所有余额",
|
"Failed to update all balances": "无法更新所有余额",
|
||||||
"Failed to update API key": "更新 API 密钥失败",
|
"Failed to update API key": "更新 API 密钥失败",
|
||||||
"Failed to update API key status": "更新 API 密钥状态失败",
|
"Failed to update API key status": "更新 API 密钥状态失败",
|
||||||
"Failed to update balance": "无法更新余额",
|
"Failed to update balance": "无法更新余额",
|
||||||
"Failed to update channel": "更新通道失败",
|
"Failed to update channel": "更新渠道失败",
|
||||||
"Failed to update models": "更新模型失败",
|
"Failed to update models": "更新模型失败",
|
||||||
"Failed to update profile": "无法更新个人资料",
|
"Failed to update profile": "无法更新个人资料",
|
||||||
"Failed to update provider": "更新提供商失败",
|
"Failed to update provider": "更新提供商失败",
|
||||||
@@ -1707,8 +1707,8 @@
|
|||||||
"Files to Retain": "保留文件数",
|
"Files to Retain": "保留文件数",
|
||||||
"Fill All Models": "填充所有模型",
|
"Fill All Models": "填充所有模型",
|
||||||
"Fill Codex CLI / Claude CLI Templates": "填充 Codex CLI / Claude CLI 模板",
|
"Fill Codex CLI / Claude CLI Templates": "填充 Codex CLI / Claude CLI 模板",
|
||||||
"Fill example (all channels)": "填充示例(全部频道)",
|
"Fill example (all channels)": "填充示例(全部渠道)",
|
||||||
"Fill example (specific channels)": "填充示例(指定频道)",
|
"Fill example (specific channels)": "填充示例(指定渠道)",
|
||||||
"Fill in both Merchant ID and API Private Key before creating.": "创建前请填写 Merchant ID 和 API 私钥。",
|
"Fill in both Merchant ID and API Private Key before creating.": "创建前请填写 Merchant ID 和 API 私钥。",
|
||||||
"Fill in the credentials above to begin.": "请先填写上方凭证。",
|
"Fill in the credentials above to begin.": "请先填写上方凭证。",
|
||||||
"Fill in the following info to create a new subscription plan": "填写以下信息创建新的订阅套餐",
|
"Fill in the following info to create a new subscription plan": "填写以下信息创建新的订阅套餐",
|
||||||
@@ -1720,7 +1720,7 @@
|
|||||||
"Filled {{count}} related model(s)": "已填充 {{count}} 个关联模型",
|
"Filled {{count}} related model(s)": "已填充 {{count}} 个关联模型",
|
||||||
"Filter": "筛选",
|
"Filter": "筛选",
|
||||||
"Filter by API key...": "按 API 密钥筛选...",
|
"Filter by API key...": "按 API 密钥筛选...",
|
||||||
"Filter by channel ID": "按通道 ID 筛选",
|
"Filter by channel ID": "按渠道 ID 筛选",
|
||||||
"Filter by group": "按分组筛选",
|
"Filter by group": "按分组筛选",
|
||||||
"Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选",
|
"Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选",
|
||||||
"Filter by model name...": "按模型名称筛选...",
|
"Filter by model name...": "按模型名称筛选...",
|
||||||
@@ -1739,6 +1739,7 @@
|
|||||||
"Filter models by provider, group, type, endpoint, and tags.": "按供应商、分组、类型、端点和标签筛选模型。",
|
"Filter models by provider, group, type, endpoint, and tags.": "按供应商、分组、类型、端点和标签筛选模型。",
|
||||||
"Filter models by type, endpoint, vendor, group and tags": "按类型、端点、供应商、分组和标签筛选模型",
|
"Filter models by type, endpoint, vendor, group and tags": "按类型、端点、供应商、分组和标签筛选模型",
|
||||||
"Filter models...": "筛选模型...",
|
"Filter models...": "筛选模型...",
|
||||||
|
"Filter the model analytics view by time range and user.": "按时间范围和用户筛选模型分析视图。",
|
||||||
"Filter...": "筛选...",
|
"Filter...": "筛选...",
|
||||||
"Filters": "筛选器",
|
"Filters": "筛选器",
|
||||||
"Filters active": "筛选已启用",
|
"Filters active": "筛选已启用",
|
||||||
@@ -1821,7 +1822,7 @@
|
|||||||
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "即使禁用适配器,Gemini 也会继续自动检测思维模式。仅当您需要对定价和预算进行更精细的控制时才启用此选项。",
|
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "即使禁用适配器,Gemini 也会继续自动检测思维模式。仅当您需要对定价和预算进行更精细的控制时才启用此选项。",
|
||||||
"General": "常规",
|
"General": "常规",
|
||||||
"General Settings": "通用设置",
|
"General Settings": "通用设置",
|
||||||
"Generate a Codex OAuth credential and paste it into the channel key field.": "生成 Codex OAuth 凭据并粘贴到频道密钥字段。",
|
"Generate a Codex OAuth credential and paste it into the channel key field.": "生成 Codex OAuth 凭据并粘贴到渠道密钥字段。",
|
||||||
"Generate and manage your API access token": "生成和管理您的 API 访问令牌",
|
"Generate and manage your API access token": "生成和管理您的 API 访问令牌",
|
||||||
"Generate credential": "生成凭据",
|
"Generate credential": "生成凭据",
|
||||||
"Generate Lyrics": "生成歌词",
|
"Generate Lyrics": "生成歌词",
|
||||||
@@ -1991,11 +1992,10 @@
|
|||||||
"Icon identifier (e.g. github, gitlab)": "图标标识符(例如 github、gitlab)",
|
"Icon identifier (e.g. github, gitlab)": "图标标识符(例如 github、gitlab)",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "如果上游错误包含以下任何关键字(不区分大小写),渠道将自动禁用。",
|
"If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "如果上游错误包含以下任何关键字(不区分大小写),渠道将自动禁用。",
|
||||||
"If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "授权成功后,生成的 JSON 将插入密钥字段。您仍需保存频道以持久化。",
|
"If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "授权成功后,生成的 JSON 将插入密钥字段。您仍需保存渠道以持久化。",
|
||||||
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "如果连接上游 One API 或 New API 中继项目,除非您知道自己在做什么,否则请使用 OpenAI 类型",
|
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "如果连接上游 One API 或 New API 中继项目,除非您知道自己在做什么,否则请使用 OpenAI 类型",
|
||||||
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "如果启用默认 auto 分组,新建令牌会默认使用 auto,而不是空分组。",
|
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "如果启用默认 auto 分组,新建令牌会默认使用 auto,而不是空分组。",
|
||||||
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。",
|
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。",
|
||||||
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。",
|
|
||||||
"If this keeps happening, please report it on GitHub Issues.": "如果问题持续出现,请到 GitHub Issues 反馈。",
|
"If this keeps happening, please report it on GitHub Issues.": "如果问题持续出现,请到 GitHub Issues 反馈。",
|
||||||
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "如果你在中国大陆向公众提供生成式人工智能服务,你将履行备案、安全评估、内容安全、投诉处理、生成内容标识、日志留存和个人信息保护等法律义务。",
|
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "如果你在中国大陆向公众提供生成式人工智能服务,你将履行备案、安全评估、内容安全、投诉处理、生成内容标识、日志留存和个人信息保护等法律义务。",
|
||||||
"Ignored upstream models": "已忽略上游模型",
|
"Ignored upstream models": "已忽略上游模型",
|
||||||
@@ -2108,6 +2108,7 @@
|
|||||||
"Just now": "刚刚",
|
"Just now": "刚刚",
|
||||||
"JustSong": "JustSong",
|
"JustSong": "JustSong",
|
||||||
"K": "K",
|
"K": "K",
|
||||||
|
"Keep affinity when channel is disabled": "渠道禁用后保留亲和",
|
||||||
"Keep enabled if you need to proxy requests for different upstream accounts.": "如果需要为不同上游账户代理请求,请保持启用。",
|
"Keep enabled if you need to proxy requests for different upstream accounts.": "如果需要为不同上游账户代理请求,请保持启用。",
|
||||||
"Keep enough balance before production traffic": "生产流量前保持充足余额",
|
"Keep enough balance before production traffic": "生产流量前保持充足余额",
|
||||||
"Keep original value": "保留原值",
|
"Keep original value": "保留原值",
|
||||||
@@ -2326,6 +2327,8 @@
|
|||||||
"Model": "模型",
|
"Model": "模型",
|
||||||
"Model Access": "模型访问",
|
"Model Access": "模型访问",
|
||||||
"Model Analytics": "模型数据分析",
|
"Model Analytics": "模型数据分析",
|
||||||
|
"Model Analytics Defaults": "模型分析默认设置",
|
||||||
|
"Model Analytics Filters": "模型分析筛选",
|
||||||
"model billing support": "模型计费支持",
|
"model billing support": "模型计费支持",
|
||||||
"Model Call Analytics": "模型调用分析",
|
"Model Call Analytics": "模型调用分析",
|
||||||
"Model context usage": "模型上下文用量",
|
"Model context usage": "模型上下文用量",
|
||||||
@@ -2369,7 +2372,7 @@
|
|||||||
"Model Square": "模型广场",
|
"Model Square": "模型广场",
|
||||||
"Model Tags": "模型标签",
|
"Model Tags": "模型标签",
|
||||||
"Model to use for testing": "用于测试的模型",
|
"Model to use for testing": "用于测试的模型",
|
||||||
"Model to use when testing channel connectivity": "测试通道连接时使用的模型",
|
"Model to use when testing channel connectivity": "测试渠道连接时使用的模型",
|
||||||
"Model Version *": "模型版本 *",
|
"Model Version *": "模型版本 *",
|
||||||
"model(s) selected out of": "已选模型(共)",
|
"model(s) selected out of": "已选模型(共)",
|
||||||
"model(s)? This action cannot be undone.": "模型?此操作无法撤销。",
|
"model(s)? This action cannot be undone.": "模型?此操作无法撤销。",
|
||||||
@@ -2423,7 +2426,7 @@
|
|||||||
"ms": "毫秒",
|
"ms": "毫秒",
|
||||||
"Multi-key channel: Keys will be": "多密钥渠道:密钥将",
|
"Multi-key channel: Keys will be": "多密钥渠道:密钥将",
|
||||||
"Multi-Key Management": "多密钥管理",
|
"Multi-Key Management": "多密钥管理",
|
||||||
"Multi-Key Mode (multiple keys, one channel)": "多密钥模式(多个密钥,一个通道)",
|
"Multi-Key Mode (multiple keys, one channel)": "多密钥模式(多个密钥,一个渠道)",
|
||||||
"Multi-Key Strategy": "多密钥策略",
|
"Multi-Key Strategy": "多密钥策略",
|
||||||
"Multi-key: Polling rotation": "多密钥:轮询",
|
"Multi-key: Polling rotation": "多密钥:轮询",
|
||||||
"Multi-key: Random rotation": "多密钥:随机",
|
"Multi-key: Random rotation": "多密钥:随机",
|
||||||
@@ -3610,6 +3613,7 @@
|
|||||||
"Set a tag for": "设置标签为",
|
"Set a tag for": "设置标签为",
|
||||||
"Set API key access restrictions": "设置令牌的访问限制",
|
"Set API key access restrictions": "设置令牌的访问限制",
|
||||||
"Set API key basic information": "设置令牌的基本信息",
|
"Set API key basic information": "设置令牌的基本信息",
|
||||||
|
"Set default ranges and charts for model analytics.": "设置模型分析的默认时间范围和图表。",
|
||||||
"Set Field": "设置字段",
|
"Set Field": "设置字段",
|
||||||
"Set filters to customize your dashboard statistics and charts.": "设置筛选器以自定义您的仪表板统计数据和图表。",
|
"Set filters to customize your dashboard statistics and charts.": "设置筛选器以自定义您的仪表板统计数据和图表。",
|
||||||
"Set filters to narrow down your log search results.": "设置筛选器以缩小日志搜索结果范围。",
|
"Set filters to narrow down your log search results.": "设置筛选器以缩小日志搜索结果范围。",
|
||||||
@@ -3811,11 +3815,13 @@
|
|||||||
"Sync Endpoints": "同步端点",
|
"Sync Endpoints": "同步端点",
|
||||||
"Sync Fields": "字段同步",
|
"Sync Fields": "字段同步",
|
||||||
"Sync from the public upstream metadata repository.": "从公共上游元数据仓库同步。",
|
"Sync from the public upstream metadata repository.": "从公共上游元数据仓库同步。",
|
||||||
|
"Sync Now": "立即同步",
|
||||||
"Sync this model with official upstream": "将此模型与官方上游同步",
|
"Sync this model with official upstream": "将此模型与官方上游同步",
|
||||||
"Sync Upstream": "同步上游",
|
"Sync Upstream": "同步上游",
|
||||||
"Sync Upstream Models": "同步上游模型",
|
"Sync Upstream Models": "同步上游模型",
|
||||||
"Synchronize models and vendors from an upstream source": "从上游源同步模型和供应商",
|
"Synchronize models and vendors from an upstream source": "从上游源同步模型和供应商",
|
||||||
"Syncing prices, please wait...": "正在同步价格,请稍候...",
|
"Syncing prices, please wait...": "正在同步价格,请稍候...",
|
||||||
|
"Syncing...": "同步中...",
|
||||||
"System": "系统",
|
"System": "系统",
|
||||||
"System Administration": "系统管理",
|
"System Administration": "系统管理",
|
||||||
"System Announcements": "系统公告",
|
"System Announcements": "系统公告",
|
||||||
@@ -3887,7 +3893,7 @@
|
|||||||
"Test Model": "测试模型",
|
"Test Model": "测试模型",
|
||||||
"Test models and prompts from the browser": "在浏览器中测试模型和提示词",
|
"Test models and prompts from the browser": "在浏览器中测试模型和提示词",
|
||||||
"Test selected models": "测试所选模型",
|
"Test selected models": "测试所选模型",
|
||||||
"Testing all enabled channels started. Please refresh to see results.": "测试所有启用的通道已开始。请刷新以查看结果。",
|
"Testing all enabled channels started. Please refresh to see results.": "测试所有启用的渠道已开始。请刷新以查看结果。",
|
||||||
"Testing...": "测试中...",
|
"Testing...": "测试中...",
|
||||||
"Text": "文本",
|
"Text": "文本",
|
||||||
"Text description of the desired image": "想要生成图像的文字描述",
|
"Text description of the desired image": "想要生成图像的文字描述",
|
||||||
@@ -3937,7 +3943,7 @@
|
|||||||
"This action cannot be undone. This will permanently delete your account and remove all your data from our servers.": "此操作无法撤消。这将永久删除您的账户并从我们的服务器中移除您的所有数据。",
|
"This action cannot be undone. This will permanently delete your account and remove all your data from our servers.": "此操作无法撤消。这将永久删除您的账户并从我们的服务器中移除您的所有数据。",
|
||||||
"This action will permanently remove 2FA protection from your account.": "此操作将永久移除您账户的 2FA 保护。",
|
"This action will permanently remove 2FA protection from your account.": "此操作将永久移除您账户的 2FA 保护。",
|
||||||
"This channel is not an Ollama channel.": "该渠道不是 Ollama 渠道。",
|
"This channel is not an Ollama channel.": "该渠道不是 Ollama 渠道。",
|
||||||
"This channel type does not support fetching models": "此通道类型不支持获取模型",
|
"This channel type does not support fetching models": "此渠道类型不支持获取模型",
|
||||||
"This channel type requires additional configuration": "此渠道类型需要填写额外配置",
|
"This channel type requires additional configuration": "此渠道类型需要填写额外配置",
|
||||||
"This confirmation unlocks payment, redemption code, subscription plan, and invitation reward features. Please read the statements carefully.": "此确认会解锁支付、兑换码、订阅套餐和邀请奖励功能。请仔细阅读相关声明。",
|
"This confirmation unlocks payment, redemption code, subscription plan, and invitation reward features. Please read the statements carefully.": "此确认会解锁支付、兑换码、订阅套餐和邀请奖励功能。请仔细阅读相关声明。",
|
||||||
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "此处仅控制模型请求速率限制。Web/API 路由限流由环境变量配置,仍可能返回 429。",
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "此处仅控制模型请求速率限制。Web/API 路由限流由环境变量配置,仍可能返回 429。",
|
||||||
@@ -4453,6 +4459,7 @@
|
|||||||
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "当令牌使用 auto 分组时,系统会按从上到下的顺序尝试,直到找到可用分组。",
|
"When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "当令牌使用 auto 分组时,系统会按从上到下的顺序尝试,直到找到可用分组。",
|
||||||
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件满足时,最终价格乘以 X;多条命中的倍率会相乘;小于 1 的值为折扣。",
|
"When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件满足时,最终价格乘以 X;多条命中的倍率会相乘;小于 1 的值为折扣。",
|
||||||
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道。",
|
"When enabled, if channels in the current group fail, it will try channels in the next group in order.": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道。",
|
||||||
|
"When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。",
|
||||||
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "启用磁盘缓存后,大请求体将临时存储到磁盘而非内存,可显著降低内存占用。建议在 SSD 环境下使用。",
|
"When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "启用磁盘缓存后,大请求体将临时存储到磁盘而非内存,可显著降低内存占用。建议在 SSD 环境下使用。",
|
||||||
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "启用时,接受 Midjourney 回调 (会泄露服务器 IP)。",
|
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "启用时,接受 Midjourney 回调 (会泄露服务器 IP)。",
|
||||||
"When enabled, newly created tokens start in the first auto group.": "启用后,新创建的令牌将从第一个自动分组开始。",
|
"When enabled, newly created tokens start in the first auto group.": "启用后,新创建的令牌将从第一个自动分组开始。",
|
||||||
|
|||||||
Reference in New Issue
Block a user