Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
50b8f2a2
authored
Jun 19, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: update styling and improve code readability across multiple components
parent
a0de4b56
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
646 additions
and
364 deletions
+646
-364
web/default/.oxlintrc.json
+1
-1
web/default/src/components/data-table/core/column-pinning.ts
+8
-3
web/default/src/components/data-table/core/data-table-view.tsx
+8
-4
web/default/src/components/data-table/index.ts
+2
-2
web/default/src/components/data-table/layout/card-grid.tsx
+8
-5
web/default/src/components/data-table/layout/data-table-page.tsx
+21
-14
web/default/src/components/data-table/layout/mobile-card-list.tsx
+9
-4
web/default/src/components/data-table/static/static-data-table-classnames.ts
+1
-1
web/default/src/components/status-badge.tsx
+5
-4
web/default/src/features/channels/components/channels-columns.tsx
+94
-61
web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx
+139
-84
web/default/src/features/channels/constants.ts
+3
-3
web/default/src/features/channels/lib/channel-utils.ts
+125
-40
web/default/src/features/models/components/dialogs/view-details-dialog.tsx
+34
-21
web/default/src/features/models/components/dialogs/view-logs-dialog.tsx
+78
-60
web/default/src/features/usage-logs/components/model-badge.tsx
+6
-5
web/default/src/lib/show-submitted-data.tsx
+2
-2
web/default/src/styles/theme-presets.css
+33
-33
web/default/src/styles/theme.css
+69
-17
No files found.
web/default/.oxlintrc.json
View file @
50b8f2a2
...
...
@@ -21,7 +21,7 @@
"src/routeTree.gen.ts"
],
"rules"
:
{
"curly"
:
"error"
,
"curly"
:
[
"error"
,
"multi-line"
]
,
"eqeqeq"
:
[
"error"
,
"always"
,
...
...
web/default/src/components/data-table/core/column-pinning.ts
View file @
50b8f2a2
...
...
@@ -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'
...
...
web/default/src/components/data-table/core/data-table-view.tsx
View file @
50b8f2a2
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
}]
})
...
...
web/default/src/components/data-table/index.ts
View file @
50b8f2a2
...
...
@@ -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)]
'
web/default/src/components/data-table/layout/card-grid.tsx
View file @
50b8f2a2
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
)
)
}
>
...
...
web/default/src/components/data-table/layout/data-table-page.tsx
View file @
50b8f2a2
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
}
...
...
web/default/src/components/data-table/layout/mobile-card-list.tsx
View file @
50b8f2a2
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
>
...
...
web/default/src/components/data-table/static/static-data-table-classnames.ts
View file @
50b8f2a2
...
...
@@ -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
:
...
...
web/default/src/components/status-badge.tsx
View file @
50b8f2a2
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'
,
...
...
web/default/src/features/channels/components/channels-columns.tsx
View file @
50b8f2a2
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
>
...
...
web/default/src/features/channels/components/dialogs/codex-usage-dialog.tsx
View file @
50b8f2a2
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 ?? ''}-${i
ndex
}`
}
key=
{
`${limitName}-${item.metered_feature ?? ''}-${i
tem.plan_type ?? ''
}`
}
title=
{
limitName
}
description=
{
t
(
'Additional metered capability'
)
}
source=
{
item
}
...
...
web/default/src/features/channels/constants.ts
View file @
50b8f2a2
...
...
@@ -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'
},
...
...
web/default/src/features/channels/lib/channel-utils.ts
View file @
50b8f2a2
...
...
@@ -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)([\u
4e00-
\u
9fff
])
/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
}
`
}
})
}
...
...
web/default/src/features/models/components/dialogs/view-details-dialog.tsx
View file @
50b8f2a2
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 />
{is
LoadingDetails || isLoadingContainers
? (
{is
DetailsLoading
? (
<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>
)
...
...
web/default/src/features/models/components/dialogs/view-logs-dialog.tsx
View file @
50b8f2a2
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
.
replace
All
(
/
\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
>
)
...
...
web/default/src/features/usage-logs/components/model-badge.tsx
View file @
50b8f2a2
...
...
@@ -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
,
1
4
)
}
{
getLobeIcon
(
provider
.
icon
,
1
8
)
}
</
span
>
)
}
<
span
className=
'whitespace-nowrap'
>
{
props
.
modelName
}
</
span
>
...
...
web/default/src/lib/show-submitted-data.tsx
View file @
50b8f2a2
...
...
@@ -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
>
),
})
...
...
web/default/src/styles/theme-presets.css
View file @
50b8f2a2
...
...
@@ -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.9
8
0
0
);
--card
:
oklch
(
0.
18
0
0
);
--card-foreground
:
oklch
(
0.9
8
0
0
);
--popover
:
oklch
(
0.2
0
0
);
--popover-foreground
:
oklch
(
0.9
8
0
0
);
--background
:
oklch
(
0.
215
0
0
);
--foreground
:
oklch
(
0.9
7
0
0
);
--card
:
oklch
(
0.
27
0
0
);
--card-foreground
:
oklch
(
0.9
7
0
0
);
--popover
:
oklch
(
0.2
9
0
0
);
--popover-foreground
:
oklch
(
0.9
7
0
0
);
--primary
:
oklch
(
0.94
0
0
);
--primary-foreground
:
oklch
(
0.1
2
0
0
);
--secondary
:
oklch
(
0.
24
0
0
);
--secondary-foreground
:
oklch
(
0.9
8
0
0
);
--muted
:
oklch
(
0.
24
0
0
);
--primary-foreground
:
oklch
(
0.1
45
0
0
);
--secondary
:
oklch
(
0.
315
0
0
);
--secondary-foreground
:
oklch
(
0.9
7
0
0
);
--muted
:
oklch
(
0.
315
0
0
);
--muted-foreground
:
oklch
(
0.82
0
0
);
--accent
:
oklch
(
0.3
0
0
);
--accent
:
oklch
(
0.3
6
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.1
2
0
0
);
--success-foreground
:
oklch
(
0.1
45
0
0
);
--warning
:
oklch
(
0.82
0.13
75
);
--warning-foreground
:
oklch
(
0.1
2
0
0
);
--warning-foreground
:
oklch
(
0.1
45
0
0
);
--info
:
oklch
(
0.72
0.12
250
);
--info-foreground
:
oklch
(
0.1
2
0
0
);
--info-foreground
:
oklch
(
0.1
45
0
0
);
--neutral
:
oklch
(
0.82
0
0
);
--neutral-foreground
:
oklch
(
0.1
2
0
0
);
--neutral-foreground
:
oklch
(
0.1
45
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.9
8
0
0
);
--sidebar
:
oklch
(
0.
205
0
0
);
--sidebar-foreground
:
oklch
(
0.9
7
0
0
);
--sidebar-primary
:
oklch
(
0.94
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
2
0
0
);
--sidebar-accent
:
oklch
(
0.
28
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
45
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.2
05
0.004
60
);
/* ≈ #181715 */
--foreground
:
oklch
(
0.965
0.005
92
);
/* ≈ #faf9f5 */
--card
:
oklch
(
0.2
4
5
0.004
60
);
--background
:
oklch
(
0.2
15
0.004
60
);
--foreground
:
oklch
(
0.965
0.005
92
);
--card
:
oklch
(
0.2
5
5
0.004
60
);
--card-foreground
:
oklch
(
0.965
0.005
92
);
--popover
:
oklch
(
0.2
6
5
0.004
60
);
--popover
:
oklch
(
0.2
7
5
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.
29
5
0.004
60
);
--secondary
:
oklch
(
0.
30
5
0.004
60
);
--secondary-foreground
:
oklch
(
0.945
0.005
92
);
--muted
:
oklch
(
0.2
7
5
0.004
60
);
--muted
:
oklch
(
0.2
8
5
0.004
60
);
--muted-foreground
:
oklch
(
0.76
0.006
75
);
--accent
:
oklch
(
0.3
2
0.006
60
);
--accent
:
oklch
(
0.3
3
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.
17
5
0.004
60
);
--sidebar
:
oklch
(
0.
20
5
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.3
1
0.006
60
);
--sidebar-accent
:
oklch
(
0.3
2
0.006
60
);
--sidebar-accent-foreground
:
oklch
(
0.985
0.005
92
);
--sidebar-border
:
oklch
(
1
0
0
/
1
0
%
);
--sidebar-border
:
oklch
(
1
0
0
/
1
1
%
);
--sidebar-ring
:
oklch
(
0.72
0.135
40
);
--skeleton-base
:
oklch
(
0.
29
5
0.004
60
);
--skeleton-highlight
:
oklch
(
0.4
0.004
60
);
--skeleton-base
:
oklch
(
0.
30
5
0.004
60
);
--skeleton-highlight
:
oklch
(
0.4
1
0.004
60
);
}
/* ── Font axis ────────────────────────────────────────────────────────────
...
...
web/default/src/styles/theme.css
View file @
50b8f2a2
...
...
@@ -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.2
2
5
0
0
);
/* OpenAI-like dark mode with a
crisp
charcoal canvas. */
--background
:
oklch
(
0.2
3
5
0
0
);
--foreground
:
oklch
(
0.965
0
0
);
--card
:
oklch
(
0.2
7
5
0
0
);
--card
:
oklch
(
0.2
8
5
0
0
);
--card-foreground
:
oklch
(
0.965
0
0
);
--popover
:
oklch
(
0.
29
5
0
0
);
--popover
:
oklch
(
0.
30
5
0
0
);
--popover-foreground
:
oklch
(
0.965
0
0
);
--primary
:
oklch
(
0.965
0
0
);
--primary-foreground
:
oklch
(
0.1
4
5
0
0
);
--secondary
:
oklch
(
0.3
2
5
0
0
);
--primary-foreground
:
oklch
(
0.1
5
5
0
0
);
--secondary
:
oklch
(
0.3
3
5
0
0
);
--secondary-foreground
:
oklch
(
0.965
0
0
);
--muted
:
oklch
(
0.
29
5
0
0
);
--muted-foreground
:
oklch
(
0.7
6
0
0
);
--muted
:
oklch
(
0.
30
5
0
0
);
--muted-foreground
:
oklch
(
0.7
8
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.1
4
5
0
0
);
--border
:
oklch
(
1
0
0
/
9
%
);
--input
:
oklch
(
1
0
0
/
1
6
%
);
--neutral-foreground
:
oklch
(
0.1
5
5
0
0
);
--border
:
oklch
(
1
0
0
/
10
%
);
--input
:
oklch
(
1
0
0
/
1
7
%
);
--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.
19
5
0
0
);
--sidebar
:
oklch
(
0.
22
5
0
0
);
--sidebar-foreground
:
oklch
(
0.95
0
0
);
--sidebar-primary
:
oklch
(
0.965
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
4
5
0
0
);
--sidebar-accent
:
oklch
(
0.35
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
5
5
0
0
);
--sidebar-accent
:
oklch
(
0.35
5
0
0
);
--sidebar-accent-foreground
:
oklch
(
0.985
0
0
);
--sidebar-border
:
oklch
(
1
0
0
/
1
0
%
);
--sidebar-border
:
oklch
(
1
0
0
/
1
1
%
);
--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
)
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment