diff --git a/web/default/src/components/dialog.tsx b/web/default/src/components/dialog.tsx new file mode 100644 index 00000000..0e93b17b --- /dev/null +++ b/web/default/src/components/dialog.tsx @@ -0,0 +1,127 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import * as React from 'react' +import { cn } from '@/lib/utils' +import { + Dialog as DialogRoot, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' + +type DialogProps = React.ComponentProps & { + title: React.ReactNode + description?: React.ReactNode + children: React.ReactNode + trigger?: React.ReactElement + footer?: React.ReactNode + contentHeight?: React.CSSProperties['height'] + contentClassName?: string + headerClassName?: string + titleClassName?: string + descriptionClassName?: string + bodyClassName?: string + footerClassName?: string + initialFocus?: boolean + showCloseButton?: boolean +} + +const dialogContentMotionClassName = + 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 duration-100' + +export function Dialog({ + title, + description, + children, + trigger, + footer, + contentHeight = 'auto', + contentClassName, + headerClassName, + titleClassName, + descriptionClassName, + bodyClassName, + footerClassName, + initialFocus, + showCloseButton, + ...dialogProps +}: DialogProps) { + return ( + + {trigger ? : null} + + + {title} + {description ? ( + + {description} + + ) : null} + + +
+
+ {children} +
+
+ + {footer ? ( + + {footer} + + ) : null} +
+
+ ) +} diff --git a/web/default/src/components/layout/components/public-header.tsx b/web/default/src/components/layout/components/public-header.tsx index 5fad6370..7030a7c5 100644 --- a/web/default/src/components/layout/components/public-header.tsx +++ b/web/default/src/components/layout/components/public-header.tsx @@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications' import { useSystemConfig } from '@/hooks/use-system-config' import { useTopNavLinks } from '@/hooks/use-top-nav-links' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Skeleton } from '@/components/ui/skeleton' +import { Dialog } from '@/components/dialog' import { LanguageSwitcher } from '@/components/language-switcher' import { NotificationPopover } from '@/components/notification-popover' import { ProfileDropdown } from '@/components/profile-dropdown' @@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) { closeAuthPrompt() } }} - > - - - {t('Sign in required')} - - {t('Please sign in to view {{module}}.', { - module: authPromptTarget?.title || '', - })} - - -
- {t('Redirecting to sign in in {{seconds}} seconds.', { - seconds: authPromptSecondsLeft, - })} -
- + title={t('Sign in required')} + description={t('Please sign in to view {{module}}.', { + module: authPromptTarget?.title || '', + })} + contentClassName='sm:max-w-md' + contentHeight='auto' + footer={ + <> - -
+ + } + > +
+ {t('Redirecting to sign in in {{seconds}} seconds.', { + seconds: authPromptSecondsLeft, + })} +
) diff --git a/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx b/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx index 88dda89f..02e8c16d 100644 --- a/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx +++ b/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx @@ -20,16 +20,9 @@ import { useMemo } from 'react' import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Dialog } from '@/components/dialog' import type { SecureVerificationState, VerificationMethod, @@ -91,122 +84,118 @@ export function SecureVerificationDialog({ (activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6)) return ( - - -
- - - - {title} - - - {description} - - + + + {title} + + } + description={description} + contentClassName='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 overflow-hidden border-none shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl' + headerClassName='border-b pb-4 text-left' + titleClassName='flex items-center gap-2 text-lg font-semibold' + descriptionClassName='text-left' + contentHeight='auto' + bodyClassName='px-1 py-1' + showCloseButton={!state.loading} + footerClassName='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end' + footer={ + <> + + + + } + > + {availableTabs.length === 0 ? ( +
+
+ +
+

+ {t( + 'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.' + )} +

+
+ ) : ( + onMethodChange(value as VerificationMethod)} + className='gap-4' + > + + {methods.has2FA && ( + {t('Authenticator code')} + )} + {methods.hasPasskey && methods.passkeySupported && ( + {t('Passkey')} + )} + -
- {availableTabs.length === 0 ? ( -
-
- -
-

- {t( - 'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.' - )} -

-
- ) : ( - - onMethodChange(value as VerificationMethod) + +

+ {t( + 'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.' + )} +

+ onCodeChange(event.target.value)} + placeholder={t('Enter verification code')} + disabled={state.loading} + autoFocus={activeMethod === '2fa'} + onKeyDown={(event) => { + if (event.key === 'Enter' && !verifyDisabled) { + event.preventDefault() + handleVerify() } - className='gap-4' - > - - {methods.has2FA && ( - - {t('Authenticator code')} - - )} - {methods.hasPasskey && methods.passkeySupported && ( - {t('Passkey')} - )} - + }} + /> +
- -

+ +

+
+ +
+

+ {t('Use your Passkey')} +

+

{t( - 'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.' + 'We will prompt your device to confirm using biometrics or your hardware key.' )}

- onCodeChange(event.target.value)} - placeholder={t('Enter verification code')} - disabled={state.loading} - autoFocus={activeMethod === '2fa'} - onKeyDown={(event) => { - if (event.key === 'Enter' && !verifyDisabled) { - event.preventDefault() - handleVerify() - } - }} - /> - - - -
-
- -
-

- {t('Use your Passkey')} -

-

- {t( - 'We will prompt your device to confirm using biometrics or your hardware key.' - )} -

-
-
-
- {!methods.passkeySupported && ( -

- {t('This device does not support Passkey verification.')} -

- )} -
- +
+
+
+ {!methods.passkeySupported && ( +

+ {t('This device does not support Passkey verification.')} +

)} -
- - - - - -
-
+ + + )}
) } diff --git a/web/default/src/features/auth/sign-in/components/user-auth-form.tsx b/web/default/src/features/auth/sign-in/components/user-auth-form.tsx index 46591ccc..1830517c 100644 --- a/web/default/src/features/auth/sign-in/components/user-auth-form.tsx +++ b/web/default/src/features/auth/sign-in/components/user-auth-form.tsx @@ -32,14 +32,6 @@ import { import { cn } from '@/lib/utils' import { useStatus } from '@/hooks/use-status' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Form, FormControl, @@ -50,6 +42,7 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Dialog } from '@/components/dialog' import { PasswordInput } from '@/components/password-input' import { Turnstile } from '@/components/turnstile' import { login, wechatLoginByCode } from '@/features/auth/api' @@ -414,43 +407,16 @@ export function UserAuthForm({ - - - {t('WeChat sign in')} - - {t( - 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' - )} - - - - {wechatQrCodeUrl ? ( -
- {t('WeChat -
- ) : ( -

- {t('QR code is not configured. Please contact support.')} -

- )} - -
- - setWeChatCode(event.target.value)} - autoComplete='one-time-code' - /> -
- - + title={t('WeChat sign in')} + description={t( + 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' + )} + contentClassName='max-w-sm' + headerClassName='text-left' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <>
)} diff --git a/web/default/src/features/auth/sign-up/components/sign-up-form.tsx b/web/default/src/features/auth/sign-up/components/sign-up-form.tsx index 9466c15e..94e262a1 100644 --- a/web/default/src/features/auth/sign-up/components/sign-up-form.tsx +++ b/web/default/src/features/auth/sign-up/components/sign-up-form.tsx @@ -26,14 +26,6 @@ import { toast } from 'sonner' import { cn } from '@/lib/utils' import { useStatus } from '@/hooks/use-status' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Form, FormControl, @@ -44,6 +36,7 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Dialog } from '@/components/dialog' import { PasswordInput } from '@/components/password-input' import { Turnstile } from '@/components/turnstile' import { register, wechatLoginByCode } from '@/features/auth/api' @@ -387,43 +380,16 @@ export function SignUpForm({ - - - {t('WeChat sign in')} - - {t( - 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' - )} - - - - {wechatQrCodeUrl ? ( -
- {t('WeChat -
- ) : ( -

- {t('QR code is not configured. Please contact support.')} -

- )} - -
- - setWeChatCode(event.target.value)} - autoComplete='one-time-code' - /> -
- - + title={t('WeChat sign in')} + description={t( + 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' + )} + contentClassName='max-w-sm' + headerClassName='text-left' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <>
)} diff --git a/web/default/src/features/channels/components/data-table-bulk-actions.tsx b/web/default/src/features/channels/components/data-table-bulk-actions.tsx index c5d5bd7f..31a30b28 100644 --- a/web/default/src/features/channels/components/data-table-bulk-actions.tsx +++ b/web/default/src/features/channels/components/data-table-bulk-actions.tsx @@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table' import { Power, PowerOff, Tag, Trash2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -38,6 +30,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' +import { Dialog } from '@/components/dialog' import { handleBatchDelete, handleBatchDisable, @@ -188,29 +181,21 @@ export function DataTableBulkActions({ {/* Set Tag Dialog */} - - - - {t('Set Tag')} - - {t('Set a tag for')} {selectedIds.length}{' '} - {t('selected channel(s). Leave empty to remove tag.')} - - - -
-
- - setTagValue(e.target.value)} - /> -
-
- - + + {t('Set a tag for')} + {selectedIds.length}{' '} + {t('selected channel(s). Leave empty to remove tag.')} + + } + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+
+ + setTagValue(e.target.value)} + /> +
+
{/* Delete Confirmation Dialog */} - - - - {t('Delete Channels?')} - - {t('Are you sure you want to delete')} {selectedIds.length}{' '} - {t('channel(s)? This action cannot be undone.')} - - - - + + {t('Are you sure you want to delete')} + {selectedIds.length}{' '} + {t('channel(s)? This action cannot be undone.')} + + } + contentHeight='auto' + footer={ + <> - - + + } + > + {' '} ) diff --git a/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx b/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx index f9e39d84..32b91f09 100644 --- a/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/balance-query-dialog.tsx @@ -24,14 +24,7 @@ import { toast } from 'sonner' import { formatCurrencyFromUSD } from '@/lib/currency' import { formatTimestampToDate } from '@/lib/format' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' +import { Dialog } from '@/components/dialog' import { getCodexUsage, updateChannelBalance } from '../../api' import { channelsQueryKeys } from '../../lib' import { useChannels } from '../channels-provider' @@ -161,53 +154,55 @@ export function BalanceQueryDialog({ } return ( - - - - {t('Query Balance')} - - {t('Update balance for:')} {currentRow.name} - - - -
- {/* Current Balance Display */} -
-
- - {t('Current Balance')} -
-
- {balance !== null - ? formatBalance(balance) - : formatBalance(currentRow.balance)} -
-
- {t('Last updated:')}{' '} - {formatDate( - balanceUpdatedTime ?? currentRow.balance_updated_time - )} -
-
- - {/* Balance Update Button */} - -
- - + + {t('Update balance for:')} + {currentRow.name} + + } + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+ {/* Current Balance Display */} +
+
+ + {t('Current Balance')} +
+
+ {balance !== null + ? formatBalance(balance) + : formatBalance(currentRow.balance)} +
+
+ {t('Last updated:')}{' '} + {formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)} +
+
+ + {/* Balance Update Button */} + +
) } diff --git a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx index d5111284..ae18648f 100644 --- a/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx @@ -33,14 +33,6 @@ import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useIsMobile } from '@/hooks/use-mobile' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -75,6 +67,7 @@ import { } from '@/components/ui/tooltip' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTablePagination } from '@/components/data-table/pagination' +import { Dialog } from '@/components/dialog' import { sideDrawerContentClassName, sideDrawerFooterClassName, @@ -529,179 +522,184 @@ export function ChannelTestDialog({ return ( <> - - - - {t('Test Channel Connection')} - - {t('Test connectivity for:')} {currentRow.name} - - - -
-
-
- - -

- {t( - 'Override the endpoint used for testing. Leave empty to auto detect.' - )} -

-
-
- -
- - - {isStreamTest ? t('Enabled') : t('Disabled')} - -
-

- {t('Enable streaming mode for the test request.')} -

-
-
- -
-
-
-

{t('Channel models')}

-

- {t('Select models to run batch tests.')} -

-
- setSearchTerm(e.target.value)} - className='sm:w-64' - /> -
- -
-
-
- - - - - - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {models.length - ? 'No models matched your search.' - : 'This channel has no configured models.'} - - - )} - -
-
-
- - -
- - -
-
- - + + {t('Test connectivity for:')} + {currentRow.name} + + } + contentClassName='max-h-[90vh] overflow-hidden sm:max-w-3xl' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+
+
+ + +

+ {t( + 'Override the endpoint used for testing. Leave empty to auto detect.' + )} +

+
+
+ +
+ + + {isStreamTest ? t('Enabled') : t('Disabled')} + +
+

+ {t('Enable streaming mode for the test request.')} +

+
+
+ +
+
+
+

{t('Channel models')}

+

+ {t('Select models to run batch tests.')} +

+
+ setSearchTerm(e.target.value)} + className='sm:w-64' + /> +
+ +
+
+
+ + + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {models.length + ? 'No models matched your search.' + : 'This channel has no configured models.'} + + + )} + +
+
+
+ + +
+ + +
+
- - - {t('Codex Authorization')} - - {t( - 'Generate a Codex OAuth credential and paste it into the channel key field.' - )} - - - -
- - - {t( - '1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".' - )} - - - -
- - - -
- -
-
{t('Callback URL')}
- - setState((prev) => ({ ...prev, callbackUrl: e.target.value })) - } - placeholder={t( - 'Paste the full callback URL (includes code & state)' - )} - autoComplete='off' - spellCheck={false} - /> -
- {t( - 'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.' - )} -
-
-
- - + + + + + +
+
{t('Callback URL')}
+ + setState((prev) => ({ ...prev, callbackUrl: e.target.value })) + } + placeholder={t( + 'Paste the full callback URL (includes code & state)' + )} + autoComplete='off' + spellCheck={false} + /> +
+ {t( + 'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.' + )} +
+
+
) } diff --git a/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx b/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx index 442db3d7..17440b55 100644 --- a/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx @@ -31,16 +31,9 @@ import { useTranslation } from 'react-i18next' import dayjs from '@/lib/dayjs' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Progress } from '@/components/ui/progress' import { ScrollArea } from '@/components/ui/scroll-area' +import { Dialog } from '@/components/dialog' import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' type CodexRateLimitWindow = { @@ -414,177 +407,23 @@ export function CodexUsageDialog({ }, [response]) return ( - - - - - {t('Codex Account & Usage')} - - - {t('Channel:')} {channelName || '-'}{' '} - {channelId ? `(#${channelId})` : ''} - - - -
- {errorMessage && ( -
- {errorMessage} -
- )} - - {/* Account summary */} -
-
-
- - {statusBadge} - {typeof response?.upstream_status === 'number' && ( - - )} -
- {onRefresh && ( - - )} -
- - {/* Account identity info */} -
- } - label='User ID' - value={payload?.user_id} - mono - /> - } - label={t('Email')} - value={payload?.email} - /> - } - label='Account ID' - value={payload?.account_id} - mono - /> -
-
- - {/* Rate limit windows */} -
-
-
- {t('Rate Limit Windows')} -
-

