From 50b8f2a2723e2a6bd6b167a54b2ee9a1df41f6bf Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 19 Jun 2026 19:29:00 +0800 Subject: [PATCH] refactor: update styling and improve code readability across multiple components --- web/default/.oxlintrc.json | 2 +- .../data-table/core/column-pinning.ts | 11 +- .../data-table/core/data-table-view.tsx | 12 +- .../src/components/data-table/index.ts | 4 +- .../data-table/layout/card-grid.tsx | 13 +- .../data-table/layout/data-table-page.tsx | 35 +-- .../data-table/layout/mobile-card-list.tsx | 13 +- .../static/static-data-table-classnames.ts | 2 +- web/default/src/components/status-badge.tsx | 9 +- .../channels/components/channels-columns.tsx | 155 +++++++----- .../components/dialogs/codex-usage-dialog.tsx | 223 +++++++++++------- .../src/features/channels/constants.ts | 6 +- .../features/channels/lib/channel-utils.ts | 165 +++++++++---- .../dialogs/view-details-dialog.tsx | 55 +++-- .../components/dialogs/view-logs-dialog.tsx | 138 ++++++----- .../usage-logs/components/model-badge.tsx | 11 +- web/default/src/lib/show-submitted-data.tsx | 4 +- web/default/src/styles/theme-presets.css | 66 +++--- web/default/src/styles/theme.css | 86 +++++-- 19 files changed, 646 insertions(+), 364 deletions(-) diff --git a/web/default/.oxlintrc.json b/web/default/.oxlintrc.json index 41c49ddc..2bbca9da 100644 --- a/web/default/.oxlintrc.json +++ b/web/default/.oxlintrc.json @@ -21,7 +21,7 @@ "src/routeTree.gen.ts" ], "rules": { - "curly": "error", + "curly": ["error", "multi-line"], "eqeqeq": [ "error", "always", diff --git a/web/default/src/components/data-table/core/column-pinning.ts b/web/default/src/components/data-table/core/column-pinning.ts index fb43dfe9..4395a5d9 100644 --- a/web/default/src/components/data-table/core/column-pinning.ts +++ b/web/default/src/components/data-table/core/column-pinning.ts @@ -17,6 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { cn } from '@/lib/utils' + import type { DataTableColumnClassName, DataTablePinnedColumn } from './types' export function getResolvedColumnClassName( @@ -37,14 +38,18 @@ export function getResolvedColumnClassNameFromMap( const customClassName = getColumnClassName?.(columnId, kind) const pinnedColumn = pinnedColumnById?.get(columnId) - if (!pinnedColumn) return customClassName + if (!pinnedColumn) { + return customClassName + } return cn(customClassName, getPinnedColumnClassName(pinnedColumn, kind)) } } export function getPinnedColumnMap(pinnedColumns?: DataTablePinnedColumn[]) { - if (!pinnedColumns?.length) return undefined + if (!pinnedColumns?.length) { + return undefined + } return new Map(pinnedColumns.map((column) => [column.columnId, column])) } @@ -63,7 +68,7 @@ function getPinnedColumnClassName( pinnedColumn.side === 'left' ? 'left-0' : 'right-0', edgeClassName, kind === 'header' - ? '[background-color:var(--table-header-bg,var(--background))] group-hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] z-30' + ? '[background-color:var(--table-header-bg,var(--table-header))] group-hover:[background-color:var(--table-header-hover)] z-30' : 'bg-background z-10 group-hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] group-data-[state=selected]:bg-muted', pinnedColumn.className, kind === 'header' diff --git a/web/default/src/components/data-table/core/data-table-view.tsx b/web/default/src/components/data-table/core/data-table-view.tsx index 3def71dc..30970506 100644 --- a/web/default/src/components/data-table/core/data-table-view.tsx +++ b/web/default/src/components/data-table/core/data-table-view.tsx @@ -1,3 +1,4 @@ +import type { Row, Table as TanstackTable } from '@tanstack/react-table' /* Copyright (C) 2023-2026 QuantumNous @@ -17,9 +18,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' -import { type Row, type Table as TanstackTable } from '@tanstack/react-table' -import { cn } from '@/lib/utils' + import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table' +import { cn } from '@/lib/utils' + import { getPinnedColumnMap, getResolvedColumnClassNameFromMap, @@ -136,7 +138,7 @@ function SplitHeaderTableView({
( ): DataTablePinnedColumn[] { return table.getAllColumns().flatMap((column) => { const side = column.columnDef.meta?.pinned - if (!side) return [] + if (!side) { + return [] + } return [{ columnId: column.id, side }] }) diff --git a/web/default/src/components/data-table/index.ts b/web/default/src/components/data-table/index.ts index c79711ca..1b6dd233 100644 --- a/web/default/src/components/data-table/index.ts +++ b/web/default/src/components/data-table/index.ts @@ -61,7 +61,7 @@ export { export { useDebouncedColumnFilter } from './hooks/use-debounced-column-filter' export const DISABLED_ROW_DESKTOP = - 'bg-muted/85 hover:bg-muted [&>td:first-child]:border-l-muted-foreground/35 [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1' + '[--data-table-card-bg:var(--table-disabled)] hover:[--data-table-card-bg:var(--table-disabled-hover)] [background-color:var(--table-disabled)] hover:[background-color:var(--table-disabled-hover)] [&>td:first-child]:[border-left-color:var(--table-disabled-border)] [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1' export const DISABLED_ROW_MOBILE = - 'border-l-4 border-l-muted-foreground/35 bg-muted/85' + '[--data-table-card-bg:var(--table-disabled)] [background-color:var(--table-disabled)] border-l-4 [border-left-color:var(--table-disabled-border)]' diff --git a/web/default/src/components/data-table/layout/card-grid.tsx b/web/default/src/components/data-table/layout/card-grid.tsx index 65ac40b0..98b49f5e 100644 --- a/web/default/src/components/data-table/layout/card-grid.tsx +++ b/web/default/src/components/data-table/layout/card-grid.tsx @@ -1,3 +1,5 @@ +import type { Row, Table } from '@tanstack/react-table' +import { Database } from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -17,10 +19,8 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' -import type { Row, Table } from '@tanstack/react-table' -import { Database } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { cn } from '@/lib/utils' + import { Empty, EmptyDescription, @@ -29,6 +29,8 @@ import { EmptyTitle, } from '@/components/ui/empty' import { Skeleton } from '@/components/ui/skeleton' +import { cn } from '@/lib/utils' + import { tableHasCompactMeta } from './card-cell-utils' import { CardRowContent } from './card-row-content' @@ -78,7 +80,7 @@ function CardGridSkeleton(props: { {[1, 2, 3, 4, 5, 6].map((i) => (
@@ -157,8 +159,9 @@ export function DataTableCardGrid(props: DataTableCardGridProps) { return (
diff --git a/web/default/src/components/data-table/layout/data-table-page.tsx b/web/default/src/components/data-table/layout/data-table-page.tsx index 99883d1a..e998d116 100644 --- a/web/default/src/components/data-table/layout/data-table-page.tsx +++ b/web/default/src/components/data-table/layout/data-table-page.tsx @@ -1,3 +1,8 @@ +import type { + ColumnDef, + Row, + Table as TanstackTable, +} from '@tanstack/react-table' /* Copyright (C) 2023-2026 QuantumNous @@ -17,14 +22,11 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' -import { - type ColumnDef, - type Row, - type Table as TanstackTable, -} from '@tanstack/react-table' + +import { PageFooterPortal } from '@/components/layout' import { useMediaQuery } from '@/hooks' import { cn } from '@/lib/utils' -import { PageFooterPortal } from '@/components/layout' + import { DataTableView, type DataTableColumnClassName, @@ -32,15 +34,15 @@ import { type DataTableRenderRowHelpers, } from '../core/data-table-view' import { DataTablePagination } from '../core/pagination' -import { DataTableToolbar } from '../toolbar/toolbar' -import { DataTableViewModeToggle } from '../toolbar/view-mode-toggle' import { DATA_TABLE_VIEW_MODES, useDataTableViewMode, type DataTableViewMode, } from '../hooks/use-data-table-view-mode' -import { MobileCardList } from './mobile-card-list' +import { DataTableToolbar } from '../toolbar/toolbar' +import { DataTableViewModeToggle } from '../toolbar/view-mode-toggle' import { DataTableCardGrid } from './card-grid' +import { MobileCardList } from './mobile-card-list' /** * Pass-through configuration for the default {@link DataTableToolbar}. @@ -376,7 +378,9 @@ function renderToolbar( function renderPagination( props: DataTablePageProps ): React.ReactNode { - if (props.showPagination === false) return null + if (props.showPagination === false) { + return null + } const pagination = @@ -391,7 +395,9 @@ function renderMobile( props: DataTablePageProps, showMobile: boolean ): React.ReactNode { - if (!showMobile) return null + if (!showMobile) { + return null + } const ownGetRowClassName = props.getRowClassName const mobileGetRowClassName = @@ -436,7 +442,9 @@ function renderDesktop( cardViewActive: boolean, viewMode: DataTableViewMode ): React.ReactNode { - if (showMobile) return null + if (showMobile) { + return null + } const isFetchingOnly = props.isFetching && !props.isLoading const fixedHeight = props.fixedHeight !== false @@ -481,8 +489,7 @@ function renderDesktop( splitHeader={fixedHeight} tableContainerClassName={fixedHeight ? 'h-full min-h-0' : undefined} tableHeaderClassName={cn( - fixedHeight && - '[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]', + fixedHeight && '[background-color:var(--table-header)]', props.tableHeaderClassName )} getColumnClassName={props.getColumnClassName} diff --git a/web/default/src/components/data-table/layout/mobile-card-list.tsx b/web/default/src/components/data-table/layout/mobile-card-list.tsx index 6c91d440..48127883 100644 --- a/web/default/src/components/data-table/layout/mobile-card-list.tsx +++ b/web/default/src/components/data-table/layout/mobile-card-list.tsx @@ -1,3 +1,5 @@ +import type { Row, Table } from '@tanstack/react-table' +import { Database } from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -17,10 +19,8 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' -import type { Row, Table } from '@tanstack/react-table' -import { Database } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { cn } from '@/lib/utils' + import { Empty, EmptyDescription, @@ -29,6 +29,8 @@ import { EmptyTitle, } from '@/components/ui/empty' import { Skeleton } from '@/components/ui/skeleton' +import { cn } from '@/lib/utils' + import { tableHasCompactMeta } from './card-cell-utils' import { CardRowContent } from './card-row-content' @@ -143,7 +145,10 @@ export function MobileCardList(props: MobileCardListProps) { return (
diff --git a/web/default/src/components/data-table/static/static-data-table-classnames.ts b/web/default/src/components/data-table/static/static-data-table-classnames.ts index 382e149a..93a39710 100644 --- a/web/default/src/components/data-table/static/static-data-table-classnames.ts +++ b/web/default/src/components/data-table/static/static-data-table-classnames.ts @@ -23,7 +23,7 @@ export const staticDataTableClassNames = { compactTable: 'text-sm', compactHeaderRow: 'hover:bg-transparent', mutedHeaderRow: - '[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))] hover:[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]', + '[background-color:var(--table-header)] hover:[background-color:var(--table-header-hover)]', compactHeaderCell: 'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase', compactHeaderCellRight: diff --git a/web/default/src/components/status-badge.tsx b/web/default/src/components/status-badge.tsx index 5858db45..61571d69 100644 --- a/web/default/src/components/status-badge.tsx +++ b/web/default/src/components/status-badge.tsx @@ -1,3 +1,4 @@ +import type { LucideIcon } from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -18,10 +19,10 @@ For commercial licensing, please contact support@quantumnous.com */ /* eslint-disable react-refresh/only-export-components */ import * as React from 'react' -import { type LucideIcon } from 'lucide-react' + +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { stringToColor } from '@/lib/colors' import { cn } from '@/lib/utils' -import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' export const dotColorMap = { success: 'bg-success', @@ -37,7 +38,7 @@ export const dotColorMap = { grey: 'bg-neutral', indigo: 'bg-chart-1', 'light-blue': 'bg-info', - 'light-green': 'bg-success', + 'light-green': 'bg-emerald-400', lime: 'bg-chart-3', orange: 'bg-warning', pink: 'bg-chart-5', @@ -61,7 +62,7 @@ export const textColorMap = { grey: 'text-muted-foreground', indigo: 'text-chart-1', 'light-blue': 'text-info', - 'light-green': 'text-success', + 'light-green': 'text-emerald-500 dark:text-emerald-300', lime: 'text-chart-3', orange: 'text-warning', pink: 'text-chart-5', diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 97e3e66d..062cd699 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -1,3 +1,13 @@ +import { useQueryClient } from '@tanstack/react-query' +import type { ColumnDef } from '@tanstack/react-table' +import { + AlertTriangle, + ChevronDown, + ChevronRight, + ListOrdered, + Shuffle, + SlidersHorizontal, +} from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -18,18 +28,24 @@ For commercial licensing, please contact support@quantumnous.com */ /* eslint-disable react-refresh/only-export-components */ import { useState } from 'react' -import { useQueryClient } from '@tanstack/react-query' -import { type ColumnDef } from '@tanstack/react-table' -import { - AlertTriangle, - ChevronDown, - ChevronRight, - ListOrdered, - Shuffle, - SlidersHorizontal, -} from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' + +import { ConfirmDialog } from '@/components/confirm-dialog' +import { BadgeListCell } from '@/components/data-table' +import { GroupBadge } from '@/components/group-badge' +import { ProviderBadge } from '@/components/provider-badge' +import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' +import { TableId } from '@/components/table-id' +import { TruncatedText } from '@/components/truncated-text' +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' import { formatCurrencyFromUSD, formatQuotaWithCurrency, @@ -40,21 +56,7 @@ import { formatQuota as formatQuotaValue, } from '@/lib/format' import { truncateText } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip' -import { ConfirmDialog } from '@/components/confirm-dialog' -import { BadgeListCell } from '@/components/data-table' -import { GroupBadge } from '@/components/group-badge' -import { ProviderBadge } from '@/components/provider-badge' -import { StatusBadge } from '@/components/status-badge' -import { TableId } from '@/components/table-id' -import { TruncatedText } from '@/components/truncated-text' + import { getCodexUsage } from '../api' import { CHANNEL_STATUS_CONFIG, MODEL_FETCHABLE_TYPES } from '../constants' import { @@ -90,7 +92,9 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | { source?: string deployment_id?: string } { - if (!otherInfo) return null + if (!otherInfo) { + return null + } try { const parsed = JSON.parse(otherInfo) if (parsed && typeof parsed === 'object') { @@ -107,14 +111,20 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | { */ function UpstreamUpdateTags({ channel }: { channel: Channel }) { const { upstream, setCurrentRow } = useChannels() - if (!MODEL_FETCHABLE_TYPES.has(channel.type)) return null + if (!MODEL_FETCHABLE_TYPES.has(channel.type)) { + return null + } const meta = parseUpstreamUpdateMeta(channel.settings) - if (!meta.enabled) return null + if (!meta.enabled) { + return null + } const addCount = meta.pendingAddModels.length const removeCount = meta.pendingRemoveModels.length - if (addCount === 0 && removeCount === 0) return null + if (addCount === 0 && removeCount === 0) { + return null + } return (
@@ -300,7 +310,9 @@ function BalanceCell({ channel }: { channel: Channel }) { const remainingFull = withSuffix(formatBalance(balance)) const usedDisplay = usedFull.length > MAX_INLINE_BALANCE_CHARS - ? withSuffix(formatQuotaWithCurrency(usedQuota, { compact: true, locale })) + ? withSuffix( + formatQuotaWithCurrency(usedQuota, { compact: true, locale }) + ) : usedFull const remainingDisplay = remainingFull.length > MAX_INLINE_BALANCE_CHARS @@ -338,7 +350,9 @@ function BalanceCell({ channel }: { channel: Channel }) { const variant = getBalanceVariant(balance) const handleClickUpdate = async () => { - if (isUpdating) return + if (isUpdating) { + return + } setIsUpdating(true) if (channel.type === 57) { @@ -362,6 +376,18 @@ function BalanceCell({ channel }: { channel: Channel }) { await handleUpdateChannelBalance(channel.id, queryClient) setIsUpdating(false) } + let remainingBadgeLabel = remainingDisplay + if (isUpdating) { + remainingBadgeLabel = t('Updating...') + } else if (channel.type === 57) { + remainingBadgeLabel = t('Account Info') + } + let remainingBadgeVariant: StatusBadgeProps['variant'] = variant + if (channel.type === 57) { + remainingBadgeVariant = 'info' + } else if (isUpdating) { + remainingBadgeVariant = 'neutral' + } return ( @@ -387,20 +413,8 @@ function BalanceCell({ channel }: { channel: Channel }) { { - if (isUpdating) return + if (isUpdating) { + return + } setIsUpdating(true) try { const res = await getCodexUsage(channel.id) @@ -565,7 +581,7 @@ export function useChannelsColumns(): ColumnDef[] { render={ } - > + /> {t( 'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.' @@ -581,7 +597,7 @@ export function useChannelsColumns(): ColumnDef[] { render={ } - > + /> {t('Override request parameters')} @@ -698,7 +714,9 @@ export function useChannelsColumns(): ColumnDef[] { className='flex cursor-pointer items-center gap-1.5 text-xs font-medium' onClick={(e) => { e.stopPropagation() - if (!deploymentId) return + if (!deploymentId) { + return + } const targetUrl = `/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}` window.open(targetUrl, '_blank', 'noopener') }} @@ -735,7 +753,9 @@ export function useChannelsColumns(): ColumnDef[] { ) }, filterFn: (row, id, value) => { - if (!value || value.length === 0 || value.includes('all')) return true + if (!value || value.length === 0 || value.includes('all')) { + return true + } return value.includes(String(row.getValue(id))) }, size: 220, @@ -856,10 +876,16 @@ export function useChannelsColumns(): ColumnDef[] { ) }, filterFn: (row, id, value) => { - if (!value || value.length === 0 || value.includes('all')) return true + if (!value || value.length === 0 || value.includes('all')) { + return true + } const status = row.getValue(id) as number - if (value.includes('enabled')) return status === 1 - if (value.includes('disabled')) return status !== 1 + if (value.includes('enabled')) { + return status === 1 + } + if (value.includes('disabled')) { + return status !== 1 + } return false }, size: 120, @@ -876,9 +902,9 @@ export function useChannelsColumns(): ColumnDef[] { const modelArray = parseModelsList(models) return ( ( + items={modelArray.map((model) => ( [] { ) }, filterFn: (row, id, value) => { - if (!value || value.length === 0 || value.includes('all')) return true + if (!value || value.length === 0 || value.includes('all')) { + return true + } const group = row.getValue(id) as string const groupArray = parseGroupsList(group) return groupArray.some((g) => value.includes(g)) @@ -925,8 +953,9 @@ export function useChannelsColumns(): ColumnDef[] { meta: { mobileHidden: true }, cell: ({ row }) => { const tag = row.getValue('tag') as string | null - if (!tag) + if (!tag) { return - + } return ( [] { + } - > - {timeText} - + />

{fullDate}

diff --git a/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx b/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx index 83797807..064d7d7f 100644 --- a/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx @@ -1,3 +1,12 @@ +import { + Copy, + Check, + RefreshCw, + ChevronDown, + ChevronUp, + RotateCcw, + AlertTriangle, +} from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -17,20 +26,11 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { type ReactNode, useCallback, useMemo, useState } from 'react' -import { - Copy, - Check, - RefreshCw, - ChevronDown, - ChevronUp, - RotateCcw, - AlertTriangle, -} from 'lucide-react' import { useTranslation } from 'react-i18next' -import dayjs from '@/lib/dayjs' -import { formatDateTimeStr, formatTimestampToDate } from '@/lib/format' -import { cn } from '@/lib/utils' -import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' + +import { ConfirmDialog } from '@/components/confirm-dialog' +import { Dialog } from '@/components/dialog' +import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { @@ -55,9 +55,11 @@ import { import { Progress } from '@/components/ui/progress' import { ScrollArea } from '@/components/ui/scroll-area' import { Skeleton } from '@/components/ui/skeleton' -import { ConfirmDialog } from '@/components/confirm-dialog' -import { Dialog } from '@/components/dialog' -import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' +import dayjs from '@/lib/dayjs' +import { formatDateTimeStr, formatTimestampToDate } from '@/lib/format' +import { cn } from '@/lib/utils' + import { getCodexResetCredits, resetCodexUsage, @@ -153,9 +155,13 @@ function formatUnixSeconds(unixSeconds: unknown): string { } function formatIsoTimestamp(value: unknown): string { - if (typeof value !== 'string' || value.trim() === '') return '-' + if (typeof value !== 'string' || value.trim() === '') { + return '-' + } const d = dayjs(value) - if (!d.isValid()) return value + if (!d.isValid()) { + return value + } return formatDateTimeStr(d.toDate()) } @@ -164,15 +170,21 @@ function formatDurationSeconds( t: (key: string) => string ): string { const s = Number(seconds) - if (!Number.isFinite(s) || s <= 0) return '-' + if (!Number.isFinite(s) || s <= 0) { + return '-' + } const total = Math.floor(s) const hours = Math.floor(total / 3600) const minutes = Math.floor((total % 3600) / 60) const secs = total % 60 - if (hours > 0) return `${hours}${t('h')} ${minutes}${t('m')}` - if (minutes > 0) return `${minutes}${t('m')} ${secs}${t('s')}` + if (hours > 0) { + return `${hours}${t('h')} ${minutes}${t('m')}` + } + if (minutes > 0) { + return `${minutes}${t('m')} ${secs}${t('s')}` + } return `${secs}${t('s')}` } @@ -180,12 +192,18 @@ function formatTimeLeftUntil( value: unknown, t: (key: string) => string ): string { - if (typeof value !== 'string' || value.trim() === '') return '-' + if (typeof value !== 'string' || value.trim() === '') { + return '-' + } const expiresAt = dayjs(value) - if (!expiresAt.isValid()) return '-' + if (!expiresAt.isValid()) { + return '-' + } const secondsLeft = expiresAt.diff(dayjs(), 'second') - if (secondsLeft <= 0) return t('Expired') + if (secondsLeft <= 0) { + return t('Expired') + } const days = Math.floor(secondsLeft / (24 * 60 * 60)) const remainingSeconds = secondsLeft % (24 * 60 * 60) @@ -198,7 +216,9 @@ function formatTimeLeftUntil( } function normalizePlanType(value: unknown): string { - if (value == null) return '' + if (value == null) { + return '' + } return String(value).trim().toLowerCase() } @@ -220,15 +240,21 @@ function sortResetCredits(credits: CodexResetCredit[]): CodexResetCredit[] { return [...credits].sort((a, b) => { const aAvailable = normalizeResetCreditStatus(a.status) === 'available' const bAvailable = normalizeResetCreditStatus(b.status) === 'available' - if (aAvailable !== bAvailable) return aAvailable ? -1 : 1 + if (aAvailable !== bAvailable) { + return aAvailable ? -1 : 1 + } const expiresDiff = parseTimeValue(a.expires_at) - parseTimeValue(b.expires_at) - if (expiresDiff !== 0) return expiresDiff + if (expiresDiff !== 0) { + return expiresDiff + } const grantedDiff = parseTimeValue(a.granted_at) - parseTimeValue(b.granted_at) - if (grantedDiff !== 0) return grantedDiff + if (grantedDiff !== 0) { + return grantedDiff + } return String(a.id || '').localeCompare(String(b.id || '')) }) @@ -238,7 +264,9 @@ function classifyWindowByDuration( windowData?: CodexRateLimitWindow | null ): 'weekly' | 'fiveHour' | null { const seconds = Number(windowData?.limit_window_seconds) - if (!Number.isFinite(seconds) || seconds <= 0) return null + if (!Number.isFinite(seconds) || seconds <= 0) { + return null + } return seconds >= 24 * 60 * 60 ? 'weekly' : 'fiveHour' } @@ -272,7 +300,9 @@ function resolveRateLimitWindows(data: RateLimitSource | null): { } if (planType === 'free') { - if (!weeklyWindow) weeklyWindow = primary ?? secondary ?? null + if (!weeklyWindow) { + weeklyWindow = primary ?? secondary ?? null + } return { fiveHourWindow: null, weeklyWindow } } @@ -338,8 +368,12 @@ function getResetCreditStatusBadge( function windowLabel(windowData?: CodexRateLimitWindow | null) { const percent = clampPercent(windowData?.used_percent) - const variant: StatusBadgeProps['variant'] = - percent >= 95 ? 'danger' : percent >= 80 ? 'warning' : 'info' + let variant: StatusBadgeProps['variant'] = 'info' + if (percent >= 95) { + variant = 'danger' + } else if (percent >= 80) { + variant = 'warning' + } return { percent, variant } } @@ -381,7 +415,7 @@ const percentTextClassName: Record< grey: 'text-muted-foreground', indigo: 'text-chart-1', 'light-blue': 'text-info', - 'light-green': 'text-success', + 'light-green': 'text-emerald-500 dark:text-emerald-300', lime: 'text-chart-3', orange: 'text-warning', pink: 'text-chart-5', @@ -708,6 +742,51 @@ function ResetCreditsPanel(props: { ? String(props.payload?.total_earned_count) : '-' const canReset = Number(availableCount) > 0 + let creditsContent: ReactNode + if (props.errorMessage) { + creditsContent = ( +
+ {props.errorMessage} +
+ ) + } else if (props.isLoading) { + creditsContent = ( +
+ + +
+ ) + } else if (credits.length > 0) { + creditsContent = ( +
+ {credits.map((credit, index) => ( + + ))} +
+ ) + } else { + creditsContent = ( + + + {t('No reset credits')} + + {t('Upstream did not return reset credit details.')} + + + + ) + } return (
@@ -796,35 +875,7 @@ function ResetCreditsPanel(props: { ) : null} - {props.errorMessage ? ( -
- {props.errorMessage} -
- ) : props.isLoading ? ( -
- - -
- ) : credits.length > 0 ? ( -
- {credits.map((credit, index) => ( - - ))} -
- ) : ( - - - {t('No reset credits')} - - {t('Upstream did not return reset credit details.')} - - - - )} + {creditsContent}
) } @@ -853,13 +904,17 @@ export function CodexUsageDialog({ const payload: CodexUsagePayload | null = useMemo(() => { const raw = response?.data - if (!raw || typeof raw !== 'object') return null + if (!raw || typeof raw !== 'object') { + return null + } return raw as CodexUsagePayload }, [response?.data]) const resetCreditsPayload: CodexResetCreditsPayload | null = useMemo(() => { const raw = resetCreditsResponse?.data - if (!raw || typeof raw !== 'object') return null + if (!raw || typeof raw !== 'object') { + return null + } return raw as CodexResetCreditsPayload }, [resetCreditsResponse?.data]) @@ -892,7 +947,9 @@ export function CodexUsageDialog({ setResetCreditsError(t('Channel ID is required')) return } - if (isLoadingResetCredits || (!force && resetCreditsResponse)) return + if (isLoadingResetCredits || (!force && resetCreditsResponse)) { + return + } setIsLoadingResetCredits(true) setResetCreditsError('') @@ -940,7 +997,9 @@ export function CodexUsageDialog({ } const handleConfirmReset = async () => { - if (!channelId || isResetting || !canResetCodexUsage) return + if (!channelId || isResetting || !canResetCodexUsage) { + return + } setIsResetting(true) setResetActionError('') @@ -975,7 +1034,9 @@ export function CodexUsageDialog({ } const rawJsonText = useMemo(() => { - if (!response) return '' + if (!response) { + return '' + } try { return JSON.stringify( { @@ -1002,15 +1063,13 @@ export function CodexUsageDialog({ contentHeight='auto' bodyClassName='flex flex-col gap-4' footer={ - <> - - + } >
@@ -1074,11 +1133,7 @@ export function CodexUsageDialog({ ) : null}
- +
- {additionalRateLimits.map((item, index) => { + {additionalRateLimits.map((item) => { const limitName = item.limit_name || item.metered_feature || - `${t('Additional Limit')} ${index + 1}` + t('Additional Limit') return ( k.trim()) @@ -190,8 +193,12 @@ export function formatChannelKey( * Format key preview for multi-key display */ export function formatKeyPreview(key: string, maxLength: number = 10): string { - if (!key) return '' - if (key.length <= maxLength) return key + if (!key) { + return '' + } + if (key.length <= maxLength) { + return key + } return `${key.slice(0, maxLength)}...` } @@ -199,7 +206,9 @@ export function formatKeyPreview(key: string, maxLength: number = 10): string { * Count keys in multi-key string */ export function countKeys(key: string): number { - if (!key) return 0 + if (!key) { + return 0 + } return key.split('\n').filter((k) => k.trim()).length } @@ -211,7 +220,9 @@ export function countKeys(key: string): number { * Parse comma-separated models list */ export function parseModelsList(models: string): string[] { - if (!models) return [] + if (!models) { + return [] + } return models .split(',') .map((m) => m.trim()) @@ -223,14 +234,20 @@ export function parseModelsList(models: string): string[] { * Sorts with 'default' group first, then locale-sorted alphabetically. */ export function parseGroupsList(groups: string): string[] { - if (!groups) return [] + if (!groups) { + return [] + } const list = groups .split(',') .map((g) => g.trim()) .filter((g) => g.length > 0) return list.sort((a, b) => { - if (a === 'default') return -1 - if (b === 'default') return 1 + if (a === 'default') { + return -1 + } + if (b === 'default') { + return 1 + } return a.localeCompare(b) }) } @@ -259,7 +276,9 @@ export function formatGroupsString(groups: string[]): string { export function parseChannelSettings( settingStr: string | null | undefined ): ChannelSettings { - if (!settingStr) return {} + if (!settingStr) { + return {} + } try { return JSON.parse(settingStr) as ChannelSettings } catch { @@ -273,7 +292,9 @@ export function parseChannelSettings( export function parseChannelOtherSettings( settingsStr: string | null | undefined ): ChannelOtherSettings { - if (!settingsStr || settingsStr === '{}') return {} + if (!settingsStr || settingsStr === '{}') { + return {} + } try { return JSON.parse(settingsStr) as ChannelOtherSettings } catch { @@ -285,7 +306,9 @@ export function parseChannelOtherSettings( * Validate JSON string */ export function validateChannelSettings(settings: string): boolean { - if (!settings || settings.trim() === '') return true + if (!settings || settings.trim() === '') { + return true + } try { JSON.parse(settings) return true @@ -302,7 +325,9 @@ export function validateChannelSettings(settings: string): boolean { * Format balance with currency symbol */ export function formatBalance(balance: number | null | undefined): string { - if (balance == null || Number.isNaN(balance)) return '-' + if (balance == null || Number.isNaN(balance)) { + return '-' + } return formatCurrencyFromUSD(balance, { digitsLarge: 2, digitsSmall: 4, @@ -316,9 +341,15 @@ export function formatBalance(balance: number | null | undefined): string { export function getBalanceVariant( balance: number ): 'success' | 'warning' | 'danger' | 'neutral' { - if (balance === 0) return 'neutral' - if (balance < 1) return 'danger' - if (balance < 10) return 'warning' + if (balance === 0) { + return 'neutral' + } + if (balance < 1) { + return 'danger' + } + if (balance < 10) { + return 'warning' + } return 'success' } @@ -334,9 +365,12 @@ type TFunction = (key: string, options?: { value?: number | string }) => string * Pass `t` from useTranslation() for i18n (e.g. "Not tested", "{{value}}ms", "{{value}}s"). */ export function formatResponseTime(timeMs: number, t?: TFunction): string { - if (timeMs === 0) return t ? t('Not tested') : 'Not tested' - if (timeMs < 1000) + if (timeMs === 0) { + return t ? t('Not tested') : 'Not tested' + } + if (timeMs < 1000) { return t ? t('{{value}}ms', { value: timeMs }) : `${timeMs}ms` + } return t ? t('{{value}}s', { value: (timeMs / 1000).toFixed(2) }) : `${(timeMs / 1000).toFixed(2)}s` @@ -346,12 +380,21 @@ export function formatResponseTime(timeMs: number, t?: TFunction): string { * Get response time performance rating */ export function getResponseTimeConfig(timeMs: number) { - if (timeMs === 0) return RESPONSE_TIME_CONFIG.UNKNOWN - if (timeMs <= RESPONSE_TIME_THRESHOLDS.EXCELLENT) + if (timeMs === 0) { + return RESPONSE_TIME_CONFIG.UNKNOWN + } + if (timeMs <= RESPONSE_TIME_THRESHOLDS.EXCELLENT) { return RESPONSE_TIME_CONFIG.EXCELLENT - if (timeMs <= RESPONSE_TIME_THRESHOLDS.GOOD) return RESPONSE_TIME_CONFIG.GOOD - if (timeMs <= RESPONSE_TIME_THRESHOLDS.FAIR) return RESPONSE_TIME_CONFIG.FAIR - if (timeMs <= RESPONSE_TIME_THRESHOLDS.POOR) return RESPONSE_TIME_CONFIG.POOR + } + if (timeMs <= RESPONSE_TIME_THRESHOLDS.GOOD) { + return RESPONSE_TIME_CONFIG.GOOD + } + if (timeMs <= RESPONSE_TIME_THRESHOLDS.FAIR) { + return RESPONSE_TIME_CONFIG.FAIR + } + if (timeMs <= RESPONSE_TIME_THRESHOLDS.POOR) { + return RESPONSE_TIME_CONFIG.POOR + } return RESPONSE_TIME_CONFIG.POOR } @@ -362,14 +405,16 @@ export function getResponseTimeConfig(timeMs: number) { /** * Format a Unix timestamp (seconds) as a compact, locale-aware relative time. * Uses `Intl.RelativeTimeFormat` with the `narrow` style so the label stays - * short inside table cells, e.g. "4h ago" / "42m ago" (en) or "4小时前" (zh), + * short inside table cells, e.g. "4h ago" / "42m ago" (en) or "4 小时前" (zh), * instead of the verbose "4 hours ago". */ export function formatRelativeTime( timestamp: number, locale?: Intl.LocalesArgument ): string { - if (!timestamp || timestamp === 0) return 'Never' + if (!timestamp || timestamp === 0) { + return 'Never' + } try { const diffSec = timestamp - Date.now() / 1000 @@ -385,12 +430,35 @@ export function formatRelativeTime( const MONTH = 30 * DAY const YEAR = 365 * DAY - if (absSec < MINUTE) return rtf.format(Math.round(diffSec), 'second') - if (absSec < HOUR) return rtf.format(Math.round(diffSec / MINUTE), 'minute') - if (absSec < DAY) return rtf.format(Math.round(diffSec / HOUR), 'hour') - if (absSec < MONTH) return rtf.format(Math.round(diffSec / DAY), 'day') - if (absSec < YEAR) return rtf.format(Math.round(diffSec / MONTH), 'month') - return rtf.format(Math.round(diffSec / YEAR), 'year') + let value: number + let unit: Intl.RelativeTimeFormatUnit + if (absSec < MINUTE) { + value = Math.round(diffSec) + unit = 'second' + } else if (absSec < HOUR) { + value = Math.round(diffSec / MINUTE) + unit = 'minute' + } else if (absSec < DAY) { + value = Math.round(diffSec / HOUR) + unit = 'hour' + } else if (absSec < MONTH) { + value = Math.round(diffSec / DAY) + unit = 'day' + } else if (absSec < YEAR) { + value = Math.round(diffSec / MONTH) + unit = 'month' + } else { + value = Math.round(diffSec / YEAR) + unit = 'year' + } + + const formatted = rtf.format(value, unit) + const primaryLocale = Array.isArray(locale) ? locale[0] : locale + const language = primaryLocale?.toString() + if (language?.startsWith('zh')) { + return formatted.replaceAll(/(\d)([\u4e00-\u9fff])/g, '$1 $2') + } + return formatted } catch { return 'Unknown' } @@ -400,7 +468,9 @@ export function formatRelativeTime( * Format Unix timestamp to date string */ export function formatTimestamp(timestamp: number): string { - if (!timestamp || timestamp === 0) return 'N/A' + if (!timestamp || timestamp === 0) { + return 'N/A' + } try { return formatTimestampToDate(timestamp) @@ -432,7 +502,9 @@ export function formatQuota(quota: number): string { export function getPriorityDisplay( priority: number | null | undefined ): string { - if (priority === null || priority === undefined) return '0' + if (priority === null || priority === undefined) { + return '0' + } return String(priority) } @@ -440,7 +512,9 @@ export function getPriorityDisplay( * Get weight display value */ export function getWeightDisplay(weight: number | null | undefined): string { - if (weight === null || weight === undefined) return '0' + if (weight === null || weight === undefined) { + return '0' + } return String(weight) } @@ -481,10 +555,14 @@ export function validateGroups(groups: string): boolean { */ export function channelNeedsAttention(channel: Channel): boolean { // Auto-disabled - if (channel.status === 3) return true + if (channel.status === 3) { + return true + } // Low balance (less than $1) - if (channel.balance > 0 && channel.balance < 1) return true + if (channel.balance > 0 && channel.balance < 1) { + return true + } // Multi-key channel with all keys disabled if ( @@ -503,8 +581,12 @@ export function channelNeedsAttention(channel: Channel): boolean { * Get attention reason for channel */ export function getAttentionReason(channel: Channel): string | null { - if (channel.status === 3) return 'Auto-disabled' - if (channel.balance > 0 && channel.balance < 1) return 'Low balance' + if (channel.status === 3) { + return 'Auto-disabled' + } + if (channel.balance > 0 && channel.balance < 1) { + return 'Low balance' + } if ( channel.channel_info?.is_multi_key && channel.channel_info.multi_key_status_list && @@ -553,7 +635,7 @@ export function aggregateChannelsByTag( ...channel, key: tag, id: tag as unknown as number, - tag: tag, + tag, name: tag, type: 0, status: undefined as unknown as number, @@ -573,7 +655,10 @@ export function aggregateChannelsByTag( result.push(tagRow) } - const tagRow = tagMap.get(tag)! + const tagRow = tagMap.get(tag) + if (!tagRow) { + continue + } // Add to children tagRow.children.push(channel) @@ -609,7 +694,7 @@ export function aggregateChannelsByTag( const newGroups = channel.group.split(',').filter(Boolean) newGroups.forEach((g) => { if (!existingGroups.has(g)) { - tagRow.group += ',' + g + tagRow.group += `,${g}` } }) } diff --git a/web/default/src/features/models/components/dialogs/view-details-dialog.tsx b/web/default/src/features/models/components/dialogs/view-details-dialog.tsx index 47660d84..3ad7fba5 100644 --- a/web/default/src/features/models/components/dialogs/view-details-dialog.tsx +++ b/web/default/src/features/models/components/dialogs/view-details-dialog.tsx @@ -1,3 +1,5 @@ +import { useQuery } from '@tanstack/react-query' +import { Copy, ExternalLink, Loader2, RefreshCcw } from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -17,10 +19,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useMemo } from 'react' -import { useQuery } from '@tanstack/react-query' -import { Copy, ExternalLink, Loader2, RefreshCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' + +import { Dialog } from '@/components/dialog' import { Button } from '@/components/ui/button' import { Collapsible, @@ -28,7 +30,7 @@ import { CollapsibleTrigger, } from '@/components/ui/collapsible' import { Separator } from '@/components/ui/separator' -import { Dialog } from '@/components/dialog' + import { getDeployment, listDeploymentContainers } from '../../api' export function ViewDetailsDialog({ @@ -73,10 +75,14 @@ export function ViewDetailsDialog({ const locations = useMemo(() => { const items = details?.locations - if (!Array.isArray(items)) return [] + if (!Array.isArray(items)) { + return [] + } return items .map((x) => { - if (!x || typeof x !== 'object') return null + if (!x || typeof x !== 'object') { + return null + } const name = (x as Record)?.name const iso2 = (x as Record)?.iso2 const id = (x as Record)?.id @@ -86,7 +92,9 @@ export function ViewDetailsDialog({ }, [details]) const handleCopyId = async () => { - if (deploymentId === null || deploymentId === undefined) return + if (deploymentId === null || deploymentId === undefined) { + return + } try { await navigator.clipboard.writeText(String(deploymentId)) toast.success(t('Copied')) @@ -108,6 +116,9 @@ export function ViewDetailsDialog({ return '' } }, [details]) + const isDetailsLoading = isLoadingDetails || isLoadingContainers + const showDetailsError = !isDetailsLoading && !detailsRes?.success + const showDetailsContent = !isDetailsLoading && detailsRes?.success return ( - - + } >
@@ -158,15 +167,17 @@ export function ViewDetailsDialog({ - {isLoadingDetails || isLoadingContainers ? ( + {isDetailsLoading ? (
- ) : !detailsRes?.success ? ( + ) : null} + {showDetailsError ? (
{detailsRes?.message || t('Failed to fetch deployment details')}
- ) : ( + ) : null} + {showDetailsContent ? ( <>
@@ -225,7 +236,9 @@ export function ViewDetailsDialog({
{containers.map((c) => { const id = c?.container_id - if (typeof id !== 'string' || !id) return null + if (typeof id !== 'string' || !id) { + return null + } const status = typeof c?.status === 'string' ? c.status : undefined const url = @@ -263,13 +276,13 @@ export function ViewDetailsDialog({ {t('Raw JSON')} -
+                
                   {payloadJson || '-'}
                 
- )} + ) : null}
) diff --git a/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx b/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx index 24315029..df1988d4 100644 --- a/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx +++ b/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx @@ -1,3 +1,5 @@ +import { useQuery } from '@tanstack/react-query' +import { Download, Loader2, RefreshCcw, Terminal } from 'lucide-react' /* Copyright (C) 2023-2026 QuantumNous @@ -16,10 +18,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useEffect, useMemo, useRef, useState } from 'react' -import { useQuery } from '@tanstack/react-query' -import { Download, Loader2, RefreshCcw, Terminal } from 'lucide-react' +import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' + +import { Dialog } from '@/components/dialog' import { Button } from '@/components/ui/button' import { Select, @@ -30,7 +32,7 @@ import { SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' -import { Dialog } from '@/components/dialog' + import { getDeploymentLogs, listDeploymentContainers } from '../../api' interface ViewLogsDialogProps { @@ -114,9 +116,17 @@ export function ViewLogsDialog({ }, [logsData?.data]) const logLines = useMemo(() => { - const normalized = logsText.replace(/\r\n?/g, '\n') + const normalized = logsText.replaceAll(/\r\n?/g, '\n') return normalized ? normalized.split('\n') : [] }, [logsText]) + const keyedLogLines = useMemo(() => { + const seen = new Map() + return logLines.map((line) => { + const count = seen.get(line) ?? 0 + seen.set(line, count + 1) + return { key: `${line}-${count}`, line } + }) + }, [logLines]) // Auto-scroll to bottom useEffect(() => { @@ -135,6 +145,44 @@ export function ViewLogsDialog({ a.click() URL.revokeObjectURL(url) } + let containerPlaceholder = t('Select') + if (isLoadingContainers) { + containerPlaceholder = t('Loading...') + } else if (containers.length === 0) { + containerPlaceholder = t('No containers') + } + let logsContent: ReactNode + if (isLoadingContainers || isLoadingLogs) { + logsContent = ( +
+ +
+ ) + } else if (containers.length === 0) { + logsContent = ( +
{t('No containers')}
+ ) + } else if (!containerId) { + logsContent = ( +
+ {t('Please select a container')} +
+ ) + } else if (!logsText.trim()) { + logsContent = ( +
{t('No logs')}
+ ) + } else { + logsContent = ( +
+ {keyedLogLines.map(({ key, line }) => ( +
+ {line} +
+ ))} +
+ ) + } return (
{t('Container')}