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 18ed1167..c3b93064 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 @@ -26,8 +26,10 @@ import { getPaginationRowModel, useReactTable, } from '@tanstack/react-table' -import { Loader2, Settings } from 'lucide-react' +import { Check, Copy, Info, Loader2, Settings } from 'lucide-react' import { useTranslation } from 'react-i18next' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' +import { useIsMobile } from '@/hooks/use-mobile' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { @@ -48,6 +50,14 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' import { Switch } from '@/components/ui/switch' import { Table, @@ -114,6 +124,88 @@ const STREAM_INCOMPATIBLE_ENDPOINTS = new Set([ 'openai-response-compact', ]) +const MODEL_PRICE_ERROR_CODE = 'model_price_error' +const FAILURE_SUMMARY_MAX_LENGTH = 96 + +type FailureStatusDisplay = { + summary: string + details?: string +} + +type FailureDetailsState = { + model: string + summary: string + details: string +} + +function normalizeInlineError(errorText: string) { + return errorText.replace(/\s+/g, ' ').trim() +} + +function getFirstErrorLine(errorText: string) { + return errorText + .split(/\r?\n/) + .map((line) => line.trim()) + .find(Boolean) +} + +function truncateFailureSummary(summary: string) { + if (summary.length <= FAILURE_SUMMARY_MAX_LENGTH) { + return summary + } + + return `${summary.slice(0, FAILURE_SUMMARY_MAX_LENGTH).trimEnd()}...` +} + +function getFailureStatusDisplay({ + errorText, + fallbackSummary, + isModelPriceError, + modelPriceSummary, +}: { + errorText?: string + fallbackSummary: string + isModelPriceError: boolean + modelPriceSummary: string +}): FailureStatusDisplay { + const rawError = errorText?.trim() + + if (!rawError) { + return { summary: fallbackSummary } + } + + if (isModelPriceError) { + return { + summary: modelPriceSummary, + details: rawError === modelPriceSummary ? undefined : rawError, + } + } + + const firstLine = getFirstErrorLine(rawError) ?? rawError + const summary = truncateFailureSummary(normalizeInlineError(firstLine)) + const normalizedRawError = normalizeInlineError(rawError) + + return { + summary, + details: summary === normalizedRawError ? undefined : rawError, + } +} + +function getTestTableColumnClass(columnId: string) { + switch (columnId) { + case 'select': + return 'w-10 min-w-10' + case 'model': + return 'w-auto whitespace-nowrap' + case 'status': + return 'w-70 min-w-70 max-w-70 whitespace-normal' + case 'actions': + return 'bg-popover sticky right-0 z-20 w-24 min-w-24 border-l shadow-[-8px_0_8px_-8px_rgb(0_0_0_/_0.2)] whitespace-nowrap sm:w-28 sm:min-w-28' + default: + return undefined + } +} + export function ChannelTestDialog({ open, onOpenChange, @@ -129,6 +221,8 @@ export function ChannelTestDialog({ () => new Set() ) const [isBatchTesting, setIsBatchTesting] = useState(false) + const [failureDetails, setFailureDetails] = + useState(null) const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, @@ -142,6 +236,7 @@ export function ChannelTestDialog({ setRowSelection({}) setTestingModels(() => new Set()) setIsBatchTesting(false) + setFailureDetails(null) setPagination({ pageIndex: 0, pageSize: 10 }) }, []) @@ -199,6 +294,7 @@ export function ChannelTestDialog({ }, []) const updateTestResult = useCallback((key: string, result: TestResult) => { + setFailureDetails((current) => (current?.model === key ? null : current)) setTestResults((prev) => ({ ...prev, [key]: result, @@ -283,14 +379,16 @@ export function ChannelTestDialog({ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value) } - aria-label='Select all models' + aria-label={t('Select all models')} /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} - aria-label={`Select model ${row.original.model}`} + aria-label={t('Select model {{model}}', { + model: row.original.model, + })} /> ), enableSorting: false, @@ -299,17 +397,19 @@ export function ChannelTestDialog({ }, { accessorKey: 'model', - header: 'Model', + header: t('Model'), cell: ({ row }) => { const model = row.original.model const isDefault = defaultTestModel === model return ( -
- {model} +
+ + {model} + {isDefault && ( { 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, @@ -391,7 +438,7 @@ export function ChannelTestDialog({ }, { id: 'actions', - header: 'Actions', + header: t('Actions'), cell: ({ row }) => { const model = row.original.model const isTestingModel = testingModels.has(model) @@ -406,7 +453,7 @@ export function ChannelTestDialog({ {isTestingModel && ( )} - Test + {t('Test')} ) }, @@ -443,160 +490,369 @@ export function ChannelTestDialog({ } return ( - - - - {t('Test Channel Connection')} - - {t('Test connectivity for:')} {currentRow.name} - - + <> + + + + {t('Test Channel Connection')} + + {t('Test connectivity for:')} {currentRow.name} + + -
-
-
- - { const itemValue = option.value - return ( - - {t(option.label)} - - ) - })} - - - -

- {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')}

+ return { value: itemValue, label: t(option.label) } + }), + ]} + value={endpointType} + onValueChange={(v) => v !== null && setEndpointType(v)} + > + + + + + + {endpointTypeOptions.map((option) => { + const itemValue = option.value + return ( + + {t(option.label)} + + ) + })} + + +

- {t('Select models to run batch tests.')} + {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.')}

- 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.'} - - - )} - -
+
+
+
+

{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.'} + + + )} + +
+
+
+ + +
+ +
- -
-
- - + + +
+ { + if (!sheetOpen) { + setFailureDetails(null) + } + }} + /> + + ) +} + +function TestStatusCell({ + result, + model, + onOpenDetails, +}: { + result?: TestResult + model: string + onOpenDetails: (details: FailureDetailsState) => void +}) { + const { t } = useTranslation() + + if (!result || result.status === 'idle') { + return ( + + ) + } + + if (result.status === 'testing') { + return ( +
+ + {t('Testing...')} +
+ ) + } + + if (result.status === 'success') { + return ( +
+ + {typeof result.responseTime === 'number' && ( + + {formatResponseTime(result.responseTime, t)} + + )} +
+ ) + } + + return ( + + ) +} + +function FailureStatusContent({ + result, + model, + onOpenDetails, +}: { + result: TestResult + model: string + onOpenDetails: (details: FailureDetailsState) => void +}) { + const { t } = useTranslation() + const errorText = result.error?.trim() + const isModelPriceError = result.errorCode === MODEL_PRICE_ERROR_CODE + const modelPriceSummary = t( + 'Model price is not configured. Please complete model pricing in settings.' + ) + const { summary, details } = getFailureStatusDisplay({ + errorText, + fallbackSummary: t('Test failed'), + isModelPriceError, + modelPriceSummary, + }) + + return ( +
+ +

+ {summary} +

+
+ {isModelPriceError && ( + - - -
+ )} + {details && ( + + )} +
+
+ ) +} + +function FailureDetailsSheet({ + details, + onOpenChange, +}: { + details: FailureDetailsState | null + onOpenChange: (open: boolean) => void +}) { + const { t } = useTranslation() + const isMobile = useIsMobile() + const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false }) + + return ( + + + {details && ( + <> + + {t('Details')} + + {details.model} + + +
+
+
+ {t('Model')} +
+

{details.model}

+
+
+
+ {t('Failed')} +
+

+ {details.summary} +

+
+
+
+ {t('Details')} +
+
+                  {details.details}
+                
+
+
+ + + + + )} +
+
) } @@ -615,8 +871,8 @@ function TestModelsBulkActions({ const buttonLabel = selectedModels.length > 0 - ? `Test ${selectedModels.length} selected` - : 'Test selected models' + ? t('Test {{count}} selected', { count: selectedModels.length }) + : t('Test selected models') return ( diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 1560b019..ab736093 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -2363,6 +2363,7 @@ "Model performance metrics": "Model performance metrics", "Model Price": "Model Price", "Model Price Not Configured": "Model Price Not Configured", + "Model price is not configured. Please complete model pricing in settings.": "Model price is not configured. Please complete model pricing in settings.", "Model prices": "Model prices", "Model prices reset successfully": "Model prices reset successfully", "Model Pricing": "Model Pricing", @@ -3512,6 +3513,7 @@ "Select all (filtered)": "Select all (filtered)", "Select all models": "Select all models", "Select All Visible": "Select All Visible", + "Select model {{model}}": "Select model {{model}}", "Select an operation mode and enter the amount": "Select an operation mode and enter the amount", "Select announcement type": "Select announcement type", "Select at least one field to overwrite.": "Select at least one field to overwrite.", @@ -3849,6 +3851,8 @@ "Templates appended": "Templates appended", "Tencent": "Tencent", "Termination requested": "Termination requested", + "Test": "Test", + "Test {{count}} selected": "Test {{count}} selected", "Test All Channels": "Test All Channels", "Test Channel Connection": "Test Channel Connection", "Test Connection": "Test Connection", @@ -3859,6 +3863,7 @@ "Test Mode": "Test Mode", "Test Model": "Test Model", "Test models and prompts from the browser": "Test models and prompts from the browser", + "Test selected models": "Test selected models", "Testing all enabled channels started. Please refresh to see results.": "Testing all enabled channels started. Please refresh to see results.", "Testing...": "Testing...", "Text": "Text", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 8b555700..7f1aae94 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -2363,6 +2363,7 @@ "Model performance metrics": "Indicateurs de performance des modèles", "Model Price": "Prix du modèle", "Model Price Not Configured": "Prix du modèle non configuré", + "Model price is not configured. Please complete model pricing in settings.": "Le prix du modèle n'est pas configuré. Veuillez compléter la tarification du modèle dans les paramètres.", "Model prices": "Prix des modèles", "Model prices reset successfully": "Prix des modèles réinitialisés avec succès", "Model Pricing": "Tarification des modèles", @@ -3512,6 +3513,7 @@ "Select all (filtered)": "Tout sélectionner (filtré)", "Select all models": "Sélectionner tous les modèles", "Select All Visible": "Sélectionner tout ce qui est visible", + "Select model {{model}}": "Sélectionner le modèle {{model}}", "Select an operation mode and enter the amount": "Sélectionnez un mode d'opération et entrez le montant", "Select announcement type": "Sélectionner le type d'annonce", "Select at least one field to overwrite.": "Sélectionnez au moins un champ à écraser.", @@ -3849,6 +3851,8 @@ "Templates appended": "Modèles ajoutés", "Tencent": "Tencent", "Termination requested": "Arrêt demandé", + "Test": "Tester", + "Test {{count}} selected": "Tester {{count}} sélectionné(s)", "Test All Channels": "Tester tous les canaux", "Test Channel Connection": "Tester la connexion du canal", "Test Connection": "Tester la connexion", @@ -3859,6 +3863,7 @@ "Test Mode": "Mode test", "Test Model": "Tester le modèle", "Test models and prompts from the browser": "Tester les modèles et les prompts depuis le navigateur", + "Test selected models": "Tester les modèles sélectionnés", "Testing all enabled channels started. Please refresh to see results.": "Test de tous les canaux activés démarré. Veuillez actualiser pour voir les résultats.", "Testing...": "Test en cours...", "Text": "Texte", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 778867e9..c317c612 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -2363,6 +2363,7 @@ "Model performance metrics": "モデル性能メトリクス", "Model Price": "モデル価格", "Model Price Not Configured": "モデル価格が未設定", + "Model price is not configured. Please complete model pricing in settings.": "モデル価格が未設定です。設定でモデル料金を補完してください。", "Model prices": "モデル価格", "Model prices reset successfully": "モデル価格が正常にリセットされました", "Model Pricing": "モデル料金", @@ -3512,6 +3513,7 @@ "Select all (filtered)": "フィルタ結果をすべて選択(S)", "Select all models": "すべてのモデルを選択", "Select All Visible": "表示中のすべてを選択", + "Select model {{model}}": "モデル {{model}} を選択", "Select an operation mode and enter the amount": "操作モードを選択し、金額を入力してください", "Select announcement type": "アナウンスメントタイプを選択", "Select at least one field to overwrite.": "上書きするフィールドを少なくとも 1 つ選択してください。", @@ -3849,6 +3851,8 @@ "Templates appended": "テンプレートが追加されました", "Tencent": "テンセント", "Termination requested": "終了リクエスト済み", + "Test": "テスト", + "Test {{count}} selected": "選択済み {{count}} 件をテスト", "Test All Channels": "すべてのチャネルをテスト", "Test Channel Connection": "チャネル接続をテスト", "Test Connection": "接続をテスト", @@ -3859,6 +3863,7 @@ "Test Mode": "テストモード", "Test Model": "モデルをテスト", "Test models and prompts from the browser": "ブラウザでモデルとプロンプトをテスト", + "Test selected models": "選択したモデルをテスト", "Testing all enabled channels started. Please refresh to see results.": "有効な全チャネルのテストを開始しました。結果を確認するにはページを更新してください。", "Testing...": "テスト中...", "Text": "テキスト", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 88ae65b0..baa9f290 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -2363,6 +2363,7 @@ "Model performance metrics": "Метрики производительности моделей", "Model Price": "Цена модели", "Model Price Not Configured": "Цена модели не настроена", + "Model price is not configured. Please complete model pricing in settings.": "Цена модели не настроена. Заполните тарификацию модели в настройках.", "Model prices": "Цены моделей", "Model prices reset successfully": "Цены моделей успешно сброшены", "Model Pricing": "Тарификация моделей", @@ -3512,6 +3513,7 @@ "Select all (filtered)": "& Выбрать все отфильтрованные", "Select all models": "Выбрать все модели", "Select All Visible": "Выбрать все видимые", + "Select model {{model}}": "Выбрать модель {{model}}", "Select an operation mode and enter the amount": "Выберите режим операции и введите сумму", "Select announcement type": "Выбрать тип объявления", "Select at least one field to overwrite.": "Выберите хотя бы одно поле для перезаписи.", @@ -3849,6 +3851,8 @@ "Templates appended": "Шаблоны добавлены", "Tencent": "Tencent", "Termination requested": "Запрошено завершение", + "Test": "Проверить", + "Test {{count}} selected": "Проверить {{count}} выбранных", "Test All Channels": "Проверить все каналы", "Test Channel Connection": "Проверить подключение канала", "Test Connection": "Проверить подключение", @@ -3859,6 +3863,7 @@ "Test Mode": "Тестовый режим", "Test Model": "Проверить модель", "Test models and prompts from the browser": "Тестируйте модели и промпты в браузере", + "Test selected models": "Проверить выбранные модели", "Testing all enabled channels started. Please refresh to see results.": "Тестирование всех включенных каналов начато. Пожалуйста, обновите страницу, чтобы увидеть результаты.", "Testing...": "Тестирование...", "Text": "Текст", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 5e2e3200..2bc63aae 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -2363,6 +2363,7 @@ "Model performance metrics": "Chỉ số hiệu năng mô hình", "Model Price": "Giá mô hình", "Model Price Not Configured": "Giá mô hình chưa được cấu hình", + "Model price is not configured. Please complete model pricing in settings.": "Giá mô hình chưa được cấu hình. Vui lòng hoàn tất định giá mô hình trong cài đặt.", "Model prices": "Giá mô hình", "Model prices reset successfully": "Đã đặt lại giá mô hình thành công", "Model Pricing": "Định giá mô hình", @@ -3512,6 +3513,7 @@ "Select all (filtered)": "Chọn tất cả (đã lọc)", "Select all models": "Chọn tất cả mô hình", "Select All Visible": "Chọn tất cả hiển thị", + "Select model {{model}}": "Chọn mô hình {{model}}", "Select an operation mode and enter the amount": "Chọn chế độ thao tác và nhập số tiền", "Select announcement type": "Select notification type", "Select at least one field to overwrite.": "Chọn ít nhất một trường để ghi đè.", @@ -3849,6 +3851,8 @@ "Templates appended": "Đã thêm mẫu", "Tencent": "Tencent", "Termination requested": "Yêu cầu chấm dứt", + "Test": "Kiểm tra", + "Test {{count}} selected": "Kiểm tra {{count}} mục đã chọn", "Test All Channels": "Kiểm tra tất cả các kênh", "Test Channel Connection": "Check channel connection", "Test Connection": "Kiểm tra kết nối", @@ -3859,6 +3863,7 @@ "Test Mode": "Chế độ thử nghiệm", "Test Model": "Kiểm tra Mô hình", "Test models and prompts from the browser": "Kiểm thử mô hình và prompt trong trình duyệt", + "Test selected models": "Kiểm tra các mô hình đã chọn", "Testing all enabled channels started. Please refresh to see results.": "Bắt đầu kiểm tra tất cả các kênh đã kích hoạt. Vui lòng làm mới để xem kết quả.", "Testing...": "Đang kiểm tra...", "Text": "Văn bản", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 6da35355..d1c5a906 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -2363,6 +2363,7 @@ "Model performance metrics": "模型性能指标", "Model Price": "模型价格", "Model Price Not Configured": "模型价格未配置", + "Model price is not configured. Please complete model pricing in settings.": "模型价格未配置,请前往设置补充模型价格。", "Model prices": "模型价格", "Model prices reset successfully": "模型价格重置成功", "Model Pricing": "模型定价", @@ -3512,6 +3513,7 @@ "Select all (filtered)": "全选(筛选结果)", "Select all models": "选择所有模型", "Select All Visible": "全选当前", + "Select model {{model}}": "选择模型 {{model}}", "Select an operation mode and enter the amount": "选择操作模式并输入金额", "Select announcement type": "选择公告类型", "Select at least one field to overwrite.": "请选择至少一个要覆盖的字段。", @@ -3849,6 +3851,8 @@ "Templates appended": "模板已追加", "Tencent": "腾讯", "Termination requested": "终止请求", + "Test": "测试", + "Test {{count}} selected": "测试 {{count}} 个已选择项", "Test All Channels": "测试所有渠道", "Test Channel Connection": "测试渠道连接", "Test Connection": "测试连接", @@ -3859,6 +3863,7 @@ "Test Mode": "测试模式", "Test Model": "测试模型", "Test models and prompts from the browser": "在浏览器中测试模型和提示词", + "Test selected models": "测试所选模型", "Testing all enabled channels started. Please refresh to see results.": "测试所有启用的通道已开始。请刷新以查看结果。", "Testing...": "测试中...", "Text": "文本",