refactor: update styling and improve code readability across multiple components
This commit is contained in:
Vendored
+1
-1
@@ -21,7 +21,7 @@
|
||||
"src/routeTree.gen.ts"
|
||||
],
|
||||
"rules": {
|
||||
"curly": "error",
|
||||
"curly": ["error", "multi-line"],
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
|
||||
@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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'
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
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<TData>({
|
||||
<div
|
||||
className={cn(
|
||||
'min-h-0 flex-1 overflow-auto',
|
||||
'**:data-[slot=table-header]:[--table-header-bg:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
|
||||
'**:data-[slot=table-header]:[--table-header-bg:var(--table-header)]',
|
||||
'**:data-[slot=table-header]:bg-(--table-header-bg)',
|
||||
props.splitHeaderScrollClassName,
|
||||
props.bodyContainerClassName
|
||||
@@ -192,7 +194,9 @@ function getMetaPinnedColumns<TData>(
|
||||
): DataTablePinnedColumn[] {
|
||||
return table.getAllColumns().flatMap((column) => {
|
||||
const side = column.columnDef.meta?.pinned
|
||||
if (!side) return []
|
||||
if (!side) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{ columnId: column.id, side }]
|
||||
})
|
||||
|
||||
+2
-2
@@ -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)]'
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
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) => (
|
||||
<div
|
||||
key={`${prefix}-${i}`}
|
||||
className='bg-card space-y-3 rounded-lg border p-3'
|
||||
className='space-y-3 rounded-lg border [background-color:var(--table-row)] p-3'
|
||||
>
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<Skeleton className='h-4 w-32' />
|
||||
@@ -157,8 +159,9 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
data-slot='data-table-card'
|
||||
className={cn(
|
||||
'bg-card rounded-lg border px-3 py-2.5',
|
||||
'rounded-lg border [background-color:var(--data-table-card-bg,var(--table-row))] px-3 py-2.5',
|
||||
props.getRowClassName?.(row)
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
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<TData>(
|
||||
function renderPagination<TData>(
|
||||
props: DataTablePageProps<TData>
|
||||
): React.ReactNode {
|
||||
if (props.showPagination === false) return null
|
||||
if (props.showPagination === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const pagination = <DataTablePagination table={props.table} />
|
||||
|
||||
@@ -391,7 +395,9 @@ function renderMobile<TData>(
|
||||
props: DataTablePageProps<TData>,
|
||||
showMobile: boolean
|
||||
): React.ReactNode {
|
||||
if (!showMobile) return null
|
||||
if (!showMobile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const ownGetRowClassName = props.getRowClassName
|
||||
const mobileGetRowClassName =
|
||||
@@ -436,7 +442,9 @@ function renderDesktop<TData>(
|
||||
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<TData>(
|
||||
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}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
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<TData>(props: MobileCardListProps<TData>) {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={cn('bg-card px-3 py-2.5', getRowClassName?.(row))}
|
||||
className={cn(
|
||||
'[background-color:var(--data-table-card-bg,var(--table-row))] px-3 py-2.5',
|
||||
getRowClassName?.(row)
|
||||
)}
|
||||
>
|
||||
<CardRowContent row={row} compact={hasCompactMeta} />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+5
-4
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<div className='flex items-center gap-0.5'>
|
||||
@@ -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 (
|
||||
<TooltipProvider>
|
||||
@@ -387,20 +413,8 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
label={
|
||||
isUpdating
|
||||
? t('Updating...')
|
||||
: channel.type === 57
|
||||
? t('Account Info')
|
||||
: remainingDisplay
|
||||
}
|
||||
variant={
|
||||
channel.type === 57
|
||||
? 'info'
|
||||
: isUpdating
|
||||
? 'neutral'
|
||||
: variant
|
||||
}
|
||||
label={remainingBadgeLabel}
|
||||
variant={remainingBadgeVariant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
showDot={false}
|
||||
@@ -427,7 +441,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
||||
channelId={channel.id}
|
||||
response={codexUsageResponse}
|
||||
onRefresh={async () => {
|
||||
if (isUpdating) return
|
||||
if (isUpdating) {
|
||||
return
|
||||
}
|
||||
setIsUpdating(true)
|
||||
try {
|
||||
const res = await getCodexUsage(channel.id)
|
||||
@@ -565,7 +581,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
render={
|
||||
<AlertTriangle className='h-3.5 w-3.5 flex-shrink-0 text-amber-500' />
|
||||
}
|
||||
></TooltipTrigger>
|
||||
/>
|
||||
<TooltipContent side='top'>
|
||||
{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<Channel>[] {
|
||||
render={
|
||||
<SlidersHorizontal className='text-info h-3.5 w-3.5 flex-shrink-0' />
|
||||
}
|
||||
></TooltipTrigger>
|
||||
/>
|
||||
<TooltipContent side='top'>
|
||||
{t('Override request parameters')}
|
||||
</TooltipContent>
|
||||
@@ -698,7 +714,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
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<Channel>[] {
|
||||
)
|
||||
},
|
||||
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<Channel>[] {
|
||||
)
|
||||
},
|
||||
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<Channel>[] {
|
||||
const modelArray = parseModelsList(models)
|
||||
return (
|
||||
<BadgeListCell
|
||||
items={modelArray.map((model, idx) => (
|
||||
items={modelArray.map((model) => (
|
||||
<StatusBadge
|
||||
key={idx}
|
||||
key={model}
|
||||
label={model}
|
||||
autoColor={model}
|
||||
size='sm'
|
||||
@@ -909,7 +935,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
)
|
||||
},
|
||||
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<Channel>[] {
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const tag = row.getValue('tag') as string | null
|
||||
if (!tag)
|
||||
if (!tag) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusBadge
|
||||
@@ -1012,11 +1041,15 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className='text-muted-foreground cursor-pointer font-mono text-sm' />
|
||||
<StatusBadge
|
||||
label={timeText}
|
||||
variant='neutral'
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5 cursor-pointer'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{timeText}
|
||||
</TooltipTrigger>
|
||||
/>
|
||||
<TooltipContent side='top'>
|
||||
<p className='font-mono text-sm'>{fullDate}</p>
|
||||
</TooltipContent>
|
||||
|
||||
+139
-84
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
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 = (
|
||||
<div className='border-destructive/40 bg-destructive/10 text-destructive rounded-lg border px-3 py-2 text-sm'>
|
||||
{props.errorMessage}
|
||||
</div>
|
||||
)
|
||||
} else if (props.isLoading) {
|
||||
creditsContent = (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Skeleton className='h-24 w-full' />
|
||||
<Skeleton className='h-24 w-full' />
|
||||
</div>
|
||||
)
|
||||
} else if (credits.length > 0) {
|
||||
creditsContent = (
|
||||
<div className='flex flex-col gap-2'>
|
||||
{credits.map((credit, index) => (
|
||||
<ResetCreditItem
|
||||
key={
|
||||
credit.id ??
|
||||
credit.expires_at ??
|
||||
credit.granted_at ??
|
||||
credit.title ??
|
||||
credit.reset_type ??
|
||||
''
|
||||
}
|
||||
credit={credit}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
creditsContent = (
|
||||
<Empty className='min-h-32 border'>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>{t('No reset credits')}</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
{t('Upstream did not return reset credit details.')}
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-3 p-3'>
|
||||
@@ -796,35 +875,7 @@ function ResetCreditsPanel(props: {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{props.errorMessage ? (
|
||||
<div className='border-destructive/40 bg-destructive/10 text-destructive rounded-lg border px-3 py-2 text-sm'>
|
||||
{props.errorMessage}
|
||||
</div>
|
||||
) : props.isLoading ? (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Skeleton className='h-24 w-full' />
|
||||
<Skeleton className='h-24 w-full' />
|
||||
</div>
|
||||
) : credits.length > 0 ? (
|
||||
<div className='flex flex-col gap-2'>
|
||||
{credits.map((credit, index) => (
|
||||
<ResetCreditItem
|
||||
key={`${credit.id ?? credit.expires_at ?? 'credit'}-${index}`}
|
||||
credit={credit}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Empty className='min-h-32 border'>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>{t('No reset credits')}</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
{t('Upstream did not return reset credit details.')}
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)}
|
||||
{creditsContent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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={
|
||||
<>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => handleDialogOpenChange(false)}
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => handleDialogOpenChange(false)}
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
@@ -1074,11 +1133,7 @@ export function CodexUsageDialog({
|
||||
) : null}
|
||||
</div>
|
||||
<div className='mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<InfoField
|
||||
label={t('Email')}
|
||||
value={payload?.email}
|
||||
copyable={true}
|
||||
/>
|
||||
<InfoField label={t('Email')} value={payload?.email} copyable />
|
||||
<InfoField
|
||||
label={t('Channel')}
|
||||
value={channelLabel}
|
||||
@@ -1171,14 +1226,14 @@ export function CodexUsageDialog({
|
||||
)}
|
||||
/>
|
||||
<div className='flex flex-col gap-3'>
|
||||
{additionalRateLimits.map((item, index) => {
|
||||
{additionalRateLimits.map((item) => {
|
||||
const limitName =
|
||||
item.limit_name ||
|
||||
item.metered_feature ||
|
||||
`${t('Additional Limit')} ${index + 1}`
|
||||
t('Additional Limit')
|
||||
return (
|
||||
<RateLimitGroupSection
|
||||
key={`${limitName}-${item.metered_feature ?? ''}-${index}`}
|
||||
key={`${limitName}-${item.metered_feature ?? ''}-${item.plan_type ?? ''}`}
|
||||
title={limitName}
|
||||
description={t('Additional metered capability')}
|
||||
source={item}
|
||||
|
||||
+3
-3
@@ -80,8 +80,8 @@ export const CHANNEL_TYPES = {
|
||||
} as const
|
||||
|
||||
const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [
|
||||
1, 14, 33, 24, 43, 3, 41, 48, 58, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, 23,
|
||||
18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36,
|
||||
1, 14, 33, 24, 43, 3, 41, 48, 58, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46,
|
||||
23, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36,
|
||||
50, 51, 52, 53, 54, 55, 56,
|
||||
]
|
||||
|
||||
@@ -322,7 +322,7 @@ export const RESPONSE_TIME_THRESHOLDS = {
|
||||
|
||||
export const RESPONSE_TIME_CONFIG = {
|
||||
EXCELLENT: { variant: 'success' as const, label: 'Excellent' },
|
||||
GOOD: { variant: 'info' as const, label: 'Good' },
|
||||
GOOD: { variant: 'success' as const, label: 'Good' },
|
||||
FAIR: { variant: 'warning' as const, label: 'Fair' },
|
||||
POOR: { variant: 'danger' as const, label: 'Poor' },
|
||||
UNKNOWN: { variant: 'neutral' as const, label: 'Not tested' },
|
||||
|
||||
+125
-40
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { formatCurrencyFromUSD, formatQuotaWithCurrency } from '@/lib/currency'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
|
||||
import {
|
||||
CHANNEL_STATUS_CONFIG,
|
||||
CHANNEL_TYPES,
|
||||
@@ -170,7 +171,9 @@ export function formatChannelKey(
|
||||
key: string,
|
||||
isMultiKey: boolean = false
|
||||
): string {
|
||||
if (!key) return ''
|
||||
if (!key) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isMultiKey) {
|
||||
const keys = key.split('\n').filter((k) => 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}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+34
-21
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
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<string, unknown>)?.name
|
||||
const iso2 = (x as Record<string, unknown>)?.iso2
|
||||
const id = (x as Record<string, unknown>)?.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 (
|
||||
<Dialog
|
||||
@@ -118,15 +129,13 @@ export function ViewDetailsDialog({
|
||||
contentHeight='auto'
|
||||
bodyClassName='space-y-4'
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => onOpenChange(false)}
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => onOpenChange(false)}
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className='max-h-[calc(100dvh-8.5rem)] space-y-3 overflow-y-auto py-2 pr-1 sm:max-h-[72vh] sm:space-y-4'>
|
||||
@@ -158,15 +167,17 @@ export function ViewDetailsDialog({
|
||||
|
||||
<Separator />
|
||||
|
||||
{isLoadingDetails || isLoadingContainers ? (
|
||||
{isDetailsLoading ? (
|
||||
<div className='flex items-center justify-center py-10'>
|
||||
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
|
||||
</div>
|
||||
) : !detailsRes?.success ? (
|
||||
) : null}
|
||||
{showDetailsError ? (
|
||||
<div className='text-muted-foreground py-10 text-center text-sm'>
|
||||
{detailsRes?.message || t('Failed to fetch deployment details')}
|
||||
</div>
|
||||
) : (
|
||||
) : null}
|
||||
{showDetailsContent ? (
|
||||
<>
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div className='rounded-lg border p-3'>
|
||||
@@ -225,7 +236,9 @@ export function ViewDetailsDialog({
|
||||
<div className='space-y-2'>
|
||||
{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')}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre className='mt-3 max-h-[360px] overflow-auto rounded-md bg-black p-3 text-xs text-gray-200'>
|
||||
<pre className='bg-muted text-foreground mt-3 max-h-[360px] overflow-auto rounded-md p-3 text-xs'>
|
||||
{payloadJson || '-'}
|
||||
</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<string, number>()
|
||||
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 = (
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<Loader2 className='h-6 w-6 animate-spin text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
} else if (containers.length === 0) {
|
||||
logsContent = (
|
||||
<div className='py-8 text-center text-gray-400'>{t('No containers')}</div>
|
||||
)
|
||||
} else if (!containerId) {
|
||||
logsContent = (
|
||||
<div className='py-8 text-center text-gray-400'>
|
||||
{t('Please select a container')}
|
||||
</div>
|
||||
)
|
||||
} else if (!logsText.trim()) {
|
||||
logsContent = (
|
||||
<div className='py-8 text-center text-gray-400'>{t('No logs')}</div>
|
||||
)
|
||||
} else {
|
||||
logsContent = (
|
||||
<div className='font-mono text-sm'>
|
||||
{keyedLogLines.map(({ key, line }) => (
|
||||
<div key={key} className='whitespace-pre-wrap text-gray-200'>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -191,47 +239,39 @@ export function ViewLogsDialog({
|
||||
<div className='space-y-1'>
|
||||
<div className='text-muted-foreground text-xs'>{t('Container')}</div>
|
||||
<Select
|
||||
items={[
|
||||
...containers.flatMap((c) => {
|
||||
const id = c?.container_id
|
||||
if (typeof id !== 'string' || !id) return []
|
||||
const status =
|
||||
typeof c?.status === 'string' && c.status
|
||||
? ` (${c.status})`
|
||||
: ''
|
||||
return [
|
||||
{
|
||||
value: id,
|
||||
label: (
|
||||
<>
|
||||
{id}
|
||||
{status}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
}),
|
||||
]}
|
||||
items={containers.flatMap((c) => {
|
||||
const id = c?.container_id
|
||||
if (typeof id !== 'string' || !id) return []
|
||||
const status =
|
||||
typeof c?.status === 'string' && c.status
|
||||
? ` (${c.status})`
|
||||
: ''
|
||||
return [
|
||||
{
|
||||
value: id,
|
||||
label: (
|
||||
<>
|
||||
{id}
|
||||
{status}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
})}
|
||||
value={containerId}
|
||||
onValueChange={(v) => v !== null && setContainerId(v)}
|
||||
disabled={isLoadingContainers || containers.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingContainers
|
||||
? t('Loading...')
|
||||
: containers.length === 0
|
||||
? t('No containers')
|
||||
: t('Select')
|
||||
}
|
||||
/>
|
||||
<SelectValue placeholder={containerPlaceholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignItemWithTrigger={false}>
|
||||
<SelectGroup>
|
||||
{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
|
||||
? ` (${c.status})`
|
||||
@@ -279,7 +319,7 @@ export function ViewLogsDialog({
|
||||
</div>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className='flex-1 overflow-auto rounded-md border bg-black p-3 sm:p-4'
|
||||
className='bg-muted flex-1 overflow-auto rounded-md border p-3 sm:p-4'
|
||||
onScroll={(e) => {
|
||||
const target = e.target as HTMLDivElement
|
||||
const isAtBottom =
|
||||
@@ -287,29 +327,7 @@ export function ViewLogsDialog({
|
||||
setAutoScroll(isAtBottom)
|
||||
}}
|
||||
>
|
||||
{isLoadingContainers || isLoadingLogs ? (
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<Loader2 className='h-6 w-6 animate-spin text-gray-400' />
|
||||
</div>
|
||||
) : containers.length === 0 ? (
|
||||
<div className='py-8 text-center text-gray-400'>
|
||||
{t('No containers')}
|
||||
</div>
|
||||
) : !containerId ? (
|
||||
<div className='py-8 text-center text-gray-400'>
|
||||
{t('Please select a container')}
|
||||
</div>
|
||||
) : !logsText.trim() ? (
|
||||
<div className='py-8 text-center text-gray-400'>{t('No logs')}</div>
|
||||
) : (
|
||||
<div className='font-mono text-sm'>
|
||||
{logLines.map((line, idx) => (
|
||||
<div key={idx} className='whitespace-pre-wrap text-gray-200'>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{logsContent}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -18,14 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { Route } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ModelBadgeProps {
|
||||
modelName: string
|
||||
@@ -109,11 +110,11 @@ function ModelBadgeContent(props: ModelBadgeProps) {
|
||||
<span className='flex max-w-none items-center gap-1.5'>
|
||||
{provider && (
|
||||
<span
|
||||
className='flex size-3.5 shrink-0 items-center justify-center'
|
||||
className='flex h-[18px] w-[18px] shrink-0 items-center justify-center'
|
||||
title={provider.label}
|
||||
aria-label={provider.label}
|
||||
>
|
||||
{getLobeIcon(provider.icon, 14)}
|
||||
{getLobeIcon(provider.icon, 18)}
|
||||
</span>
|
||||
)}
|
||||
<span className='whitespace-nowrap'>{props.modelName}</span>
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ export function showSubmittedData(
|
||||
toast.message(title, {
|
||||
description: (
|
||||
// w-[340px]
|
||||
<pre className='mt-2 w-full overflow-x-auto rounded-md bg-slate-950 p-4'>
|
||||
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
|
||||
<pre className='bg-muted text-foreground mt-2 w-full overflow-x-auto rounded-md p-4'>
|
||||
<code>{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
|
||||
+33
-33
@@ -353,32 +353,32 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
--spacing: 0.3rem;
|
||||
}
|
||||
.dark [data-theme-preset='simple-large'] {
|
||||
--background: oklch(0.12 0 0);
|
||||
--foreground: oklch(0.98 0 0);
|
||||
--card: oklch(0.18 0 0);
|
||||
--card-foreground: oklch(0.98 0 0);
|
||||
--popover: oklch(0.2 0 0);
|
||||
--popover-foreground: oklch(0.98 0 0);
|
||||
--background: oklch(0.215 0 0);
|
||||
--foreground: oklch(0.97 0 0);
|
||||
--card: oklch(0.27 0 0);
|
||||
--card-foreground: oklch(0.97 0 0);
|
||||
--popover: oklch(0.29 0 0);
|
||||
--popover-foreground: oklch(0.97 0 0);
|
||||
|
||||
--primary: oklch(0.94 0 0);
|
||||
--primary-foreground: oklch(0.12 0 0);
|
||||
--secondary: oklch(0.24 0 0);
|
||||
--secondary-foreground: oklch(0.98 0 0);
|
||||
--muted: oklch(0.24 0 0);
|
||||
--primary-foreground: oklch(0.145 0 0);
|
||||
--secondary: oklch(0.315 0 0);
|
||||
--secondary-foreground: oklch(0.97 0 0);
|
||||
--muted: oklch(0.315 0 0);
|
||||
--muted-foreground: oklch(0.82 0 0);
|
||||
--accent: oklch(0.3 0 0);
|
||||
--accent: oklch(0.36 0 0);
|
||||
--accent-foreground: oklch(0.98 0 0);
|
||||
|
||||
--destructive: oklch(0.68 0.19 25);
|
||||
--destructive-foreground: oklch(0.98 0 0);
|
||||
--success: oklch(0.72 0.13 145);
|
||||
--success-foreground: oklch(0.12 0 0);
|
||||
--success-foreground: oklch(0.145 0 0);
|
||||
--warning: oklch(0.82 0.13 75);
|
||||
--warning-foreground: oklch(0.12 0 0);
|
||||
--warning-foreground: oklch(0.145 0 0);
|
||||
--info: oklch(0.72 0.12 250);
|
||||
--info-foreground: oklch(0.12 0 0);
|
||||
--info-foreground: oklch(0.145 0 0);
|
||||
--neutral: oklch(0.82 0 0);
|
||||
--neutral-foreground: oklch(0.12 0 0);
|
||||
--neutral-foreground: oklch(0.145 0 0);
|
||||
|
||||
--border: oklch(1 0 0 / 18%);
|
||||
--input: oklch(1 0 0 / 24%);
|
||||
@@ -390,17 +390,17 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
--chart-4: oklch(0.82 0.13 75);
|
||||
--chart-5: oklch(0.68 0.19 25);
|
||||
|
||||
--sidebar: oklch(0.16 0 0);
|
||||
--sidebar-foreground: oklch(0.98 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.97 0 0);
|
||||
--sidebar-primary: oklch(0.94 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.12 0 0);
|
||||
--sidebar-accent: oklch(0.28 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.145 0 0);
|
||||
--sidebar-accent: oklch(0.34 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.98 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 18%);
|
||||
--sidebar-ring: oklch(0.94 0 0);
|
||||
|
||||
--skeleton-base: oklch(0.24 0 0);
|
||||
--skeleton-highlight: oklch(0.34 0 0);
|
||||
--skeleton-base: oklch(0.315 0 0);
|
||||
--skeleton-highlight: oklch(0.415 0 0);
|
||||
}
|
||||
|
||||
/* ── Semantic surface bridge ──────────────────────────────────────────── */
|
||||
@@ -552,21 +552,21 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
.dark [data-theme-preset='anthropic'] {
|
||||
/* Warm near-black product surfaces, not pure black — keeps the editorial
|
||||
* personality even when inverted. Coral lifts slightly for legibility. */
|
||||
--background: oklch(0.205 0.004 60); /* ≈ #181715 */
|
||||
--foreground: oklch(0.965 0.005 92); /* ≈ #faf9f5 */
|
||||
--card: oklch(0.245 0.004 60);
|
||||
--background: oklch(0.215 0.004 60);
|
||||
--foreground: oklch(0.965 0.005 92);
|
||||
--card: oklch(0.255 0.004 60);
|
||||
--card-foreground: oklch(0.965 0.005 92);
|
||||
--popover: oklch(0.265 0.004 60);
|
||||
--popover: oklch(0.275 0.004 60);
|
||||
--popover-foreground: oklch(0.965 0.005 92);
|
||||
|
||||
--primary: oklch(0.72 0.135 40);
|
||||
--primary-foreground: oklch(0.18 0.005 60);
|
||||
--secondary: oklch(0.295 0.004 60);
|
||||
--secondary: oklch(0.305 0.004 60);
|
||||
--secondary-foreground: oklch(0.945 0.005 92);
|
||||
|
||||
--muted: oklch(0.275 0.004 60);
|
||||
--muted: oklch(0.285 0.004 60);
|
||||
--muted-foreground: oklch(0.76 0.006 75);
|
||||
--accent: oklch(0.32 0.006 60);
|
||||
--accent: oklch(0.33 0.006 60);
|
||||
--accent-foreground: oklch(0.985 0.005 92);
|
||||
|
||||
--destructive: oklch(0.7 0.19 22);
|
||||
@@ -590,17 +590,17 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
--chart-4: oklch(0.78 0.13 0);
|
||||
--chart-5: oklch(0.83 0.027 175);
|
||||
|
||||
--sidebar: oklch(0.175 0.004 60);
|
||||
--sidebar: oklch(0.205 0.004 60);
|
||||
--sidebar-foreground: oklch(0.95 0.005 92);
|
||||
--sidebar-primary: oklch(0.72 0.135 40);
|
||||
--sidebar-primary-foreground: oklch(0.18 0.005 60);
|
||||
--sidebar-accent: oklch(0.31 0.006 60);
|
||||
--sidebar-accent: oklch(0.32 0.006 60);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.005 92);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-border: oklch(1 0 0 / 11%);
|
||||
--sidebar-ring: oklch(0.72 0.135 40);
|
||||
|
||||
--skeleton-base: oklch(0.295 0.004 60);
|
||||
--skeleton-highlight: oklch(0.4 0.004 60);
|
||||
--skeleton-base: oklch(0.305 0.004 60);
|
||||
--skeleton-highlight: oklch(0.41 0.004 60);
|
||||
}
|
||||
|
||||
/* ── Font axis ────────────────────────────────────────────────────────────
|
||||
|
||||
Vendored
+69
-17
@@ -142,22 +142,48 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--skeleton-base: oklch(0.97 0 0);
|
||||
--skeleton-highlight: oklch(0.985 0 0);
|
||||
--table-row: var(--background);
|
||||
--table-header: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 1.5%,
|
||||
var(--background)
|
||||
);
|
||||
--table-header-hover: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 3%,
|
||||
var(--background)
|
||||
);
|
||||
--table-disabled: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 5.5%,
|
||||
var(--background)
|
||||
);
|
||||
--table-disabled-hover: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 7%,
|
||||
var(--background)
|
||||
);
|
||||
--table-disabled-border: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 24%,
|
||||
var(--background)
|
||||
);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* OpenAI-like dark mode with a softer charcoal canvas. */
|
||||
--background: oklch(0.225 0 0);
|
||||
/* OpenAI-like dark mode with a crisp charcoal canvas. */
|
||||
--background: oklch(0.235 0 0);
|
||||
--foreground: oklch(0.965 0 0);
|
||||
--card: oklch(0.275 0 0);
|
||||
--card: oklch(0.285 0 0);
|
||||
--card-foreground: oklch(0.965 0 0);
|
||||
--popover: oklch(0.295 0 0);
|
||||
--popover: oklch(0.305 0 0);
|
||||
--popover-foreground: oklch(0.965 0 0);
|
||||
--primary: oklch(0.965 0 0);
|
||||
--primary-foreground: oklch(0.145 0 0);
|
||||
--secondary: oklch(0.325 0 0);
|
||||
--primary-foreground: oklch(0.155 0 0);
|
||||
--secondary: oklch(0.335 0 0);
|
||||
--secondary-foreground: oklch(0.965 0 0);
|
||||
--muted: oklch(0.295 0 0);
|
||||
--muted-foreground: oklch(0.76 0 0);
|
||||
--muted: oklch(0.305 0 0);
|
||||
--muted-foreground: oklch(0.78 0 0);
|
||||
--accent: oklch(0.365 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
@@ -169,23 +195,49 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
--info: oklch(0.68 0.17 237.323);
|
||||
--info-foreground: oklch(0.145 0 0);
|
||||
--neutral: oklch(0.76 0 0);
|
||||
--neutral-foreground: oklch(0.145 0 0);
|
||||
--border: oklch(1 0 0 / 9%);
|
||||
--input: oklch(1 0 0 / 16%);
|
||||
--neutral-foreground: oklch(0.155 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 17%);
|
||||
--ring: oklch(0.68 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.195 0 0);
|
||||
--sidebar: oklch(0.225 0 0);
|
||||
--sidebar-foreground: oklch(0.95 0 0);
|
||||
--sidebar-primary: oklch(0.965 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.145 0 0);
|
||||
--sidebar-accent: oklch(0.35 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.155 0 0);
|
||||
--sidebar-accent: oklch(0.355 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-border: oklch(1 0 0 / 11%);
|
||||
--sidebar-ring: oklch(0.68 0 0);
|
||||
--skeleton-base: oklch(0.325 0 0);
|
||||
--skeleton-highlight: oklch(0.43 0 0);
|
||||
--skeleton-base: oklch(0.335 0 0);
|
||||
--skeleton-highlight: oklch(0.44 0 0);
|
||||
--table-row: var(--background);
|
||||
--table-header: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 6%,
|
||||
var(--background)
|
||||
);
|
||||
--table-header-hover: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 9%,
|
||||
var(--background)
|
||||
);
|
||||
--table-disabled: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 10%,
|
||||
var(--background)
|
||||
);
|
||||
--table-disabled-hover: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 13%,
|
||||
var(--background)
|
||||
);
|
||||
--table-disabled-border: color-mix(
|
||||
in oklch,
|
||||
var(--foreground) 34%,
|
||||
var(--background)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user