fix: improve channel test failure details UX (#4988)

* fix: improve channel test failure details UX

* fix: add accessible label to channel models region
This commit is contained in:
yyhhyyyyyy
2026-05-21 11:09:51 +08:00
committed by GitHub
parent 20d3e73734
commit 58ba867dd6
7 changed files with 496 additions and 210 deletions
@@ -26,8 +26,10 @@ import {
getPaginationRowModel, getPaginationRowModel,
useReactTable, useReactTable,
} from '@tanstack/react-table' } 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 { 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 { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import {
@@ -48,6 +50,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
@@ -114,6 +124,88 @@ const STREAM_INCOMPATIBLE_ENDPOINTS = new Set([
'openai-response-compact', '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({ export function ChannelTestDialog({
open, open,
onOpenChange, onOpenChange,
@@ -129,6 +221,8 @@ export function ChannelTestDialog({
() => new Set() () => new Set()
) )
const [isBatchTesting, setIsBatchTesting] = useState(false) const [isBatchTesting, setIsBatchTesting] = useState(false)
const [failureDetails, setFailureDetails] =
useState<FailureDetailsState | null>(null)
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
@@ -142,6 +236,7 @@ export function ChannelTestDialog({
setRowSelection({}) setRowSelection({})
setTestingModels(() => new Set()) setTestingModels(() => new Set())
setIsBatchTesting(false) setIsBatchTesting(false)
setFailureDetails(null)
setPagination({ pageIndex: 0, pageSize: 10 }) setPagination({ pageIndex: 0, pageSize: 10 })
}, []) }, [])
@@ -199,6 +294,7 @@ export function ChannelTestDialog({
}, []) }, [])
const updateTestResult = useCallback((key: string, result: TestResult) => { const updateTestResult = useCallback((key: string, result: TestResult) => {
setFailureDetails((current) => (current?.model === key ? null : current))
setTestResults((prev) => ({ setTestResults((prev) => ({
...prev, ...prev,
[key]: result, [key]: result,
@@ -283,14 +379,16 @@ export function ChannelTestDialog({
onCheckedChange={(value) => onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value) table.toggleAllPageRowsSelected(!!value)
} }
aria-label='Select all models' aria-label={t('Select all models')}
/> />
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<Checkbox <Checkbox
checked={row.getIsSelected()} checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)} onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label={`Select model ${row.original.model}`} aria-label={t('Select model {{model}}', {
model: row.original.model,
})}
/> />
), ),
enableSorting: false, enableSorting: false,
@@ -299,17 +397,19 @@ export function ChannelTestDialog({
}, },
{ {
accessorKey: 'model', accessorKey: 'model',
header: 'Model', header: t('Model'),
cell: ({ row }) => { cell: ({ row }) => {
const model = row.original.model const model = row.original.model
const isDefault = defaultTestModel === model const isDefault = defaultTestModel === model
return ( return (
<div className='flex items-center gap-2'> <div className='flex w-max items-center gap-2 whitespace-nowrap'>
<span className='font-medium'>{model}</span> <span className='font-medium whitespace-nowrap' title={model}>
{model}
</span>
{isDefault && ( {isDefault && (
<StatusBadge <StatusBadge
label='Default' label={t('Default')}
variant='info' variant='info'
size='sm' size='sm'
copyable={false} copyable={false}
@@ -321,69 +421,16 @@ export function ChannelTestDialog({
}, },
{ {
id: 'status', id: 'status',
header: 'Status', header: t('Status'),
cell: ({ row }) => { cell: ({ row }) => {
const model = row.original.model const model = row.original.model
const result = testResults[model] const result = testResults[model]
if (!result || result.status === 'idle') {
return (
<StatusBadge
label={t('Not tested')}
variant='neutral'
copyable={false}
/>
)
}
if (result.status === 'testing') {
return (
<div className='text-muted-foreground flex items-center gap-2 text-sm'>
<Loader2 className='h-4 w-4 animate-spin' />
Testing...
</div>
)
}
if (result.status === 'success') {
return (
<div className='flex flex-col gap-1 text-xs'>
<StatusBadge
label='Success'
variant='success'
copyable={false}
/>
{typeof result.responseTime === 'number' && (
<span className='text-muted-foreground'>
{formatResponseTime(result.responseTime, t)}
</span>
)}
</div>
)
}
return ( return (
<div className='flex flex-col gap-1 text-xs'> <TestStatusCell
<StatusBadge label='Failed' variant='danger' copyable={false} /> result={result}
{result.error && ( model={model}
<span className='text-muted-foreground break-all'> onOpenDetails={setFailureDetails}
{result.error} />
</span>
)}
{result.errorCode === 'model_price_error' && (
<Button
variant='outline'
size='sm'
className='w-fit'
onClick={() =>
window.open('/console/setting?tab=ratio', '_blank')
}
>
<Settings className='mr-1 h-3 w-3' />
{t('Go to Settings')}
</Button>
)}
</div>
) )
}, },
enableSorting: false, enableSorting: false,
@@ -391,7 +438,7 @@ export function ChannelTestDialog({
}, },
{ {
id: 'actions', id: 'actions',
header: 'Actions', header: t('Actions'),
cell: ({ row }) => { cell: ({ row }) => {
const model = row.original.model const model = row.original.model
const isTestingModel = testingModels.has(model) const isTestingModel = testingModels.has(model)
@@ -406,7 +453,7 @@ export function ChannelTestDialog({
{isTestingModel && ( {isTestingModel && (
<Loader2 className='mr-2 h-4 w-4 animate-spin' /> <Loader2 className='mr-2 h-4 w-4 animate-spin' />
)} )}
Test {t('Test')}
</Button> </Button>
) )
}, },
@@ -443,160 +490,369 @@ export function ChannelTestDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <>
<DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'> <Dialog open={open} onOpenChange={handleClose}>
<DialogHeader> <DialogContent className='max-h-[90vh] overflow-hidden sm:max-w-3xl'>
<DialogTitle>{t('Test Channel Connection')}</DialogTitle> <DialogHeader>
<DialogDescription> <DialogTitle>{t('Test Channel Connection')}</DialogTitle>
{t('Test connectivity for:')} <strong>{currentRow.name}</strong> <DialogDescription>
</DialogDescription> {t('Test connectivity for:')} <strong>{currentRow.name}</strong>
</DialogHeader> </DialogDescription>
</DialogHeader>
<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'>
<div className='grid gap-4 md:grid-cols-2'> <div className='grid gap-4 md:grid-cols-2'>
<div className='grid gap-2'> <div className='grid gap-2'>
<Label htmlFor='endpoint-type'>{t('Endpoint Type')}</Label> <Label htmlFor='endpoint-type'>{t('Endpoint Type')}</Label>
<Select <Select
items={[ items={[
...endpointTypeOptions.map((option) => { ...endpointTypeOptions.map((option) => {
const itemValue = option.value
return { value: itemValue, label: t(option.label) }
}),
]}
value={endpointType}
onValueChange={(v) => v !== null && setEndpointType(v)}
>
<SelectTrigger id='endpoint-type'>
<SelectValue placeholder={t('Auto detect (default)')} />
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
{endpointTypeOptions.map((option) => {
const itemValue = option.value const itemValue = option.value
return ( return { value: itemValue, label: t(option.label) }
<SelectItem key={itemValue} value={itemValue}> }),
{t(option.label)} ]}
</SelectItem> value={endpointType}
) onValueChange={(v) => v !== null && setEndpointType(v)}
})} >
</SelectGroup> <SelectTrigger id='endpoint-type'>
</SelectContent> <SelectValue placeholder={t('Auto detect (default)')} />
</Select> </SelectTrigger>
<p className='text-muted-foreground text-xs'> <SelectContent alignItemWithTrigger={false}>
{t( <SelectGroup>
'Override the endpoint used for testing. Leave empty to auto detect.' {endpointTypeOptions.map((option) => {
)} const itemValue = option.value
</p> return (
</div> <SelectItem key={itemValue} value={itemValue}>
<div className='grid gap-2'> {t(option.label)}
<Label htmlFor='stream-toggle'>{t('Stream Mode')}</Label> </SelectItem>
<div className='flex items-center gap-2'> )
<Switch })}
id='stream-toggle' </SelectGroup>
checked={isStreamTest} </SelectContent>
onCheckedChange={setIsStreamTest} </Select>
disabled={streamDisabled}
/>
<span className='text-sm'>
{isStreamTest ? t('Enabled') : t('Disabled')}
</span>
</div>
<p className='text-muted-foreground text-xs'>
{t('Enable streaming mode for the test request.')}
</p>
</div>
</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>
<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(
'Override the endpoint used for testing. Leave empty to auto detect.'
)}
</p>
</div>
<div className='grid gap-2'>
<Label htmlFor='stream-toggle'>{t('Stream Mode')}</Label>
<div className='flex items-center gap-2'>
<Switch
id='stream-toggle'
checked={isStreamTest}
onCheckedChange={setIsStreamTest}
disabled={streamDisabled}
/>
<span className='text-sm'>
{isStreamTest ? t('Enabled') : t('Disabled')}
</span>
</div>
<p className='text-muted-foreground text-xs'>
{t('Enable streaming mode for the test request.')}
</p> </p>
</div> </div>
<Input
placeholder={t('Filter models...')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className='sm:w-64'
/>
</div> </div>
<div className='space-y-3'> <div className='space-y-3 max-sm:has-[div[role="toolbar"]]:pb-16'>
<div className='overflow-hidden rounded-md border' role='region'> <div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<div className='max-h-[360px] overflow-y-auto'> <div>
<Table> <p className='text-sm font-medium'>{t('Channel models')}</p>
<TableHeader> <p className='text-muted-foreground text-xs'>
{table.getHeaderGroups().map((headerGroup) => ( {t('Select models to run batch tests.')}
<TableRow key={headerGroup.id}> </p>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() ? 'selected' : undefined
}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getVisibleLeafColumns().length}
className='text-muted-foreground h-16 text-center text-sm'
>
{models.length
? 'No models matched your search.'
: 'This channel has no configured models.'}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div> </div>
<Input
placeholder={t('Filter models...')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className='sm:w-64'
/>
</div> </div>
<DataTablePagination table={table} /> <div className='space-y-3'>
<div
className='overflow-hidden rounded-md border'
role='region'
aria-label={t('Channel models')}
>
<div className='max-h-90 overflow-auto **:data-[slot=table-container]:overflow-visible'>
<Table className='w-max min-w-full table-auto'>
<colgroup>
<col className='w-10 min-w-10' />
<col className='w-auto' />
<col className='w-70' />
<col className='w-24 sm:w-28' />
</colgroup>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className={getTestTableColumnClass(
header.column.id
)}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() ? 'selected' : undefined
}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={getTestTableColumnClass(
cell.column.id
)}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getVisibleLeafColumns().length}
className='text-muted-foreground h-16 text-center text-sm'
>
{models.length
? 'No models matched your search.'
: 'This channel has no configured models.'}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
<DataTablePagination table={table} />
</div>
<TestModelsBulkActions
table={table}
disabled={isAnyTesting}
onTestSelected={handleBatchTest}
/>
</div> </div>
<TestModelsBulkActions
table={table}
disabled={isAnyTesting}
onTestSelected={handleBatchTest}
/>
</div> </div>
</div>
<DialogFooter> <DialogFooter>
<Button variant='outline' onClick={handleClose}> <Button variant='outline' onClick={handleClose}>
{t('Close')} {t('Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<FailureDetailsSheet
details={failureDetails}
onOpenChange={(sheetOpen) => {
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 (
<StatusBadge label={t('Not tested')} variant='neutral' copyable={false} />
)
}
if (result.status === 'testing') {
return (
<div className='text-muted-foreground flex min-w-0 items-center gap-2 text-sm'>
<Loader2 className='h-4 w-4 shrink-0 animate-spin' />
<span className='truncate'>{t('Testing...')}</span>
</div>
)
}
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 (
<FailureStatusContent
result={result}
model={model}
onOpenDetails={onOpenDetails}
/>
)
}
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 (
<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'>
{summary}
</p>
<div className='flex min-w-0 flex-wrap items-center gap-1.5'>
{isModelPriceError && (
<Button
variant='outline'
size='sm'
className='h-7 w-fit px-2 text-xs'
onClick={() => window.open('/console/setting?tab=ratio', '_blank')}
>
<Settings className='mr-1 h-3 w-3 shrink-0' />
{t('Go to Settings')}
</Button> </Button>
</DialogFooter> )}
</DialogContent> {details && (
</Dialog> <Button
variant='ghost'
size='sm'
className='h-7 w-fit px-2 text-xs'
aria-haspopup='dialog'
onClick={() => onOpenDetails({ model, summary, details })}
>
<Info className='mr-1 h-3 w-3 shrink-0' />
{t('Details')}
</Button>
)}
</div>
</div>
)
}
function FailureDetailsSheet({
details,
onOpenChange,
}: {
details: FailureDetailsState | null
onOpenChange: (open: boolean) => void
}) {
const { t } = useTranslation()
const isMobile = useIsMobile()
const { copiedText, copyToClipboard } = useCopyToClipboard({ notify: false })
return (
<Sheet open={Boolean(details)} onOpenChange={onOpenChange}>
<SheetContent
side={isMobile ? 'bottom' : 'right'}
className={
isMobile
? 'max-h-[85dvh] gap-0 overflow-hidden rounded-t-xl p-0'
: 'h-dvh w-full gap-0 overflow-hidden p-0 sm:max-w-lg'
}
>
{details && (
<>
<SheetHeader className='border-b px-4 py-3 text-start sm:px-5 sm:py-4'>
<SheetTitle className='pr-10'>{t('Details')}</SheetTitle>
<SheetDescription className='pr-10 wrap-break-word'>
{details.model}
</SheetDescription>
</SheetHeader>
<div className='min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-3 sm:px-5 sm:py-4'>
<section className='space-y-1'>
<div className='text-muted-foreground text-xs font-medium'>
{t('Model')}
</div>
<p className='text-sm font-medium break-all'>{details.model}</p>
</section>
<section className='space-y-1'>
<div className='text-muted-foreground text-xs font-medium'>
{t('Failed')}
</div>
<p className='text-muted-foreground text-sm leading-relaxed wrap-break-word'>
{details.summary}
</p>
</section>
<section className='space-y-2'>
<div className='text-muted-foreground text-xs font-medium'>
{t('Details')}
</div>
<pre className='bg-muted/30 text-muted-foreground m-0 max-w-full rounded-md border p-3 text-xs leading-relaxed wrap-break-word whitespace-pre-wrap'>
{details.details}
</pre>
</section>
</div>
<SheetFooter className='border-t px-4 py-3 sm:flex-row sm:justify-end sm:px-5'>
<Button
variant='outline'
className='w-full sm:w-auto'
onClick={() => copyToClipboard(details.details)}
>
{copiedText === details.details ? (
<Check className='mr-2 h-4 w-4 text-green-600' />
) : (
<Copy className='mr-2 h-4 w-4' />
)}
{t('Copy')}
</Button>
</SheetFooter>
</>
)}
</SheetContent>
</Sheet>
) )
} }
@@ -615,8 +871,8 @@ function TestModelsBulkActions({
const buttonLabel = const buttonLabel =
selectedModels.length > 0 selectedModels.length > 0
? `Test ${selectedModels.length} selected` ? t('Test {{count}} selected', { count: selectedModels.length })
: 'Test selected models' : t('Test selected models')
return ( return (
<BulkActionsToolbar table={table} entityName='model'> <BulkActionsToolbar table={table} entityName='model'>
+5
View File
@@ -2363,6 +2363,7 @@
"Model performance metrics": "Model performance metrics", "Model performance metrics": "Model performance metrics",
"Model Price": "Model Price", "Model Price": "Model Price",
"Model Price Not Configured": "Model Price Not Configured", "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": "Model prices",
"Model prices reset successfully": "Model prices reset successfully", "Model prices reset successfully": "Model prices reset successfully",
"Model Pricing": "Model Pricing", "Model Pricing": "Model Pricing",
@@ -3512,6 +3513,7 @@
"Select all (filtered)": "Select all (filtered)", "Select all (filtered)": "Select all (filtered)",
"Select all models": "Select all models", "Select all models": "Select all models",
"Select All Visible": "Select All Visible", "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 an operation mode and enter the amount": "Select an operation mode and enter the amount",
"Select announcement type": "Select announcement type", "Select announcement type": "Select announcement type",
"Select at least one field to overwrite.": "Select at least one field to overwrite.", "Select at least one field to overwrite.": "Select at least one field to overwrite.",
@@ -3849,6 +3851,8 @@
"Templates appended": "Templates appended", "Templates appended": "Templates appended",
"Tencent": "Tencent", "Tencent": "Tencent",
"Termination requested": "Termination requested", "Termination requested": "Termination requested",
"Test": "Test",
"Test {{count}} selected": "Test {{count}} selected",
"Test All Channels": "Test All Channels", "Test All Channels": "Test All Channels",
"Test Channel Connection": "Test Channel Connection", "Test Channel Connection": "Test Channel Connection",
"Test Connection": "Test Connection", "Test Connection": "Test Connection",
@@ -3859,6 +3863,7 @@
"Test Mode": "Test Mode", "Test Mode": "Test Mode",
"Test Model": "Test Model", "Test Model": "Test Model",
"Test models and prompts from the browser": "Test models and prompts from the browser", "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 all enabled channels started. Please refresh to see results.": "Testing all enabled channels started. Please refresh to see results.",
"Testing...": "Testing...", "Testing...": "Testing...",
"Text": "Text", "Text": "Text",
+5
View File
@@ -2363,6 +2363,7 @@
"Model performance metrics": "Indicateurs de performance des modèles", "Model performance metrics": "Indicateurs de performance des modèles",
"Model Price": "Prix du modèle", "Model Price": "Prix du modèle",
"Model Price Not Configured": "Prix du modèle non configuré", "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": "Prix des modèles",
"Model prices reset successfully": "Prix des modèles réinitialisés avec succès", "Model prices reset successfully": "Prix des modèles réinitialisés avec succès",
"Model Pricing": "Tarification des modèles", "Model Pricing": "Tarification des modèles",
@@ -3512,6 +3513,7 @@
"Select all (filtered)": "Tout sélectionner (filtré)", "Select all (filtered)": "Tout sélectionner (filtré)",
"Select all models": "Sélectionner tous les modèles", "Select all models": "Sélectionner tous les modèles",
"Select All Visible": "Sélectionner tout ce qui est visible", "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 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 announcement type": "Sélectionner le type d'annonce",
"Select at least one field to overwrite.": "Sélectionnez au moins un champ à écraser.", "Select at least one field to overwrite.": "Sélectionnez au moins un champ à écraser.",
@@ -3849,6 +3851,8 @@
"Templates appended": "Modèles ajoutés", "Templates appended": "Modèles ajoutés",
"Tencent": "Tencent", "Tencent": "Tencent",
"Termination requested": "Arrêt demandé", "Termination requested": "Arrêt demandé",
"Test": "Tester",
"Test {{count}} selected": "Tester {{count}} sélectionné(s)",
"Test All Channels": "Tester tous les canaux", "Test All Channels": "Tester tous les canaux",
"Test Channel Connection": "Tester la connexion du canal", "Test Channel Connection": "Tester la connexion du canal",
"Test Connection": "Tester la connexion", "Test Connection": "Tester la connexion",
@@ -3859,6 +3863,7 @@
"Test Mode": "Mode test", "Test Mode": "Mode test",
"Test Model": "Tester le modèle", "Test Model": "Tester le modèle",
"Test models and prompts from the browser": "Tester les modèles et les prompts depuis le navigateur", "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 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...", "Testing...": "Test en cours...",
"Text": "Texte", "Text": "Texte",
+5
View File
@@ -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 prices": "モデル価格", "Model prices": "モデル価格",
"Model prices reset successfully": "モデル価格が正常にリセットされました", "Model prices reset successfully": "モデル価格が正常にリセットされました",
"Model Pricing": "モデル料金", "Model Pricing": "モデル料金",
@@ -3512,6 +3513,7 @@
"Select all (filtered)": "フィルタ結果をすべて選択(S)", "Select all (filtered)": "フィルタ結果をすべて選択(S)",
"Select all models": "すべてのモデルを選択", "Select all models": "すべてのモデルを選択",
"Select All Visible": "表示中のすべてを選択", "Select All Visible": "表示中のすべてを選択",
"Select model {{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.": "上書きするフィールドを少なくとも 1 つ選択してください。", "Select at least one field to overwrite.": "上書きするフィールドを少なくとも 1 つ選択してください。",
@@ -3849,6 +3851,8 @@
"Templates appended": "テンプレートが追加されました", "Templates appended": "テンプレートが追加されました",
"Tencent": "テンセント", "Tencent": "テンセント",
"Termination requested": "終了リクエスト済み", "Termination requested": "終了リクエスト済み",
"Test": "テスト",
"Test {{count}} selected": "選択済み {{count}} 件をテスト",
"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": "選択したモデルをテスト",
"Testing all enabled channels started. Please refresh to see results.": "有効な全チャネルのテストを開始しました。結果を確認するにはページを更新してください。", "Testing all enabled channels started. Please refresh to see results.": "有効な全チャネルのテストを開始しました。結果を確認するにはページを更新してください。",
"Testing...": "テスト中...", "Testing...": "テスト中...",
"Text": "テキスト", "Text": "テキスト",
+5
View File
@@ -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 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}}": "Выбрать модель {{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", "Tencent": "Tencent",
"Termination requested": "Запрошено завершение", "Termination requested": "Запрошено завершение",
"Test": "Проверить",
"Test {{count}} selected": "Проверить {{count}} выбранных",
"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": "Проверить выбранные модели",
"Testing all enabled channels started. Please refresh to see results.": "Тестирование всех включенных каналов начато. Пожалуйста, обновите страницу, чтобы увидеть результаты.", "Testing all enabled channels started. Please refresh to see results.": "Тестирование всех включенных каналов начато. Пожалуйста, обновите страницу, чтобы увидеть результаты.",
"Testing...": "Тестирование...", "Testing...": "Тестирование...",
"Text": "Текст", "Text": "Текст",
+5
View File
@@ -2363,6 +2363,7 @@
"Model performance metrics": "Chỉ số hiệu năng mô hình", "Model performance metrics": "Chỉ số hiệu năng mô hình",
"Model Price": "Giá 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 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": "Giá mô hình",
"Model prices reset successfully": "Đã đặt lại giá mô hình thành công", "Model prices reset successfully": "Đã đặt lại giá mô hình thành công",
"Model Pricing": "Định giá mô hình", "Model Pricing": "Định giá mô hình",
@@ -3512,6 +3513,7 @@
"Select all (filtered)": "Chọn tất cả (đã lọc)", "Select all (filtered)": "Chọn tất cả (đã lọc)",
"Select all models": "Chọn tất cả mô hình", "Select all models": "Chọn tất cả mô hình",
"Select All Visible": "Chọn tất cả hiển thị", "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 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 announcement type": "Select notification type",
"Select at least one field to overwrite.": "Chọn ít nhất một trường để ghi đè.", "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", "Templates appended": "Đã thêm mẫu",
"Tencent": "Tencent", "Tencent": "Tencent",
"Termination requested": "Yêu cầu chấm dứt", "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 All Channels": "Kiểm tra tất cả các kênh",
"Test Channel Connection": "Check channel connection", "Test Channel Connection": "Check channel connection",
"Test Connection": "Kiểm tra kết nối", "Test Connection": "Kiểm tra kết nối",
@@ -3859,6 +3863,7 @@
"Test Mode": "Chế độ thử nghiệm", "Test Mode": "Chế độ thử nghiệm",
"Test Model": "Kiểm tra Mô hình", "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 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 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...", "Testing...": "Đang kiểm tra...",
"Text": "Văn bản", "Text": "Văn bản",
+5
View File
@@ -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 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}}": "选择模型 {{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 {{count}} selected": "测试 {{count}} 个已选择项",
"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": "测试所选模型",
"Testing all enabled channels started. Please refresh to see results.": "测试所有启用的通道已开始。请刷新以查看结果。", "Testing all enabled channels started. Please refresh to see results.": "测试所有启用的通道已开始。请刷新以查看结果。",
"Testing...": "测试中...", "Testing...": "测试中...",
"Text": "文本", "Text": "文本",