From 3cc2b1bea44f25dfe2aee70215ed43a25c7b3769 Mon Sep 17 00:00:00 2001 From: CaIon Date: Wed, 17 Jun 2026 21:31:20 +0800 Subject: [PATCH] feat(channel-test-dialog): add functionality to delete failed models and copy selected models --- .../dialogs/channel-test-dialog.tsx | 182 +++++++++++++++--- web/default/src/i18n/locales/en.json | 8 + web/default/src/i18n/locales/fr.json | 8 + web/default/src/i18n/locales/ja.json | 8 + web/default/src/i18n/locales/ru.json | 8 + web/default/src/i18n/locales/vi.json | 8 + web/default/src/i18n/locales/zh.json | 8 + 7 files changed, 202 insertions(+), 28 deletions(-) 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 bd478c65..164b4847 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 @@ -23,7 +23,15 @@ import { type RowSelectionState, type Table as TanStackTable, } from '@tanstack/react-table' -import { Check, Copy, Info, Loader2, Settings } from 'lucide-react' +import { + Check, + CheckCircle2, + Copy, + Info, + Loader2, + Settings, + Trash2, +} from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' @@ -61,6 +69,7 @@ import { DataTableView, useDataTable, } from '@/components/data-table' +import { ConfirmDialog } from '@/components/confirm-dialog' import { Dialog } from '@/components/dialog' import { sideDrawerContentClassName, @@ -69,6 +78,7 @@ import { sideDrawerHeaderClassName, } from '@/components/drawer-layout' import { StatusBadge } from '@/components/status-badge' +import { updateChannel } from '../../api' import { channelsQueryKeys, formatResponseTime, @@ -315,11 +325,17 @@ function ChannelTestDialogContent({ const [isBatchTesting, setIsBatchTesting] = useState(false) const [isBatchStopRequested, setIsBatchStopRequested] = useState(false) const [batchProgress, setBatchProgress] = useState(null) + const [removedModels, setRemovedModels] = useState>( + () => new Set() + ) + const [isDeleteFailedDialogOpen, setIsDeleteFailedDialogOpen] = + useState(false) + const [isDeletingFailed, setIsDeletingFailed] = useState(false) const [failureDetails, setFailureDetails] = useState(null) const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: 10, + pageSize: 30, }) const endpointSelectItems = useMemo( () => @@ -341,8 +357,11 @@ function ChannelTestDialogContent({ setIsBatchTesting(false) setIsBatchStopRequested(false) setBatchProgress(null) + setRemovedModels(() => new Set()) + setIsDeleteFailedDialogOpen(false) + setIsDeletingFailed(false) setFailureDetails(null) - setPagination({ pageIndex: 0, pageSize: 10 }) + setPagination({ pageIndex: 0, pageSize: 30 }) }, []) const streamDisabled = STREAM_INCOMPATIBLE_ENDPOINTS.has(endpointType) @@ -370,7 +389,7 @@ function ChannelTestDialogContent({ const modelsValue = currentRow.models const defaultTestModel = currentRow.test_model?.trim() - const models = useMemo(() => { + const baseModels = useMemo(() => { if (!modelsValue) return [] return modelsValue .split(',') @@ -378,6 +397,21 @@ function ChannelTestDialogContent({ .filter(Boolean) }, [modelsValue]) + const models = useMemo( + () => baseModels.filter((model) => !removedModels.has(model)), + [baseModels, removedModels] + ) + + const successModels = useMemo( + () => models.filter((model) => testResults[model]?.status === 'success'), + [models, testResults] + ) + + const failedModels = useMemo( + () => models.filter((model) => testResults[model]?.status === 'error'), + [models, testResults] + ) + const filteredModels = useMemo(() => { if (!searchTerm) return models const keyword = searchTerm.toLowerCase() @@ -661,6 +695,68 @@ function ChannelTestDialogContent({ [refreshChannelLists, t, testSingleModel, updateTestResult] ) + const handleSelectSuccessfulModels = useCallback(() => { + setRowSelection(() => { + const next: RowSelectionState = {} + for (const model of successModels) { + next[model] = true + } + return next + }) + }, [successModels]) + + const handleDeleteFailedModels = useCallback(async () => { + const failed = models.filter( + (model) => testResults[model]?.status === 'error' + ) + if (!failed.length) { + setIsDeleteFailedDialogOpen(false) + return + } + + const failedSet = new Set(failed) + const remaining = models.filter((model) => !failedSet.has(model)) + + setIsDeletingFailed(true) + try { + const response = await updateChannel(currentRow.id, { + models: remaining.join(','), + }) + if (response.success) { + setRemovedModels((prev) => { + const next = new Set(prev) + for (const model of failed) next.add(model) + return next + }) + setTestResults((prev) => { + const next = { ...prev } + for (const model of failed) delete next[model] + return next + }) + setRowSelection((prev) => { + const next = { ...prev } + for (const model of failed) delete next[model] + return next + }) + toast.success( + t('Deleted {{count}} failed models', { count: failed.length }) + ) + refreshChannelLists() + setIsDeleteFailedDialogOpen(false) + } else { + toast.error(response.message || t('Failed to delete failed models')) + } + } catch (error: unknown) { + toast.error( + error instanceof Error + ? error.message + : t('Failed to delete failed models') + ) + } finally { + setIsDeletingFailed(false) + } + }, [currentRow.id, models, refreshChannelLists, t, testResults]) + const handleClose = useCallback(() => { resetState() onOpenChange(false) @@ -911,6 +1007,36 @@ function ChannelTestDialogContent({ /> )} + {!isAnyTesting && + (successModels.length > 0 || failedModels.length > 0) && ( +
+ {successModels.length > 0 && ( + + )} + {failedModels.length > 0 && ( + + )} +
+ )} +
- + + { @@ -1193,21 +1328,18 @@ function FailureDetailsSheet({ function TestModelsBulkActions({ table, - disabled, - onTestSelected, }: { table: TanStackTable - disabled?: boolean - onTestSelected: (models: string[]) => void }) { const { t } = useTranslation() + const { copyToClipboard } = useCopyToClipboard() const selectedRows = table.getFilteredSelectedRowModel().rows const selectedModels = selectedRows.map((row) => row.original.model) - const buttonLabel = - selectedModels.length > 0 - ? t('Test {{count}} selected', { count: selectedModels.length }) - : t('Test selected models') + const handleCopySelected = useCallback(() => { + if (selectedModels.length === 0) return + void copyToClipboard(selectedModels.join(',')) + }, [copyToClipboard, selectedModels]) return ( @@ -1216,22 +1348,16 @@ function TestModelsBulkActions({ render={