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/>. ...@@ -17,10 +17,10 @@ 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 { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { import type {
type ColumnDef, ColumnDef,
type RowSelectionState, RowSelectionState,
type Table as TanStackTable, Table as TanStackTable,
} from '@tanstack/react-table' } from '@tanstack/react-table'
import { import {
Check, Check,
...@@ -31,7 +31,14 @@ import { ...@@ -31,7 +31,14 @@ import {
Settings, Settings,
Trash2, Trash2,
} from 'lucide-react' } 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 { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
...@@ -54,7 +61,6 @@ import { Button } from '@/components/ui/button' ...@@ -54,7 +61,6 @@ import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
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 { import {
Select, Select,
SelectContent, SelectContent,
...@@ -224,7 +230,7 @@ function sleep(ms: number) { ...@@ -224,7 +230,7 @@ function sleep(ms: number) {
} }
function normalizeInlineError(errorText: string) { function normalizeInlineError(errorText: string) {
return errorText.replace(/\s+/g, ' ').trim() return errorText.replaceAll(/\s+/g, ' ').trim()
} }
function getFirstErrorLine(errorText: string) { function getFirstErrorLine(errorText: string) {
...@@ -281,9 +287,11 @@ function getTestTableColumnClass(columnId: string) { ...@@ -281,9 +287,11 @@ function getTestTableColumnClass(columnId: string) {
case 'select': case 'select':
return 'w-10 min-w-10' return 'w-10 min-w-10'
case 'model': case 'model':
return 'w-auto whitespace-nowrap' return 'w-auto min-w-48 whitespace-nowrap'
case 'status': 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': case 'actions':
return 'bg-popover w-24 min-w-24 whitespace-nowrap sm:w-28 sm:min-w-28' return 'bg-popover w-24 min-w-24 whitespace-nowrap sm:w-28 sm:min-w-28'
default: default:
...@@ -320,6 +328,9 @@ function ChannelTestDialogContent({ ...@@ -320,6 +328,9 @@ function ChannelTestDialogContent({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const currentChannelId = currentRow.id const currentChannelId = currentRow.id
const batchStopRequestedRef = useRef(false) const batchStopRequestedRef = useRef(false)
const batchProgressToastIdRef = useRef<ReturnType<
typeof toast.loading
> | null>(null)
const [endpointType, setEndpointType] = useState('auto') const [endpointType, setEndpointType] = useState('auto')
const [isStreamTest, setIsStreamTest] = useState(false) const [isStreamTest, setIsStreamTest] = useState(false)
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
...@@ -352,6 +363,39 @@ function ChannelTestDialogContent({ ...@@ -352,6 +363,39 @@ function ChannelTestDialogContent({
[t] [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(() => { const resetState = useCallback(() => {
batchStopRequestedRef.current = true batchStopRequestedRef.current = true
setEndpointType('auto') setEndpointType('auto')
...@@ -571,9 +615,9 @@ function ChannelTestDialogContent({ ...@@ -571,9 +615,9 @@ function ChannelTestDialogContent({
const handleBatchTest = useCallback( const handleBatchTest = useCallback(
async (modelsToTest: string[]) => { async (modelsToTest: string[]) => {
const uniqueModels = Array.from( const uniqueModels = [
new Set(modelsToTest.map((model) => model.trim()).filter(Boolean)) ...new Set(modelsToTest.map((model) => model.trim()).filter(Boolean)),
) ]
if (!uniqueModels.length) return if (!uniqueModels.length) return
batchStopRequestedRef.current = false batchStopRequestedRef.current = false
...@@ -661,6 +705,7 @@ function ChannelTestDialogContent({ ...@@ -661,6 +705,7 @@ function ChannelTestDialogContent({
const stopped = const stopped =
batchStopRequestedRef.current && completedCount < uniqueModels.length batchStopRequestedRef.current && completedCount < uniqueModels.length
dismissBatchProgressToast()
if (stopped) { if (stopped) {
toast.info( toast.info(
t( t(
...@@ -699,7 +744,13 @@ function ChannelTestDialogContent({ ...@@ -699,7 +744,13 @@ function ChannelTestDialogContent({
refreshChannelLists(resultPatch) refreshChannelLists(resultPatch)
} }
}, },
[refreshChannelLists, t, testSingleModel, updateTestResult] [
dismissBatchProgressToast,
refreshChannelLists,
t,
testSingleModel,
updateTestResult,
]
) )
const handleSelectSuccessfulModels = useCallback(() => { const handleSelectSuccessfulModels = useCallback(() => {
...@@ -841,8 +892,19 @@ function ChannelTestDialogContent({ ...@@ -841,8 +892,19 @@ function ChannelTestDialogContent({
cell: ({ row }) => { cell: ({ row }) => {
const model = row.original.model const model = row.original.model
const result = testResults[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 ( return (
<TestStatusCell <TestResultCell
result={result} result={result}
model={model} model={model}
onOpenDetails={setFailureDetails} onOpenDetails={setFailureDetails}
...@@ -850,7 +912,7 @@ function ChannelTestDialogContent({ ...@@ -850,7 +912,7 @@ function ChannelTestDialogContent({
) )
}, },
enableSorting: false, enableSorting: false,
size: 220, size: 320,
}, },
{ {
id: 'actions', id: 'actions',
...@@ -905,22 +967,19 @@ function ChannelTestDialogContent({ ...@@ -905,22 +967,19 @@ function ChannelTestDialogContent({
<Dialog <Dialog
open={open} open={open}
onOpenChange={handleDialogOpenChange} onOpenChange={handleDialogOpenChange}
title={t('Test Channel Connection')} title={
description={ <span className='inline-flex max-w-full min-w-0 items-center gap-1.5'>
<> <span className='shrink-0'>{t('Test Channel Connection')}:</span>
{t('Test connectivity for:')} <span className='min-w-0 truncate'>{currentRow.name}</span>
<strong>{currentRow.name}</strong> </span>
</>
} }
contentClassName='max-h-[90vh] overflow-hidden sm:max-w-3xl' contentClassName='max-h-[90vh] overflow-hidden sm:max-w-4xl'
contentHeight='auto' contentHeight='auto'
bodyClassName='space-y-4' bodyClassName='space-y-4'
footer={ footer={
<> <Button variant='outline' onClick={handleClose}>
<Button variant='outline' onClick={handleClose}> {t('Close')}
{t('Close')} </Button>
</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'>
...@@ -983,12 +1042,60 @@ function ChannelTestDialogContent({ ...@@ -983,12 +1042,60 @@ function ChannelTestDialogContent({
</div> </div>
<div className='space-y-3 max-sm:has-[div[role="toolbar"]]:pb-16'> <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 className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div> <div className='min-w-0 space-y-2'>
<p className='text-sm font-medium'>{t('Channel models')}</p> <p className='text-sm font-medium'>{t('Channel models')}</p>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('Select models to run batch tests.')} {t('Select models to run batch tests.')}
</p> </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>
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'> <div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
<Input <Input
...@@ -997,64 +1104,9 @@ function ChannelTestDialogContent({ ...@@ -997,64 +1104,9 @@ function ChannelTestDialogContent({
onChange={handleSearchTermChange} onChange={handleSearchTermChange}
className='sm:w-64' 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>
</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'> <div className='space-y-3'>
<DataTableView <DataTableView
table={table} table={table}
...@@ -1076,7 +1128,8 @@ function ChannelTestDialogContent({ ...@@ -1076,7 +1128,8 @@ function ChannelTestDialogContent({
<colgroup> <colgroup>
<col className='w-10 min-w-10' /> <col className='w-10 min-w-10' />
<col className='w-auto' /> <col className='w-auto' />
<col className='w-70' /> <col className='w-28' />
<col className='w-80' />
<col className='w-auto' /> <col className='w-auto' />
</colgroup> </colgroup>
} }
...@@ -1123,46 +1176,36 @@ function ChannelTestDialogContent({ ...@@ -1123,46 +1176,36 @@ function ChannelTestDialogContent({
) )
} }
function BatchProgressSummary({ function TestStatusCell({ result }: { result?: TestResult }) {
progress,
isStopping,
}: {
progress: BatchProgress
isStopping: boolean
}) {
const { t } = useTranslation() const { t } = useTranslation()
const progressValue =
progress.total > 0
? Math.min(100, Math.round((progress.completed / progress.total) * 100))
: 0
return ( if (!result || result.status === 'idle') {
<div className='bg-muted/30 flex flex-col gap-2 rounded-md border p-3'> return (
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between'> <StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
<p className='text-sm font-medium'> )
{isStopping }
? t('Stopping batch test...')
: t('Batch testing models...')} if (result.status === 'testing') {
</p> return (
<p className='text-muted-foreground text-xs tabular-nums'> <StatusBadge variant='info' copyable={false}>
{t('{{completed}}/{{total}} completed', { <Loader2 className='size-3.5 shrink-0 animate-spin' />
completed: progress.completed, <span className='min-w-0 truncate leading-normal'>
total: progress.total, {t('Testing...')}
})} </span>
</p> </StatusBadge>
</div> )
<Progress value={progressValue} /> }
<p className='text-muted-foreground text-xs'>
{t('{{success}} succeeded, {{failed}} failed', { if (result.status === 'success') {
success: progress.success, return (
failed: progress.failed, <StatusBadge label={t('Success')} variant='success' copyable={false} />
})} )
</p> }
</div>
) return <StatusBadge label={t('Failed')} variant='danger' copyable={false} />
} }
function TestStatusCell({ function TestResultCell({
result, result,
model, model,
onOpenDetails, onOpenDetails,
...@@ -1174,9 +1217,7 @@ function TestStatusCell({ ...@@ -1174,9 +1217,7 @@ function TestStatusCell({
const { t } = useTranslation() const { t } = useTranslation()
if (!result || result.status === 'idle') { if (!result || result.status === 'idle') {
return ( return <span className='text-muted-foreground text-sm'>-</span>
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
)
} }
if (result.status === 'testing') { if (result.status === 'testing') {
...@@ -1189,20 +1230,17 @@ function TestStatusCell({ ...@@ -1189,20 +1230,17 @@ function TestStatusCell({
} }
if (result.status === 'success') { if (result.status === 'success') {
return ( return typeof result.responseTime === 'number' ? (
<div className='flex min-w-0 flex-col gap-1 text-xs'> <span className='text-muted-foreground text-sm'>
<StatusBadge label={t('Success')} variant='success' copyable={false} /> {formatResponseTime(result.responseTime, t)}
{typeof result.responseTime === 'number' && ( </span>
<span className='text-muted-foreground truncate'> ) : (
{formatResponseTime(result.responseTime, t)} <span className='text-muted-foreground text-sm'>-</span>
</span>
)}
</div>
) )
} }
return ( return (
<FailureStatusContent <FailureResultContent
result={result} result={result}
model={model} model={model}
onOpenDetails={onOpenDetails} onOpenDetails={onOpenDetails}
...@@ -1210,7 +1248,7 @@ function TestStatusCell({ ...@@ -1210,7 +1248,7 @@ function TestStatusCell({
) )
} }
function FailureStatusContent({ function FailureResultContent({
result, result,
model, model,
onOpenDetails, onOpenDetails,
...@@ -1233,12 +1271,11 @@ function FailureStatusContent({ ...@@ -1233,12 +1271,11 @@ function FailureStatusContent({
}) })
return ( return (
<div className='flex min-w-0 flex-col gap-1.5 text-xs whitespace-normal'> <div className='flex min-w-0 items-center gap-2 text-xs whitespace-normal'>
<StatusBadge label={t('Failed')} variant='danger' copyable={false} /> <p className='text-muted-foreground line-clamp-2 min-w-0 flex-1 leading-snug wrap-break-word'>
<p className='text-muted-foreground line-clamp-2 min-w-0 leading-snug wrap-break-word'>
{summary} {summary}
</p> </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 && ( {isModelPriceError && (
<Button <Button
variant='outline' 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