Commit b35dfa32 by QuentinHsu

perf(channels): streamline channel test dialog layout

- split model test status from result details so failures and latency no longer crowd one column.
- move batch progress into toast updates to keep the dialog height stable during tests.
- consolidate the channel title and model actions to reduce vertical churn.
parent 917a2cff
......@@ -17,10 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useQueryClient } from '@tanstack/react-query'
import {
type ColumnDef,
type RowSelectionState,
type Table as TanStackTable,
import type {
ColumnDef,
RowSelectionState,
Table as TanStackTable,
} from '@tanstack/react-table'
import {
Check,
......@@ -31,7 +31,14 @@ import {
Settings,
Trash2,
} from 'lucide-react'
import { type ChangeEvent, useCallback, useMemo, useRef, useState } from 'react'
import {
type ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
......@@ -54,7 +61,6 @@ import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import {
Select,
SelectContent,
......@@ -224,7 +230,7 @@ function sleep(ms: number) {
}
function normalizeInlineError(errorText: string) {
return errorText.replace(/\s+/g, ' ').trim()
return errorText.replaceAll(/\s+/g, ' ').trim()
}
function getFirstErrorLine(errorText: string) {
......@@ -281,9 +287,11 @@ function getTestTableColumnClass(columnId: string) {
case 'select':
return 'w-10 min-w-10'
case 'model':
return 'w-auto whitespace-nowrap'
return 'w-auto min-w-48 whitespace-nowrap'
case 'status':
return 'w-70 min-w-70 max-w-70 whitespace-normal'
return 'w-28 min-w-28 whitespace-nowrap'
case 'result':
return 'w-80 min-w-80 max-w-80 whitespace-normal'
case 'actions':
return 'bg-popover w-24 min-w-24 whitespace-nowrap sm:w-28 sm:min-w-28'
default:
......@@ -320,6 +328,9 @@ function ChannelTestDialogContent({
const queryClient = useQueryClient()
const currentChannelId = currentRow.id
const batchStopRequestedRef = useRef(false)
const batchProgressToastIdRef = useRef<ReturnType<
typeof toast.loading
> | null>(null)
const [endpointType, setEndpointType] = useState('auto')
const [isStreamTest, setIsStreamTest] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
......@@ -352,6 +363,39 @@ function ChannelTestDialogContent({
[t]
)
const dismissBatchProgressToast = useCallback(() => {
if (batchProgressToastIdRef.current === null) return
toast.dismiss(batchProgressToastIdRef.current)
batchProgressToastIdRef.current = null
}, [])
useEffect(() => {
if (!batchProgress) {
dismissBatchProgressToast()
return
}
const title = isBatchStopRequested
? t('Stopping batch test...')
: t('Batch testing models...')
const completedText = t('{{completed}}/{{total}} completed', {
completed: batchProgress.completed,
total: batchProgress.total,
})
const resultText = t('{{success}} succeeded, {{failed}} failed', {
success: batchProgress.success,
failed: batchProgress.failed,
})
batchProgressToastIdRef.current = toast.loading(title, {
id: batchProgressToastIdRef.current ?? undefined,
description: `${completedText} · ${resultText}`,
})
}, [batchProgress, dismissBatchProgressToast, isBatchStopRequested, t])
useEffect(() => dismissBatchProgressToast, [dismissBatchProgressToast])
const resetState = useCallback(() => {
batchStopRequestedRef.current = true
setEndpointType('auto')
......@@ -571,9 +615,9 @@ function ChannelTestDialogContent({
const handleBatchTest = useCallback(
async (modelsToTest: string[]) => {
const uniqueModels = Array.from(
new Set(modelsToTest.map((model) => model.trim()).filter(Boolean))
)
const uniqueModels = [
...new Set(modelsToTest.map((model) => model.trim()).filter(Boolean)),
]
if (!uniqueModels.length) return
batchStopRequestedRef.current = false
......@@ -661,6 +705,7 @@ function ChannelTestDialogContent({
const stopped =
batchStopRequestedRef.current && completedCount < uniqueModels.length
dismissBatchProgressToast()
if (stopped) {
toast.info(
t(
......@@ -699,7 +744,13 @@ function ChannelTestDialogContent({
refreshChannelLists(resultPatch)
}
},
[refreshChannelLists, t, testSingleModel, updateTestResult]
[
dismissBatchProgressToast,
refreshChannelLists,
t,
testSingleModel,
updateTestResult,
]
)
const handleSelectSuccessfulModels = useCallback(() => {
......@@ -841,8 +892,19 @@ function ChannelTestDialogContent({
cell: ({ row }) => {
const model = row.original.model
const result = testResults[model]
return <TestStatusCell result={result} />
},
enableSorting: false,
size: 112,
},
{
id: 'result',
header: t('Result'),
cell: ({ row }) => {
const model = row.original.model
const result = testResults[model]
return (
<TestStatusCell
<TestResultCell
result={result}
model={model}
onOpenDetails={setFailureDetails}
......@@ -850,7 +912,7 @@ function ChannelTestDialogContent({
)
},
enableSorting: false,
size: 220,
size: 320,
},
{
id: 'actions',
......@@ -905,22 +967,19 @@ function ChannelTestDialogContent({
<Dialog
open={open}
onOpenChange={handleDialogOpenChange}
title={t('Test Channel Connection')}
description={
<>
{t('Test connectivity for:')}
<strong>{currentRow.name}</strong>
</>
title={
<span className='inline-flex max-w-full min-w-0 items-center gap-1.5'>
<span className='shrink-0'>{t('Test Channel Connection')}:</span>
<span className='min-w-0 truncate'>{currentRow.name}</span>
</span>
}
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-3xl'
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-4xl'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button variant='outline' onClick={handleClose}>
{t('Close')}
</Button>
</>
<Button variant='outline' onClick={handleClose}>
{t('Close')}
</Button>
}
>
<div className='max-h-[78vh] space-y-4 overflow-y-auto py-4 pr-1'>
......@@ -983,12 +1042,60 @@ function ChannelTestDialogContent({
</div>
<div className='space-y-3 max-sm:has-[div[role="toolbar"]]:pb-16'>
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<div>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='min-w-0 space-y-2'>
<p className='text-sm font-medium'>{t('Channel models')}</p>
<p className='text-muted-foreground text-xs'>
{t('Select models to run batch tests.')}
</p>
<div className='flex flex-wrap items-center gap-2'>
{isBatchTesting ? (
<Button
variant='outline'
size='sm'
onClick={handleStopBatchTest}
disabled={isBatchStopRequested}
>
{isBatchStopRequested
? t('Stopping...')
: t('Stop testing')}
</Button>
) : (
<>
<Button
size='sm'
onClick={() => handleBatchTest(filteredModels)}
disabled={isAnyTesting || filteredModels.length === 0}
>
{testAllButtonLabel}
</Button>
{successModels.length > 0 && (
<Button
variant='outline'
size='sm'
onClick={handleSelectSuccessfulModels}
>
<CheckCircle2 data-icon='inline-start' />
{t('Select successful models ({{count}})', {
count: successModels.length,
})}
</Button>
)}
{failedModels.length > 0 && (
<Button
variant='outline'
size='sm'
onClick={() => setIsDeleteFailedDialogOpen(true)}
>
<Trash2 data-icon='inline-start' />
{t('Delete failed models ({{count}})', {
count: failedModels.length,
})}
</Button>
)}
</>
)}
</div>
</div>
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
<Input
......@@ -997,64 +1104,9 @@ function ChannelTestDialogContent({
onChange={handleSearchTermChange}
className='sm:w-64'
/>
{isBatchTesting ? (
<Button
variant='outline'
onClick={handleStopBatchTest}
disabled={isBatchStopRequested}
>
{isBatchStopRequested
? t('Stopping...')
: t('Stop testing')}
</Button>
) : (
<Button
onClick={() => handleBatchTest(filteredModels)}
disabled={isAnyTesting || filteredModels.length === 0}
>
{testAllButtonLabel}
</Button>
)}
</div>
</div>
{batchProgress && (
<BatchProgressSummary
progress={batchProgress}
isStopping={isBatchStopRequested}
/>
)}
{!isAnyTesting &&
(successModels.length > 0 || failedModels.length > 0) && (
<div className='flex flex-wrap items-center gap-2'>
{successModels.length > 0 && (
<Button
variant='outline'
size='sm'
onClick={handleSelectSuccessfulModels}
>
<CheckCircle2 data-icon='inline-start' />
{t('Select successful models ({{count}})', {
count: successModels.length,
})}
</Button>
)}
{failedModels.length > 0 && (
<Button
variant='outline'
size='sm'
onClick={() => setIsDeleteFailedDialogOpen(true)}
>
<Trash2 data-icon='inline-start' />
{t('Delete failed models ({{count}})', {
count: failedModels.length,
})}
</Button>
)}
</div>
)}
<div className='space-y-3'>
<DataTableView
table={table}
......@@ -1076,7 +1128,8 @@ function ChannelTestDialogContent({
<colgroup>
<col className='w-10 min-w-10' />
<col className='w-auto' />
<col className='w-70' />
<col className='w-28' />
<col className='w-80' />
<col className='w-auto' />
</colgroup>
}
......@@ -1123,46 +1176,36 @@ function ChannelTestDialogContent({
)
}
function BatchProgressSummary({
progress,
isStopping,
}: {
progress: BatchProgress
isStopping: boolean
}) {
function TestStatusCell({ result }: { result?: TestResult }) {
const { t } = useTranslation()
const progressValue =
progress.total > 0
? Math.min(100, Math.round((progress.completed / progress.total) * 100))
: 0
return (
<div className='bg-muted/30 flex flex-col gap-2 rounded-md border p-3'>
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between'>
<p className='text-sm font-medium'>
{isStopping
? t('Stopping batch test...')
: t('Batch testing models...')}
</p>
<p className='text-muted-foreground text-xs tabular-nums'>
{t('{{completed}}/{{total}} completed', {
completed: progress.completed,
total: progress.total,
})}
</p>
</div>
<Progress value={progressValue} />
<p className='text-muted-foreground text-xs'>
{t('{{success}} succeeded, {{failed}} failed', {
success: progress.success,
failed: progress.failed,
})}
</p>
</div>
)
if (!result || result.status === 'idle') {
return (
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
)
}
if (result.status === 'testing') {
return (
<StatusBadge variant='info' copyable={false}>
<Loader2 className='size-3.5 shrink-0 animate-spin' />
<span className='min-w-0 truncate leading-normal'>
{t('Testing...')}
</span>
</StatusBadge>
)
}
if (result.status === 'success') {
return (
<StatusBadge label={t('Success')} variant='success' copyable={false} />
)
}
return <StatusBadge label={t('Failed')} variant='danger' copyable={false} />
}
function TestStatusCell({
function TestResultCell({
result,
model,
onOpenDetails,
......@@ -1174,9 +1217,7 @@ function TestStatusCell({
const { t } = useTranslation()
if (!result || result.status === 'idle') {
return (
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
)
return <span className='text-muted-foreground text-sm'>-</span>
}
if (result.status === 'testing') {
......@@ -1189,20 +1230,17 @@ function TestStatusCell({
}
if (result.status === 'success') {
return (
<div className='flex min-w-0 flex-col gap-1 text-xs'>
<StatusBadge label={t('Success')} variant='success' copyable={false} />
{typeof result.responseTime === 'number' && (
<span className='text-muted-foreground truncate'>
{formatResponseTime(result.responseTime, t)}
</span>
)}
</div>
return typeof result.responseTime === 'number' ? (
<span className='text-muted-foreground text-sm'>
{formatResponseTime(result.responseTime, t)}
</span>
) : (
<span className='text-muted-foreground text-sm'>-</span>
)
}
return (
<FailureStatusContent
<FailureResultContent
result={result}
model={model}
onOpenDetails={onOpenDetails}
......@@ -1210,7 +1248,7 @@ function TestStatusCell({
)
}
function FailureStatusContent({
function FailureResultContent({
result,
model,
onOpenDetails,
......@@ -1233,12 +1271,11 @@ function FailureStatusContent({
})
return (
<div className='flex min-w-0 flex-col gap-1.5 text-xs whitespace-normal'>
<StatusBadge label={t('Failed')} variant='danger' copyable={false} />
<p className='text-muted-foreground line-clamp-2 min-w-0 leading-snug wrap-break-word'>
<div className='flex min-w-0 items-center gap-2 text-xs whitespace-normal'>
<p className='text-muted-foreground line-clamp-2 min-w-0 flex-1 leading-snug wrap-break-word'>
{summary}
</p>
<div className='flex min-w-0 flex-wrap items-center gap-1.5'>
<div className='flex shrink-0 flex-wrap items-center justify-end gap-1.5'>
{isModelPriceError && (
<Button
variant='outline'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment