feat: limit dashboard flow nodes
This commit is contained in:
+165
-109
@@ -32,13 +32,8 @@ import {
|
|||||||
WalletCards,
|
WalletCards,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
|
||||||
import { formatNumber, formatQuota } from '@/lib/format'
|
import { MultiSelect } from '@/components/multi-select'
|
||||||
import { ROLE } from '@/lib/roles'
|
|
||||||
import { computeTimeRange } from '@/lib/time'
|
|
||||||
import { useChartTheme } from '@/lib/use-chart-theme'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { VCHART_OPTION } from '@/lib/vchart'
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
@@ -56,7 +51,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { MultiSelect } from '@/components/multi-select'
|
|
||||||
import { getFlowQuotaDates } from '@/features/dashboard/api'
|
import { getFlowQuotaDates } from '@/features/dashboard/api'
|
||||||
import {
|
import {
|
||||||
buildDashboardFlowData,
|
buildDashboardFlowData,
|
||||||
@@ -74,23 +68,34 @@ import type {
|
|||||||
DashboardFilters,
|
DashboardFilters,
|
||||||
FlowMetric,
|
FlowMetric,
|
||||||
FlowNodeKind,
|
FlowNodeKind,
|
||||||
|
FlowOverflowMode,
|
||||||
FlowRole,
|
FlowRole,
|
||||||
FlowSummary,
|
|
||||||
} from '@/features/dashboard/types'
|
} from '@/features/dashboard/types'
|
||||||
|
import { formatQuota } from '@/lib/format'
|
||||||
|
import { ROLE } from '@/lib/roles'
|
||||||
|
import { computeTimeRange } from '@/lib/time'
|
||||||
|
import { useChartTheme } from '@/lib/use-chart-theme'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { VCHART_OPTION } from '@/lib/vchart'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
|
||||||
interface FlowChartsProps {
|
interface FlowChartsProps {
|
||||||
filters?: DashboardFilters
|
filters?: DashboardFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlowStatsProps {
|
|
||||||
summary: FlowSummary
|
|
||||||
loading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const FLOW_METRIC_OPTIONS = [
|
const FLOW_METRIC_OPTIONS = [
|
||||||
{ value: 'quota', labelKey: 'Quota', icon: WalletCards },
|
{ value: 'quota', labelKey: 'By quota', icon: WalletCards },
|
||||||
{ value: 'tokens', labelKey: 'Tokens', icon: Hash },
|
{ value: 'tokens', labelKey: 'By tokens', icon: Hash },
|
||||||
{ value: 'requests', labelKey: 'Requests', icon: Activity },
|
{ value: 'requests', labelKey: 'By requests', icon: Activity },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const FLOW_TOP_LIMIT_OPTIONS = [10, 20, 50, 100] as const
|
||||||
|
|
||||||
|
const DEFAULT_FLOW_TOP_NODE_LIMIT = 50
|
||||||
|
|
||||||
|
const FLOW_OVERFLOW_MODE_OPTIONS = [
|
||||||
|
{ value: 'aggregate', labelKey: 'Merge into Other' },
|
||||||
|
{ value: 'hide', labelKey: 'Hide' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
// A Sankey needs at least two columns to render any link.
|
// A Sankey needs at least two columns to render any link.
|
||||||
@@ -126,58 +131,13 @@ const FLOW_STAGE_META: Record<
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlowStats(props: FlowStatsProps) {
|
const FLOW_OTHER_NODE_LABEL_KEYS: Record<FlowNodeKind, string> = {
|
||||||
const { t } = useTranslation()
|
user: 'Other users',
|
||||||
const items = [
|
node: 'Other nodes',
|
||||||
{
|
token: 'Other tokens',
|
||||||
key: 'quota',
|
group: 'Other groups',
|
||||||
title: 'Quota',
|
model: 'Other models',
|
||||||
value: formatQuota(props.summary.quota),
|
channel: 'Other channels',
|
||||||
icon: WalletCards,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tokens',
|
|
||||||
title: 'Tokens',
|
|
||||||
value: formatNumber(props.summary.tokens),
|
|
||||||
icon: Hash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'requests',
|
|
||||||
title: 'Requests',
|
|
||||||
value: formatNumber(props.summary.requests),
|
|
||||||
icon: Activity,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='overflow-hidden rounded-lg border'>
|
|
||||||
<div className='divide-border/60 grid grid-cols-3 divide-x'>
|
|
||||||
{items.map((item) => {
|
|
||||||
const Icon = item.icon
|
|
||||||
return (
|
|
||||||
<div key={item.key} className='px-3 py-2.5 sm:px-5 sm:py-4'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
|
|
||||||
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
|
|
||||||
{t(item.title)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{props.loading ? (
|
|
||||||
<div className='mt-2 flex flex-col gap-1.5'>
|
|
||||||
<Skeleton className='h-7 w-20' />
|
|
||||||
<Skeleton className='h-3.5 w-28' />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='text-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
|
||||||
{item.value}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlowCharts(props: FlowChartsProps) {
|
export function FlowCharts(props: FlowChartsProps) {
|
||||||
@@ -188,6 +148,9 @@ export function FlowCharts(props: FlowChartsProps) {
|
|||||||
const isAdmin = Boolean(user?.role && user.role >= ROLE.ADMIN)
|
const isAdmin = Boolean(user?.role && user.role >= ROLE.ADMIN)
|
||||||
const flowRole: FlowRole = isRoot ? 'root' : isAdmin ? 'admin' : 'user'
|
const flowRole: FlowRole = isRoot ? 'root' : isAdmin ? 'admin' : 'user'
|
||||||
const [metric, setMetric] = useState<FlowMetric>('quota')
|
const [metric, setMetric] = useState<FlowMetric>('quota')
|
||||||
|
const [topNodeLimit, setTopNodeLimit] = useState(DEFAULT_FLOW_TOP_NODE_LIMIT)
|
||||||
|
const [overflowMode, setOverflowMode] =
|
||||||
|
useState<FlowOverflowMode>('aggregate')
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
||||||
const [hiddenStages, setHiddenStages] = useState<FlowNodeKind[]>([])
|
const [hiddenStages, setHiddenStages] = useState<FlowNodeKind[]>([])
|
||||||
|
|
||||||
@@ -247,9 +210,22 @@ export function FlowCharts(props: FlowChartsProps) {
|
|||||||
role: flowRole,
|
role: flowRole,
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
visibleStages,
|
visibleStages,
|
||||||
|
topNodeLimit,
|
||||||
|
overflowMode,
|
||||||
deletedTokenLabel: (tokenId) => t('Deleted ({{id}})', { id: tokenId }),
|
deletedTokenLabel: (tokenId) => t('Deleted ({{id}})', { id: tokenId }),
|
||||||
|
otherNodeLabel: (kind) => t(FLOW_OTHER_NODE_LABEL_KEYS[kind]),
|
||||||
}),
|
}),
|
||||||
[flowRole, flowRows, isLoading, metric, selectedUsers, visibleStages, t]
|
[
|
||||||
|
flowRole,
|
||||||
|
flowRows,
|
||||||
|
isLoading,
|
||||||
|
metric,
|
||||||
|
overflowMode,
|
||||||
|
selectedUsers,
|
||||||
|
topNodeLimit,
|
||||||
|
visibleStages,
|
||||||
|
t,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
const userFilterOptions = useMemo(
|
const userFilterOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -273,6 +249,8 @@ export function FlowCharts(props: FlowChartsProps) {
|
|||||||
const chartTheme = resolvedTheme === 'dark' ? 'dark' : 'light'
|
const chartTheme = resolvedTheme === 'dark' ? 'dark' : 'light'
|
||||||
const chartKey = [
|
const chartKey = [
|
||||||
metric,
|
metric,
|
||||||
|
topNodeLimit,
|
||||||
|
overflowMode,
|
||||||
flowRole,
|
flowRole,
|
||||||
selectedUsers.join(','),
|
selectedUsers.join(','),
|
||||||
visibleStages.join(','),
|
visibleStages.join(','),
|
||||||
@@ -292,46 +270,124 @@ export function FlowCharts(props: FlowChartsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-3'>
|
<div className='flex flex-col gap-3'>
|
||||||
<FlowStats summary={flowData.summary} loading={isLoading} />
|
<div className='flex flex-col gap-2 xl:flex-row xl:items-end xl:justify-between'>
|
||||||
|
<div className='flex min-w-0 flex-wrap items-end gap-2'>
|
||||||
<div className='flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between'>
|
<div className='flex min-w-0 flex-col gap-1.5'>
|
||||||
<div className='flex flex-wrap items-center gap-2'>
|
<div className='flex items-center gap-1.5'>
|
||||||
<Tabs
|
<span className='text-muted-foreground text-xs font-medium'>
|
||||||
value={metric}
|
{t('Flow width metric')}
|
||||||
onValueChange={(value) => setMetric(value as FlowMetric)}
|
</span>
|
||||||
className='shrink-0'
|
<TooltipProvider>
|
||||||
>
|
<Tooltip>
|
||||||
<TabsList>
|
<TooltipTrigger
|
||||||
{FLOW_METRIC_OPTIONS.map((option) => (
|
render={
|
||||||
<TabsTrigger
|
<button
|
||||||
key={option.value}
|
type='button'
|
||||||
value={option.value}
|
className='text-muted-foreground/60 hover:text-foreground flex size-5 shrink-0 items-center justify-center rounded-md'
|
||||||
className='px-2.5 text-xs'
|
aria-label={t('Flow width metric')}
|
||||||
>
|
/>
|
||||||
{t(option.labelKey)}
|
}
|
||||||
</TabsTrigger>
|
>
|
||||||
))}
|
<Info className='size-3.5' />
|
||||||
</TabsList>
|
</TooltipTrigger>
|
||||||
</Tabs>
|
<TooltipContent className='max-w-[14rem]'>
|
||||||
</div>
|
{t('Choose how flow widths are calculated.')}
|
||||||
{isAdmin && (
|
</TooltipContent>
|
||||||
<div className='flex min-w-0 flex-col gap-2 sm:flex-row lg:w-[min(24rem,34vw)]'>
|
</Tooltip>
|
||||||
<MultiSelect
|
</TooltipProvider>
|
||||||
options={userFilterOptions}
|
</div>
|
||||||
selected={selectedUsers}
|
<Tabs
|
||||||
onChange={setSelectedUsers}
|
value={metric}
|
||||||
placeholder={t('All users')}
|
onValueChange={(value) => setMetric(value as FlowMetric)}
|
||||||
emptyText={t('No users')}
|
className='shrink-0'
|
||||||
maxVisibleChips={2}
|
>
|
||||||
renderSelectedSummary={(values) =>
|
<TabsList aria-label={t('Flow width metric')}>
|
||||||
compactFlowSelectionLabel(values.length)
|
{FLOW_METRIC_OPTIONS.map((option) => {
|
||||||
}
|
const Icon = option.icon
|
||||||
/>
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className='gap-1.5 px-2.5 text-xs'
|
||||||
|
>
|
||||||
|
<Icon data-icon='inline-start' aria-hidden='true' />
|
||||||
|
{t(option.labelKey)}
|
||||||
|
</TabsTrigger>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{isLoading && (
|
<div className='flex min-w-0 flex-col gap-1.5'>
|
||||||
<Loader2 className='text-muted-foreground size-4 animate-spin' />
|
<span className='text-muted-foreground text-xs font-medium'>
|
||||||
)}
|
{t('Display limit')}
|
||||||
|
</span>
|
||||||
|
<Tabs
|
||||||
|
value={String(topNodeLimit)}
|
||||||
|
onValueChange={(value) => setTopNodeLimit(Number(value))}
|
||||||
|
className='shrink-0'
|
||||||
|
>
|
||||||
|
<TabsList aria-label={t('Display limit')}>
|
||||||
|
{FLOW_TOP_LIMIT_OPTIONS.map((limit) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={limit}
|
||||||
|
value={String(limit)}
|
||||||
|
className='px-2.5 text-xs'
|
||||||
|
>
|
||||||
|
{t('Top {{count}}', { count: limit })}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex min-w-0 flex-col gap-1.5'>
|
||||||
|
<span className='text-muted-foreground text-xs font-medium'>
|
||||||
|
{t('Overflow items')}
|
||||||
|
</span>
|
||||||
|
<Tabs
|
||||||
|
value={overflowMode}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setOverflowMode(value as FlowOverflowMode)
|
||||||
|
}
|
||||||
|
className='shrink-0'
|
||||||
|
>
|
||||||
|
<TabsList aria-label={t('Overflow items')}>
|
||||||
|
{FLOW_OVERFLOW_MODE_OPTIONS.map((option) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className='px-2.5 text-xs'
|
||||||
|
>
|
||||||
|
{t(option.labelKey)}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex min-w-0 items-center gap-2 xl:justify-end'>
|
||||||
|
{isAdmin && (
|
||||||
|
<div className='flex min-w-0 flex-col gap-2 sm:flex-row xl:w-[min(24rem,34vw)]'>
|
||||||
|
<MultiSelect
|
||||||
|
options={userFilterOptions}
|
||||||
|
selected={selectedUsers}
|
||||||
|
onChange={setSelectedUsers}
|
||||||
|
placeholder={t('All users')}
|
||||||
|
emptyText={t('No users')}
|
||||||
|
maxVisibleChips={2}
|
||||||
|
renderSelectedSummary={(values) =>
|
||||||
|
compactFlowSelectionLabel(values.length)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<Loader2 className='text-muted-foreground size-4 animate-spin' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-hidden rounded-lg border'>
|
<div className='overflow-hidden rounded-lg border'>
|
||||||
|
|||||||
+144
@@ -1,5 +1,6 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { describe, test } from 'node:test'
|
import { describe, test } from 'node:test'
|
||||||
|
|
||||||
import type { FlowQuotaDataItem } from '../types'
|
import type { FlowQuotaDataItem } from '../types'
|
||||||
import {
|
import {
|
||||||
buildDashboardFlowData,
|
buildDashboardFlowData,
|
||||||
@@ -52,6 +53,42 @@ const rows: FlowQuotaDataItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const topLimitRows: FlowQuotaDataItem[] = [
|
||||||
|
{
|
||||||
|
user_id: 1,
|
||||||
|
username: 'alpha',
|
||||||
|
use_group: 'vip',
|
||||||
|
channel_id: 201,
|
||||||
|
channel_name: 'channel-a',
|
||||||
|
model_name: 'model-a',
|
||||||
|
quota: 100,
|
||||||
|
token_used: 1_000,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user_id: 2,
|
||||||
|
username: 'beta',
|
||||||
|
use_group: 'default',
|
||||||
|
channel_id: 202,
|
||||||
|
channel_name: 'channel-b',
|
||||||
|
model_name: 'model-b',
|
||||||
|
quota: 80,
|
||||||
|
token_used: 10,
|
||||||
|
count: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user_id: 3,
|
||||||
|
username: 'gamma',
|
||||||
|
use_group: 'free',
|
||||||
|
channel_id: 203,
|
||||||
|
channel_name: 'channel-c',
|
||||||
|
model_name: 'model-c',
|
||||||
|
quota: 10,
|
||||||
|
token_used: 2_000,
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
describe('dashboard flow data', () => {
|
describe('dashboard flow data', () => {
|
||||||
test('builds normal user token-group-model flow', () => {
|
test('builds normal user token-group-model flow', () => {
|
||||||
const result = buildDashboardFlowData(rows.slice(0, 2), 'quota', {
|
const result = buildDashboardFlowData(rows.slice(0, 2), 'quota', {
|
||||||
@@ -183,6 +220,113 @@ describe('dashboard flow data', () => {
|
|||||||
assert.notEqual(options.users[0].color, options.users[1].color)
|
assert.notEqual(options.users[0].color, options.users[1].color)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('aggregates overflow nodes into per-column Other buckets', () => {
|
||||||
|
const result = buildDashboardFlowData(topLimitRows, 'quota', {
|
||||||
|
role: 'admin',
|
||||||
|
topNodeLimit: 2,
|
||||||
|
overflowMode: 'aggregate',
|
||||||
|
otherNodeLabel: (kind) => `Other ${kind}`,
|
||||||
|
})
|
||||||
|
const nodeIds = result.flow.nodes.map((node) => node.id)
|
||||||
|
const otherUser = result.flow.nodes.find(
|
||||||
|
(node) => node.id === 'user:__other__'
|
||||||
|
)
|
||||||
|
const otherFirstStepLink = result.flow.links.find(
|
||||||
|
(link) =>
|
||||||
|
link.source === 'user:__other__' && link.target === 'group:__other__'
|
||||||
|
)
|
||||||
|
const firstStepTotal = result.flow.links
|
||||||
|
.filter((link) => link.source.startsWith('user:'))
|
||||||
|
.reduce((sum, link) => sum + link.value, 0)
|
||||||
|
|
||||||
|
assert.equal(result.summary.quota, 190)
|
||||||
|
assert.equal(firstStepTotal, 190)
|
||||||
|
assert.equal(otherUser?.label, 'Other user')
|
||||||
|
assert.equal(otherFirstStepLink?.value, 10)
|
||||||
|
assert.equal(nodeIds.includes('user:3'), false)
|
||||||
|
assert.equal(nodeIds.includes('group:free'), false)
|
||||||
|
assert.equal(nodeIds.includes('model:model-c'), false)
|
||||||
|
assert.equal(nodeIds.includes('channel:203'), false)
|
||||||
|
assert.equal(nodeIds.includes('user:__other__'), true)
|
||||||
|
assert.equal(nodeIds.includes('group:__other__'), true)
|
||||||
|
assert.equal(nodeIds.includes('model:__other__'), true)
|
||||||
|
assert.equal(nodeIds.includes('channel:__other__'), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hides overflow paths when overflow mode is hide', () => {
|
||||||
|
const result = buildDashboardFlowData(topLimitRows, 'quota', {
|
||||||
|
role: 'admin',
|
||||||
|
topNodeLimit: 2,
|
||||||
|
overflowMode: 'hide',
|
||||||
|
otherNodeLabel: (kind) => `Other ${kind}`,
|
||||||
|
})
|
||||||
|
const nodeIds = result.flow.nodes.map((node) => node.id)
|
||||||
|
const firstStepTotal = result.flow.links
|
||||||
|
.filter((link) => link.source.startsWith('user:'))
|
||||||
|
.reduce((sum, link) => sum + link.value, 0)
|
||||||
|
|
||||||
|
assert.equal(result.summary.quota, 190)
|
||||||
|
assert.equal(firstStepTotal, 180)
|
||||||
|
assert.equal(nodeIds.includes('user:3'), false)
|
||||||
|
assert.equal(nodeIds.includes('user:__other__'), false)
|
||||||
|
assert.equal(nodeIds.includes('model:__other__'), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ranks top nodes using the selected flow metric', () => {
|
||||||
|
const byQuota = buildDashboardFlowData(topLimitRows, 'quota', {
|
||||||
|
role: 'admin',
|
||||||
|
topNodeLimit: 1,
|
||||||
|
overflowMode: 'aggregate',
|
||||||
|
})
|
||||||
|
const byRequests = buildDashboardFlowData(topLimitRows, 'requests', {
|
||||||
|
role: 'admin',
|
||||||
|
topNodeLimit: 1,
|
||||||
|
overflowMode: 'aggregate',
|
||||||
|
})
|
||||||
|
const byTokens = buildDashboardFlowData(topLimitRows, 'tokens', {
|
||||||
|
role: 'admin',
|
||||||
|
topNodeLimit: 1,
|
||||||
|
overflowMode: 'aggregate',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
byQuota.flow.nodes.some((node) => node.id === 'user:1'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
byRequests.flow.nodes.some((node) => node.id === 'user:2'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
byTokens.flow.nodes.some((node) => node.id === 'user:3'),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('applies top limits only to visible stages', () => {
|
||||||
|
const result = buildDashboardFlowData(topLimitRows, 'quota', {
|
||||||
|
role: 'admin',
|
||||||
|
visibleStages: ['user', 'model'],
|
||||||
|
topNodeLimit: 1,
|
||||||
|
overflowMode: 'aggregate',
|
||||||
|
})
|
||||||
|
const nodeIds = result.flow.nodes.map((node) => node.id)
|
||||||
|
|
||||||
|
assert.equal(nodeIds.includes('user:1'), true)
|
||||||
|
assert.equal(nodeIds.includes('user:__other__'), true)
|
||||||
|
assert.equal(nodeIds.includes('model:model-a'), true)
|
||||||
|
assert.equal(nodeIds.includes('model:__other__'), true)
|
||||||
|
assert.equal(nodeIds.includes('group:__other__'), false)
|
||||||
|
assert.equal(nodeIds.includes('channel:__other__'), false)
|
||||||
|
assert.deepEqual(
|
||||||
|
result.flow.links.map((link) => [link.source, link.target, link.value]),
|
||||||
|
[
|
||||||
|
['user:__other__', 'model:__other__', 90],
|
||||||
|
['user:1', 'model:model-a', 100],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('builds Sankey spec with quota token request tooltips', () => {
|
test('builds Sankey spec with quota token request tooltips', () => {
|
||||||
const result = buildDashboardFlowData(rows.slice(0, 1), 'quota', {
|
const result = buildDashboardFlowData(rows.slice(0, 1), 'quota', {
|
||||||
role: 'root',
|
role: 'root',
|
||||||
|
|||||||
+177
-33
@@ -24,11 +24,13 @@ import type {
|
|||||||
FlowFilterOptions,
|
FlowFilterOptions,
|
||||||
FlowMetric,
|
FlowMetric,
|
||||||
FlowNodeKind,
|
FlowNodeKind,
|
||||||
|
FlowOverflowMode,
|
||||||
FlowQuotaDataItem,
|
FlowQuotaDataItem,
|
||||||
FlowRole,
|
FlowRole,
|
||||||
FlowSummary,
|
FlowSummary,
|
||||||
ProcessedFlowData,
|
ProcessedFlowData,
|
||||||
} from '@/features/dashboard/types'
|
} from '@/features/dashboard/types'
|
||||||
|
|
||||||
import { getDashboardChartColors } from './charts'
|
import { getDashboardChartColors } from './charts'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -53,14 +55,32 @@ type FlowPathNode = {
|
|||||||
kind: FlowNodeKind
|
kind: FlowNodeKind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreparedFlowPath = {
|
||||||
|
path: FlowPathNode[]
|
||||||
|
metrics: FlowMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlowNodeRank = {
|
||||||
|
node: FlowPathNode
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
type FlowPathContext = {
|
type FlowPathContext = {
|
||||||
deletedTokenLabel?: (tokenId: number) => string
|
deletedTokenLabel?: (tokenId: number) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FlowGraphOptions = {
|
||||||
|
topNodeLimit?: number
|
||||||
|
overflowMode?: FlowOverflowMode
|
||||||
|
otherNodeLabel?: (kind: FlowNodeKind) => string
|
||||||
|
}
|
||||||
|
|
||||||
const EMPTY_FLOW_PATH_CONTEXT: FlowPathContext = {}
|
const EMPTY_FLOW_PATH_CONTEXT: FlowPathContext = {}
|
||||||
|
|
||||||
const DEFAULT_FLOW_ROLE: FlowRole = 'user'
|
const DEFAULT_FLOW_ROLE: FlowRole = 'user'
|
||||||
|
|
||||||
|
const DEFAULT_FLOW_OVERFLOW_MODE: FlowOverflowMode = 'aggregate'
|
||||||
|
|
||||||
const DEFAULT_FLOW_SANKEY_LABELS: FlowSankeyLabels = {
|
const DEFAULT_FLOW_SANKEY_LABELS: FlowSankeyLabels = {
|
||||||
quota: 'Quota',
|
quota: 'Quota',
|
||||||
tokens: 'Tokens',
|
tokens: 'Tokens',
|
||||||
@@ -70,6 +90,24 @@ const DEFAULT_FLOW_SANKEY_LABELS: FlowSankeyLabels = {
|
|||||||
|
|
||||||
const DEFAULT_FLOW_CHART_COLOR = '#1664FF'
|
const DEFAULT_FLOW_CHART_COLOR = '#1664FF'
|
||||||
|
|
||||||
|
const OTHER_FLOW_NODE_IDS: Record<FlowNodeKind, string> = {
|
||||||
|
user: 'user:__other__',
|
||||||
|
node: 'node:__other__',
|
||||||
|
token: 'token:__other__',
|
||||||
|
group: 'group:__other__',
|
||||||
|
model: 'model:__other__',
|
||||||
|
channel: 'channel:__other__',
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_OTHER_FLOW_NODE_LABELS: Record<FlowNodeKind, string> = {
|
||||||
|
user: 'Other users',
|
||||||
|
node: 'Other nodes',
|
||||||
|
token: 'Other tokens',
|
||||||
|
group: 'Other groups',
|
||||||
|
model: 'Other models',
|
||||||
|
channel: 'Other channels',
|
||||||
|
}
|
||||||
|
|
||||||
function numberValue(value: unknown): number {
|
function numberValue(value: unknown): number {
|
||||||
const n = Number(value)
|
const n = Number(value)
|
||||||
return Number.isFinite(n) ? n : 0
|
return Number.isFinite(n) ? n : 0
|
||||||
@@ -107,13 +145,11 @@ function nodeNameNode(row: FlowQuotaDataItem): FlowPathNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenNode(
|
function tokenNode(row: FlowQuotaDataItem, ctx: FlowPathContext): FlowPathNode {
|
||||||
row: FlowQuotaDataItem,
|
|
||||||
ctx: FlowPathContext
|
|
||||||
): FlowPathNode {
|
|
||||||
const tokenID = numberValue(row.token_id)
|
const tokenID = numberValue(row.token_id)
|
||||||
return {
|
return {
|
||||||
id: tokenID > 0 ? `token:${tokenID}` : `token:${row.token_name || 'unknown'}`,
|
id:
|
||||||
|
tokenID > 0 ? `token:${tokenID}` : `token:${row.token_name || 'unknown'}`,
|
||||||
label: row.token_name || deletedTokenLabel(tokenID, ctx),
|
label: row.token_name || deletedTokenLabel(tokenID, ctx),
|
||||||
kind: 'token',
|
kind: 'token',
|
||||||
}
|
}
|
||||||
@@ -192,15 +228,12 @@ function resolveVisibleStages(
|
|||||||
return filtered.length >= MIN_FLOW_STAGES ? filtered : stages
|
return filtered.length >= MIN_FLOW_STAGES ? filtered : stages
|
||||||
}
|
}
|
||||||
|
|
||||||
function flowPath(
|
function flowPathForStages(
|
||||||
row: FlowQuotaDataItem,
|
row: FlowQuotaDataItem,
|
||||||
role: FlowRole,
|
stages: FlowNodeKind[],
|
||||||
visibleStages?: FlowNodeKind[],
|
|
||||||
ctx: FlowPathContext = EMPTY_FLOW_PATH_CONTEXT
|
ctx: FlowPathContext = EMPTY_FLOW_PATH_CONTEXT
|
||||||
): FlowPathNode[] {
|
): FlowPathNode[] {
|
||||||
return resolveVisibleStages(role, visibleStages).map((stage) =>
|
return stages.map((stage) => NODE_BUILDERS[stage](row, ctx))
|
||||||
NODE_BUILDERS[stage](row, ctx)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorAt(index: number, palette?: readonly string[]): string {
|
function colorAt(index: number, palette?: readonly string[]): string {
|
||||||
@@ -252,18 +285,6 @@ function stableColorMap(
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
function rootColorKeys(
|
|
||||||
rows: FlowQuotaDataItem[],
|
|
||||||
role: FlowRole,
|
|
||||||
visibleStages?: FlowNodeKind[]
|
|
||||||
): string[] {
|
|
||||||
return Array.from(
|
|
||||||
new Set(
|
|
||||||
rows.map((row) => flowPath(row, role, visibleStages)[0]?.id ?? 'unknown')
|
|
||||||
)
|
|
||||||
).sort((a, b) => a.localeCompare(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterRows(
|
function filterRows(
|
||||||
rows: FlowQuotaDataItem[],
|
rows: FlowQuotaDataItem[],
|
||||||
options: FlowBuildOptions = {}
|
options: FlowBuildOptions = {}
|
||||||
@@ -392,26 +413,137 @@ function buildSummary(rows: FlowQuotaDataItem[]): FlowSummary {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTopNodeLimit(limit?: number): number | undefined {
|
||||||
|
if (limit === undefined) return undefined
|
||||||
|
const parsed = Math.floor(limit)
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function otherFlowNode(
|
||||||
|
kind: FlowNodeKind,
|
||||||
|
labeler?: (kind: FlowNodeKind) => string
|
||||||
|
): FlowPathNode {
|
||||||
|
return {
|
||||||
|
id: OTHER_FLOW_NODE_IDS[kind],
|
||||||
|
label: labeler?.(kind) ?? DEFAULT_OTHER_FLOW_NODE_LABELS[kind],
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTopNodeSets(
|
||||||
|
rows: FlowQuotaDataItem[],
|
||||||
|
metric: FlowMetric,
|
||||||
|
stages: FlowNodeKind[],
|
||||||
|
limit: number | undefined,
|
||||||
|
ctx: FlowPathContext
|
||||||
|
): Map<FlowNodeKind, Set<string>> | undefined {
|
||||||
|
if (!limit) return undefined
|
||||||
|
|
||||||
|
const totals = new Map<FlowNodeKind, Map<string, FlowNodeRank>>()
|
||||||
|
for (const stage of stages) {
|
||||||
|
totals.set(stage, new Map())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const metrics = rowMetrics(row)
|
||||||
|
const value = metricValue(metrics, metric)
|
||||||
|
const path = flowPathForStages(row, stages, ctx)
|
||||||
|
for (const node of path) {
|
||||||
|
const stageTotals = totals.get(node.kind)
|
||||||
|
if (!stageTotals) continue
|
||||||
|
const current = stageTotals.get(node.id) ?? { node, value: 0 }
|
||||||
|
current.value += value
|
||||||
|
stageTotals.set(node.id, current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topSets = new Map<FlowNodeKind, Set<string>>()
|
||||||
|
for (const [kind, stageTotals] of totals) {
|
||||||
|
const topIds = Array.from(stageTotals.values())
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.value - a.value ||
|
||||||
|
a.node.label.localeCompare(b.node.label) ||
|
||||||
|
a.node.id.localeCompare(b.node.id)
|
||||||
|
)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((rank) => rank.node.id)
|
||||||
|
topSets.set(kind, new Set(topIds))
|
||||||
|
}
|
||||||
|
|
||||||
|
return topSets
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTopFlowNode(
|
||||||
|
node: FlowPathNode,
|
||||||
|
topNodeSets?: Map<FlowNodeKind, Set<string>>
|
||||||
|
): boolean {
|
||||||
|
const topNodes = topNodeSets?.get(node.kind)
|
||||||
|
return !topNodes || topNodes.has(node.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTopNodeLimit(
|
||||||
|
path: FlowPathNode[],
|
||||||
|
topNodeSets: Map<FlowNodeKind, Set<string>> | undefined,
|
||||||
|
mode: FlowOverflowMode,
|
||||||
|
labeler?: (kind: FlowNodeKind) => string
|
||||||
|
): FlowPathNode[] | undefined {
|
||||||
|
if (!topNodeSets) return path
|
||||||
|
const containsOverflowNode = path.some(
|
||||||
|
(node) => !isTopFlowNode(node, topNodeSets)
|
||||||
|
)
|
||||||
|
if (!containsOverflowNode) return path
|
||||||
|
if (mode === 'hide') return undefined
|
||||||
|
return path.map((node) =>
|
||||||
|
isTopFlowNode(node, topNodeSets) ? node : otherFlowNode(node.kind, labeler)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function buildFlowGraph(
|
function buildFlowGraph(
|
||||||
rows: FlowQuotaDataItem[],
|
rows: FlowQuotaDataItem[],
|
||||||
metric: FlowMetric,
|
metric: FlowMetric,
|
||||||
role: FlowRole,
|
role: FlowRole,
|
||||||
palette?: readonly string[],
|
palette?: readonly string[],
|
||||||
visibleStages?: FlowNodeKind[],
|
visibleStages?: FlowNodeKind[],
|
||||||
ctx: FlowPathContext = EMPTY_FLOW_PATH_CONTEXT
|
ctx: FlowPathContext = EMPTY_FLOW_PATH_CONTEXT,
|
||||||
|
options: FlowGraphOptions = {}
|
||||||
): DashboardFlowGraph {
|
): DashboardFlowGraph {
|
||||||
|
const stages = resolveVisibleStages(role, visibleStages)
|
||||||
|
const topNodeSets = buildTopNodeSets(
|
||||||
|
rows,
|
||||||
|
metric,
|
||||||
|
stages,
|
||||||
|
normalizeTopNodeLimit(options.topNodeLimit),
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
const overflowMode = options.overflowMode ?? DEFAULT_FLOW_OVERFLOW_MODE
|
||||||
|
const preparedPaths: PreparedFlowPath[] = []
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const path = applyTopNodeLimit(
|
||||||
|
flowPathForStages(row, stages, ctx),
|
||||||
|
topNodeSets,
|
||||||
|
overflowMode,
|
||||||
|
options.otherNodeLabel
|
||||||
|
)
|
||||||
|
if (!path) continue
|
||||||
|
preparedPaths.push({ path, metrics: rowMetrics(row) })
|
||||||
|
}
|
||||||
|
|
||||||
const nodes = new Map<string, DashboardFlowNode>()
|
const nodes = new Map<string, DashboardFlowNode>()
|
||||||
const links = new Map<string, DashboardFlowLink>()
|
const links = new Map<string, DashboardFlowLink>()
|
||||||
const colors = stableColorMap(
|
const colors = stableColorMap(
|
||||||
rootColorKeys(rows, role, visibleStages),
|
preparedPaths
|
||||||
|
.map((prepared) => prepared.path[0]?.id)
|
||||||
|
.filter((id): id is string => Boolean(id))
|
||||||
|
.sort((a, b) => a.localeCompare(b)),
|
||||||
palette
|
palette
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const prepared of preparedPaths) {
|
||||||
const path = flowPath(row, role, visibleStages, ctx)
|
const { path, metrics } = prepared
|
||||||
const root = path[0]
|
const root = path[0]
|
||||||
if (!root) continue
|
if (!root) continue
|
||||||
const metrics = rowMetrics(row)
|
|
||||||
const color = colors.get(root.id) ?? colorAt(0, palette)
|
const color = colors.get(root.id) ?? colorAt(0, palette)
|
||||||
|
|
||||||
for (const node of path) {
|
for (const node of path) {
|
||||||
@@ -430,8 +562,8 @@ function buildFlowGraph(
|
|||||||
a.source.localeCompare(b.source) || a.target.localeCompare(b.target)
|
a.source.localeCompare(b.source) || a.target.localeCompare(b.target)
|
||||||
)
|
)
|
||||||
const firstStepSources = new Set(
|
const firstStepSources = new Set(
|
||||||
rows
|
preparedPaths
|
||||||
.map((row) => flowPath(row, role, visibleStages)[0]?.id)
|
.map((prepared) => prepared.path[0]?.id)
|
||||||
.filter((id): id is string => Boolean(id))
|
.filter((id): id is string => Boolean(id))
|
||||||
)
|
)
|
||||||
const total = flowLinks
|
const total = flowLinks
|
||||||
@@ -512,9 +644,21 @@ export function buildDashboardFlowData(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
summary: buildSummary(filteredRows),
|
summary: buildSummary(filteredRows),
|
||||||
flow: buildFlowGraph(filteredRows, metric, role, palette, options.visibleStages, {
|
flow: buildFlowGraph(
|
||||||
deletedTokenLabel: options.deletedTokenLabel,
|
filteredRows,
|
||||||
}),
|
metric,
|
||||||
|
role,
|
||||||
|
palette,
|
||||||
|
options.visibleStages,
|
||||||
|
{
|
||||||
|
deletedTokenLabel: options.deletedTokenLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topNodeLimit: options.topNodeLimit,
|
||||||
|
overflowMode: options.overflowMode,
|
||||||
|
otherNodeLabel: options.otherNodeLabel,
|
||||||
|
}
|
||||||
|
),
|
||||||
filterOptions: buildFlowFilterOptions(rows, metric, palette),
|
filterOptions: buildFlowFilterOptions(rows, metric, palette),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -50,6 +50,8 @@ export interface FlowQuotaDataItem {
|
|||||||
|
|
||||||
export type FlowMetric = 'quota' | 'tokens' | 'requests'
|
export type FlowMetric = 'quota' | 'tokens' | 'requests'
|
||||||
|
|
||||||
|
export type FlowOverflowMode = 'aggregate' | 'hide'
|
||||||
|
|
||||||
export type FlowRole = 'user' | 'admin' | 'root'
|
export type FlowRole = 'user' | 'admin' | 'root'
|
||||||
|
|
||||||
export type FlowNodeKind =
|
export type FlowNodeKind =
|
||||||
@@ -65,9 +67,12 @@ export interface FlowBuildOptions {
|
|||||||
selectedUsers?: string[]
|
selectedUsers?: string[]
|
||||||
colorPalette?: readonly string[]
|
colorPalette?: readonly string[]
|
||||||
visibleStages?: FlowNodeKind[]
|
visibleStages?: FlowNodeKind[]
|
||||||
|
topNodeLimit?: number
|
||||||
|
overflowMode?: FlowOverflowMode
|
||||||
// Resolves the label for a token whose record no longer exists (deleted).
|
// Resolves the label for a token whose record no longer exists (deleted).
|
||||||
// Lets the caller inject a localized string such as "Deleted (123)".
|
// Lets the caller inject a localized string such as "Deleted (123)".
|
||||||
deletedTokenLabel?: (tokenId: number) => string
|
deletedTokenLabel?: (tokenId: number) => string
|
||||||
|
otherNodeLabel?: (kind: FlowNodeKind) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardFlowNode {
|
export interface DashboardFlowNode {
|
||||||
|
|||||||
Vendored
+14
@@ -625,6 +625,9 @@
|
|||||||
"by": "by",
|
"by": "by",
|
||||||
"By category": "By category",
|
"By category": "By category",
|
||||||
"By model author": "By model author",
|
"By model author": "By model author",
|
||||||
|
"By quota": "By quota",
|
||||||
|
"By requests": "By requests",
|
||||||
|
"By tokens": "By tokens",
|
||||||
"Cache": "Cache",
|
"Cache": "Cache",
|
||||||
"Cache create (1h) price": "Cache create (1h) price",
|
"Cache create (1h) price": "Cache create (1h) price",
|
||||||
"Cache create price": "Cache create price",
|
"Cache create price": "Cache create price",
|
||||||
@@ -746,6 +749,7 @@
|
|||||||
"Choose between system preference, light mode, or dark mode": "Choose between system preference, light mode, or dark mode",
|
"Choose between system preference, light mode, or dark mode": "Choose between system preference, light mode, or dark mode",
|
||||||
"Choose channels to sync upstream ratio configurations from": "Choose channels to sync upstream ratio configurations from",
|
"Choose channels to sync upstream ratio configurations from": "Choose channels to sync upstream ratio configurations from",
|
||||||
"Choose Group": "Choose Group",
|
"Choose Group": "Choose Group",
|
||||||
|
"Choose how flow widths are calculated.": "Choose how flow widths are calculated.",
|
||||||
"Choose how quota values are shown to users": "Choose how quota values are shown to users",
|
"Choose how quota values are shown to users": "Choose how quota values are shown to users",
|
||||||
"Choose how the platform will operate": "Choose how the platform will operate",
|
"Choose how the platform will operate": "Choose how the platform will operate",
|
||||||
"Choose how to filter domains": "Choose how to filter domains",
|
"Choose how to filter domains": "Choose how to filter domains",
|
||||||
@@ -1315,6 +1319,7 @@
|
|||||||
"Disk Hits": "Disk Hits",
|
"Disk Hits": "Disk Hits",
|
||||||
"Disk Threshold (%)": "Disk Threshold (%)",
|
"Disk Threshold (%)": "Disk Threshold (%)",
|
||||||
"Display in Currency": "Display in Currency",
|
"Display in Currency": "Display in Currency",
|
||||||
|
"Display limit": "Display limit",
|
||||||
"Display Mode": "Display Mode",
|
"Display Mode": "Display Mode",
|
||||||
"Display name": "Display name",
|
"Display name": "Display name",
|
||||||
"Display Name": "Display Name",
|
"Display Name": "Display Name",
|
||||||
@@ -1868,6 +1873,7 @@
|
|||||||
"Floating": "Floating",
|
"Floating": "Floating",
|
||||||
"Flow": "Flow",
|
"Flow": "Flow",
|
||||||
"Flow Filters": "Flow Filters",
|
"Flow Filters": "Flow Filters",
|
||||||
|
"Flow width metric": "Flow width metric",
|
||||||
"FluentRead extension not detected. Please ensure it is installed and active.": "FluentRead extension not detected. Please ensure it is installed and active.",
|
"FluentRead extension not detected. Please ensure it is installed and active.": "FluentRead extension not detected. Please ensure it is installed and active.",
|
||||||
"Flush interval (minutes)": "Flush interval (minutes)",
|
"Flush interval (minutes)": "Flush interval (minutes)",
|
||||||
"Follow the guided steps to prepare your workspace before the first login.": "Follow the guided steps to prepare your workspace before the first login.",
|
"Follow the guided steps to prepare your workspace before the first login.": "Follow the guided steps to prepare your workspace before the first login.",
|
||||||
@@ -2551,6 +2557,7 @@
|
|||||||
"More than 999 days left": "More than 999 days left",
|
"More than 999 days left": "More than 999 days left",
|
||||||
"More...": "More...",
|
"More...": "More...",
|
||||||
"Most-used models in the selected period and category": "Most-used models in the selected period and category",
|
"Most-used models in the selected period and category": "Most-used models in the selected period and category",
|
||||||
|
"Merge into Other": "Merge into Other",
|
||||||
"Move": "Move",
|
"Move": "Move",
|
||||||
"Move a request header": "Move a request header",
|
"Move a request header": "Move a request header",
|
||||||
"Move affiliate rewards to your main balance": "Move affiliate rewards to your main balance",
|
"Move affiliate rewards to your main balance": "Move affiliate rewards to your main balance",
|
||||||
@@ -2936,6 +2943,12 @@
|
|||||||
"org-...": "org-...",
|
"org-...": "org-...",
|
||||||
"Original Model": "Original Model",
|
"Original Model": "Original Model",
|
||||||
"Other": "Other",
|
"Other": "Other",
|
||||||
|
"Other channels": "Other channels",
|
||||||
|
"Other groups": "Other groups",
|
||||||
|
"Other models": "Other models",
|
||||||
|
"Other nodes": "Other nodes",
|
||||||
|
"Other tokens": "Other tokens",
|
||||||
|
"Other users": "Other users",
|
||||||
"Outage": "Outage",
|
"Outage": "Outage",
|
||||||
"Output": "Output",
|
"Output": "Output",
|
||||||
"Output aspect ratio": "Output aspect ratio",
|
"Output aspect ratio": "Output aspect ratio",
|
||||||
@@ -2945,6 +2958,7 @@
|
|||||||
"Output tokens": "Output tokens",
|
"Output tokens": "Output tokens",
|
||||||
"Output Tokens": "Output Tokens",
|
"Output Tokens": "Output Tokens",
|
||||||
"Overage limited": "Overage limited",
|
"Overage limited": "Overage limited",
|
||||||
|
"Overflow items": "Overflow items",
|
||||||
"overall": "overall",
|
"overall": "overall",
|
||||||
"Overnight range": "Overnight range",
|
"Overnight range": "Overnight range",
|
||||||
"override": "override",
|
"override": "override",
|
||||||
|
|||||||
Vendored
+14
@@ -625,6 +625,9 @@
|
|||||||
"by": "par",
|
"by": "par",
|
||||||
"By category": "Par catégorie",
|
"By category": "Par catégorie",
|
||||||
"By model author": "Par auteur du modèle",
|
"By model author": "Par auteur du modèle",
|
||||||
|
"By quota": "Par quota",
|
||||||
|
"By requests": "Par requêtes",
|
||||||
|
"By tokens": "Par jetons",
|
||||||
"Cache": "Cache",
|
"Cache": "Cache",
|
||||||
"Cache create (1h) price": "Prix de création du cache (1 h)",
|
"Cache create (1h) price": "Prix de création du cache (1 h)",
|
||||||
"Cache create price": "Prix de création du cache",
|
"Cache create price": "Prix de création du cache",
|
||||||
@@ -746,6 +749,7 @@
|
|||||||
"Choose between system preference, light mode, or dark mode": "Choisissez entre la préférence système, le mode clair ou le mode sombre",
|
"Choose between system preference, light mode, or dark mode": "Choisissez entre la préférence système, le mode clair ou le mode sombre",
|
||||||
"Choose channels to sync upstream ratio configurations from": "Choisissez les canaux à partir desquels synchroniser les configurations de ratio amont",
|
"Choose channels to sync upstream ratio configurations from": "Choisissez les canaux à partir desquels synchroniser les configurations de ratio amont",
|
||||||
"Choose Group": "Choisir un groupe",
|
"Choose Group": "Choisir un groupe",
|
||||||
|
"Choose how flow widths are calculated.": "Choisissez comment l'épaisseur des flux est calculée.",
|
||||||
"Choose how quota values are shown to users": "Choisissez comment les valeurs de quota sont affichées aux utilisateurs",
|
"Choose how quota values are shown to users": "Choisissez comment les valeurs de quota sont affichées aux utilisateurs",
|
||||||
"Choose how the platform will operate": "Choisissez le mode de fonctionnement de la plateforme",
|
"Choose how the platform will operate": "Choisissez le mode de fonctionnement de la plateforme",
|
||||||
"Choose how to filter domains": "Choisissez comment filtrer les domaines",
|
"Choose how to filter domains": "Choisissez comment filtrer les domaines",
|
||||||
@@ -1315,6 +1319,7 @@
|
|||||||
"Disk Hits": "Hits disque",
|
"Disk Hits": "Hits disque",
|
||||||
"Disk Threshold (%)": "Seuil disque (%)",
|
"Disk Threshold (%)": "Seuil disque (%)",
|
||||||
"Display in Currency": "Afficher en devise",
|
"Display in Currency": "Afficher en devise",
|
||||||
|
"Display limit": "Limite d'affichage",
|
||||||
"Display Mode": "Mode d'affichage",
|
"Display Mode": "Mode d'affichage",
|
||||||
"Display name": "Nom d'affichage",
|
"Display name": "Nom d'affichage",
|
||||||
"Display Name": "Nom d'affichage",
|
"Display Name": "Nom d'affichage",
|
||||||
@@ -1868,6 +1873,7 @@
|
|||||||
"Floating": "Flottant",
|
"Floating": "Flottant",
|
||||||
"Flow": "Flux",
|
"Flow": "Flux",
|
||||||
"Flow Filters": "Filtres de flux",
|
"Flow Filters": "Filtres de flux",
|
||||||
|
"Flow width metric": "Indicateur de largeur du flux",
|
||||||
"FluentRead extension not detected. Please ensure it is installed and active.": "Extension FluentRead non détectée. Veuillez vous assurer qu'elle est installée et activée.",
|
"FluentRead extension not detected. Please ensure it is installed and active.": "Extension FluentRead non détectée. Veuillez vous assurer qu'elle est installée et activée.",
|
||||||
"Flush interval (minutes)": "Intervalle d’écriture (minutes)",
|
"Flush interval (minutes)": "Intervalle d’écriture (minutes)",
|
||||||
"Follow the guided steps to prepare your workspace before the first login.": "Suivez les étapes guidées pour préparer votre espace de travail avant la première connexion.",
|
"Follow the guided steps to prepare your workspace before the first login.": "Suivez les étapes guidées pour préparer votre espace de travail avant la première connexion.",
|
||||||
@@ -2551,6 +2557,7 @@
|
|||||||
"More than 999 days left": "Plus de 999 jours restants",
|
"More than 999 days left": "Plus de 999 jours restants",
|
||||||
"More...": "Plus...",
|
"More...": "Plus...",
|
||||||
"Most-used models in the selected period and category": "Modèles les plus utilisés dans la période et catégorie choisies",
|
"Most-used models in the selected period and category": "Modèles les plus utilisés dans la période et catégorie choisies",
|
||||||
|
"Merge into Other": "Fusionner dans Autres",
|
||||||
"Move": "Déplacer",
|
"Move": "Déplacer",
|
||||||
"Move a request header": "Déplacer un en-tête de requête",
|
"Move a request header": "Déplacer un en-tête de requête",
|
||||||
"Move affiliate rewards to your main balance": "Transférer les récompenses d'affiliation vers votre solde principal",
|
"Move affiliate rewards to your main balance": "Transférer les récompenses d'affiliation vers votre solde principal",
|
||||||
@@ -2936,6 +2943,12 @@
|
|||||||
"org-...": "org-...",
|
"org-...": "org-...",
|
||||||
"Original Model": "Modèle Original",
|
"Original Model": "Modèle Original",
|
||||||
"Other": "Autre",
|
"Other": "Autre",
|
||||||
|
"Other channels": "Autres canaux",
|
||||||
|
"Other groups": "Autres groupes",
|
||||||
|
"Other models": "Autres modèles",
|
||||||
|
"Other nodes": "Autres nœuds",
|
||||||
|
"Other tokens": "Autres jetons",
|
||||||
|
"Other users": "Autres utilisateurs",
|
||||||
"Outage": "Interruption",
|
"Outage": "Interruption",
|
||||||
"Output": "Sortie",
|
"Output": "Sortie",
|
||||||
"Output aspect ratio": "Format d'image de sortie",
|
"Output aspect ratio": "Format d'image de sortie",
|
||||||
@@ -2945,6 +2958,7 @@
|
|||||||
"Output tokens": "Jetons de sortie",
|
"Output tokens": "Jetons de sortie",
|
||||||
"Output Tokens": "Tokens de sortie",
|
"Output Tokens": "Tokens de sortie",
|
||||||
"Overage limited": "Dépassement limité",
|
"Overage limited": "Dépassement limité",
|
||||||
|
"Overflow items": "Éléments excédentaires",
|
||||||
"overall": "global",
|
"overall": "global",
|
||||||
"Overnight range": "Plage nocturne",
|
"Overnight range": "Plage nocturne",
|
||||||
"override": "remplacer",
|
"override": "remplacer",
|
||||||
|
|||||||
Vendored
+14
@@ -625,6 +625,9 @@
|
|||||||
"by": "によって",
|
"by": "によって",
|
||||||
"By category": "カテゴリ別",
|
"By category": "カテゴリ別",
|
||||||
"By model author": "モデル提供者別",
|
"By model author": "モデル提供者別",
|
||||||
|
"By quota": "クォータ別",
|
||||||
|
"By requests": "リクエスト数別",
|
||||||
|
"By tokens": "トークン数別",
|
||||||
"Cache": "キャッシュ",
|
"Cache": "キャッシュ",
|
||||||
"Cache create (1h) price": "キャッシュ作成価格 (1時間)",
|
"Cache create (1h) price": "キャッシュ作成価格 (1時間)",
|
||||||
"Cache create price": "キャッシュ作成価格",
|
"Cache create price": "キャッシュ作成価格",
|
||||||
@@ -746,6 +749,7 @@
|
|||||||
"Choose between system preference, light mode, or dark mode": "システム設定、ライトモード、またはダークモードから選択します",
|
"Choose between system preference, light mode, or dark mode": "システム設定、ライトモード、またはダークモードから選択します",
|
||||||
"Choose channels to sync upstream ratio configurations from": "アップストリームの比率設定を同期するチャネルを選択してください",
|
"Choose channels to sync upstream ratio configurations from": "アップストリームの比率設定を同期するチャネルを選択してください",
|
||||||
"Choose Group": "グループを選択",
|
"Choose Group": "グループを選択",
|
||||||
|
"Choose how flow widths are calculated.": "フローの線幅をどの指標で計算するかを選択します。",
|
||||||
"Choose how quota values are shown to users": "クォータ値がユーザーにどのように表示されるかを選択してください",
|
"Choose how quota values are shown to users": "クォータ値がユーザーにどのように表示されるかを選択してください",
|
||||||
"Choose how the platform will operate": "プラットフォームの運用方法を選択",
|
"Choose how the platform will operate": "プラットフォームの運用方法を選択",
|
||||||
"Choose how to filter domains": "ドメインをフィルタリングする方法を選択してください",
|
"Choose how to filter domains": "ドメインをフィルタリングする方法を選択してください",
|
||||||
@@ -1315,6 +1319,7 @@
|
|||||||
"Disk Hits": "ディスクヒット",
|
"Disk Hits": "ディスクヒット",
|
||||||
"Disk Threshold (%)": "ディスク閾値 (%)",
|
"Disk Threshold (%)": "ディスク閾値 (%)",
|
||||||
"Display in Currency": "通貨で表示",
|
"Display in Currency": "通貨で表示",
|
||||||
|
"Display limit": "表示数",
|
||||||
"Display Mode": "表示モード",
|
"Display Mode": "表示モード",
|
||||||
"Display name": "表示名",
|
"Display name": "表示名",
|
||||||
"Display Name": "表示名",
|
"Display Name": "表示名",
|
||||||
@@ -1868,6 +1873,7 @@
|
|||||||
"Floating": "フローティング",
|
"Floating": "フローティング",
|
||||||
"Flow": "フロー",
|
"Flow": "フロー",
|
||||||
"Flow Filters": "フローフィルター",
|
"Flow Filters": "フローフィルター",
|
||||||
|
"Flow width metric": "線幅の指標",
|
||||||
"FluentRead extension not detected. Please ensure it is installed and active.": "FluentRead 拡張機能が検出されませんでした。インストールされていて有効になっていることを確認してください。",
|
"FluentRead extension not detected. Please ensure it is installed and active.": "FluentRead 拡張機能が検出されませんでした。インストールされていて有効になっていることを確認してください。",
|
||||||
"Flush interval (minutes)": "書き込み間隔(分)",
|
"Flush interval (minutes)": "書き込み間隔(分)",
|
||||||
"Follow the guided steps to prepare your workspace before the first login.": "初回ログイン前に、ガイド付きの手順に従ってワークスペースを準備してください。",
|
"Follow the guided steps to prepare your workspace before the first login.": "初回ログイン前に、ガイド付きの手順に従ってワークスペースを準備してください。",
|
||||||
@@ -2551,6 +2557,7 @@
|
|||||||
"More than 999 days left": "999日以上",
|
"More than 999 days left": "999日以上",
|
||||||
"More...": "その他...",
|
"More...": "その他...",
|
||||||
"Most-used models in the selected period and category": "選択した期間とカテゴリで最も使われているモデル",
|
"Most-used models in the selected period and category": "選択した期間とカテゴリで最も使われているモデル",
|
||||||
|
"Merge into Other": "その他にまとめる",
|
||||||
"Move": "移動",
|
"Move": "移動",
|
||||||
"Move a request header": "リクエストヘッダーを移動",
|
"Move a request header": "リクエストヘッダーを移動",
|
||||||
"Move affiliate rewards to your main balance": "アフィリエイト報酬をメイン残高に移動する",
|
"Move affiliate rewards to your main balance": "アフィリエイト報酬をメイン残高に移動する",
|
||||||
@@ -2936,6 +2943,12 @@
|
|||||||
"org-...": "org-...",
|
"org-...": "org-...",
|
||||||
"Original Model": "オリジナルモデル",
|
"Original Model": "オリジナルモデル",
|
||||||
"Other": "その他",
|
"Other": "その他",
|
||||||
|
"Other channels": "その他のチャネル",
|
||||||
|
"Other groups": "その他のグループ",
|
||||||
|
"Other models": "その他のモデル",
|
||||||
|
"Other nodes": "その他のノード",
|
||||||
|
"Other tokens": "その他のトークン",
|
||||||
|
"Other users": "その他のユーザー",
|
||||||
"Outage": "ダウンタイム",
|
"Outage": "ダウンタイム",
|
||||||
"Output": "出力",
|
"Output": "出力",
|
||||||
"Output aspect ratio": "出力アスペクト比",
|
"Output aspect ratio": "出力アスペクト比",
|
||||||
@@ -2945,6 +2958,7 @@
|
|||||||
"Output tokens": "出力トークン",
|
"Output tokens": "出力トークン",
|
||||||
"Output Tokens": "出力トークン",
|
"Output Tokens": "出力トークン",
|
||||||
"Overage limited": "超過利用制限中",
|
"Overage limited": "超過利用制限中",
|
||||||
|
"Overflow items": "超過項目",
|
||||||
"overall": "全体",
|
"overall": "全体",
|
||||||
"Overnight range": "日跨ぎ範囲",
|
"Overnight range": "日跨ぎ範囲",
|
||||||
"override": "上書き",
|
"override": "上書き",
|
||||||
|
|||||||
Vendored
+14
@@ -625,6 +625,9 @@
|
|||||||
"by": "от",
|
"by": "от",
|
||||||
"By category": "По категориям",
|
"By category": "По категориям",
|
||||||
"By model author": "По автору модели",
|
"By model author": "По автору модели",
|
||||||
|
"By quota": "По квоте",
|
||||||
|
"By requests": "По запросам",
|
||||||
|
"By tokens": "По токенам",
|
||||||
"Cache": "Кэш",
|
"Cache": "Кэш",
|
||||||
"Cache create (1h) price": "Цена создания кэша (1 ч)",
|
"Cache create (1h) price": "Цена создания кэша (1 ч)",
|
||||||
"Cache create price": "Цена создания кэша",
|
"Cache create price": "Цена создания кэша",
|
||||||
@@ -746,6 +749,7 @@
|
|||||||
"Choose between system preference, light mode, or dark mode": "Выберите между системными настройками, светлым режимом или темным режимом",
|
"Choose between system preference, light mode, or dark mode": "Выберите между системными настройками, светлым режимом или темным режимом",
|
||||||
"Choose channels to sync upstream ratio configurations from": "Выберите каналы для синхронизации конфигураций соотношений из вышестоящих источников",
|
"Choose channels to sync upstream ratio configurations from": "Выберите каналы для синхронизации конфигураций соотношений из вышестоящих источников",
|
||||||
"Choose Group": "Выбрать группу",
|
"Choose Group": "Выбрать группу",
|
||||||
|
"Choose how flow widths are calculated.": "Выберите, как рассчитывается ширина потоков.",
|
||||||
"Choose how quota values are shown to users": "Выберите, как значения квоты отображаются пользователям",
|
"Choose how quota values are shown to users": "Выберите, как значения квоты отображаются пользователям",
|
||||||
"Choose how the platform will operate": "Выберите режим работы платформы",
|
"Choose how the platform will operate": "Выберите режим работы платформы",
|
||||||
"Choose how to filter domains": "Выберите, как фильтровать домены",
|
"Choose how to filter domains": "Выберите, как фильтровать домены",
|
||||||
@@ -1315,6 +1319,7 @@
|
|||||||
"Disk Hits": "Попаданий диска",
|
"Disk Hits": "Попаданий диска",
|
||||||
"Disk Threshold (%)": "Порог диска (%)",
|
"Disk Threshold (%)": "Порог диска (%)",
|
||||||
"Display in Currency": "Отображать в валюте",
|
"Display in Currency": "Отображать в валюте",
|
||||||
|
"Display limit": "Лимит отображения",
|
||||||
"Display Mode": "Режим отображения",
|
"Display Mode": "Режим отображения",
|
||||||
"Display name": "Отображаемое имя",
|
"Display name": "Отображаемое имя",
|
||||||
"Display Name": "Отображаемое имя",
|
"Display Name": "Отображаемое имя",
|
||||||
@@ -1868,6 +1873,7 @@
|
|||||||
"Floating": "Плавающая",
|
"Floating": "Плавающая",
|
||||||
"Flow": "Поток",
|
"Flow": "Поток",
|
||||||
"Flow Filters": "Фильтры потока",
|
"Flow Filters": "Фильтры потока",
|
||||||
|
"Flow width metric": "Метрика ширины потока",
|
||||||
"FluentRead extension not detected. Please ensure it is installed and active.": "Расширение FluentRead не обнаружено. Убедитесь, что оно установлено и активно.",
|
"FluentRead extension not detected. Please ensure it is installed and active.": "Расширение FluentRead не обнаружено. Убедитесь, что оно установлено и активно.",
|
||||||
"Flush interval (minutes)": "Интервал записи (минуты)",
|
"Flush interval (minutes)": "Интервал записи (минуты)",
|
||||||
"Follow the guided steps to prepare your workspace before the first login.": "Следуйте пошаговым инструкциям, чтобы подготовить рабочее пространство перед первым входом.",
|
"Follow the guided steps to prepare your workspace before the first login.": "Следуйте пошаговым инструкциям, чтобы подготовить рабочее пространство перед первым входом.",
|
||||||
@@ -2551,6 +2557,7 @@
|
|||||||
"More than 999 days left": "Более 999 дней",
|
"More than 999 days left": "Более 999 дней",
|
||||||
"More...": "Подробнее...",
|
"More...": "Подробнее...",
|
||||||
"Most-used models in the selected period and category": "Самые используемые модели в выбранном периоде и категории",
|
"Most-used models in the selected period and category": "Самые используемые модели в выбранном периоде и категории",
|
||||||
|
"Merge into Other": "Объединить в «Другое»",
|
||||||
"Move": "Переместить",
|
"Move": "Переместить",
|
||||||
"Move a request header": "Переместить заголовок запроса",
|
"Move a request header": "Переместить заголовок запроса",
|
||||||
"Move affiliate rewards to your main balance": "Перевести партнерские вознаграждения на основной баланс",
|
"Move affiliate rewards to your main balance": "Перевести партнерские вознаграждения на основной баланс",
|
||||||
@@ -2936,6 +2943,12 @@
|
|||||||
"org-...": "орг-...",
|
"org-...": "орг-...",
|
||||||
"Original Model": "Оригинальная модель",
|
"Original Model": "Оригинальная модель",
|
||||||
"Other": "Другое",
|
"Other": "Другое",
|
||||||
|
"Other channels": "Другие каналы",
|
||||||
|
"Other groups": "Другие группы",
|
||||||
|
"Other models": "Другие модели",
|
||||||
|
"Other nodes": "Другие узлы",
|
||||||
|
"Other tokens": "Другие токены",
|
||||||
|
"Other users": "Другие пользователи",
|
||||||
"Outage": "Простой",
|
"Outage": "Простой",
|
||||||
"Output": "Вывод",
|
"Output": "Вывод",
|
||||||
"Output aspect ratio": "Соотношение сторон",
|
"Output aspect ratio": "Соотношение сторон",
|
||||||
@@ -2945,6 +2958,7 @@
|
|||||||
"Output tokens": "Выходные токены",
|
"Output tokens": "Выходные токены",
|
||||||
"Output Tokens": "Выходные токены",
|
"Output Tokens": "Выходные токены",
|
||||||
"Overage limited": "Ограничение перерасхода",
|
"Overage limited": "Ограничение перерасхода",
|
||||||
|
"Overflow items": "Элементы сверх лимита",
|
||||||
"overall": "всего",
|
"overall": "всего",
|
||||||
"Overnight range": "Диапазон через полночь",
|
"Overnight range": "Диапазон через полночь",
|
||||||
"override": "переопределить",
|
"override": "переопределить",
|
||||||
|
|||||||
Vendored
+14
@@ -625,6 +625,9 @@
|
|||||||
"by": "by",
|
"by": "by",
|
||||||
"By category": "Theo danh mục",
|
"By category": "Theo danh mục",
|
||||||
"By model author": "Theo nhà phát triển mô hình",
|
"By model author": "Theo nhà phát triển mô hình",
|
||||||
|
"By quota": "Theo hạn ngạch",
|
||||||
|
"By requests": "Theo số yêu cầu",
|
||||||
|
"By tokens": "Theo số token",
|
||||||
"Cache": "Bộ nhớ đệm",
|
"Cache": "Bộ nhớ đệm",
|
||||||
"Cache create (1h) price": "Giá tạo cache (1 giờ)",
|
"Cache create (1h) price": "Giá tạo cache (1 giờ)",
|
||||||
"Cache create price": "Giá tạo cache",
|
"Cache create price": "Giá tạo cache",
|
||||||
@@ -746,6 +749,7 @@
|
|||||||
"Choose between system preference, light mode, or dark mode": "Lựa chọn giữa tùy chọn hệ thống, chế độ sáng hoặc chế độ tối",
|
"Choose between system preference, light mode, or dark mode": "Lựa chọn giữa tùy chọn hệ thống, chế độ sáng hoặc chế độ tối",
|
||||||
"Choose channels to sync upstream ratio configurations from": "Chọn các kênh để đồng bộ cấu hình tỷ lệ đường lên từ",
|
"Choose channels to sync upstream ratio configurations from": "Chọn các kênh để đồng bộ cấu hình tỷ lệ đường lên từ",
|
||||||
"Choose Group": "Chọn Nhóm",
|
"Choose Group": "Chọn Nhóm",
|
||||||
|
"Choose how flow widths are calculated.": "Chọn cách tính độ rộng của các luồng.",
|
||||||
"Choose how quota values are shown to users": "Chọn cách hiển thị giá trị hạn ngạch cho người dùng",
|
"Choose how quota values are shown to users": "Chọn cách hiển thị giá trị hạn ngạch cho người dùng",
|
||||||
"Choose how the platform will operate": "Chọn cách nền tảng sẽ hoạt động",
|
"Choose how the platform will operate": "Chọn cách nền tảng sẽ hoạt động",
|
||||||
"Choose how to filter domains": "Chọn cách lọc tên miền",
|
"Choose how to filter domains": "Chọn cách lọc tên miền",
|
||||||
@@ -1315,6 +1319,7 @@
|
|||||||
"Disk Hits": "Lượt truy cập đĩa",
|
"Disk Hits": "Lượt truy cập đĩa",
|
||||||
"Disk Threshold (%)": "Ngưỡng đĩa (%)",
|
"Disk Threshold (%)": "Ngưỡng đĩa (%)",
|
||||||
"Display in Currency": "Hiển thị tiền tệ",
|
"Display in Currency": "Hiển thị tiền tệ",
|
||||||
|
"Display limit": "Giới hạn hiển thị",
|
||||||
"Display Mode": "Chế độ hiển thị",
|
"Display Mode": "Chế độ hiển thị",
|
||||||
"Display name": "Tên hiển thị",
|
"Display name": "Tên hiển thị",
|
||||||
"Display Name": "Tên hiển thị",
|
"Display Name": "Tên hiển thị",
|
||||||
@@ -1868,6 +1873,7 @@
|
|||||||
"Floating": "Nổi",
|
"Floating": "Nổi",
|
||||||
"Flow": "Luồng",
|
"Flow": "Luồng",
|
||||||
"Flow Filters": "Bộ lọc luồng",
|
"Flow Filters": "Bộ lọc luồng",
|
||||||
|
"Flow width metric": "Chỉ số độ rộng luồng",
|
||||||
"FluentRead extension not detected. Please ensure it is installed and active.": "Không phát hiện tiện ích mở rộng FluentRead. Vui lòng đảm bảo nó đã được cài đặt và kích hoạt.",
|
"FluentRead extension not detected. Please ensure it is installed and active.": "Không phát hiện tiện ích mở rộng FluentRead. Vui lòng đảm bảo nó đã được cài đặt và kích hoạt.",
|
||||||
"Flush interval (minutes)": "Khoảng ghi xuống DB (phút)",
|
"Flush interval (minutes)": "Khoảng ghi xuống DB (phút)",
|
||||||
"Follow the guided steps to prepare your workspace before the first login.": "Thực hiện theo các bước hướng dẫn để chuẩn bị không gian làm việc của bạn trước lần đăng nhập đầu tiên.",
|
"Follow the guided steps to prepare your workspace before the first login.": "Thực hiện theo các bước hướng dẫn để chuẩn bị không gian làm việc của bạn trước lần đăng nhập đầu tiên.",
|
||||||
@@ -2551,6 +2557,7 @@
|
|||||||
"More than 999 days left": "Hơn 999 ngày",
|
"More than 999 days left": "Hơn 999 ngày",
|
||||||
"More...": "Thêm...",
|
"More...": "Thêm...",
|
||||||
"Most-used models in the selected period and category": "Mô hình được dùng nhiều nhất trong khoảng thời gian và danh mục đã chọn",
|
"Most-used models in the selected period and category": "Mô hình được dùng nhiều nhất trong khoảng thời gian và danh mục đã chọn",
|
||||||
|
"Merge into Other": "Gộp vào Khác",
|
||||||
"Move": "Di chuyển",
|
"Move": "Di chuyển",
|
||||||
"Move a request header": "Di chuyển header yêu cầu",
|
"Move a request header": "Di chuyển header yêu cầu",
|
||||||
"Move affiliate rewards to your main balance": "Chuyển phần thưởng liên kết vào số dư chính của bạn",
|
"Move affiliate rewards to your main balance": "Chuyển phần thưởng liên kết vào số dư chính của bạn",
|
||||||
@@ -2936,6 +2943,12 @@
|
|||||||
"org-...": "org-...",
|
"org-...": "org-...",
|
||||||
"Original Model": "Nguyên mẫu",
|
"Original Model": "Nguyên mẫu",
|
||||||
"Other": "Khác",
|
"Other": "Khác",
|
||||||
|
"Other channels": "Kênh khác",
|
||||||
|
"Other groups": "Nhóm khác",
|
||||||
|
"Other models": "Mô hình khác",
|
||||||
|
"Other nodes": "Nút khác",
|
||||||
|
"Other tokens": "Token khác",
|
||||||
|
"Other users": "Người dùng khác",
|
||||||
"Outage": "Gián đoạn",
|
"Outage": "Gián đoạn",
|
||||||
"Output": "Đầu ra",
|
"Output": "Đầu ra",
|
||||||
"Output aspect ratio": "Tỉ lệ khung hình",
|
"Output aspect ratio": "Tỉ lệ khung hình",
|
||||||
@@ -2945,6 +2958,7 @@
|
|||||||
"Output tokens": "Token đầu ra",
|
"Output tokens": "Token đầu ra",
|
||||||
"Output Tokens": "Token đầu ra",
|
"Output Tokens": "Token đầu ra",
|
||||||
"Overage limited": "Đã giới hạn vượt mức",
|
"Overage limited": "Đã giới hạn vượt mức",
|
||||||
|
"Overflow items": "Mục vượt giới hạn",
|
||||||
"overall": "tổng",
|
"overall": "tổng",
|
||||||
"Overnight range": "Khoảng qua nửa đêm",
|
"Overnight range": "Khoảng qua nửa đêm",
|
||||||
"override": "ghi đè",
|
"override": "ghi đè",
|
||||||
|
|||||||
Vendored
+14
@@ -625,6 +625,9 @@
|
|||||||
"by": "由",
|
"by": "由",
|
||||||
"By category": "按行业",
|
"By category": "按行业",
|
||||||
"By model author": "按模型厂商",
|
"By model author": "按模型厂商",
|
||||||
|
"By quota": "按额度",
|
||||||
|
"By requests": "按请求数",
|
||||||
|
"By tokens": "按 Token",
|
||||||
"Cache": "缓存",
|
"Cache": "缓存",
|
||||||
"Cache create (1h) price": "1 小时缓存写入价格",
|
"Cache create (1h) price": "1 小时缓存写入价格",
|
||||||
"Cache create price": "缓存写入价格",
|
"Cache create price": "缓存写入价格",
|
||||||
@@ -746,6 +749,7 @@
|
|||||||
"Choose between system preference, light mode, or dark mode": "选择系统偏好、浅色模式或深色模式",
|
"Choose between system preference, light mode, or dark mode": "选择系统偏好、浅色模式或深色模式",
|
||||||
"Choose channels to sync upstream ratio configurations from": "选择要同步上游比例配置的渠道",
|
"Choose channels to sync upstream ratio configurations from": "选择要同步上游比例配置的渠道",
|
||||||
"Choose Group": "选择分组",
|
"Choose Group": "选择分组",
|
||||||
|
"Choose how flow widths are calculated.": "选择分流图连线宽度的计算方式。",
|
||||||
"Choose how quota values are shown to users": "选择如何向用户展示配额值",
|
"Choose how quota values are shown to users": "选择如何向用户展示配额值",
|
||||||
"Choose how the platform will operate": "选择平台的运行模式",
|
"Choose how the platform will operate": "选择平台的运行模式",
|
||||||
"Choose how to filter domains": "选择如何过滤域名",
|
"Choose how to filter domains": "选择如何过滤域名",
|
||||||
@@ -1315,6 +1319,7 @@
|
|||||||
"Disk Hits": "磁盘命中",
|
"Disk Hits": "磁盘命中",
|
||||||
"Disk Threshold (%)": "磁盘阈值 (%)",
|
"Disk Threshold (%)": "磁盘阈值 (%)",
|
||||||
"Display in Currency": "以货币显示",
|
"Display in Currency": "以货币显示",
|
||||||
|
"Display limit": "显示数量",
|
||||||
"Display Mode": "显示模式",
|
"Display Mode": "显示模式",
|
||||||
"Display name": "显示名称",
|
"Display name": "显示名称",
|
||||||
"Display Name": "显示名称",
|
"Display Name": "显示名称",
|
||||||
@@ -1868,6 +1873,7 @@
|
|||||||
"Floating": "浮动",
|
"Floating": "浮动",
|
||||||
"Flow": "分流",
|
"Flow": "分流",
|
||||||
"Flow Filters": "分流筛选",
|
"Flow Filters": "分流筛选",
|
||||||
|
"Flow width metric": "连线粗细",
|
||||||
"FluentRead extension not detected. Please ensure it is installed and active.": "未检测到 FluentRead 扩展。请确保已安装并激活。",
|
"FluentRead extension not detected. Please ensure it is installed and active.": "未检测到 FluentRead 扩展。请确保已安装并激活。",
|
||||||
"Flush interval (minutes)": "刷库间隔(分钟)",
|
"Flush interval (minutes)": "刷库间隔(分钟)",
|
||||||
"Follow the guided steps to prepare your workspace before the first login.": "请按照引导步骤在首次登录前准备您的工作区。",
|
"Follow the guided steps to prepare your workspace before the first login.": "请按照引导步骤在首次登录前准备您的工作区。",
|
||||||
@@ -2551,6 +2557,7 @@
|
|||||||
"More than 999 days left": "剩余超过 999 天",
|
"More than 999 days left": "剩余超过 999 天",
|
||||||
"More...": "更多...",
|
"More...": "更多...",
|
||||||
"Most-used models in the selected period and category": "所选时间范围与分类下使用率最高的模型",
|
"Most-used models in the selected period and category": "所选时间范围与分类下使用率最高的模型",
|
||||||
|
"Merge into Other": "合并为其他",
|
||||||
"Move": "移动",
|
"Move": "移动",
|
||||||
"Move a request header": "移动请求头",
|
"Move a request header": "移动请求头",
|
||||||
"Move affiliate rewards to your main balance": "将推广奖励转移到您的主余额",
|
"Move affiliate rewards to your main balance": "将推广奖励转移到您的主余额",
|
||||||
@@ -2936,6 +2943,12 @@
|
|||||||
"org-...": "org-...",
|
"org-...": "org-...",
|
||||||
"Original Model": "原始模型",
|
"Original Model": "原始模型",
|
||||||
"Other": "其他",
|
"Other": "其他",
|
||||||
|
"Other channels": "其他渠道",
|
||||||
|
"Other groups": "其他分组",
|
||||||
|
"Other models": "其他模型",
|
||||||
|
"Other nodes": "其他节点",
|
||||||
|
"Other tokens": "其他令牌",
|
||||||
|
"Other users": "其他用户",
|
||||||
"Outage": "中断",
|
"Outage": "中断",
|
||||||
"Output": "输出",
|
"Output": "输出",
|
||||||
"Output aspect ratio": "输出宽高比",
|
"Output aspect ratio": "输出宽高比",
|
||||||
@@ -2945,6 +2958,7 @@
|
|||||||
"Output tokens": "输出 token",
|
"Output tokens": "输出 token",
|
||||||
"Output Tokens": "输出 Token",
|
"Output Tokens": "输出 Token",
|
||||||
"Overage limited": "超额受限",
|
"Overage limited": "超额受限",
|
||||||
|
"Overflow items": "超出项",
|
||||||
"overall": "总体",
|
"overall": "总体",
|
||||||
"Overnight range": "跨日范围",
|
"Overnight range": "跨日范围",
|
||||||
"override": "覆盖",
|
"override": "覆盖",
|
||||||
|
|||||||
Reference in New Issue
Block a user