Commit 8ad83bf6 by CaIon

feat(dashboard): interactive sankey highlighting and persistent filters

- Highlight full paths through a clicked node or link in the flow Sankey,
  dimming unrelated nodes/links instead of removing them
- Disable VChart built-in emphasis to avoid crash, use custom highlight sets
- Initialize models filter dialog from currently applied filters so manual
  time ranges are not overridden by preferences; auto-pick granularity by range
- Lift user charts time range/granularity/limit to dashboard as controlled state
parent 06194801
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useMemo } from 'react'
import { Filter, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from '@/components/ui/popover'
import type {
FlowNodeFilter,
FlowNodeFilterOption,
FlowNodeKind,
} from '@/features/dashboard/types'
interface FlowNodeFilterControlProps {
stages: FlowNodeKind[]
stageLabels: Record<FlowNodeKind, string>
metricLabel: string
formatMetricValue: (value: number) => string
options: FlowNodeFilterOption[]
selectedNodes: FlowNodeFilter[]
onToggleNode: (filter: FlowNodeFilter) => void
onRemoveNode: (filter: FlowNodeFilter) => void
onClearNodes: () => void
}
function flowNodeFilterKey(filter: FlowNodeFilter): string {
return `${filter.kind}\u0000${filter.id}`
}
export function FlowNodeFilterControl(props: FlowNodeFilterControlProps) {
const { t } = useTranslation()
const selectedKeys = useMemo(
() => new Set(props.selectedNodes.map(flowNodeFilterKey)),
[props.selectedNodes]
)
const optionLabels = useMemo(() => {
const labels = new Map<string, FlowNodeFilterOption>()
for (const option of props.options) {
labels.set(
flowNodeFilterKey({ kind: option.kind, id: option.value }),
option
)
}
return labels
}, [props.options])
const optionsByStage = useMemo(
() =>
props.stages
.map((stage) => ({
stage,
options: props.options.filter((option) => option.kind === stage),
}))
.filter((group) => group.options.length > 0),
[props.options, props.stages]
)
const selectedOptions = props.selectedNodes.map((filter) => {
const option = optionLabels.get(flowNodeFilterKey(filter))
return {
...filter,
label: option?.label ?? filter.id,
}
})
const selectedCount = selectedOptions.length
return (
<div className='flex min-w-0 flex-col gap-1.5'>
<span className='text-muted-foreground text-xs font-medium'>
{t('Node filters')}
</span>
<div className='flex min-w-0 flex-wrap items-center gap-1.5'>
<Popover>
<PopoverTrigger
render={
<Button
type='button'
variant='outline'
size='sm'
aria-label={t('Filter by node')}
/>
}
>
<Filter data-icon='inline-start' aria-hidden='true' />
{selectedCount > 0 ? t('Selected nodes') : t('All nodes')}
{selectedCount > 0 && (
<Badge variant='secondary' className='rounded-sm px-1'>
{selectedCount}
</Badge>
)}
</PopoverTrigger>
<PopoverContent
className='w-[min(28rem,calc(100vw-2rem))] p-0'
align='start'
>
<PopoverHeader className='px-3 pt-3'>
<PopoverTitle>{t('Node filters')}</PopoverTitle>
<PopoverDescription>
{t('Value metric')}: {props.metricLabel}
</PopoverDescription>
</PopoverHeader>
<Command>
<CommandInput placeholder={t('Filter by node')} />
<CommandList>
<CommandEmpty>{t('No nodes')}</CommandEmpty>
{optionsByStage.map((group) => {
const stageLabel = t(props.stageLabels[group.stage])
return (
<CommandGroup key={group.stage} heading={stageLabel}>
{group.options.map((option) => {
const key = flowNodeFilterKey({
kind: option.kind,
id: option.value,
})
const metricValueLabel = props.formatMetricValue(
option.valueRaw
)
return (
<CommandItem
key={key}
value={`${stageLabel} ${option.label} ${props.metricLabel} ${metricValueLabel}`}
data-checked={selectedKeys.has(key)}
onSelect={() =>
props.onToggleNode({
kind: option.kind,
id: option.value,
})
}
>
<span
className='size-2.5 shrink-0 rounded-full'
style={{ backgroundColor: option.color }}
aria-hidden='true'
/>
<span className='min-w-0 flex-1 truncate'>
{option.label}
</span>
<span className='text-muted-foreground flex shrink-0 items-center gap-1 text-xs'>
<span>{props.metricLabel}</span>
<span className='font-mono'>
{metricValueLabel}
</span>
</span>
</CommandItem>
)
})}
</CommandGroup>
)
})}
{selectedCount > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={props.onClearNodes}
className='justify-center text-center'
>
{t('Clear node filters')}
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
{selectedOptions.map((option) => (
<Badge
key={flowNodeFilterKey(option)}
variant='secondary'
className='max-w-[14rem] rounded-sm pr-1'
>
<span className='truncate'>
{t(props.stageLabels[option.kind])}: {option.label}
</span>
<button
type='button'
className='hover:bg-muted-foreground/15 flex size-4 shrink-0 items-center justify-center rounded-sm'
aria-label={t('Remove node filter')}
onClick={() =>
props.onRemoveNode({ kind: option.kind, id: option.id })
}
>
<X aria-hidden='true' />
</button>
</Badge>
))}
{selectedCount > 1 && (
<Button
type='button'
variant='ghost'
size='xs'
onClick={props.onClearNodes}
>
{t('Clear')}
</Button>
)}
</div>
</div>
)
}
......@@ -51,12 +51,36 @@ import type {
interface ModelsFilterProps {
preferences: DashboardChartPreferences
// The filters currently applied to the dashboard. The dialog edits a copy of
// these so reopening it never discards a manually picked range.
currentFilters: DashboardFilters
onFilterChange: (filters: DashboardFilters) => void
onReset: () => void
titleKey?: string
descriptionKey?: string
}
// Quick-range presets imply a sensible granularity (matching the app's
// range<->granularity pairing), so picking "7 Days" requests daily buckets
// instead of leaving the granularity on its previous value (e.g. hourly).
function granularityForRangeDays(days: number): TimeGranularity {
if (days <= 1) return 'hour'
if (days >= 29) return 'week'
return 'day'
}
// Highlights the matching quick-range button when the applied range spans an
// exact preset; custom ranges leave every quick button unselected.
function detectQuickRangeDays(
filters: DashboardFilters | undefined
): number | null {
const start = filters?.start_timestamp
const end = filters?.end_timestamp
if (!start || !end) return null
const days = Math.round((end.getTime() - start.getTime()) / 86_400_000)
return TIME_RANGE_PRESETS.some((preset) => preset.days === days) ? days : null
}
/**
* Section divider component for better visual organization
*/
......@@ -78,20 +102,22 @@ export function ModelsFilter(props: ModelsFilterProps) {
const isAdmin = user?.role && user.role >= 10
const [open, setOpen] = useState(false)
const [filters, setFilters] = useState<DashboardFilters>(() =>
buildDefaultDashboardFilters(props.preferences)
const [filters, setFilters] = useState<DashboardFilters>(
() => props.currentFilters ?? buildDefaultDashboardFilters(props.preferences)
)
const [selectedRange, setSelectedRange] = useState<number | null>(
() => props.preferences.defaultTimeRangeDays
const [selectedRange, setSelectedRange] = useState<number | null>(() =>
detectQuickRangeDays(props.currentFilters)
)
const resetFiltersFromPreferences = () => {
setFilters(buildDefaultDashboardFilters(props.preferences))
setSelectedRange(props.preferences.defaultTimeRangeDays)
}
const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen) resetFiltersFromPreferences()
// Sync the editing state from the applied filters every time the dialog
// opens so a previously applied manual range is preserved.
if (nextOpen) {
const applied =
props.currentFilters ?? buildDefaultDashboardFilters(props.preferences)
setFilters(applied)
setSelectedRange(detectQuickRangeDays(applied))
}
setOpen(nextOpen)
}
......@@ -133,6 +159,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
...prev,
start_timestamp: start,
end_timestamp: end,
time_granularity: granularityForRangeDays(days),
}))
setSelectedRange(days)
}
......
......@@ -33,11 +33,13 @@ import {
} from '@/features/dashboard/constants'
import {
getDefaultDays,
getSavedGranularity,
saveGranularity,
processUserChartData,
} from '@/features/dashboard/lib'
import type { ProcessedUserChartData } from '@/features/dashboard/types'
import type {
ProcessedUserChartData,
UserChartsFilters,
} from '@/features/dashboard/types'
let themeManagerPromise: Promise<
(typeof import('@visactor/vchart'))['ThemeManager']
......@@ -62,7 +64,12 @@ const USER_CHARTS: {
const TOP_USER_LIMIT_OPTIONS = [5, 10, 20, 50]
export function UserCharts() {
interface UserChartsProps {
filters: UserChartsFilters
onFiltersChange: (filters: UserChartsFilters) => void
}
export function UserCharts(props: UserChartsProps) {
const { t } = useTranslation()
const { resolvedTheme } = useTheme()
const [themeReady, setThemeReady] = useState(false)
......@@ -70,41 +77,45 @@ export function UserCharts() {
(typeof import('@visactor/vchart'))['ThemeManager'] | null
>(null)
const [timeGranularity, setTimeGranularity] = useState<TimeGranularity>(() =>
getSavedGranularity()
)
const [selectedRange, setSelectedRange] = useState<number>(() =>
getDefaultDays(timeGranularity)
)
const [topUserLimit, setTopUserLimit] = useState(10)
const [timeRange, setTimeRange] = useState(() => {
const days = getDefaultDays(timeGranularity)
const { start, end } = getRollingDateRange(days)
// The selection is owned by the dashboard parent so it persists across
// sub-section switches; the rolling window is derived from the chosen range.
const timeGranularity = props.filters.timeGranularity
const selectedRange = props.filters.selectedRange
const topUserLimit = props.filters.topUserLimit
const onFiltersChange = props.onFiltersChange
const timeRange = useMemo(() => {
const { start, end } = getRollingDateRange(selectedRange)
return {
start_timestamp: Math.floor(start.getTime() / 1000),
end_timestamp: Math.floor(end.getTime() / 1000),
}
})
}, [selectedRange])
const handleRangeChange = useCallback((days: number) => {
setSelectedRange(days)
const { start, end } = getRollingDateRange(days)
setTimeRange({
start_timestamp: Math.floor(start.getTime() / 1000),
end_timestamp: Math.floor(end.getTime() / 1000),
})
}, [])
const handleRangeChange = useCallback(
(days: number) => {
onFiltersChange({ ...props.filters, selectedRange: days })
},
[onFiltersChange, props.filters]
)
const handleGranularityChange = useCallback(
(g: TimeGranularity) => {
setTimeGranularity(g)
saveGranularity(g)
const days = getDefaultDays(g)
if (days !== selectedRange) {
handleRangeChange(days)
}
onFiltersChange({
...props.filters,
timeGranularity: g,
selectedRange: getDefaultDays(g),
})
},
[onFiltersChange, props.filters]
)
const handleTopUserLimitChange = useCallback(
(limit: number) => {
onFiltersChange({ ...props.filters, topUserLimit: limit })
},
[selectedRange, handleRangeChange]
[onFiltersChange, props.filters]
)
useEffect(() => {
......@@ -184,7 +195,7 @@ export function UserCharts() {
<Tabs
value={String(topUserLimit)}
onValueChange={(value) => setTopUserLimit(Number(value))}
onValueChange={(value) => handleTopUserLimitChange(Number(value))}
className='shrink-0'
>
<TabsList>
......
......@@ -31,7 +31,9 @@ import { OverviewDashboard } from './components/overview/overview-dashboard'
import { DEFAULT_TIME_GRANULARITY } from './constants'
import {
buildDefaultDashboardFilters,
getDefaultDays,
getSavedChartPreferences,
getSavedGranularity,
saveChartPreferences,
} from './lib'
import {
......@@ -43,6 +45,7 @@ import {
type DashboardChartPreferences,
type DashboardFilters,
type QuotaDataItem,
type UserChartsFilters,
} from './types'
const route = getRouteApi('/_authenticated/dashboard/$section')
......@@ -166,6 +169,16 @@ export function Dashboard() {
const [modelFilters, setModelFilters] = useState<DashboardFilters>(() =>
buildDefaultDashboardFilters(getSavedChartPreferences())
)
const [userChartsFilters, setUserChartsFilters] = useState<UserChartsFilters>(
() => {
const granularity = getSavedGranularity()
return {
timeGranularity: granularity,
selectedRange: getDefaultDays(granularity),
topUserLimit: 10,
}
}
)
const handleFilterChange = useCallback((filters: DashboardFilters) => {
setModelFilters(filters)
......@@ -221,6 +234,7 @@ export function Dashboard() {
/>
<ModelsFilter
preferences={chartPreferences}
currentFilters={modelFilters}
onFilterChange={handleFilterChange}
onReset={handleResetFilters}
/>
......@@ -230,6 +244,7 @@ export function Dashboard() {
activeSection === 'flow' ? (
<ModelsFilter
preferences={chartPreferences}
currentFilters={modelFilters}
onFilterChange={handleFilterChange}
onReset={handleResetFilters}
titleKey='Flow Filters'
......@@ -314,7 +329,10 @@ export function Dashboard() {
{activeSection === 'users' && (
<FadeIn>
<Suspense fallback={<ModelChartsFallback />}>
<LazyUserCharts />
<LazyUserCharts
filters={userChartsFilters}
onFiltersChange={setUserChartsFilters}
/>
</Suspense>
</FadeIn>
)}
......
......@@ -36,6 +36,8 @@ export { processChartData, processUserChartData } from './charts'
export {
buildDashboardFlowData,
buildFlowSankeySpec,
flowNodeFilterFromSankeyDatum,
flowSankeyDatumValue,
getFlowStages,
} from './flow'
export { safeDivide, calculateDashboardStats } from './stats'
......
......@@ -62,9 +62,22 @@ export type FlowNodeKind =
| 'model'
| 'channel'
export interface FlowNodeFilter {
kind: FlowNodeKind
id: string
}
export interface FlowLinkSelection {
source: string
target: string
}
export interface FlowBuildOptions {
role?: FlowRole
selectedUsers?: string[]
selectedNodes?: FlowNodeFilter[]
activeNode?: FlowNodeFilter
activeLink?: FlowLinkSelection
colorPalette?: readonly string[]
visibleStages?: FlowNodeKind[]
topNodeLimit?: number
......@@ -85,6 +98,8 @@ export interface DashboardFlowNode {
tokens: number
color: string
colorKey: string
highlighted?: boolean
dimmed?: boolean
}
export interface DashboardFlowLink {
......@@ -102,6 +117,8 @@ export interface DashboardFlowLink {
hoverColor: string
colorKey: string
share: number
highlighted?: boolean
dimmed?: boolean
}
export interface DashboardFlowGraph {
......@@ -117,8 +134,18 @@ export interface FlowUserFilterOption {
color: string
}
export interface FlowNodeFilterOption {
kind: FlowNodeKind
value: string
label: string
valueLabel: string
valueRaw: number
color: string
}
export interface FlowFilterOptions {
users: FlowUserFilterOption[]
nodes: FlowNodeFilterOption[]
}
export interface FlowSummary {
......@@ -171,6 +198,14 @@ export interface DashboardChartPreferences {
defaultTimeGranularity: TimeGranularity
}
// User analytics selections are held by the dashboard parent so they survive
// switching between dashboard sub-sections, matching the model/flow filters.
export interface UserChartsFilters {
timeGranularity: TimeGranularity
selectedRange: number
topUserLimit: number
}
// ============================================================================
// API Info Types
// ============================================================================
......
......@@ -271,6 +271,7 @@
"All Models": "All Models",
"All models in use are properly configured.": "All models in use are properly configured.",
"All Must Match (AND)": "All Must Match (AND)",
"All nodes": "All nodes",
"All requests must include": "All requests must include",
"All Status": "All Status",
"All Sync Status": "All Sync Status",
......@@ -780,6 +781,7 @@
"Clear filters": "Clear filters",
"Clear Mapping": "Clear Mapping",
"Clear mode flags in prompts": "Clear mode flags in prompts",
"Clear node filters": "Clear node filters",
"Clear search": "Clear search",
"Clear selection": "Clear selection",
"Clear selection (Escape)": "Clear selection (Escape)",
......@@ -1842,6 +1844,7 @@
"Filter by name or ID...": "Filter by name or ID...",
"Filter by name, ID, or key...": "Filter by name, ID, or key...",
"Filter by name...": "Filter by name...",
"Filter by node": "Filter by node",
"Filter by price field": "Filter by price field",
"Filter by ratio type": "Filter by ratio type",
"Filter by request ID": "Filter by request ID",
......@@ -2431,6 +2434,7 @@
"Memory Threshold (%)": "Memory Threshold (%)",
"Merchant ID": "Merchant ID",
"Merchant ID is required": "Merchant ID is required",
"Merge into Other": "Merge into Other",
"Message Priority": "Message Priority",
"Metadata": "Metadata",
"min downtime": "min downtime",
......@@ -2557,7 +2561,6 @@
"More than 999 days left": "More than 999 days left",
"More...": "More...",
"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 a request header": "Move a request header",
"Move affiliate rewards to your main balance": "Move affiliate rewards to your main balance",
......@@ -2733,6 +2736,7 @@
"No models to remove": "No models to remove",
"No new models to add": "No new models to add",
"No new models yet": "No new models yet",
"No nodes": "No nodes",
"No notable climbers right now": "No notable climbers right now",
"No notable drops right now": "No notable drops right now",
"No parameter overrides configured.": "No parameter overrides configured.",
......@@ -2791,6 +2795,7 @@
"No vendor data available": "No vendor data available",
"No X Found": "No X Found",
"Node": "Node",
"Node filters": "Node filters",
"Node Name": "Node Name",
"Non-stream": "Non-stream",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.",
......@@ -2958,8 +2963,8 @@
"Output tokens": "Output tokens",
"Output Tokens": "Output Tokens",
"Overage limited": "Overage limited",
"Overflow items": "Overflow items",
"overall": "overall",
"Overflow items": "Overflow items",
"Overnight range": "Overnight range",
"override": "override",
"Override": "Override",
......@@ -3460,6 +3465,7 @@
"Remove functionResponse.id field": "Remove functionResponse.id field",
"Remove mapped targets": "Remove mapped targets",
"Remove Models": "Remove Models",
"Remove node filter": "Remove node filter",
"Remove Passkey": "Remove Passkey",
"Remove Passkey?": "Remove Passkey?",
"Remove rule group": "Remove rule group",
......@@ -3804,6 +3810,7 @@
"Selected {{count}}": "Selected {{count}}",
"selected channel(s). Leave empty to remove tag.": "selected channel(s). Leave empty to remove tag.",
"Selected conflicts were overwritten successfully.": "Selected conflicts were overwritten successfully.",
"Selected nodes": "Selected nodes",
"Selected when creating a token and used as the default billing group for API calls.": "Selected when creating a token and used as the default billing group for API calls.",
"Self-Use Mode": "Self-Use Mode",
"Send": "Send",
......@@ -4582,6 +4589,7 @@
"Value": "Value",
"Value (supports JSON or plain text)": "Value (supports JSON or plain text)",
"Value is required": "Value is required",
"Value metric": "Value metric",
"Value must be at least 0": "Value must be at least 0",
"Value Regex": "Value Regex",
"variable": "variable",
......
......@@ -271,6 +271,7 @@
"All Models": "Tous les modèles",
"All models in use are properly configured.": "Tous les modèles utilisés sont correctement configurés.",
"All Must Match (AND)": "Toutes doivent correspondre (AND)",
"All nodes": "Tous les nœuds",
"All requests must include": "Toutes les requêtes doivent inclure",
"All Status": "Tous les statuts",
"All Sync Status": "Tous les statuts de synchronisation",
......@@ -780,6 +781,7 @@
"Clear filters": "Effacer les filtres",
"Clear Mapping": "Effacer le mappage",
"Clear mode flags in prompts": "Effacer les indicateurs de mode dans les prompts",
"Clear node filters": "Effacer les filtres de nœuds",
"Clear search": "Effacer la recherche",
"Clear selection": "Effacer la sélection",
"Clear selection (Escape)": "Effacer la sélection (Échap)",
......@@ -1842,6 +1844,7 @@
"Filter by name or ID...": "Filtrer par nom ou ID...",
"Filter by name, ID, or key...": "Filtrer par nom, ID ou clé...",
"Filter by name...": "Filtrer par nom...",
"Filter by node": "Filtrer par nœud",
"Filter by price field": "Filtrer par champ de prix",
"Filter by ratio type": "Filtrer par type de ratio",
"Filter by request ID": "Filtrer par ID de requête",
......@@ -2431,6 +2434,7 @@
"Memory Threshold (%)": "Seuil mémoire (%)",
"Merchant ID": "ID du commerçant",
"Merchant ID is required": "L'ID marchand est requis",
"Merge into Other": "Fusionner dans Autres",
"Message Priority": "Priorité du message",
"Metadata": "Métadonnées",
"min downtime": "min d'interruption",
......@@ -2557,7 +2561,6 @@
"More than 999 days left": "Plus de 999 jours restants",
"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",
"Merge into Other": "Fusionner dans Autres",
"Move": "Déplacer",
"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",
......@@ -2733,6 +2736,7 @@
"No models to remove": "Aucun modèle à supprimer",
"No new models to add": "Aucun nouveau modèle à ajouter",
"No new models yet": "Pas encore de nouveaux modèles",
"No nodes": "Aucun nœud",
"No notable climbers right now": "Aucune progression notable pour le moment",
"No notable drops right now": "Aucune chute notable pour le moment",
"No parameter overrides configured.": "Aucune surcharge de paramètres configurée.",
......@@ -2791,6 +2795,7 @@
"No vendor data available": "Aucune donnée de fournisseur disponible",
"No X Found": "Aucun X trouvé",
"Node": "Nœud",
"Node filters": "Filtres de nœuds",
"Node Name": "Nom du nœud",
"Non-stream": "Non-streaming",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Les récompenses d’invitation non nulles nécessitent une confirmation de conformité dans les paramètres de la passerelle de paiement.",
......@@ -2958,8 +2963,8 @@
"Output tokens": "Jetons de sortie",
"Output Tokens": "Tokens de sortie",
"Overage limited": "Dépassement limité",
"Overflow items": "Éléments excédentaires",
"overall": "global",
"Overflow items": "Éléments excédentaires",
"Overnight range": "Plage nocturne",
"override": "remplacer",
"Override": "Remplacer",
......@@ -3460,6 +3465,7 @@
"Remove functionResponse.id field": "Supprimer le champ functionResponse.id",
"Remove mapped targets": "Retirer les cibles mappées",
"Remove Models": "Supprimer des modèles",
"Remove node filter": "Supprimer le filtre de nœud",
"Remove Passkey": "Supprimer le Passkey",
"Remove Passkey?": "Supprimer la clé d'accès ?",
"Remove rule group": "Supprimer le groupe de règles",
......@@ -3804,6 +3810,7 @@
"Selected {{count}}": "{{count}} sélectionné(s)",
"selected channel(s). Leave empty to remove tag.": "canal(aux) sélectionné(s). Laisser vide pour supprimer l'étiquette.",
"Selected conflicts were overwritten successfully.": "Les conflits sélectionnés ont été écrasés avec succès.",
"Selected nodes": "Nœuds sélectionnés",
"Selected when creating a token and used as the default billing group for API calls.": "Sélectionné lors de la création d’un jeton et utilisé comme groupe de facturation par défaut pour les appels API.",
"Self-Use Mode": "Mode d'utilisation personnelle",
"Send": "Envoyer",
......@@ -4582,6 +4589,7 @@
"Value": "Valeur",
"Value (supports JSON or plain text)": "Valeur (JSON ou texte brut)",
"Value is required": "La valeur est obligatoire",
"Value metric": "Métrique de valeur",
"Value must be at least 0": "La valeur doit être au moins 0",
"Value Regex": "Regex de valeur",
"variable": "variable",
......
......@@ -271,6 +271,7 @@
"All Models": "すべてのモデル",
"All models in use are properly configured.": "使用中のすべてのモデルが適切に構成されています。",
"All Must Match (AND)": "すべて一致(AND)",
"All nodes": "すべてのノード",
"All requests must include": "すべてのリクエストには",
"All Status": "すべてのステータス",
"All Sync Status": "すべての同期状態",
......@@ -780,6 +781,7 @@
"Clear filters": "フィルターをクリア",
"Clear Mapping": "マッピングをクリア",
"Clear mode flags in prompts": "プロンプト内のモードフラグをクリア",
"Clear node filters": "ノードフィルターをクリア",
"Clear search": "検索をクリア",
"Clear selection": "選択をクリア",
"Clear selection (Escape)": "選択をクリア (Escape)",
......@@ -1842,6 +1844,7 @@
"Filter by name or ID...": "名前またはIDでフィルター...",
"Filter by name, ID, or key...": "名前、ID、またはキーでフィルター...",
"Filter by name...": "名前でフィルター...",
"Filter by node": "ノードでフィルター",
"Filter by price field": "価格フィールドでフィルター",
"Filter by ratio type": "倍率タイプで絞り込み",
"Filter by request ID": "リクエストIDで絞り込み",
......@@ -2431,6 +2434,7 @@
"Memory Threshold (%)": "メモリ閾値 (%)",
"Merchant ID": "マーチャントID",
"Merchant ID is required": "マーチャント ID は必須です",
"Merge into Other": "その他にまとめる",
"Message Priority": "メッセージの優先度",
"Metadata": "メタデータ",
"min downtime": "分のダウンタイム",
......@@ -2557,7 +2561,6 @@
"More than 999 days left": "999日以上",
"More...": "その他...",
"Most-used models in the selected period and category": "選択した期間とカテゴリで最も使われているモデル",
"Merge into Other": "その他にまとめる",
"Move": "移動",
"Move a request header": "リクエストヘッダーを移動",
"Move affiliate rewards to your main balance": "アフィリエイト報酬をメイン残高に移動する",
......@@ -2733,6 +2736,7 @@
"No models to remove": "削除するモデルがありません",
"No new models to add": "追加する新しいモデルはありません",
"No new models yet": "新しいモデルはまだありません",
"No nodes": "ノードなし",
"No notable climbers right now": "現在、目立った上昇はありません",
"No notable drops right now": "現在、目立った下降はありません",
"No parameter overrides configured.": "パラメータのオーバーライドが設定されていません。",
......@@ -2791,6 +2795,7 @@
"No vendor data available": "ベンダーデータがありません",
"No X Found": "X が見つかりません",
"Node": "ノード",
"Node filters": "ノードフィルター",
"Node Name": "ノード名",
"Non-stream": "非ストリーミング",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "0 以外の招待報酬には、支払いゲートウェイ設定でのコンプライアンス確認が必要です。",
......@@ -2958,8 +2963,8 @@
"Output tokens": "出力トークン",
"Output Tokens": "出力トークン",
"Overage limited": "超過利用制限中",
"Overflow items": "超過項目",
"overall": "全体",
"Overflow items": "超過項目",
"Overnight range": "日跨ぎ範囲",
"override": "上書き",
"Override": "上書き",
......@@ -3460,6 +3465,7 @@
"Remove functionResponse.id field": "functionResponse.id フィールドを削除",
"Remove mapped targets": "マッピング先を削除",
"Remove Models": "モデルを削除",
"Remove node filter": "ノードフィルターを削除",
"Remove Passkey": "Passkey連携解除",
"Remove Passkey?": "Passkeyを削除しますか?",
"Remove rule group": "ルールグループを削除",
......@@ -3804,6 +3810,7 @@
"Selected {{count}}": "{{count}} 件選択済み",
"selected channel(s). Leave empty to remove tag.": "選択されたチャネル。タグを削除するには空のままにしてください。",
"Selected conflicts were overwritten successfully.": "選択した競合が正常に上書きされました。",
"Selected nodes": "選択したノード",
"Selected when creating a token and used as the default billing group for API calls.": "トークン作成時に選択され、API 呼び出しのデフォルト課金グループとして使われます。",
"Self-Use Mode": "セルフユースモード",
"Send": "送信",
......@@ -4582,6 +4589,7 @@
"Value": "値",
"Value (supports JSON or plain text)": "値(JSONまたはプレーンテキスト対応)",
"Value is required": "値は必須です",
"Value metric": "値の指標",
"Value must be at least 0": "値は 0 以上である必要があります",
"Value Regex": "Value 正規表現",
"variable": "変数",
......
......@@ -271,6 +271,7 @@
"All Models": "Все модели",
"All models in use are properly configured.": "Все используемые модели настроены правильно.",
"All Must Match (AND)": "Все должны совпасть (AND)",
"All nodes": "Все узлы",
"All requests must include": "Все запросы должны содержать",
"All Status": "Все статусы",
"All Sync Status": "Все статусы синхронизации",
......@@ -780,6 +781,7 @@
"Clear filters": "Очистить фильтры",
"Clear Mapping": "Очистить сопоставление",
"Clear mode flags in prompts": "Очистить флаги режимов в промптах",
"Clear node filters": "Очистить фильтры узлов",
"Clear search": "Очистить поиск",
"Clear selection": "Снять выделение",
"Clear selection (Escape)": "Снять выделение (Escape)",
......@@ -1842,6 +1844,7 @@
"Filter by name or ID...": "Фильтр по имени или ID...",
"Filter by name, ID, or key...": "Фильтровать по имени, ID или ключу...",
"Filter by name...": "Фильтр по имени...",
"Filter by node": "Фильтр по узлу",
"Filter by price field": "Фильтр по полю цены",
"Filter by ratio type": "Фильтровать по типу коэффициента",
"Filter by request ID": "Фильтр по ID запроса",
......@@ -2431,6 +2434,7 @@
"Memory Threshold (%)": "Порог памяти (%)",
"Merchant ID": "ID мерчанта",
"Merchant ID is required": "Требуется ID мерчанта",
"Merge into Other": "Объединить в «Другое»",
"Message Priority": "Приоритет сообщения",
"Metadata": "Метаданные",
"min downtime": "мин простоя",
......@@ -2557,7 +2561,6 @@
"More than 999 days left": "Более 999 дней",
"More...": "Подробнее...",
"Most-used models in the selected period and category": "Самые используемые модели в выбранном периоде и категории",
"Merge into Other": "Объединить в «Другое»",
"Move": "Переместить",
"Move a request header": "Переместить заголовок запроса",
"Move affiliate rewards to your main balance": "Перевести партнерские вознаграждения на основной баланс",
......@@ -2733,6 +2736,7 @@
"No models to remove": "Нет моделей для удаления",
"No new models to add": "Нет новых моделей для добавления",
"No new models yet": "Новых моделей пока нет",
"No nodes": "Нет узлов",
"No notable climbers right now": "Сейчас нет значимых поднявшихся моделей",
"No notable drops right now": "Сейчас нет значимых упавших моделей",
"No parameter overrides configured.": "Нет настроенных переопределений параметров.",
......@@ -2791,6 +2795,7 @@
"No vendor data available": "Данных по поставщикам нет",
"No X Found": "X не найдено",
"Node": "Узел",
"Node filters": "Фильтры узлов",
"Node Name": "Имя узла",
"Non-stream": "Не потоковый",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Ненулевые награды за приглашения требуют подтверждения соответствия в настройках платежного шлюза.",
......@@ -2958,8 +2963,8 @@
"Output tokens": "Выходные токены",
"Output Tokens": "Выходные токены",
"Overage limited": "Ограничение перерасхода",
"Overflow items": "Элементы сверх лимита",
"overall": "всего",
"Overflow items": "Элементы сверх лимита",
"Overnight range": "Диапазон через полночь",
"override": "переопределить",
"Override": "Перезаписать",
......@@ -3460,6 +3465,7 @@
"Remove functionResponse.id field": "Удалить поле functionResponse.id",
"Remove mapped targets": "Удалить сопоставленные цели",
"Remove Models": "Удалить модели",
"Remove node filter": "Удалить фильтр узла",
"Remove Passkey": "Отвязать Passkey",
"Remove Passkey?": "Удалить ключ доступа?",
"Remove rule group": "Удалить группу правил",
......@@ -3804,6 +3810,7 @@
"Selected {{count}}": "Выбрано: {{count}}",
"selected channel(s). Leave empty to remove tag.": "выбранный канал(ы). Оставьте пустым, чтобы удалить тег.",
"Selected conflicts were overwritten successfully.": "Выбранные конфликты успешно перезаписаны.",
"Selected nodes": "Выбранные узлы",
"Selected when creating a token and used as the default billing group for API calls.": "Выбирается при создании токена и используется как группа тарификации по умолчанию для вызовов API.",
"Self-Use Mode": "Режим самоиспользования",
"Send": "Отправить",
......@@ -4582,6 +4589,7 @@
"Value": "Значение",
"Value (supports JSON or plain text)": "Значение (JSON или текст)",
"Value is required": "Значение обязательно",
"Value metric": "Метрика значения",
"Value must be at least 0": "Значение должно быть не менее 0",
"Value Regex": "Регулярное выражение значения",
"variable": "переменная",
......
......@@ -271,6 +271,7 @@
"All Models": "Tất cả các mẫu",
"All models in use are properly configured.": "Tất cả các mô hình đang được sử dụng đều được cấu hình đúng cách.",
"All Must Match (AND)": "Tất cả phải khớp (AND)",
"All nodes": "Tất cả nút",
"All requests must include": "Mọi yêu cầu phải có header",
"All Status": "Tất cả trạng thái",
"All Sync Status": "Tất cả Trạng thái Đồng bộ",
......@@ -780,6 +781,7 @@
"Clear filters": "Clear filter",
"Clear Mapping": "Xóa Ánh xạ",
"Clear mode flags in prompts": "Xóa các cờ chế độ trong lời nhắc",
"Clear node filters": "Xóa bộ lọc nút",
"Clear search": "Xóa tìm kiếm",
"Clear selection": "Bỏ chọn",
"Clear selection (Escape)": "Bỏ chọn (Escape)",
......@@ -1842,6 +1844,7 @@
"Filter by name or ID...": "Lọc theo tên hoặc ID...",
"Filter by name, ID, or key...": "Lọc theo tên, ID hoặc khóa...",
"Filter by name...": "Lọc theo tên...",
"Filter by node": "Lọc theo nút",
"Filter by price field": "Lọc theo trường giá",
"Filter by ratio type": "Lọc theo loại tỷ lệ",
"Filter by request ID": "Lọc theo ID yêu cầu",
......@@ -2431,6 +2434,7 @@
"Memory Threshold (%)": "Ngưỡng bộ nhớ (%)",
"Merchant ID": "Mã thương gia",
"Merchant ID is required": "Bắt buộc nhập Merchant ID",
"Merge into Other": "Gộp vào Khác",
"Message Priority": "Ưu tiên tin nhắn",
"Metadata": "Siêu dữ liệu",
"min downtime": "phút gián đoạn",
......@@ -2557,7 +2561,6 @@
"More than 999 days left": "Hơn 999 ngày",
"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",
"Merge into Other": "Gộp vào Khác",
"Move": "Di chuyển",
"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",
......@@ -2733,6 +2736,7 @@
"No models to remove": "Không có mô hình để xóa",
"No new models to add": "Không có mô hình mới để thêm",
"No new models yet": "Chưa có mô hình mới",
"No nodes": "Không có nút",
"No notable climbers right now": "Hiện chưa có mô hình nào tăng đáng kể",
"No notable drops right now": "Hiện chưa có mô hình nào giảm đáng kể",
"No parameter overrides configured.": "Không có ghi đè tham số nào được cấu hình.",
......@@ -2791,6 +2795,7 @@
"No vendor data available": "Không có dữ liệu nhà cung cấp",
"No X Found": "Không tìm thấy X",
"Node": "Nút",
"Node filters": "Bộ lọc nút",
"Node Name": "Tên nút",
"Non-stream": "Không phát trực tuyến",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Phần thưởng mời khác 0 yêu cầu xác nhận tuân thủ trong cài đặt Cổng thanh toán.",
......@@ -2958,8 +2963,8 @@
"Output tokens": "Token đầu ra",
"Output Tokens": "Token đầu ra",
"Overage limited": "Đã giới hạn vượt mức",
"Overflow items": "Mục vượt giới hạn",
"overall": "tổng",
"Overflow items": "Mục vượt giới hạn",
"Overnight range": "Khoảng qua nửa đêm",
"override": "ghi đè",
"Override": "Ghi đè",
......@@ -3460,6 +3465,7 @@
"Remove functionResponse.id field": "Loại bỏ trường functionResponse.id",
"Remove mapped targets": "Xóa đích đã ánh xạ",
"Remove Models": "Xóa mô hình",
"Remove node filter": "Xóa bộ lọc nút này",
"Remove Passkey": "Xóa Khóa truy cập",
"Remove Passkey?": "Xóa khóa truy cập?",
"Remove rule group": "Gỡ nhóm quy tắc",
......@@ -3804,6 +3810,7 @@
"Selected {{count}}": "Đã chọn {{count}}",
"selected channel(s). Leave empty to remove tag.": "Kênh đã chọn. Để trống để xóa thẻ.",
"Selected conflicts were overwritten successfully.": "Các xung đột được chọn đã được ghi đè thành công.",
"Selected nodes": "Nút đã chọn",
"Selected when creating a token and used as the default billing group for API calls.": "Được chọn khi tạo token và dùng làm nhóm tính phí mặc định cho các lệnh gọi API.",
"Self-Use Mode": "Chế độ tự sử dụng",
"Send": "Gửi",
......@@ -4582,6 +4589,7 @@
"Value": "Giá trị",
"Value (supports JSON or plain text)": "Giá trị (hỗ trợ JSON hoặc văn bản thuần)",
"Value is required": "Giá trị là bắt buộc",
"Value metric": "Chỉ số giá trị",
"Value must be at least 0": "Giá trị phải ít nhất là 0",
"Value Regex": "Regex giá trị",
"variable": "biến",
......
......@@ -271,6 +271,7 @@
"All Models": "所有模型",
"All models in use are properly configured.": "所有正在使用的模型都已正确配置。",
"All Must Match (AND)": "全部满足(AND)",
"All nodes": "全部节点",
"All requests must include": "所有请求必须携带",
"All Status": "所有状态",
"All Sync Status": "所有同步状态",
......@@ -780,6 +781,7 @@
"Clear filters": "清除筛选器",
"Clear Mapping": "清除映射",
"Clear mode flags in prompts": "在提示中清除模式标志",
"Clear node filters": "清空节点筛选",
"Clear search": "清除搜索",
"Clear selection": "清除选择",
"Clear selection (Escape)": "清除选择 (Escape)",
......@@ -1842,6 +1844,7 @@
"Filter by name or ID...": "按名称或 ID 筛选...",
"Filter by name, ID, or key...": "按名称、ID 或密钥筛选...",
"Filter by name...": "按名称筛选...",
"Filter by node": "按节点筛选",
"Filter by price field": "按价格字段筛选",
"Filter by ratio type": "按倍率类型筛选",
"Filter by request ID": "按请求 ID 筛选",
......@@ -2431,6 +2434,7 @@
"Memory Threshold (%)": "内存阈值 (%)",
"Merchant ID": "商户 ID",
"Merchant ID is required": "商户 ID 为必填项",
"Merge into Other": "合并为其他",
"Message Priority": "消息优先级",
"Metadata": "元信息",
"min downtime": "分钟停机",
......@@ -2557,7 +2561,6 @@
"More than 999 days left": "剩余超过 999 天",
"More...": "更多...",
"Most-used models in the selected period and category": "所选时间范围与分类下使用率最高的模型",
"Merge into Other": "合并为其他",
"Move": "移动",
"Move a request header": "移动请求头",
"Move affiliate rewards to your main balance": "将推广奖励转移到您的主余额",
......@@ -2733,6 +2736,7 @@
"No models to remove": "无待删除模型",
"No new models to add": "没有新模型可添加",
"No new models yet": "暂无新模型",
"No nodes": "无节点",
"No notable climbers right now": "当前没有显著上升的模型",
"No notable drops right now": "当前没有显著下降的模型",
"No parameter overrides configured.": "未配置参数覆盖。",
......@@ -2791,6 +2795,7 @@
"No vendor data available": "暂无厂商数据",
"No X Found": "未找到 X",
"Node": "节点",
"Node filters": "节点筛选",
"Node Name": "节点名称",
"Non-stream": "非流式",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "非零邀请奖励需要先在支付网关设置中确认合规条款。",
......@@ -2958,8 +2963,8 @@
"Output tokens": "输出 token",
"Output Tokens": "输出 Token",
"Overage limited": "超额受限",
"Overflow items": "超出项",
"overall": "总体",
"Overflow items": "超出项",
"Overnight range": "跨日范围",
"override": "覆盖",
"Override": "覆盖",
......@@ -3460,6 +3465,7 @@
"Remove functionResponse.id field": "移除 functionResponse.id 字段",
"Remove mapped targets": "移除映射目标",
"Remove Models": "删除模型",
"Remove node filter": "移除节点筛选",
"Remove Passkey": "解绑 Passkey",
"Remove Passkey?": "移除通行密钥?",
"Remove rule group": "移除规则组",
......@@ -3804,6 +3810,7 @@
"Selected {{count}}": "已选 {{count}} 个",
"selected channel(s). Leave empty to remove tag.": "选定的渠道。留空以移除标签。",
"Selected conflicts were overwritten successfully.": "选中的冲突已成功覆盖。",
"Selected nodes": "已选节点",
"Selected when creating a token and used as the default billing group for API calls.": "创建令牌时选择,用作 API 调用的默认计费分组。",
"Self-Use Mode": "自用模式",
"Send": "发送",
......@@ -4582,6 +4589,7 @@
"Value": "值",
"Value (supports JSON or plain text)": "值(支持 JSON 或普通文本)",
"Value is required": "值为必填项",
"Value metric": "数值口径",
"Value must be at least 0": "值必须至少为 0",
"Value Regex": "Value 正则",
"variable": "变量",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment