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:
+466
-210
@@ -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'>
|
||||||
|
|||||||
Vendored
+5
@@ -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",
|
||||||
|
|||||||
Vendored
+5
@@ -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",
|
||||||
|
|||||||
Vendored
+5
@@ -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": "テキスト",
|
||||||
|
|||||||
Vendored
+5
@@ -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": "Текст",
|
||||||
|
|||||||
Vendored
+5
@@ -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",
|
||||||
|
|||||||
Vendored
+5
@@ -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": "文本",
|
||||||
|
|||||||
Reference in New Issue
Block a user