import { useCallback, useEffect, useMemo, useState } from 'react' import { type ColumnDef, type RowSelectionState, type Table as TanStackTable, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable, } from '@tanstack/react-table' import { Loader2, Settings } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTablePagination } from '@/components/data-table/pagination' import { StatusBadge } from '@/components/status-badge' import { formatResponseTime, handleTestChannel } from '../../lib' import { useChannels } from '../channels-provider' type ChannelTestDialogProps = { open: boolean onOpenChange: (open: boolean) => void } type ModelRow = { model: string } type TestStatus = 'idle' | 'testing' | 'success' | 'error' type TestResult = { status: TestStatus responseTime?: number error?: string errorCode?: string } const endpointTypeOptions: Array<{ value: string; label: string }> = [ { value: 'auto', label: 'Auto detect (default)' }, { value: 'openai', label: 'OpenAI (/v1/chat/completions)' }, { value: 'openai-response', label: 'OpenAI Responses (/v1/responses)' }, { value: 'openai-response-compact', label: 'OpenAI Response Compaction (/v1/responses/compact)', }, { value: 'anthropic', label: 'Anthropic (/v1/messages)' }, { value: 'gemini', label: 'Gemini (/v1beta/models/{model}:generateContent)', }, { value: 'jina-rerank', label: 'Jina Rerank (/v1/rerank)' }, { value: 'image-generation', label: 'Image Generation (/v1/images/generations)', }, { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' }, ] const STREAM_INCOMPATIBLE_ENDPOINTS = new Set([ 'embeddings', 'image-generation', 'jina-rerank', 'openai-response-compact', ]) export function ChannelTestDialog({ open, onOpenChange, }: ChannelTestDialogProps) { const { t } = useTranslation() const { currentRow } = useChannels() const [endpointType, setEndpointType] = useState('auto') const [isStreamTest, setIsStreamTest] = useState(false) const [searchTerm, setSearchTerm] = useState('') const [testResults, setTestResults] = useState>({}) const [rowSelection, setRowSelection] = useState({}) const [testingModels, setTestingModels] = useState>( () => new Set() ) const [isBatchTesting, setIsBatchTesting] = useState(false) const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }) const resetState = useCallback(() => { setEndpointType('auto') setIsStreamTest(false) setSearchTerm('') setTestResults({}) setRowSelection({}) setTestingModels(() => new Set()) setIsBatchTesting(false) setPagination({ pageIndex: 0, pageSize: 10 }) }, []) useEffect(() => { if (open && currentRow) { resetState() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, currentRow?.id, resetState]) const streamDisabled = STREAM_INCOMPATIBLE_ENDPOINTS.has(endpointType) useEffect(() => { if (streamDisabled) { setIsStreamTest(false) } }, [streamDisabled]) const modelsValue = currentRow?.models ?? '' const defaultTestModel = currentRow?.test_model?.trim() const models = useMemo(() => { if (!modelsValue) return [] return modelsValue .split(',') .map((model) => model.trim()) .filter(Boolean) }, [modelsValue]) const filteredModels = useMemo(() => { if (!searchTerm) return models const keyword = searchTerm.toLowerCase() return models.filter((model) => model.toLowerCase().includes(keyword)) }, [models, searchTerm]) useEffect(() => { setPagination((prev) => ({ ...prev, pageIndex: 0 })) }, [searchTerm, modelsValue]) const tableData = useMemo( () => filteredModels.map((model) => ({ model })), [filteredModels] ) const markModelTesting = useCallback((key: string, isTesting: boolean) => { setTestingModels((prev) => { const next = new Set(prev) if (isTesting) { next.add(key) } else { next.delete(key) } return next }) }, []) const updateTestResult = useCallback((key: string, result: TestResult) => { setTestResults((prev) => ({ ...prev, [key]: result, })) }, []) const testSingleModel = useCallback( async (model: string) => { if (!currentRow) return markModelTesting(model, true) updateTestResult(model, { status: 'testing' }) try { await handleTestChannel( currentRow.id, { testModel: model, endpointType: endpointType === 'auto' ? undefined : endpointType, stream: isStreamTest || undefined, }, (success, responseTime, error, errorCode) => { updateTestResult(model, { status: success ? 'success' : 'error', responseTime, error, errorCode, }) } ) } catch (error: unknown) { updateTestResult(model, { status: 'error', error: error instanceof Error ? error.message : 'Test failed', }) } finally { markModelTesting(model, false) } }, [currentRow, endpointType, isStreamTest, markModelTesting, updateTestResult] ) const handleBatchTest = useCallback( async (modelsToTest: string[]) => { if (!modelsToTest.length) return setIsBatchTesting(true) try { await Promise.allSettled( modelsToTest.map((modelName) => testSingleModel(modelName)) ) } finally { setIsBatchTesting(false) setRowSelection({}) } }, [testSingleModel] ) const handleClose = () => { resetState() onOpenChange(false) } const isAnyTesting = testingModels.size > 0 || isBatchTesting const columns = useMemo[]>( () => [ { id: 'select', header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value) } aria-label='Select all models' /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label={`Select model ${row.original.model}`} /> ), enableSorting: false, enableHiding: false, size: 40, }, { accessorKey: 'model', header: 'Model', cell: ({ row }) => { const model = row.original.model const isDefault = defaultTestModel === model return (
{model} {isDefault && ( )}
) }, }, { id: 'status', header: 'Status', cell: ({ row }) => { const model = row.original.model const result = testResults[model] if (!result || result.status === 'idle') { return ( ) } if (result.status === 'testing') { return (
Testing...
) } if (result.status === 'success') { return (
{typeof result.responseTime === 'number' && ( {formatResponseTime(result.responseTime, t)} )}
) } return (
{result.error && ( {result.error} )} {result.errorCode === 'model_price_error' && ( )}
) }, enableSorting: false, size: 220, }, { id: 'actions', header: 'Actions', cell: ({ row }) => { const model = row.original.model const isTestingModel = testingModels.has(model) return ( ) }, enableSorting: false, size: 120, }, ], [ defaultTestModel, isBatchTesting, t, testResults, testingModels, testSingleModel, ] ) const table = useReactTable({ data: tableData, columns, state: { rowSelection, pagination, }, enableRowSelection: true, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onRowSelectionChange: setRowSelection, onPaginationChange: setPagination, }) if (!currentRow) { return null } 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.'} )}
) } function TestModelsBulkActions({ table, disabled, onTestSelected, }: { table: TanStackTable disabled?: boolean onTestSelected: (models: string[]) => void }) { const { t } = useTranslation() const selectedRows = table.getFilteredSelectedRowModel().rows const selectedModels = selectedRows.map((row) => row.original.model) const buttonLabel = selectedModels.length > 0 ? `Test ${selectedModels.length} selected` : 'Test selected models' return (

{t('Run tests for the selected models')}

) }