From 8f3c41ae77f7d0fefd2c154dbb89f53f18218f0f Mon Sep 17 00:00:00 2001 From: CaIon Date: Thu, 30 Apr 2026 13:57:10 +0800 Subject: [PATCH] feat(ui): improve table controls and analytics filters --- .../src/components/data-table/index.ts | 6 + .../channels/components/channels-columns.tsx | 7 +- .../channels/components/channels-table.tsx | 8 +- .../components/data-table-row-actions.tsx | 301 +++++++++++------- .../components/dialogs/edit-tag-dialog.tsx | 16 +- .../models/consumption-distribution-chart.tsx | 38 ++- .../components/models/model-charts.tsx | 43 +-- .../models/models-filter-dialog.tsx | 49 ++- .../src/features/dashboard/constants.ts | 22 +- web/default/src/features/dashboard/index.tsx | 88 +++-- .../src/features/dashboard/lib/charts.ts | 14 +- .../src/features/dashboard/lib/filters.ts | 106 +++++- .../src/features/dashboard/lib/index.ts | 3 + web/default/src/features/dashboard/types.ts | 11 + .../keys/components/api-keys-columns.tsx | 31 +- .../keys/components/api-keys-table.tsx | 22 +- .../components/data-table-row-actions.tsx | 263 ++++++++------- .../models/components/models-columns.tsx | 5 +- .../pricing/components/model-details.tsx | 9 +- .../pricing/components/pricing-columns.tsx | 31 +- .../components/redemptions-table.tsx | 26 +- .../dialogs/subscription-purchase-dialog.tsx | 5 +- .../components/subscriptions-columns.tsx | 10 +- .../dialogs/usage-logs-filter-dialog.tsx | 2 +- .../components/usage-logs-table.tsx | 13 +- web/default/src/features/usage-logs/index.tsx | 6 - .../users/components/users-columns.tsx | 81 +++-- .../features/users/components/users-table.tsx | 18 +- web/default/src/hooks/use-sidebar-data.ts | 6 + web/default/src/i18n/locales/en.json | 15 +- web/default/src/i18n/locales/fr.json | 15 +- web/default/src/i18n/locales/ja.json | 15 +- web/default/src/i18n/locales/ru.json | 15 +- web/default/src/i18n/locales/vi.json | 15 +- web/default/src/i18n/locales/zh.json | 15 +- 35 files changed, 858 insertions(+), 472 deletions(-) diff --git a/web/default/src/components/data-table/index.ts b/web/default/src/components/data-table/index.ts index cde46533..adb13de1 100644 --- a/web/default/src/components/data-table/index.ts +++ b/web/default/src/components/data-table/index.ts @@ -7,3 +7,9 @@ export { DataTableBulkActions } from './bulk-actions' export { TableSkeleton } from './table-skeleton' export { TableEmpty } from './table-empty' export { MobileCardList } from './mobile-card-list' + +export const DISABLED_ROW_DESKTOP = + 'bg-muted/85 hover:bg-muted [&>td:first-child]:border-l-muted-foreground/35 dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70' + +export const DISABLED_ROW_MOBILE = + 'border-l-4 border-l-muted-foreground/35 bg-muted/85 dark:border-l-zinc-300/70 dark:bg-zinc-700/55' diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index c78b6a62..fb89a0fc 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -28,6 +28,7 @@ import { } from '@/components/ui/tooltip' import { ConfirmDialog } from '@/components/confirm-dialog' import { DataTableColumnHeader } from '@/components/data-table/column-header' +import { GroupBadge } from '@/components/group-badge' import { StatusBadge, dotColorMap, @@ -876,8 +877,8 @@ export function useChannelsColumns(): ColumnDef[] { const group = row.getValue('group') as string const groupArray = parseGroupsList(group) - const groupBadges = groupArray.map((g, idx) => ( - + const groupBadges = groupArray.map((g) => ( + )) return ( @@ -1035,7 +1036,7 @@ export function useChannelsColumns(): ColumnDef[] { return }, - size: 100, + size: 132, enableSorting: false, enableHiding: false, }, diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index cf7509da..7bb2f36c 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -26,6 +26,8 @@ import { TableRow, } from '@/components/ui/table' import { + DISABLED_ROW_DESKTOP, + DISABLED_ROW_MOBILE, DataTableToolbar, TableSkeleton, TableEmpty, @@ -368,9 +370,7 @@ export function ChannelsTable() { emptyTitle='No Channels Found' emptyDescription='No channels available. Create your first channel to get started.' getRowClassName={(row) => - isDisabledChannelRow(row.original) - ? 'border-l-4 border-l-muted-foreground/35 bg-muted/85 dark:border-l-zinc-300/70 dark:bg-zinc-700/55' - : undefined + isDisabledChannelRow(row.original) ? DISABLED_ROW_MOBILE : undefined } /> ) : ( @@ -419,7 +419,7 @@ export function ChannelsTable() { data-state={row.getIsSelected() && 'selected'} className={cn( isDisabledChannelRow(row.original) && - 'bg-muted/85 hover:bg-muted [&>td:first-child]:border-l-muted-foreground/35 dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70' + DISABLED_ROW_DESKTOP )} > {row.getVisibleCells().map((cell) => ( diff --git a/web/default/src/features/channels/components/data-table-row-actions.tsx b/web/default/src/features/channels/components/data-table-row-actions.tsx index c626a197..a15b17a0 100644 --- a/web/default/src/features/channels/components/data-table-row-actions.tsx +++ b/web/default/src/features/channels/components/data-table-row-actions.tsx @@ -6,6 +6,7 @@ import { Boxes, Pencil, TestTube, + Gauge, DollarSign, Download, Copy, @@ -14,6 +15,7 @@ import { Key, Trash2, RefreshCw, + Loader2, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' @@ -25,10 +27,17 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' import { ConfirmDialog } from '@/components/confirm-dialog' import { MODEL_FETCHABLE_TYPES } from '../constants' import { + channelsQueryKeys, handleDeleteChannel, + handleTestChannel, handleToggleChannelStatus, isChannelEnabled, isMultiKeyChannel, @@ -47,6 +56,8 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { const { setOpen, setCurrentRow, upstream } = useChannels() const queryClient = useQueryClient() const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) + const [isTesting, setIsTesting] = useState(false) + const [isTogglingStatus, setIsTogglingStatus] = useState(false) const isEnabled = isChannelEnabled(channel) const isMultiKey = isMultiKeyChannel(channel) @@ -61,6 +72,18 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { setOpen('test-channel') } + const handleDirectTest = async (e: React.MouseEvent) => { + e.stopPropagation() + setIsTesting(true) + try { + await handleTestChannel(channel.id, undefined, () => { + queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() }) + }) + } finally { + setIsTesting(false) + } + } + const handleQueryBalance = () => { setCurrentRow(channel) setOpen('balance-query') @@ -86,148 +109,184 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { setOpen('multi-key-manage') } - const handleToggleStatus = () => { - handleToggleChannelStatus(channel.id, channel.status, queryClient) + const handleToggleStatus = async ( + e?: React.MouseEvent + ) => { + e?.stopPropagation() + setIsTogglingStatus(true) + try { + await handleToggleChannelStatus(channel.id, channel.status, queryClient) + } finally { + setIsTogglingStatus(false) + } } return ( - - - - - - {/* Edit */} - - {t('Edit')} - - - - - - {/* Test Connection */} - - {t('Test Connection')} - - - - - - {/* Query Balance */} - - {t('Query Balance')} - - - - - - {/* Fetch Models */} - - {t('Fetch Models')} - - - - - - {/* Detect Upstream Updates (only for fetchable channel types) */} - {MODEL_FETCHABLE_TYPES.has(channel.type) && ( - { - const meta = parseUpstreamUpdateMeta(channel.settings) - if ( - meta.pendingAddModels.length > 0 || - meta.pendingRemoveModels.length > 0 - ) { - upstream.openModal( - channel, - meta.pendingAddModels, - meta.pendingRemoveModels, - meta.pendingAddModels.length > 0 ? 'add' : 'remove' - ) - } else { - upstream.detectChannelUpdates(channel) - } - }} +
+ + + + + {t('Test Connection')} + + + + + + + + {isEnabled ? t('Disable') : t('Enable')} + + + + + + + + + {/* Edit */} + + {t('Edit')} - + - )} - {/* Ollama Models (only for Ollama channels) */} - {channel.type === 4 && ( - - {t('Manage Ollama Models')} + {/* Test Connection */} + + {t('Test Connection')} - + - )} - - - {/* Copy Channel */} - - {t('Copy Channel')} - - - - - - {/* Manage Keys (only for multi-key channels) */} - {isMultiKey && ( - - {t('Manage Keys')} + {/* Query Balance */} + + {t('Query Balance')} - + - )} - + {/* Fetch Models */} + + {t('Fetch Models')} + + + + - {/* Enable/Disable */} - - {isEnabled ? ( - <> - {t('Disable')} + {/* Detect Upstream Updates (only for fetchable channel types) */} + {MODEL_FETCHABLE_TYPES.has(channel.type) && ( + { + const meta = parseUpstreamUpdateMeta(channel.settings) + if ( + meta.pendingAddModels.length > 0 || + meta.pendingRemoveModels.length > 0 + ) { + upstream.openModal( + channel, + meta.pendingAddModels, + meta.pendingRemoveModels, + meta.pendingAddModels.length > 0 ? 'add' : 'remove' + ) + } else { + upstream.detectChannelUpdates(channel) + } + }} + > + {t('Upstream Updates')} - + - - ) : ( - <> - {t('Enable')} - - - - + )} - - + {/* Ollama Models (only for Ollama channels) */} + {channel.type === 4 && ( + + {t('Manage Ollama Models')} + + + + + )} - {/* Delete */} - { - e.preventDefault() - setDeleteConfirmOpen(true) - }} - className='text-destructive focus:text-destructive' - > - {t('Delete')} - - - - - + + + {/* Copy Channel */} + + {t('Copy Channel')} + + + + + + {/* Manage Keys (only for multi-key channels) */} + {isMultiKey && ( + + {t('Manage Keys')} + + + + + )} + + + + {/* Delete */} + { + e.preventDefault() + setDeleteConfirmOpen(true) + }} + className='text-destructive focus:text-destructive' + > + {t('Delete')} + + + + + + - +
) } diff --git a/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx b/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx index fe228d42..0348cfa8 100644 --- a/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/edit-tag-dialog.tsx @@ -24,6 +24,7 @@ import { } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Textarea } from '@/components/ui/textarea' +import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { editTagChannels, @@ -388,17 +389,14 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
{availableGroups.map((group) => ( - handleToggleGroup(group)} - > - {group} - + /> ))}
diff --git a/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx b/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx index 33565cd7..9313aedc 100644 --- a/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx +++ b/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx @@ -5,43 +5,51 @@ import { useTranslation } from 'react-i18next' import type { TimeGranularity } from '@/lib/time' import { VCHART_OPTION } from '@/lib/vchart' import { useTheme } from '@/context/theme-provider' -import { DEFAULT_TIME_GRANULARITY } from '@/features/dashboard/constants' +import { + CONSUMPTION_DISTRIBUTION_CHART_OPTIONS, + DEFAULT_TIME_GRANULARITY, +} from '@/features/dashboard/constants' import { processChartData } from '@/features/dashboard/lib' -import type { QuotaDataItem } from '@/features/dashboard/types' +import type { + ConsumptionDistributionChartType, + QuotaDataItem, +} from '@/features/dashboard/types' let themeManagerPromise: Promise< (typeof import('@visactor/vchart'))['ThemeManager'] > | null = null -type DistributionChartType = 'bar' | 'area' - interface ConsumptionDistributionChartProps { data: QuotaDataItem[] loading?: boolean timeGranularity?: TimeGranularity + defaultChartType?: ConsumptionDistributionChartType } -const CHART_TYPES: Array<{ - value: DistributionChartType - labelKey: string - icon: typeof BarChart3 -}> = [ - { value: 'bar', labelKey: 'Bar Chart', icon: BarChart3 }, - { value: 'area', labelKey: 'Area Chart', icon: AreaChart }, -] +const CHART_TYPE_ICONS: Record = + { + bar: BarChart3, + area: AreaChart, + } export function ConsumptionDistributionChart( props: ConsumptionDistributionChartProps ) { const { t } = useTranslation() const { resolvedTheme } = useTheme() - const [chartType, setChartType] = useState('bar') + const [chartType, setChartType] = useState( + props.defaultChartType ?? 'bar' + ) const [themeReady, setThemeReady] = useState(false) const themeManagerRef = useRef< (typeof import('@visactor/vchart'))['ThemeManager'] | null >(null) const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY + useEffect(() => { + if (props.defaultChartType) setChartType(props.defaultChartType) + }, [props.defaultChartType]) + useEffect(() => { const updateTheme = async () => { setThemeReady(false) @@ -81,8 +89,8 @@ export function ConsumptionDistributionChart(
- {CHART_TYPES.map((item) => { - const Icon = item.icon + {CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => { + const Icon = CHART_TYPE_ICONS[item.value] return (
) -export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) { +export function ModelsFilter(props: ModelsFilterProps) { const { t } = useTranslation() // 使用已缓存的用户数据,避免重复调用 API const user = useAuthStore((state) => state.auth.user) const isAdmin = user?.role && user.role >= 10 const [open, setOpen] = useState(false) - const [filters, setFilters] = useState(() => { - const granularity = getSavedGranularity() - const days = getDefaultDays(granularity) - const { start, end } = getNormalizedDateRange(days) - return { - ...EMPTY_DASHBOARD_FILTERS, - start_timestamp: start, - end_timestamp: end, - time_granularity: granularity, - } - }) + const [filters, setFilters] = useState(() => + buildDefaultDashboardFilters(props.preferences) + ) const [selectedRange, setSelectedRange] = useState(() => - getDefaultDays() + props.preferences.defaultTimeRangeDays ) + useEffect(() => { + setFilters(buildDefaultDashboardFilters(props.preferences)) + setSelectedRange(props.preferences.defaultTimeRangeDays) + }, [props.preferences]) + const handleApply = () => { - onFilterChange( + props.onFilterChange( cleanFilters( filters as unknown as Record ) as typeof filters @@ -90,17 +87,15 @@ export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) { } const handleReset = () => { - const days = getDefaultDays(DEFAULT_TIME_GRANULARITY) + const days = props.preferences.defaultTimeRangeDays const { start, end } = getNormalizedDateRange(days) setFilters({ - ...EMPTY_DASHBOARD_FILTERS, + ...buildDefaultDashboardFilters(props.preferences), start_timestamp: start, end_timestamp: end, - time_granularity: DEFAULT_TIME_GRANULARITY, }) setSelectedRange(days) - saveGranularity(DEFAULT_TIME_GRANULARITY) - onReset() + props.onReset() setOpen(false) } @@ -111,8 +106,6 @@ export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) { setFilters((prev) => ({ ...prev, [field]: value })) if (field === 'start_timestamp' || field === 'end_timestamp') setSelectedRange(null) - if (field === 'time_granularity' && typeof value === 'string') - saveGranularity(value as TimeGranularity) } const handleQuickRange = (days: number) => { diff --git a/web/default/src/features/dashboard/constants.ts b/web/default/src/features/dashboard/constants.ts index 5aafaf11..3d0c83b7 100644 --- a/web/default/src/features/dashboard/constants.ts +++ b/web/default/src/features/dashboard/constants.ts @@ -1,9 +1,18 @@ -import type { DashboardFilters } from './types' +import type { DashboardChartPreferences, DashboardFilters } from './types' export const TIME_GRANULARITY_STORAGE_KEY = 'data_export_default_time' +export const DASHBOARD_CHART_PREFERENCES_STORAGE_KEY = + 'dashboard_models_chart_preferences' export const DEFAULT_TIME_GRANULARITY = 'hour' as const export const MAX_CHART_TREND_POINTS = 7 +export const DEFAULT_DASHBOARD_CHART_PREFERENCES: DashboardChartPreferences = { + consumptionDistributionChart: 'bar', + modelAnalyticsChart: 'trend', + defaultTimeRangeDays: 1, + defaultTimeGranularity: DEFAULT_TIME_GRANULARITY, +} + export const TIME_RANGE_BY_GRANULARITY = { hour: 1, day: 7, @@ -23,6 +32,17 @@ export const TIME_RANGE_PRESETS = [ { label: '29 Days', days: 29 }, ] as const +export const CONSUMPTION_DISTRIBUTION_CHART_OPTIONS = [ + { value: 'bar', labelKey: 'Bar Chart' }, + { value: 'area', labelKey: 'Area Chart' }, +] as const + +export const MODEL_ANALYTICS_CHART_OPTIONS = [ + { value: 'trend', labelKey: 'Call Trend' }, + { value: 'proportion', labelKey: 'Call Count Distribution' }, + { value: 'top', labelKey: 'Call Count Ranking' }, +] as const + export const EMPTY_DASHBOARD_FILTERS: DashboardFilters = { start_timestamp: undefined, end_timestamp: undefined, diff --git a/web/default/src/features/dashboard/index.tsx b/web/default/src/features/dashboard/index.tsx index ee6ed59f..70d2d41d 100644 --- a/web/default/src/features/dashboard/index.tsx +++ b/web/default/src/features/dashboard/index.tsx @@ -11,6 +11,12 @@ import { CardStaggerItem, FadeIn, } from '@/components/page-transition' +import { + buildDefaultDashboardFilters, + getSavedChartPreferences, + saveChartPreferences, +} from './lib' +import { ModelsChartPreferences } from './components/models/models-chart-preferences' import { ModelsFilter } from './components/models/models-filter-dialog' import { AnnouncementsPanel } from './components/overview/announcements-panel' import { ApiInfoPanel } from './components/overview/api-info-panel' @@ -23,7 +29,11 @@ import { DASHBOARD_DEFAULT_SECTION, DASHBOARD_SECTION_IDS, } from './section-registry' -import { type DashboardFilters, type QuotaDataItem } from './types' +import { + type DashboardChartPreferences, + type DashboardFilters, + type QuotaDataItem, +} from './types' const route = getRouteApi('/_authenticated/dashboard/$section') @@ -107,17 +117,21 @@ export function Dashboard() { const activeSection = (params.section ?? DASHBOARD_DEFAULT_SECTION) as DashboardSectionId - const [modelFilters, setModelFilters] = useState({}) const [modelData, setModelData] = useState([]) const [dataLoading, setDataLoading] = useState(false) + const [chartPreferences, setChartPreferences] = + useState(() => getSavedChartPreferences()) + const [modelFilters, setModelFilters] = useState(() => + buildDefaultDashboardFilters(getSavedChartPreferences()) + ) const handleFilterChange = useCallback((filters: DashboardFilters) => { setModelFilters(filters) }, []) const handleResetFilters = useCallback(() => { - setModelFilters({}) - }, []) + setModelFilters(buildDefaultDashboardFilters(chartPreferences)) + }, [chartPreferences]) const handleDataUpdate = useCallback( (data: QuotaDataItem[], loading: boolean) => { @@ -127,6 +141,15 @@ export function Dashboard() { [] ) + const handleChartPreferencesChange = useCallback( + (preferences: DashboardChartPreferences) => { + setChartPreferences(preferences) + setModelFilters(buildDefaultDashboardFilters(preferences)) + saveChartPreferences(preferences) + }, + [] + ) + const meta = SECTION_META[activeSection] ?? SECTION_META.overview const isAdmin = Boolean(userRole && userRole >= ROLE.ADMIN) const visibleSections = useMemo( @@ -146,6 +169,20 @@ export function Dashboard() { [navigate] ) const showSectionTabs = activeSection !== 'overview' && visibleSections.length > 1 + const modelActions = + activeSection === 'models' ? ( + <> + + + + ) : null return ( @@ -153,26 +190,29 @@ export function Dashboard() { {t(meta.descriptionKey)} - {activeSection === 'models' && ( - - - - )}
- {showSectionTabs && ( - - - {visibleSections.map((section) => ( - - {t(SECTION_META[section].titleKey)} - - ))} - - + {activeSection !== 'overview' && ( +
+ {showSectionTabs ? ( + + + {visibleSections.map((section) => ( + + {t(SECTION_META[section].titleKey)} + + ))} + + + ) : ( +
+ )} + {modelActions != null && ( +
+ {modelActions} +
+ )} +
)} {activeSection === 'overview' && ( <> @@ -208,6 +248,9 @@ export function Dashboard() { preset.days === value) +} export function cleanFilters>( filters: T @@ -25,20 +62,81 @@ export function getSavedGranularity( override?: TimeGranularity ): TimeGranularity { if (override) return override - if (typeof window === 'undefined') return DEFAULT_TIME_GRANULARITY - const saved = localStorage.getItem(TIME_GRANULARITY_STORAGE_KEY) - if (saved === 'hour' || saved === 'day' || saved === 'week') return saved - return DEFAULT_TIME_GRANULARITY + return getSavedChartPreferences().defaultTimeGranularity } export function saveGranularity(granularity: TimeGranularity): void { + if (typeof window === 'undefined') return + saveChartPreferences({ + ...getSavedChartPreferences(), + defaultTimeGranularity: granularity, + }) localStorage.setItem(TIME_GRANULARITY_STORAGE_KEY, granularity) } +export function getSavedChartPreferences(): DashboardChartPreferences { + if (typeof window === 'undefined') return DEFAULT_DASHBOARD_CHART_PREFERENCES + + const fallbackPreferences = { + ...DEFAULT_DASHBOARD_CHART_PREFERENCES, + defaultTimeGranularity: getLegacySavedGranularity(), + } + + try { + const raw = localStorage.getItem(DASHBOARD_CHART_PREFERENCES_STORAGE_KEY) + if (!raw) return fallbackPreferences + + const parsed = JSON.parse(raw) as Partial + return { + consumptionDistributionChart: isConsumptionDistributionChartType( + parsed.consumptionDistributionChart + ) + ? parsed.consumptionDistributionChart + : fallbackPreferences.consumptionDistributionChart, + modelAnalyticsChart: isModelAnalyticsChartTab(parsed.modelAnalyticsChart) + ? parsed.modelAnalyticsChart + : fallbackPreferences.modelAnalyticsChart, + defaultTimeRangeDays: isTimeRangePresetDays(parsed.defaultTimeRangeDays) + ? parsed.defaultTimeRangeDays + : fallbackPreferences.defaultTimeRangeDays, + defaultTimeGranularity: isTimeGranularity( + parsed.defaultTimeGranularity + ) + ? parsed.defaultTimeGranularity + : fallbackPreferences.defaultTimeGranularity, + } + } catch { + return fallbackPreferences + } +} + +export function saveChartPreferences( + preferences: DashboardChartPreferences +): void { + if (typeof window === 'undefined') return + localStorage.setItem( + DASHBOARD_CHART_PREFERENCES_STORAGE_KEY, + JSON.stringify(preferences) + ) +} + export function getDefaultDays(granularity?: TimeGranularity): number { + if (!granularity) return getSavedChartPreferences().defaultTimeRangeDays return TIME_RANGE_BY_GRANULARITY[getSavedGranularity(granularity)] } +export function buildDefaultDashboardFilters( + preferences: DashboardChartPreferences = getSavedChartPreferences() +): DashboardFilters { + const { start, end } = getNormalizedDateRange(preferences.defaultTimeRangeDays) + return { + ...EMPTY_DASHBOARD_FILTERS, + start_timestamp: start, + end_timestamp: end, + time_granularity: preferences.defaultTimeGranularity, + } +} + export function buildQueryParams( timeRange: { start_timestamp: number; end_timestamp: number }, filters?: { time_granularity?: TimeGranularity; username?: string } diff --git a/web/default/src/features/dashboard/lib/index.ts b/web/default/src/features/dashboard/lib/index.ts index 77ac7b2e..bf945021 100644 --- a/web/default/src/features/dashboard/lib/index.ts +++ b/web/default/src/features/dashboard/lib/index.ts @@ -4,6 +4,9 @@ export { getSavedGranularity, saveGranularity, getDefaultDays, + getSavedChartPreferences, + saveChartPreferences, + buildDefaultDashboardFilters, } from './filters' export { getLatencyColorClass, diff --git a/web/default/src/features/dashboard/types.ts b/web/default/src/features/dashboard/types.ts index d046e5dc..0537d4bc 100644 --- a/web/default/src/features/dashboard/types.ts +++ b/web/default/src/features/dashboard/types.ts @@ -42,6 +42,17 @@ export interface DashboardFilters { username?: string } +export type ConsumptionDistributionChartType = 'bar' | 'area' + +export type ModelAnalyticsChartTab = 'trend' | 'proportion' | 'top' + +export interface DashboardChartPreferences { + consumptionDistributionChart: ConsumptionDistributionChartType + modelAnalyticsChart: ModelAnalyticsChartTab + defaultTimeRangeDays: number + defaultTimeGranularity: TimeGranularity +} + // ============================================================================ // API Info Types // ============================================================================ diff --git a/web/default/src/features/keys/components/api-keys-columns.tsx b/web/default/src/features/keys/components/api-keys-columns.tsx index e06ae156..e74948f7 100644 --- a/web/default/src/features/keys/components/api-keys-columns.tsx +++ b/web/default/src/features/keys/components/api-keys-columns.tsx @@ -14,6 +14,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table' +import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { getSystemOptions } from '@/features/system-settings/api' import { API_KEY_STATUSES } from '../constants' @@ -31,16 +32,6 @@ function getQuotaProgressColor(percentage: number): string { return '[&_[data-slot=progress-indicator]]:bg-emerald-500' } -function getGroupRatioClassName(ratio: number): string { - if (ratio > 1) { - return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300' - } - if (ratio < 1) { - return 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900/60 dark:bg-sky-950/40 dark:text-sky-300' - } - return 'border-border bg-muted text-muted-foreground' -} - function useGroupRatios(): Record { const isAdmin = useAuthStore((s) => Boolean(s.auth.user?.role && s.auth.user.role >= 10) @@ -230,7 +221,7 @@ export function useApiKeysColumns(): ColumnDef[] { - {t('Auto')} + {apiKey.cross_group_retry && ( <> · @@ -251,22 +242,7 @@ export function useApiKeysColumns(): ColumnDef[] { ) } - return ( - - {group || t('Default')} - {ratio != null && ( - - - {ratio}x - - )} - - ) + return }, meta: { label: t('Group'), mobileHidden: true }, }, @@ -354,6 +330,7 @@ export function useApiKeysColumns(): ColumnDef[] { id: 'actions', cell: ({ row }) => , meta: { label: t('Actions') }, + size: 88, }, ] } diff --git a/web/default/src/features/keys/components/api-keys-table.tsx b/web/default/src/features/keys/components/api-keys-table.tsx index 402fb2b3..6c55e34f 100644 --- a/web/default/src/features/keys/components/api-keys-table.tsx +++ b/web/default/src/features/keys/components/api-keys-table.tsx @@ -27,6 +27,8 @@ import { TableRow, } from '@/components/ui/table' import { + DISABLED_ROW_DESKTOP, + DISABLED_ROW_MOBILE, DataTablePagination, DataTableToolbar, TableSkeleton, @@ -35,7 +37,7 @@ import { } from '@/components/data-table' import { PageFooterPortal } from '@/components/layout' import { getApiKeys, searchApiKeys } from '../api' -import { API_KEY_STATUS_OPTIONS, ERROR_MESSAGES } from '../constants' +import { API_KEY_STATUS, API_KEY_STATUS_OPTIONS, ERROR_MESSAGES } from '../constants' import { type ApiKey } from '../types' import { useApiKeysColumns } from './api-keys-columns' import { ApiKeysPrimaryButtons } from './api-keys-primary-buttons' @@ -44,6 +46,10 @@ import { DataTableBulkActions } from './data-table-bulk-actions' const route = getRouteApi('/_authenticated/keys/') +function isDisabledApiKeyRow(apiKey: ApiKey) { + return apiKey.status !== API_KEY_STATUS.ENABLED +} + export function ApiKeysTable() { const { t } = useTranslation() const { refreshTrigger } = useApiKeys() @@ -185,6 +191,11 @@ export function ApiKeysTable() { emptyDescription={t( 'No API keys available. Create your first API key to get started.' )} + getRowClassName={(row) => + isDisabledApiKeyRow(row.original) + ? DISABLED_ROW_MOBILE + : undefined + } /> ) : (
{row.getVisibleCells().map((cell) => ( diff --git a/web/default/src/features/keys/components/data-table-row-actions.tsx b/web/default/src/features/keys/components/data-table-row-actions.tsx index 09dfe090..569985f1 100644 --- a/web/default/src/features/keys/components/data-table-row-actions.tsx +++ b/web/default/src/features/keys/components/data-table-row-actions.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import { DotsHorizontalIcon } from '@radix-ui/react-icons' import { type Row } from '@tanstack/react-table' import { @@ -10,6 +10,7 @@ import { ArrowRightLeft, Copy, Link, + Loader2, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -26,6 +27,11 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' import { useChatPresets } from '@/features/chat/hooks/use-chat-presets' import { resolveChatUrl, type ChatPreset } from '@/features/chat/lib/chat-links' import { sendToFluent } from '@/features/chat/lib/send-to-fluent' @@ -73,6 +79,7 @@ export function DataTableRowActions({ } = useApiKeys() const isEnabled = apiKey.status === API_KEY_STATUS.ENABLED const { chatPresets, serverAddress } = useChatPresets() + const [isTogglingStatus, setIsTogglingStatus] = useState(false) const hasChatPresets = chatPresets.length > 0 @@ -117,11 +124,15 @@ export function DataTableRowActions({ [resolveRealKey, apiKey.id, serverAddress, t] ) - const handleToggleStatus = async () => { + const handleToggleStatus = async ( + e?: React.MouseEvent + ) => { + e?.stopPropagation() const newStatus = isEnabled ? API_KEY_STATUS.DISABLED : API_KEY_STATUS.ENABLED + setIsTogglingStatus(true) try { const result = await updateApiKeyStatus(apiKey.id, newStatus) if (result.success) { @@ -135,125 +146,143 @@ export function DataTableRowActions({ } } catch { toast.error(t(ERROR_MESSAGES.UNEXPECTED)) + } finally { + setIsTogglingStatus(false) } } return ( - - - - - - { - const realKey = await resolveRealKey(apiKey.id) - if (!realKey) return - const ok = await copyToClipboard(realKey) - if (ok) toast.success(t('Copied')) - }} - > - {t('Copy Key')} - - - - - { - const realKey = await resolveRealKey(apiKey.id) - if (!realKey) return - const connStr = encodeConnectionString(realKey, getServerAddress()) - const ok = await copyToClipboard(connStr) - if (ok) toast.success(t('Copied')) - }} - > - {t('Copy Connection Info')} - - - - - - { - setCurrentRow(apiKey) - setOpen('update') - }} - > - {t('Edit')} - - - - - - {isEnabled ? ( - <> - {t('Disable')} - - - - - ) : ( - <> - {t('Enable')} - - - - +
+ + + + + + {isEnabled ? t('Disable') : t('Enable')} + + + + + + + + + { + const realKey = await resolveRealKey(apiKey.id) + if (!realKey) return + const ok = await copyToClipboard(realKey) + if (ok) toast.success(t('Copied')) + }} + > + {t('Copy Key')} + + + + + { + const realKey = await resolveRealKey(apiKey.id) + if (!realKey) return + const connStr = encodeConnectionString( + realKey, + getServerAddress() + ) + const ok = await copyToClipboard(connStr) + if (ok) toast.success(t('Copied')) + }} + > + {t('Copy Connection Info')} + + + + + + { + setCurrentRow(apiKey) + setOpen('update') + }} + > + {t('Edit')} + + + + + { + const realKey = await resolveRealKey(apiKey.id) + if (!realKey) return + setResolvedKey(realKey) + setCurrentRow(apiKey) + setOpen('cc-switch') + }} + > + {t('CC Switch')} + + + + + {hasChatPresets && ( + + {t('Chat')} + + {chatPresets.map((preset) => ( + handleOpenChatPreset(preset)} + > + {preset.name} + {preset.type !== 'web' && ( + + + + )} + + ))} + + )} - - { - const realKey = await resolveRealKey(apiKey.id) - if (!realKey) return - setResolvedKey(realKey) - setCurrentRow(apiKey) - setOpen('cc-switch') - }} - > - {t('CC Switch')} - - - - - {hasChatPresets && ( - - {t('Chat')} - - {chatPresets.map((preset) => ( - handleOpenChatPreset(preset)} - > - {preset.name} - {preset.type !== 'web' && ( - - - - )} - - ))} - - - )} - - { - setCurrentRow(apiKey) - setOpen('delete') - }} - className='text-destructive focus:text-destructive' - > - {t('Delete')} - - - - - - + + { + setCurrentRow(apiKey) + setOpen('delete') + }} + className='text-destructive focus:text-destructive' + > + {t('Delete')} + + + + + + +
) } diff --git a/web/default/src/features/models/components/models-columns.tsx b/web/default/src/features/models/components/models-columns.tsx index 5a16b574..08eb35c6 100644 --- a/web/default/src/features/models/components/models-columns.tsx +++ b/web/default/src/features/models/components/models-columns.tsx @@ -10,6 +10,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table/column-header' +import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { getModelStatusConfig, @@ -443,8 +444,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef[] { return - } - const groupBadges = groups.map((g, idx) => ( - + const groupBadges = groups.map((g) => ( + )) return ( diff --git a/web/default/src/features/pricing/components/model-details.tsx b/web/default/src/features/pricing/components/model-details.tsx index bebc686b..10ad6f90 100644 --- a/web/default/src/features/pricing/components/model-details.tsx +++ b/web/default/src/features/pricing/components/model-details.tsx @@ -14,6 +14,7 @@ import { TableRow, } from '@/components/ui/table' import { CopyButton } from '@/components/copy-button' +import { GroupBadge } from '@/components/group-badge' import { PublicLayout } from '@/components/layout' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' import { usePricingData } from '../hooks/use-pricing-data' @@ -275,9 +276,7 @@ function AutoGroupChain(props: { model: PricingModel; autoGroups: string[] }) { {autoChain.map((g, idx) => ( - - {g} - + {idx < autoChain.length - 1 && ( )} @@ -388,7 +387,9 @@ function GroupPricingSection(props: { const ratio = groupRatio[group] || 1 return ( - {group} + + + {ratio}x diff --git a/web/default/src/features/pricing/components/pricing-columns.tsx b/web/default/src/features/pricing/components/pricing-columns.tsx index e88d45c7..c64baef2 100644 --- a/web/default/src/features/pricing/components/pricing-columns.tsx +++ b/web/default/src/features/pricing/components/pricing-columns.tsx @@ -8,6 +8,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table/column-header' +import { GroupBadge } from '@/components/group-badge' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' import { parseTags } from '../lib/filters' import { isTokenBasedModel } from '../lib/model-helpers' @@ -49,6 +50,28 @@ function renderLimitedTags( ) } +function renderLimitedGroupBadges( + groups: string[], + maxDisplay: number = 2 +): React.ReactNode { + if (groups.length === 0) + return + + const displayed = groups.slice(0, maxDisplay) + const remaining = groups.length - maxDisplay + + return ( +
+ {displayed.map((group) => ( + + ))} + {remaining > 0 && ( + +{remaining} + )} +
+ ) +} + export function usePricingColumns( options: PricingColumnsOptions = {} ): ColumnDef[] { @@ -312,11 +335,15 @@ export function usePricingColumns( -
{renderLimitedTags(groups, 2)}
+
{renderLimitedGroupBadges(groups, 2)}
{groups.length > 2 && ( - {groups.join(', ')} +
+ {groups.map((group) => ( + + ))} +
)}
diff --git a/web/default/src/features/redemption-codes/components/redemptions-table.tsx b/web/default/src/features/redemption-codes/components/redemptions-table.tsx index a61dec67..646071a5 100644 --- a/web/default/src/features/redemption-codes/components/redemptions-table.tsx +++ b/web/default/src/features/redemption-codes/components/redemptions-table.tsx @@ -26,6 +26,8 @@ import { TableRow, } from '@/components/ui/table' import { + DISABLED_ROW_DESKTOP, + DISABLED_ROW_MOBILE, DataTablePagination, DataTableToolbar, TableSkeleton, @@ -36,12 +38,20 @@ import { PageFooterPortal } from '@/components/layout' import { getRedemptions, searchRedemptions } from '../api' import { REDEMPTION_STATUS, getRedemptionStatusOptions } from '../constants' import { isRedemptionExpired } from '../lib' +import type { Redemption } from '../types' import { DataTableBulkActions } from './data-table-bulk-actions' import { useRedemptionsColumns } from './redemptions-columns' import { useRedemptions } from './redemptions-provider' const route = getRouteApi('/_authenticated/redemption-codes/') +function isDisabledRedemptionRow(redemption: Redemption) { + return ( + redemption.status !== REDEMPTION_STATUS.ENABLED || + isRedemptionExpired(redemption.expired_time, redemption.status) + ) +} + export function RedemptionsTable() { const { t } = useTranslation() const columns = useRedemptionsColumns() @@ -164,6 +174,11 @@ export function RedemptionsTable() { emptyDescription={t( 'No redemption codes available. Create your first redemption code to get started.' )} + getRowClassName={(row) => + isDisabledRedemptionRow(row.original) + ? DISABLED_ROW_MOBILE + : undefined + } /> ) : ( <> @@ -209,18 +224,15 @@ export function RedemptionsTable() { ) : ( table.getRowModel().rows.map((row) => { const redemption = row.original - const isDisabled = - redemption.status !== REDEMPTION_STATUS.ENABLED || - isRedemptionExpired( - redemption.expired_time, - redemption.status - ) return ( {row.getVisibleCells().map((cell) => ( diff --git a/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx b/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx index b50254fe..c7083ca3 100644 --- a/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx +++ b/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx @@ -17,6 +17,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { GroupBadge } from '@/components/group-badge' import { Separator } from '@/components/ui/separator' import { paySubscriptionStripe, @@ -209,11 +210,11 @@ export function SubscriptionPurchaseDialog(props: Props) {
{plan.upgrade_group && ( -
+
{t('Upgrade Group')} - {plan.upgrade_group} +
)} diff --git a/web/default/src/features/subscriptions/components/subscriptions-columns.tsx b/web/default/src/features/subscriptions/components/subscriptions-columns.tsx index cbf6e543..fff81818 100644 --- a/web/default/src/features/subscriptions/components/subscriptions-columns.tsx +++ b/web/default/src/features/subscriptions/components/subscriptions-columns.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react' import { type ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' import { DataTableColumnHeader } from '@/components/data-table' +import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { formatDuration, formatResetPeriod } from '../lib' import type { PlanRecord } from '../types' @@ -172,11 +173,10 @@ export function useSubscriptionsColumns(): ColumnDef[] { ), cell: ({ row }) => { const group = row.original.plan.upgrade_group - return ( - - {group || t('No Upgrade')} - - ) + if (!group) { + return {t('No Upgrade')} + } + return }, size: 100, }, diff --git a/web/default/src/features/usage-logs/components/dialogs/usage-logs-filter-dialog.tsx b/web/default/src/features/usage-logs/components/dialogs/usage-logs-filter-dialog.tsx index f1ff9216..cd7cc322 100644 --- a/web/default/src/features/usage-logs/components/dialogs/usage-logs-filter-dialog.tsx +++ b/web/default/src/features/usage-logs/components/dialogs/usage-logs-filter-dialog.tsx @@ -246,7 +246,7 @@ export function UsageLogsFilterDialog({ handleChange('mjId', value)} /> diff --git a/web/default/src/features/usage-logs/components/usage-logs-table.tsx b/web/default/src/features/usage-logs/components/usage-logs-table.tsx index 3fd340b2..0895f23c 100644 --- a/web/default/src/features/usage-logs/components/usage-logs-table.tsx +++ b/web/default/src/features/usage-logs/components/usage-logs-table.tsx @@ -27,7 +27,6 @@ import { } from '@/components/ui/table' import { DataTablePagination, - DataTableToolbar, DataTableViewOptions, TableSkeleton, TableEmpty, @@ -40,6 +39,7 @@ import { fetchLogsByCategory } from '../lib/utils' import type { LogCategory } from '../types' import { CommonLogsFilterBar } from './common-logs-filter-bar' import { CommonLogsStats } from './common-logs-stats' +import { TaskLogsFilterBar } from './task-logs-filter-bar' const route = getRouteApi('/_authenticated/usage-logs/$section') @@ -194,11 +194,12 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) { />
) : ( - +
+ } + /> +
)} {isMobile ? ( {t(pageMeta.descriptionKey)} - - {activeCategory !== 'common' && ( - - )} -
{showTaskSwitcher && ( diff --git a/web/default/src/features/users/components/users-columns.tsx b/web/default/src/features/users/components/users-columns.tsx index 72afc586..9d834664 100644 --- a/web/default/src/features/users/components/users-columns.tsx +++ b/web/default/src/features/users/components/users-columns.tsx @@ -10,17 +10,23 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table' +import { GroupBadge } from '@/components/group-badge' import { LongText } from '@/components/long-text' import { StatusBadge, dotColorMap } from '@/components/status-badge' import { USER_STATUSES, USER_ROLES, - DEFAULT_GROUP, isUserDeleted, } from '../constants' import { type User } from '../types' import { DataTableRowActions } from './data-table-row-actions' +function getQuotaProgressColor(percentage: number): string { + if (percentage <= 10) return '[&_[data-slot=progress-indicator]]:bg-rose-500' + if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-amber-500' + return '[&_[data-slot=progress-indicator]]:bg-emerald-500' +} + export function useUsersColumns(): ColumnDef[] { const { t } = useTranslation() return [ @@ -66,24 +72,32 @@ export function useUsersColumns(): ColumnDef[] { ), cell: ({ row }) => { const username = row.getValue('username') as string + const displayName = row.original.display_name const remark = row.original.remark return ( -
- - {username} - - {remark && ( - - - - {remark} - - - -

{remark}

-
-
+
+
+ + {username} + + {remark && ( + + + + {remark} + + + +

{remark}

+
+
+ )} +
+ {displayName && displayName !== username && ( + + {displayName} + )}
) @@ -91,20 +105,6 @@ export function useUsersColumns(): ColumnDef[] { enableHiding: false, meta: { label: t('Username'), mobileTitle: true }, }, - { - accessorKey: 'display_name', - header: ({ column }) => ( - - ), - cell: ({ row }) => { - return ( - - {row.getValue('display_name') || '-'} - - ) - }, - meta: { label: t('Display Name'), mobileHidden: true }, - }, { accessorKey: 'status', header: ({ column }) => ( @@ -176,12 +176,17 @@ export function useUsersColumns(): ColumnDef[] {
- {formatQuota(remaining)} - + + {formatQuota(remaining)} + + {formatQuota(total)}
- +
@@ -212,16 +217,10 @@ export function useUsersColumns(): ColumnDef[] { ), cell: ({ row }) => { const group = row.getValue('group') as string - return ( - - ) + return }, filterFn: (row, id, value) => { - const group = String(row.getValue(id) || DEFAULT_GROUP).toLowerCase() + const group = String(row.getValue(id) || t('User Group')).toLowerCase() const searchValue = String(value).toLowerCase() return group.includes(searchValue) }, diff --git a/web/default/src/features/users/components/users-table.tsx b/web/default/src/features/users/components/users-table.tsx index 280da7fe..54f39c8a 100644 --- a/web/default/src/features/users/components/users-table.tsx +++ b/web/default/src/features/users/components/users-table.tsx @@ -27,6 +27,8 @@ import { TableRow, } from '@/components/ui/table' import { + DISABLED_ROW_DESKTOP, + DISABLED_ROW_MOBILE, DataTablePagination, DataTableToolbar, TableSkeleton, @@ -41,12 +43,17 @@ import { getUserRoleOptions, isUserDeleted, } from '../constants' +import type { User } from '../types' import { DataTableBulkActions } from './data-table-bulk-actions' import { useUsersColumns } from './users-columns' import { useUsers } from './users-provider' const route = getRouteApi('/_authenticated/users/') +function isDisabledUserRow(user: User) { + return isUserDeleted(user) || user.status === USER_STATUS.DISABLED +} + export function UsersTable() { const { t } = useTranslation() const columns = useUsersColumns() @@ -186,6 +193,9 @@ export function UsersTable() { emptyDescription={t( 'No users available. Try adjusting your search or filters.' )} + getRowClassName={(row) => + isDisabledUserRow(row.original) ? DISABLED_ROW_MOBILE : undefined + } /> ) : ( <> @@ -226,16 +236,14 @@ export function UsersTable() { ) : ( table.getRowModel().rows.map((row) => { const user = row.original - const isDeleted = isUserDeleted(user) - const isDisabled = user.status === USER_STATUS.DISABLED return ( {row.getVisibleCells().map((cell) => ( diff --git a/web/default/src/hooks/use-sidebar-data.ts b/web/default/src/hooks/use-sidebar-data.ts index 66265da5..b540e22f 100644 --- a/web/default/src/hooks/use-sidebar-data.ts +++ b/web/default/src/hooks/use-sidebar-data.ts @@ -80,6 +80,12 @@ export function useSidebarData(): SidebarData { configUrls: ['/usage-logs/drawing', '/usage-logs/task'], icon: ListTodo, }, + ], + }, + { + id: 'personal', + title: t('Personal'), + items: [ { title: t('Wallet'), url: '/wallet', diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 4a47ab78..5ac72f78 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -561,6 +561,7 @@ "channel(s)? This action cannot be undone.": "channel(s)? This action cannot be undone.", "Channels": "Channels", "Channels deleted successfully": "Channels deleted successfully", + "Chart Preferences": "Chart Preferences", "Chart Settings": "Chart Settings", "Chat": "Chat", "Chat area": "Chat area", @@ -600,6 +601,8 @@ "Choose how to filter IP addresses": "Choose how to filter IP addresses", "Choose the bundle type and define the items inside it.": "Choose the bundle type and define the items inside it.", "Choose where to fetch upstream metadata.": "Choose where to fetch upstream metadata.", + "Choose the default charts, range, and time granularity for model analytics.": "Choose the default charts, range, and time granularity for model analytics.", + "Choose which charts are selected by default when opening model analytics.": "Choose which charts are selected by default when opening model analytics.", "Classic (Legacy Frontend)": "Classic (Legacy Frontend)", "Claude": "Claude", "Claude CLI Header Passthrough": "Claude CLI Header Passthrough", @@ -929,6 +932,7 @@ "Daily Check-in": "Daily Check-in", "Dark": "Dark", "Dashboard": "Dashboard", + "Dashboard Preferences": "Dashboard Preferences", "Dashboards, tokens, and usage analytics.": "Dashboards, tokens, and usage analytics.", "Data Dashboard": "Data Dashboard", "Data directory:": "Data directory:", @@ -949,7 +953,10 @@ "Default API Version *": "Default API Version *", "Default API version for this channel": "Default API version for this channel", "Default Collapse Sidebar": "Default Collapse Sidebar", + "Default consumption chart": "Default consumption chart", "Default Max Tokens": "Default Max Tokens", + "Default model call chart": "Default model call chart", + "Default range": "Default range", "Default Responses API version, if empty, will use the API version above": "Default Responses API version, if empty, will use the API version above", "Default system prompt for this channel": "Default system prompt for this channel", "Default time granularity": "Default time granularity", @@ -1901,6 +1908,7 @@ "Leave empty to use username": "Leave empty to use username", "Legacy Format (JSON Object)": "Legacy Format (JSON Object)", "Legacy format must be a JSON object": "Legacy format must be a JSON object", + "Legacy Format Template": "Legacy Format Template", "Less": "Less", "Less Than": "Less Than", "Less Than or Equal": "Less Than or Equal", @@ -2524,6 +2532,7 @@ "Permit Passkey registration on non-HTTPS origins (only recommended for development)": "Permit Passkey registration on non-HTTPS origins (only recommended for development)", "Perplexity": "Perplexity", "Persist your data file": "Persist your data file", + "Personal": "Personal", "Personal area": "Personal area", "Personal Center Area": "Personal Center Area", "Personal info settings": "Personal info settings", @@ -2981,6 +2990,7 @@ "Save drawing settings": "Save drawing settings", "Save Epay settings": "Save Epay settings", "Save failed": "Save failed", + "Save Preferences": "Save Preferences", "Save failed, please retry": "Save failed, please retry", "Save general settings": "Save general settings", "Save group ratios": "Save group ratios", @@ -3064,6 +3074,8 @@ "Select channel type": "Select channel type", "Select currency": "Select currency", "Select date": "Select date", + "Select default chart": "Select default chart", + "Select default range": "Select default range", "Select display mode": "Select display mode", "Select end time": "Select end time", "Select from presets or type custom identifier.": "Select from presets or type custom identifier.", @@ -3863,7 +3875,6 @@ "Your Turnstile site key": "Your Turnstile site key", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom", - "Legacy Format Template": "Legacy Format Template" + "Zoom": "Zoom" } } diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index b35aff9a..fbc2ee7d 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -561,6 +561,7 @@ "channel(s)? This action cannot be undone.": "canal(aux) ? Cette action ne peut pas être annulée.", "Channels": "Canaux", "Channels deleted successfully": "Canaux supprimés avec succès", + "Chart Preferences": "Préférences des graphiques", "Chart Settings": "Paramètres du graphique", "Chat": "Discuter", "Chat area": "Zone de chat", @@ -600,6 +601,8 @@ "Choose how to filter IP addresses": "Choisissez comment filtrer les adresses IP", "Choose the bundle type and define the items inside it.": "Choisissez le type de bundle et définissez les éléments qu'il contient.", "Choose where to fetch upstream metadata.": "Choisissez où récupérer les métadonnées amont.", + "Choose the default charts, range, and time granularity for model analytics.": "Choisissez les graphiques, la plage et la granularité temporelle par défaut pour l'analyse des modèles.", + "Choose which charts are selected by default when opening model analytics.": "Choisissez les graphiques sélectionnés par défaut à l'ouverture de l'analyse des modèles.", "Classic (Legacy Frontend)": "Classique (Ancien frontend)", "Claude": "Claude", "Claude CLI Header Passthrough": "Passthrough en-tête Claude CLI", @@ -929,6 +932,7 @@ "Daily Check-in": "Connexion quotidienne", "Dark": "Sombre", "Dashboard": "Tableau de bord", + "Dashboard Preferences": "Préférences du tableau de bord", "Dashboards, tokens, and usage analytics.": "Tableaux de bord, jetons et analyses d'utilisation.", "Data Dashboard": "Tableau de bord des données", "Data directory:": "Répertoire des données :", @@ -949,7 +953,10 @@ "Default API Version *": "Version API par défaut *", "Default API version for this channel": "Version API par défaut pour ce canal", "Default Collapse Sidebar": "Réduire la barre latérale par défaut", + "Default consumption chart": "Graphique de consommation par défaut", "Default Max Tokens": "Jetons max par défaut", + "Default model call chart": "Graphique d'appels de modèle par défaut", + "Default range": "Plage par défaut", "Default Responses API version, if empty, will use the API version above": "Version API des réponses par défaut, si vide, utilisera la version API ci-dessus", "Default system prompt for this channel": "Invite système par défaut pour ce canal", "Default time granularity": "Granularité temporelle par défaut", @@ -1901,6 +1908,7 @@ "Leave empty to use username": "Laissez vide pour utiliser le nom d'utilisateur", "Legacy Format (JSON Object)": "Ancien format (objet JSON)", "Legacy format must be a JSON object": "L'ancien format doit être un objet JSON", + "Legacy Format Template": "Modèle ancien format", "Less": "Moins", "Less Than": "Inférieur à", "Less Than or Equal": "Inférieur ou égal", @@ -2524,6 +2532,7 @@ "Permit Passkey registration on non-HTTPS origins (only recommended for development)": "Autoriser l'enregistrement de Passkey sur des origines non-HTTPS (recommandé uniquement pour le développement)", "Perplexity": "Perplexity", "Persist your data file": "Conserver votre fichier de données", + "Personal": "Personnel", "Personal area": "Espace personnel", "Personal Center Area": "Espace personnel", "Personal info settings": "Paramètres d'informations personnelles", @@ -2981,6 +2990,7 @@ "Save drawing settings": "Enregistrer les paramètres de dessin", "Save Epay settings": "Enregistrer les paramètres Epay", "Save failed": "Échec de l'enregistrement", + "Save Preferences": "Enregistrer les préférences", "Save failed, please retry": "Échec de l'enregistrement, veuillez réessayer", "Save general settings": "Enregistrer les paramètres généraux", "Save group ratios": "Enregistrer les ratios de groupes", @@ -3064,6 +3074,8 @@ "Select channel type": "Sélectionner le type de canal", "Select currency": "Sélectionner la devise", "Select date": "Sélectionner la date", + "Select default chart": "Sélectionner le graphique par défaut", + "Select default range": "Sélectionner la plage par défaut", "Select display mode": "Sélectionner le mode d'affichage", "Select end time": "Sélectionner l'heure de fin", "Select from presets or type custom identifier.": "Sélectionner parmi les préréglages ou saisir un identifiant personnalisé.", @@ -3863,7 +3875,6 @@ "Your Turnstile site key": "Votre clé de site Turnstile", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom", - "Legacy Format Template": "Modèle ancien format" + "Zoom": "Zoom" } } diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index e72397b0..c7e64bde 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -561,6 +561,7 @@ "channel(s)? This action cannot be undone.": "チャネルを削除しますか?この操作は元に戻せません。", "Channels": "チャネル", "Channels deleted successfully": "チャンネルが正常に削除されました", + "Chart Preferences": "チャートの環境設定", "Chart Settings": "チャート設定", "Chat": "チャット", "Chat area": "チャットエリア", @@ -600,6 +601,8 @@ "Choose how to filter IP addresses": "IPアドレスをフィルタリングする方法を選択してください", "Choose the bundle type and define the items inside it.": "バンドルタイプを選択し、その中のアイテムを定義してください。", "Choose where to fetch upstream metadata.": "アップストリームのメタデータをどこからフェッチするかを選択してください。", + "Choose the default charts, range, and time granularity for model analytics.": "モデル分析のデフォルトチャート、範囲、時間粒度を選択します。", + "Choose which charts are selected by default when opening model analytics.": "モデル分析を開いたときにデフォルトで選択されるチャートを選択します。", "Classic (Legacy Frontend)": "クラシック(旧フロントエンド)", "Claude": "Claude", "Claude CLI Header Passthrough": "Claude CLI ヘッダーパススルー", @@ -929,6 +932,7 @@ "Daily Check-in": "毎日のチェックイン", "Dark": "ダーク", "Dashboard": "ダッシュボード", + "Dashboard Preferences": "ダッシュボードの環境設定", "Dashboards, tokens, and usage analytics.": "ダッシュボード、トークン、使用状況アナリティクス。", "Data Dashboard": "データダッシュボード", "Data directory:": "データディレクトリ:", @@ -949,7 +953,10 @@ "Default API Version *": "デフォルトのAPIバージョン *", "Default API version for this channel": "このチャンネルのデフォルトのAPIバージョン", "Default Collapse Sidebar": "デフォルトのサイドバー折りたたみ", + "Default consumption chart": "デフォルトの消費チャート", "Default Max Tokens": "デフォルトの最大トークン", + "Default model call chart": "デフォルトのモデル呼び出しチャート", + "Default range": "デフォルト範囲", "Default Responses API version, if empty, will use the API version above": "デフォルトの応答APIバージョン。空の場合、上記のAPIバージョンが使用されます", "Default system prompt for this channel": "このチャンネルのデフォルトのシステムプロンプト", "Default time granularity": "デフォルトの時間粒度", @@ -1901,6 +1908,7 @@ "Leave empty to use username": "ユーザー名を使用するには空のままにしてください", "Legacy Format (JSON Object)": "旧形式(JSONオブジェクト)", "Legacy format must be a JSON object": "旧形式はJSONオブジェクトである必要があります", + "Legacy Format Template": "旧フォーマットテンプレート", "Less": "少ない", "Less Than": "より小さい", "Less Than or Equal": "以下", @@ -2524,6 +2532,7 @@ "Permit Passkey registration on non-HTTPS origins (only recommended for development)": "非HTTPSオリジンでのパスキー登録を許可する(開発でのみ推奨)", "Perplexity": "Perplexity", "Persist your data file": "データファイルを永続化する", + "Personal": "個人", "Personal area": "個人エリア", "Personal Center Area": "パーソナルセンターエリア", "Personal info settings": "個人情報設定", @@ -2981,6 +2990,7 @@ "Save drawing settings": "描画設定を保存", "Save Epay settings": "Epay設定を保存", "Save failed": "保存に失敗しました", + "Save Preferences": "設定を保存", "Save failed, please retry": "保存に失敗しました。もう一度お試しください", "Save general settings": "一般設定を保存", "Save group ratios": "グループ比率を保存", @@ -3064,6 +3074,8 @@ "Select channel type": "チャネルタイプを選択", "Select currency": "通貨を選択", "Select date": "日付を選択", + "Select default chart": "デフォルトチャートを選択", + "Select default range": "デフォルト範囲を選択", "Select display mode": "表示モードを選択", "Select end time": "終了時間を選択", "Select from presets or type custom identifier.": "プリセットから選択するか、カスタム識別子を入力してください。", @@ -3863,7 +3875,6 @@ "Your Turnstile site key": "あなたのTurnstileサイトキー", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V 4", - "Zoom": "ズーム", - "Legacy Format Template": "旧フォーマットテンプレート" + "Zoom": "ズーム" } } diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index c908e7fc..0d101a9e 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -561,6 +561,7 @@ "channel(s)? This action cannot be undone.": "канал(ы)? Это действие нельзя отменить.", "Channels": "Каналы", "Channels deleted successfully": "Каналы успешно удалены", + "Chart Preferences": "Настройки графиков", "Chart Settings": "Настройки диаграммы", "Chat": "Чат", "Chat area": "Область чата", @@ -600,6 +601,8 @@ "Choose how to filter IP addresses": "Выберите, как фильтровать IP-адреса", "Choose the bundle type and define the items inside it.": "Выберите тип пакета и определите элементы внутри него.", "Choose where to fetch upstream metadata.": "Выберите, откуда получать метаданные вышестоящего источника.", + "Choose the default charts, range, and time granularity for model analytics.": "Выберите графики, диапазон и временную детализацию по умолчанию для аналитики моделей.", + "Choose which charts are selected by default when opening model analytics.": "Выберите графики, которые будут выбраны по умолчанию при открытии аналитики моделей.", "Classic (Legacy Frontend)": "Классический (Старый интерфейс)", "Claude": "Клод", "Claude CLI Header Passthrough": "Проброс заголовков Claude CLI", @@ -929,6 +932,7 @@ "Daily Check-in": "Ежедневный вход", "Dark": "Тёмная", "Dashboard": "Панель управления", + "Dashboard Preferences": "Настройки панели управления", "Dashboards, tokens, and usage analytics.": "Панели управления, токены и аналитика использования.", "Data Dashboard": "Панель мониторинга данных", "Data directory:": "Каталог данных:", @@ -949,7 +953,10 @@ "Default API Version *": "Версия API по умолчанию *", "Default API version for this channel": "Версия API по умолчанию для этого канала", "Default Collapse Sidebar": "Сворачивать боковую панель по умолчанию", + "Default consumption chart": "График потребления по умолчанию", "Default Max Tokens": "Максимальное количество токенов по умолчанию", + "Default model call chart": "График вызовов моделей по умолчанию", + "Default range": "Диапазон по умолчанию", "Default Responses API version, if empty, will use the API version above": "Версия API ответов по умолчанию; если пусто, будет использоваться версия API, указанная выше", "Default system prompt for this channel": "Системный промпт по умолчанию для этого канала", "Default time granularity": "Гранулярность времени по умолчанию", @@ -1901,6 +1908,7 @@ "Leave empty to use username": "Оставьте пустым, чтобы использовать имя пользователя", "Legacy Format (JSON Object)": "Старый формат (JSON-объект)", "Legacy format must be a JSON object": "Старый формат должен быть JSON-объектом", + "Legacy Format Template": "Шаблон старого формата", "Less": "Меньше", "Less Than": "Меньше", "Less Than or Equal": "Меньше или равно", @@ -2524,6 +2532,7 @@ "Permit Passkey registration on non-HTTPS origins (only recommended for development)": "Разрешить регистрацию Passkey на не-HTTPS источниках (рекомендуется только для разработки)", "Perplexity": "Perplexity", "Persist your data file": "Сохранить ваш файл данных", + "Personal": "Личное", "Personal area": "Личный кабинет", "Personal Center Area": "Область личного кабинета", "Personal info settings": "Настройки личной информации", @@ -2981,6 +2990,7 @@ "Save drawing settings": "Сохранить настройки рисования", "Save Epay settings": "Сохранить настройки Epay", "Save failed": "Не удалось сохранить", + "Save Preferences": "Сохранить настройки", "Save failed, please retry": "Не удалось сохранить, попробуйте снова", "Save general settings": "Сохранить общие настройки", "Save group ratios": "Сохранить коэффициенты групп", @@ -3064,6 +3074,8 @@ "Select channel type": "Выбрать тип канала", "Select currency": "Выберите валюту", "Select date": "Выберите дату", + "Select default chart": "Выберите график по умолчанию", + "Select default range": "Выберите диапазон по умолчанию", "Select display mode": "Выбрать режим отображения", "Select end time": "Выбрать время окончания", "Select from presets or type custom identifier.": "Выберите из предустановок или введите пользовательский идентификатор.", @@ -3863,7 +3875,6 @@ "Your Turnstile site key": "Ключ сайта Turnstile", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom", - "Legacy Format Template": "Шаблон старого формата" + "Zoom": "Zoom" } } diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index e16627c6..36a4bfa9 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -561,6 +561,7 @@ "channel(s)? This action cannot be undone.": "kênh(s)? Hành động này không thể hoàn tác.", "Channels": "Kênh", "Channels deleted successfully": "Xóa kênh thành công", + "Chart Preferences": "Tùy chọn biểu đồ", "Chart Settings": "Cài đặt Biểu đồ", "Chat": "Trò chuyện", "Chat area": "Khu vực trò chuyện", @@ -600,6 +601,8 @@ "Choose how to filter IP addresses": "Chọn cách lọc địa chỉ IP", "Choose the bundle type and define the items inside it.": "Chọn loại gói và định nghĩa các mục bên trong nó.", "Choose where to fetch upstream metadata.": "Chọn nơi để tìm nạp siêu dữ liệu thượng nguồn.", + "Choose the default charts, range, and time granularity for model analytics.": "Chọn biểu đồ, khoảng thời gian và độ chi tiết thời gian mặc định cho phân tích mô hình.", + "Choose which charts are selected by default when opening model analytics.": "Chọn biểu đồ được chọn mặc định khi mở phân tích mô hình.", "Classic (Legacy Frontend)": "Cổ điển (Frontend cũ)", "Claude": "Claude", "Claude CLI Header Passthrough": "Chuyển tiếp header Claude CLI", @@ -929,6 +932,7 @@ "Daily Check-in": "Điểm danh hàng ngày", "Dark": "Tối", "Dashboard": "Bảng điều khiển", + "Dashboard Preferences": "Tùy chọn bảng điều khiển", "Dashboards, tokens, and usage analytics.": "Bảng điều khiển, token và phân tích sử dụng.", "Data Dashboard": "Bảng dữ liệu", "Data directory:": "Thư mục dữ liệu:", @@ -949,7 +953,10 @@ "Default API Version *": "Phiên bản API mặc định *", "Default API version for this channel": "Phiên bản API mặc định cho kênh này", "Default Collapse Sidebar": "Mặc định Thu gọn Thanh bên", + "Default consumption chart": "Biểu đồ tiêu thụ mặc định", "Default Max Tokens": "Tokens Tối đa Mặc định", + "Default model call chart": "Biểu đồ lượt gọi mô hình mặc định", + "Default range": "Khoảng mặc định", "Default Responses API version, if empty, will use the API version above": "Phiên bản API phản hồi mặc định, nếu để trống, sẽ sử dụng phiên bản API ở trên", "Default system prompt for this channel": "Lời nhắc hệ thống mặc định cho kênh này", "Default time granularity": "Độ chi tiết thời gian mặc định", @@ -1901,6 +1908,7 @@ "Leave empty to use username": "Để trống để sử dụng tên người dùng", "Legacy Format (JSON Object)": "Định dạng cũ (đối tượng JSON)", "Legacy format must be a JSON object": "Định dạng cũ phải là đối tượng JSON", + "Legacy Format Template": "Mẫu định dạng cũ", "Less": "Ít hơn", "Less Than": "Nhỏ hơn", "Less Than or Equal": "Nhỏ hơn hoặc bằng", @@ -2524,6 +2532,7 @@ "Permit Passkey registration on non-HTTPS origins (only recommended for development)": "Cho phép đăng ký Passkey trên các nguồn gốc không phải HTTPS (chỉ khuyến nghị cho mục đích phát triển)", "Perplexity": "Sự bối rối", "Persist your data file": "Lưu trữ tệp dữ liệu của bạn", + "Personal": "Cá nhân", "Personal area": "Khu vực cá nhân", "Personal Center Area": "Khu vực cá nhân", "Personal info settings": "Cài đặt thông tin cá nhân", @@ -2981,6 +2990,7 @@ "Save drawing settings": "Lưu cài đặt bản vẽ", "Save Epay settings": "Lưu cài đặt Epay", "Save failed": "Lưu thất bại", + "Save Preferences": "Lưu tùy chọn", "Save failed, please retry": "Lưu thất bại, vui lòng thử lại", "Save general settings": "Lưu cài đặt chung", "Save group ratios": "Lưu tỷ lệ nhóm", @@ -3064,6 +3074,8 @@ "Select channel type": "Chọn loại kênh", "Select currency": "Chọn tiền tệ", "Select date": "Chọn ngày", + "Select default chart": "Chọn biểu đồ mặc định", + "Select default range": "Chọn khoảng mặc định", "Select display mode": "Chọn chế độ hiển thị", "Select end time": "Chọn thời gian kết thúc", "Select from presets or type custom identifier.": "Chọn từ các cài đặt sẵn hoặc nhập mã định danh tùy chỉnh.", @@ -3863,7 +3875,6 @@ "Your Turnstile site key": "Khóa site Turnstile của bạn", "Zhipu": "Zhipu", "Zhipu V4": "Zhipu V4", - "Zoom": "Zoom", - "Legacy Format Template": "Mẫu định dạng cũ" + "Zoom": "Zoom" } } diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 2e01a17b..33ee6176 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -561,6 +561,7 @@ "channel(s)? This action cannot be undone.": "渠道?此操作无法撤销。", "Channels": "渠道", "Channels deleted successfully": "渠道删除成功", + "Chart Preferences": "图表偏好设置", "Chart Settings": "图表设置", "Chat": "聊天", "Chat area": "聊天区域", @@ -600,6 +601,8 @@ "Choose how to filter IP addresses": "选择如何过滤 IP 地址", "Choose the bundle type and define the items inside it.": "选择捆绑包类型并定义其中的项目。", "Choose where to fetch upstream metadata.": "选择从何处获取上游元数据。", + "Choose the default charts, range, and time granularity for model analytics.": "选择模型调用分析的默认图表、范围和时间粒度。", + "Choose which charts are selected by default when opening model analytics.": "选择打开模型调用分析时默认选中的图表。", "Classic (Legacy Frontend)": "经典前端", "Claude": "Claude", "Claude CLI Header Passthrough": "Claude CLI 请求头透传", @@ -929,6 +932,7 @@ "Daily Check-in": "每日签到", "Dark": "深色", "Dashboard": "数据看板", + "Dashboard Preferences": "看板偏好设置", "Dashboards, tokens, and usage analytics.": "仪表板、令牌和使用分析。", "Data Dashboard": "数据仪表板", "Data directory:": "数据目录:", @@ -949,7 +953,10 @@ "Default API Version *": "默认 API 版本 *", "Default API version for this channel": "此渠道的默认 API 版本", "Default Collapse Sidebar": "默认折叠侧边栏", + "Default consumption chart": "默认消耗分布图", "Default Max Tokens": "默认最大 Token 数", + "Default model call chart": "默认模型调用图", + "Default range": "默认范围", "Default Responses API version, if empty, will use the API version above": "默认响应 API 版本,如果为空,将使用上面的 API 版本", "Default system prompt for this channel": "此渠道的默认系统提示", "Default time granularity": "默认时间粒度", @@ -1901,6 +1908,7 @@ "Leave empty to use username": "留空以使用用户名", "Legacy Format (JSON Object)": "旧格式(JSON 对象)", "Legacy format must be a JSON object": "旧格式必须是 JSON 对象", + "Legacy Format Template": "旧格式模板", "Less": "更少", "Less Than": "小于", "Less Than or Equal": "小于等于", @@ -2524,6 +2532,7 @@ "Permit Passkey registration on non-HTTPS origins (only recommended for development)": "允许在非 HTTPS 源上注册通行密钥(仅建议用于开发)", "Perplexity": "Perplexity", "Persist your data file": "持久化您的数据文件", + "Personal": "个人", "Personal area": "个人中心", "Personal Center Area": "个人中心区域", "Personal info settings": "个人信息设置", @@ -2981,6 +2990,7 @@ "Save drawing settings": "保存绘图设置", "Save Epay settings": "保存 Epay 设置", "Save failed": "保存失败", + "Save Preferences": "保存偏好设置", "Save failed, please retry": "保存失败,请重试", "Save general settings": "保存通用设置", "Save group ratios": "保存分组比率", @@ -3064,6 +3074,8 @@ "Select channel type": "选择渠道类型", "Select currency": "选择货币", "Select date": "选择日期", + "Select default chart": "选择默认图表", + "Select default range": "选择默认范围", "Select display mode": "选择显示模式", "Select end time": "选择结束时间", "Select from presets or type custom identifier.": "从预设中选择或输入自定义标识符。", @@ -3863,7 +3875,6 @@ "Your Turnstile site key": "您的 Turnstile 站点密钥", "Zhipu": "智谱", "Zhipu V4": "智谱 V4", - "Zoom": "缩放", - "Legacy Format Template": "旧格式模板" + "Zoom": "缩放" } }