- {t( - 'Tracks current account base limits and additional metered usage on Codex upstream.' - )} -

- -
- - {additionalRateLimits.length > 0 && ( -
-
-
- {t('Additional Limits')} -
-

- {t( - 'Per-feature metered windows split by model or capability.' - )} -

-
-
- {additionalRateLimits.map((item, index) => { - const limitName = - item.limit_name || - item.metered_feature || - `${t('Additional Limit')} ${index + 1}` - return ( -
0 ? 'border-t pt-4' : ''} - > - -
- ) - })} -
-
- )} -
- - {/* Raw JSON collapsible */} -
- - {showRawJson && ( - <> -
- -
- -
-                    {rawJsonText || '-'}
-                  
-
- - )} -
-
- - + + {t('Channel:')} + {channelName || '-'}{' '} + {channelId ? `(#${channelId})` : ''} + + } + contentClassName='sm:max-w-3xl' + titleClassName='flex items-center gap-2' + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+ {errorMessage && ( +
+ {errorMessage} +
+ )} + + {/* Account summary */} +
+
+
+ + {statusBadge} + {typeof response?.upstream_status === 'number' && ( + + )} +
+ {onRefresh && ( + + )} +
+ + {/* Account identity info */} +
+ } + label='User ID' + value={payload?.user_id} + mono + /> + } + label={t('Email')} + value={payload?.email} + /> + } + label='Account ID' + value={payload?.account_id} + mono + /> +
+
+ + {/* Rate limit windows */} +
+
+
+ {t('Rate Limit Windows')} +
+

+ {t( + 'Tracks current account base limits and additional metered usage on Codex upstream.' + )} +

+ +
+ + {additionalRateLimits.length > 0 && ( +
+
+
+ {t('Additional Limits')} +
+

+ {t( + 'Per-feature metered windows split by model or capability.' + )} +

+
+
+ {additionalRateLimits.map((item, index) => { + const limitName = + item.limit_name || + item.metered_feature || + `${t('Additional Limit')} ${index + 1}` + return ( +
0 ? 'border-t pt-4' : ''} + > + +
+ ) + })} +
+
+ )} +
+ + {/* Raw JSON collapsible */} +
+ + {showRawJson && ( + <> +
+ +
+ +
+                  {rawJsonText || '-'}
+                
+
+ + )} +
+
) } diff --git a/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx b/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx index 36a1a8e3..9edaaaeb 100644 --- a/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/copy-channel-dialog.tsx @@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Dialog } from '@/components/dialog' import { handleCopyChannel } from '../../lib' import { useChannels } from '../channels-provider' @@ -74,45 +67,20 @@ export function CopyChannelDialog({ } return ( - - - - {t('Copy Channel')} - - {t('Create a copy of:')} {currentRow.name} - - - -
-
- - setSuffix(e.target.value)} - disabled={isCopying} - /> -

- {t('New name will be:')} {currentRow.name} - {suffix} -

-
- -
- setResetBalance(!!checked)} - disabled={isCopying} - /> - -
-
- - + + {t('Create a copy of:')} + {currentRow.name} + + } + contentHeight='auto' + bodyClassName='space-y-4' + footer={ + <> - -
+ + } + > +
+
+ + setSuffix(e.target.value)} + disabled={isCopying} + /> +

+ {t('New name will be:')} {currentRow.name} + {suffix} +

+
+ +
+ setResetBalance(!!checked)} + disabled={isCopying} + /> + +
+
) } diff --git a/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx b/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx index 30fa660f..9967fa98 100644 --- a/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx @@ -22,14 +22,6 @@ import { Loader2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' @@ -43,6 +35,7 @@ import { } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Textarea } from '@/components/ui/textarea' +import { Dialog } from '@/components/dialog' import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { @@ -222,216 +215,23 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) { if (!currentTag) return null return ( - - - - - {t('Edit Tag:')} {currentTag} - - - {t( - 'Batch edit all channels with this tag. Leave fields empty to keep current values.' - )} - - - - -
- {/* Tag Name */} -
- - setNewTag(e.target.value)} - placeholder={t('Enter new tag name or leave empty')} - /> -
- - - - {/* Models */} -
- - - {isLoadingTagModels ? ( -
- - - {t('Loading current models...')} - -
- ) : ( - <> -
- {selectedModels.length > 0 ? ( - selectedModels.map((model) => ( - handleRemoveModel(model)} - > - {model} × - - )) - ) : ( - - {t('No models selected')} - - )} -
- -
- - items={[ - ...availableModels.map((model) => ({ - value: model, - label: model, - })), - ]} - onValueChange={(value) => { - if (value === null) return - if (!selectedModels.includes(value)) { - setSelectedModels([...selectedModels, value]) - } - }} - > - - - - - - - {availableModels.map((model) => ( - - {model} - - ))} - - - - -
- -
- setCustomModel(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - handleAddCustomModel() - } - }} - /> - -
- - )} -
- - - - {/* Model Mapping */} -
- -