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:
QuentinHsu
2026-06-06 21:49:33 +08:00
parent 7a5348caa3
commit 2eaa943d9f
80 changed files with 8137 additions and 8517 deletions
@@ -21,13 +21,8 @@ import { ExternalLink, Copy, Music } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge'
export interface AudioClip {
@@ -152,32 +147,33 @@ export function AudioPreviewDialog(props: AudioPreviewDialogProps) {
const clips = Array.isArray(props.clips) ? props.clips : []
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle className='flex items-center gap-2'>
<Music className='h-5 w-5' />
{t('Audio Preview')}
</DialogTitle>
</DialogHeader>
{clips.length === 0 ? (
<p className='text-muted-foreground py-4 text-center text-sm'>
{t('None')}
</p>
) : (
<ScrollArea className='max-h-[60vh]'>
<div className='space-y-3 pr-2'>
{clips.map((clip, idx) => (
<AudioClipCard
key={clip.clip_id || clip.id || idx}
clip={clip}
/>
))}
</div>
</ScrollArea>
)}
</DialogContent>
<Dialog
open={props.open}
onOpenChange={props.onOpenChange}
title={
<>
<Music className='h-5 w-5' />
{t('Audio Preview')}
</>
}
contentClassName='sm:max-w-lg'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
>
{clips.length === 0 ? (
<p className='text-muted-foreground py-4 text-center text-sm'>
{t('None')}
</p>
) : (
<ScrollArea className='max-h-[60vh]'>
<div className='space-y-3 pr-2'>
{clips.map((clip, idx) => (
<AudioClipCard key={clip.clip_id || clip.id || idx} clip={clip} />
))}
</div>
</ScrollArea>
)}
</Dialog>
)
}
File diff suppressed because it is too large Load Diff
@@ -20,15 +20,9 @@ import { Copy, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
interface FailReasonDialogProps {
failReason: string
@@ -45,43 +39,42 @@ export function FailReasonDialog({
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Fail Reason Details')}</DialogTitle>
<DialogDescription>
{t('View the complete error message and details')}
</DialogDescription>
</DialogHeader>
<ScrollArea className='max-h-[500px] pr-4'>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label className='text-sm font-semibold'>
{t('Error Message')}
</Label>
<div className='bg-muted/50 relative rounded-md border border-red-200 p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
onClick={() => copyToClipboard(failReason)}
title={t('Copy to clipboard')}
>
{copiedText === failReason ? (
<Check className='size-4 text-green-600' />
) : (
<Copy className='size-4' />
)}
</Button>
<p className='overflow-wrap-anywhere pr-10 text-sm leading-relaxed break-all whitespace-pre-wrap text-red-600'>
{failReason || '-'}
</p>
</div>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Fail Reason Details')}
description={t('View the complete error message and details')}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-[500px] pr-4'>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label className='text-sm font-semibold'>
{t('Error Message')}
</Label>
<div className='bg-muted/50 relative rounded-md border border-red-200 p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
onClick={() => copyToClipboard(failReason)}
title={t('Copy to clipboard')}
>
{copiedText === failReason ? (
<Check className='size-4 text-green-600' />
) : (
<Copy className='size-4' />
)}
</Button>
<p className='overflow-wrap-anywhere pr-10 text-sm leading-relaxed break-all whitespace-pre-wrap text-red-600'>
{failReason || '-'}
</p>
</div>
</div>
</ScrollArea>
</DialogContent>
</div>
</ScrollArea>
</Dialog>
)
}
@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { Dialog } from '@/components/dialog'
interface ImageDialogProps {
imageUrl: string
@@ -65,56 +59,55 @@ export function ImageDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-3xl'>
<DialogHeader>
<DialogTitle>{t('Image Preview')}</DialogTitle>
<DialogDescription>
{taskId
? `${t('Task ID:')} ${taskId}`
: t('View the generated image')}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={t('Image Preview')}
description={
taskId ? `${t('Task ID:')} ${taskId}` : t('View the generated image')
}
contentClassName='sm:max-w-3xl'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-[600px]'>
<div className='py-4'>
<div className='bg-muted/50 relative flex min-h-[300px] items-center justify-center rounded-lg border'>
{/* Skeleton - show when loading or error */}
{(isLoading || hasError) && (
<Skeleton className='absolute inset-0 h-full w-full rounded-lg' />
)}
<ScrollArea className='max-h-[600px]'>
<div className='py-4'>
<div className='bg-muted/50 relative flex min-h-[300px] items-center justify-center rounded-lg border'>
{/* Skeleton - show when loading or error */}
{(isLoading || hasError) && (
<Skeleton className='absolute inset-0 h-full w-full rounded-lg' />
)}
{/* Actual Image */}
<img
src={imageUrl}
alt={t('Generated image')}
className={`max-h-[550px] w-full rounded-lg object-contain ${
isLoading || hasError ? 'opacity-0' : 'opacity-100'
}`}
onLoad={handleImageLoad}
onError={handleImageError}
loading='lazy'
/>
{/* Actual Image */}
<img
src={imageUrl}
alt={t('Generated image')}
className={`max-h-[550px] w-full rounded-lg object-contain ${
isLoading || hasError ? 'opacity-0' : 'opacity-100'
}`}
onLoad={handleImageLoad}
onError={handleImageError}
loading='lazy'
/>
{/* Error text overlay (shown on skeleton) */}
{hasError && (
<div className='absolute inset-0 flex items-center justify-center'>
<p className='text-muted-foreground text-sm'>
{t('Failed to load image')}
</p>
</div>
)}
</div>
{/* Image URL */}
<div className='bg-muted mt-4 rounded-md p-3'>
<p className='text-muted-foreground font-mono text-xs break-all'>
{imageUrl}
</p>
</div>
{/* Error text overlay (shown on skeleton) */}
{hasError && (
<div className='absolute inset-0 flex items-center justify-center'>
<p className='text-muted-foreground text-sm'>
{t('Failed to load image')}
</p>
</div>
)}
</div>
</ScrollArea>
</DialogContent>
{/* Image URL */}
<div className='bg-muted mt-4 rounded-md p-3'>
<p className='text-muted-foreground font-mono text-xs break-all'>
{imageUrl}
</p>
</div>
</div>
</ScrollArea>
</Dialog>
)
}
@@ -20,15 +20,9 @@ import { Copy, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
interface PromptDialogProps {
prompt: string
@@ -47,69 +41,68 @@ export function PromptDialog({
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Prompt Details')}</DialogTitle>
<DialogDescription>
{t('View the complete prompt and its English translation')}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Prompt Details')}
description={t('View the complete prompt and its English translation')}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-[500px] pr-4'>
<div className='space-y-4 py-4'>
{/* Original Prompt */}
<div className='space-y-2'>
<Label className='text-sm font-semibold'>{t('Prompt')}</Label>
<div className='bg-muted/50 relative rounded-md border p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
onClick={() => copyToClipboard(prompt)}
title={t('Copy to clipboard')}
>
{copiedText === prompt ? (
<Check className='size-4 text-green-600' />
) : (
<Copy className='size-4' />
)}
</Button>
<p className='pr-10 text-sm leading-relaxed break-words whitespace-pre-wrap'>
{prompt || '-'}
</p>
</div>
</div>
<ScrollArea className='max-h-[500px] pr-4'>
<div className='space-y-4 py-4'>
{/* Original Prompt */}
{/* English Prompt */}
{promptEn && (
<div className='space-y-2'>
<Label className='text-sm font-semibold'>{t('Prompt')}</Label>
<Label className='text-sm font-semibold'>
{t('Prompt (EN)')}
</Label>
<div className='bg-muted/50 relative rounded-md border p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
onClick={() => copyToClipboard(prompt)}
onClick={() => copyToClipboard(promptEn)}
title={t('Copy to clipboard')}
>
{copiedText === prompt ? (
{copiedText === promptEn ? (
<Check className='size-4 text-green-600' />
) : (
<Copy className='size-4' />
)}
</Button>
<p className='pr-10 text-sm leading-relaxed break-words whitespace-pre-wrap'>
{prompt || '-'}
{promptEn}
</p>
</div>
</div>
{/* English Prompt */}
{promptEn && (
<div className='space-y-2'>
<Label className='text-sm font-semibold'>
{t('Prompt (EN)')}
</Label>
<div className='bg-muted/50 relative rounded-md border p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
onClick={() => copyToClipboard(promptEn)}
title={t('Copy to clipboard')}
>
{copiedText === promptEn ? (
<Check className='size-4 text-green-600' />
) : (
<Copy className='size-4' />
)}
</Button>
<p className='pr-10 text-sm leading-relaxed break-words whitespace-pre-wrap'>
{promptEn}
</p>
</div>
</div>
)}
</div>
</ScrollArea>
</DialogContent>
)}
</div>
</ScrollArea>
</Dialog>
)
}
@@ -21,14 +21,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { formatQuota, formatCompactNumber } from '@/lib/format'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { getUserInfo } from '../../api'
import type { UserInfo } from '../../types'
@@ -88,104 +82,103 @@ export function UserInfoDialog({
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('User Information')}</DialogTitle>
<DialogDescription>
{t(
'View detailed information about this user including balance, usage statistics, and invitation details.'
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('User Information')}
description={t(
'View detailed information about this user including balance, usage statistics, and invitation details.'
)}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
>
{isLoading ? (
<div className='flex items-center justify-center py-8'>
<Loader2 className='text-muted-foreground size-6 animate-spin' />
</div>
) : userInfo ? (
<div className='space-y-4 py-4'>
{/* Basic Info */}
<div className='grid grid-cols-2 gap-4'>
<InfoItem label={t('Username')} value={userInfo.username} />
{userInfo.display_name && (
<InfoItem
label={t('Display Name')}
value={userInfo.display_name}
/>
)}
</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className='flex items-center justify-center py-8'>
<Loader2 className='text-muted-foreground size-6 animate-spin' />
</div>
) : userInfo ? (
<div className='space-y-4 py-4'>
{/* Basic Info */}
<div className='grid grid-cols-2 gap-4'>
<InfoItem label={t('Username')} value={userInfo.username} />
{userInfo.display_name && (
<InfoItem
label={t('Display Name')}
value={userInfo.display_name}
/>
)}
</div>
{/* Balance Info */}
<div className='grid grid-cols-2 gap-4'>
<InfoItem
label={t('Balance')}
value={formatQuota(userInfo.quota)}
/>
<InfoItem
label={t('Used Quota')}
value={formatQuota(userInfo.used_quota)}
/>
</div>
{/* Balance Info */}
<div className='grid grid-cols-2 gap-4'>
<InfoItem
label={t('Balance')}
value={formatQuota(userInfo.quota)}
/>
<InfoItem
label={t('Used Quota')}
value={formatQuota(userInfo.used_quota)}
/>
</div>
{/* Statistics */}
<div className='grid grid-cols-2 gap-4'>
<InfoItem
label={t('Request Count')}
value={formatCompactNumber(userInfo.request_count)}
/>
{userInfo.group && (
<InfoItem label={t('User Group')} value={userInfo.group} />
)}
</div>
{/* Statistics */}
<div className='grid grid-cols-2 gap-4'>
<InfoItem
label={t('Request Count')}
value={formatCompactNumber(userInfo.request_count)}
/>
{userInfo.group && (
<InfoItem label={t('User Group')} value={userInfo.group} />
)}
</div>
{/* Invitation Info */}
{(userInfo.aff_code ||
userInfo.aff_count !== undefined ||
(userInfo.aff_quota !== undefined && userInfo.aff_quota > 0)) && (
<>
<div className='grid grid-cols-2 gap-4'>
{userInfo.aff_code && (
<InfoItem
label={t('Invitation Code')}
value={userInfo.aff_code}
/>
)}
{userInfo.aff_count !== undefined && (
<InfoItem
label={t('Invited Users')}
value={formatCompactNumber(userInfo.aff_count)}
/>
)}
</div>
{userInfo.aff_quota !== undefined && userInfo.aff_quota > 0 && (
{/* Invitation Info */}
{(userInfo.aff_code ||
userInfo.aff_count !== undefined ||
(userInfo.aff_quota !== undefined && userInfo.aff_quota > 0)) && (
<>
<div className='grid grid-cols-2 gap-4'>
{userInfo.aff_code && (
<InfoItem
label={t('Invitation Quota')}
value={formatQuota(userInfo.aff_quota)}
label={t('Invitation Code')}
value={userInfo.aff_code}
/>
)}
{userInfo.aff_count !== undefined && (
<InfoItem
label={t('Invited Users')}
value={formatCompactNumber(userInfo.aff_count)}
/>
)}
</>
)}
{/* Remark */}
{userInfo.remark && (
<div className='space-y-1.5'>
<Label className='text-muted-foreground text-xs'>
{t('Remark')}
</Label>
<div className='text-sm leading-relaxed font-semibold break-words'>
{userInfo.remark}
</div>
</div>
)}
</div>
) : (
<div className='text-muted-foreground py-8 text-center text-sm'>
{t('No user information available')}
</div>
)}
</DialogContent>
{userInfo.aff_quota !== undefined && userInfo.aff_quota > 0 && (
<InfoItem
label={t('Invitation Quota')}
value={formatQuota(userInfo.aff_quota)}
/>
)}
</>
)}
{/* Remark */}
{userInfo.remark && (
<div className='space-y-1.5'>
<Label className='text-muted-foreground text-xs'>
{t('Remark')}
</Label>
<div className='text-sm leading-relaxed font-semibold break-words'>
{userInfo.remark}
</div>
</div>
)}
</div>
) : (
<div className='text-muted-foreground py-8 text-center text-sm'>
{t('No user information available')}
</div>
)}
</Dialog>
)
}