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 @@
...
@@ -21,7 +21,7 @@
"src/routeTree.gen.ts"
"src/routeTree.gen.ts"
],
],
"rules"
:
{
"rules"
:
{
"curly"
:
"error"
,
"curly"
:
[
"error"
,
"multi-line"
]
,
"eqeqeq"
:
[
"eqeqeq"
:
[
"error"
,
"error"
,
"always"
,
"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/>.
...
@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
type
{
DataTableColumnClassName
,
DataTablePinnedColumn
}
from
'./types'
import
type
{
DataTableColumnClassName
,
DataTablePinnedColumn
}
from
'./types'
export
function
getResolvedColumnClassName
(
export
function
getResolvedColumnClassName
(
...
@@ -37,14 +38,18 @@ export function getResolvedColumnClassNameFromMap(
...
@@ -37,14 +38,18 @@ export function getResolvedColumnClassNameFromMap(
const
customClassName
=
getColumnClassName
?.(
columnId
,
kind
)
const
customClassName
=
getColumnClassName
?.(
columnId
,
kind
)
const
pinnedColumn
=
pinnedColumnById
?.
get
(
columnId
)
const
pinnedColumn
=
pinnedColumnById
?.
get
(
columnId
)
if
(
!
pinnedColumn
)
return
customClassName
if
(
!
pinnedColumn
)
{
return
customClassName
}
return
cn
(
customClassName
,
getPinnedColumnClassName
(
pinnedColumn
,
kind
))
return
cn
(
customClassName
,
getPinnedColumnClassName
(
pinnedColumn
,
kind
))
}
}
}
}
export
function
getPinnedColumnMap
(
pinnedColumns
?:
DataTablePinnedColumn
[])
{
export
function
getPinnedColumnMap
(
pinnedColumns
?:
DataTablePinnedColumn
[])
{
if
(
!
pinnedColumns
?.
length
)
return
undefined
if
(
!
pinnedColumns
?.
length
)
{
return
undefined
}
return
new
Map
(
pinnedColumns
.
map
((
column
)
=>
[
column
.
columnId
,
column
]))
return
new
Map
(
pinnedColumns
.
map
((
column
)
=>
[
column
.
columnId
,
column
]))
}
}
...
@@ -63,7 +68,7 @@ function getPinnedColumnClassName(
...
@@ -63,7 +68,7 @@ function getPinnedColumnClassName(
pinnedColumn
.
side
===
'left'
?
'left-0'
:
'right-0'
,
pinnedColumn
.
side
===
'left'
?
'left-0'
:
'right-0'
,
edgeClassName
,
edgeClassName
,
kind
===
'header'
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'
,
:
'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
,
pinnedColumn
.
className
,
kind
===
'header'
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -17,9 +18,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,9 +18,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
*
as
React
from
'react'
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
{
Table
,
TableBody
,
TableCell
,
TableRow
}
from
'@/components/ui/table'
import
{
cn
}
from
'@/lib/utils'
import
{
import
{
getPinnedColumnMap
,
getPinnedColumnMap
,
getResolvedColumnClassNameFromMap
,
getResolvedColumnClassNameFromMap
,
...
@@ -136,7 +138,7 @@ function SplitHeaderTableView<TData>({
...
@@ -136,7 +138,7 @@ function SplitHeaderTableView<TData>({
<
div
<
div
className=
{
cn
(
className=
{
cn
(
'min-h-0 flex-1 overflow-auto'
,
'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)'
,
'**:data-[slot=table-header]:bg-(--table-header-bg)'
,
props
.
splitHeaderScrollClassName
,
props
.
splitHeaderScrollClassName
,
props
.
bodyContainerClassName
props
.
bodyContainerClassName
...
@@ -192,7 +194,9 @@ function getMetaPinnedColumns<TData>(
...
@@ -192,7 +194,9 @@ function getMetaPinnedColumns<TData>(
):
DataTablePinnedColumn
[]
{
):
DataTablePinnedColumn
[]
{
return
table
.
getAllColumns
().
flatMap
((
column
)
=>
{
return
table
.
getAllColumns
().
flatMap
((
column
)
=>
{
const
side
=
column
.
columnDef
.
meta
?.
pinned
const
side
=
column
.
columnDef
.
meta
?.
pinned
if
(
!
side
)
return
[]
if
(
!
side
)
{
return
[]
}
return
[{
columnId
:
column
.
id
,
side
}]
return
[{
columnId
:
column
.
id
,
side
}]
})
})
...
...
web/default/src/components/data-table/index.ts
View file @
50b8f2a2
...
@@ -61,7 +61,7 @@ export {
...
@@ -61,7 +61,7 @@ export {
export
{
useDebouncedColumnFilter
}
from
'./hooks/use-debounced-column-filter'
export
{
useDebouncedColumnFilter
}
from
'./hooks/use-debounced-column-filter'
export
const
DISABLED_ROW_DESKTOP
=
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
=
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -17,10 +19,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,10 +19,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
*
as
React
from
'react'
import
*
as
React
from
'react'
import
type
{
Row
,
Table
}
from
'@tanstack/react-table'
import
{
Database
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
import
{
Empty
,
Empty
,
EmptyDescription
,
EmptyDescription
,
...
@@ -29,6 +29,8 @@ import {
...
@@ -29,6 +29,8 @@ import {
EmptyTitle
,
EmptyTitle
,
}
from
'@/components/ui/empty'
}
from
'@/components/ui/empty'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
cn
}
from
'@/lib/utils'
import
{
tableHasCompactMeta
}
from
'./card-cell-utils'
import
{
tableHasCompactMeta
}
from
'./card-cell-utils'
import
{
CardRowContent
}
from
'./card-row-content'
import
{
CardRowContent
}
from
'./card-row-content'
...
@@ -78,7 +80,7 @@ function CardGridSkeleton(props: {
...
@@ -78,7 +80,7 @@ function CardGridSkeleton(props: {
{
[
1
,
2
,
3
,
4
,
5
,
6
].
map
((
i
)
=>
(
{
[
1
,
2
,
3
,
4
,
5
,
6
].
map
((
i
)
=>
(
<
div
<
div
key=
{
`${prefix}-${i}`
}
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'
>
<
div
className=
'flex items-center justify-between gap-2'
>
<
Skeleton
className=
'h-4 w-32'
/>
<
Skeleton
className=
'h-4 w-32'
/>
...
@@ -157,8 +159,9 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
...
@@ -157,8 +159,9 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
return
(
return
(
<
div
<
div
key=
{
key
}
key=
{
key
}
data
-
slot=
'data-table-card'
className=
{
cn
(
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
)
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -17,14 +22,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,14 +22,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
*
as
React
from
'react'
import
*
as
React
from
'react'
import
{
type
ColumnDef
,
import
{
PageFooterPortal
}
from
'@/components/layout'
type
Row
,
type
Table
as
TanstackTable
,
}
from
'@tanstack/react-table'
import
{
useMediaQuery
}
from
'@/hooks'
import
{
useMediaQuery
}
from
'@/hooks'
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
{
PageFooterPortal
}
from
'@/components/layout'
import
{
import
{
DataTableView
,
DataTableView
,
type
DataTableColumnClassName
,
type
DataTableColumnClassName
,
...
@@ -32,15 +34,15 @@ import {
...
@@ -32,15 +34,15 @@ import {
type
DataTableRenderRowHelpers
,
type
DataTableRenderRowHelpers
,
}
from
'../core/data-table-view'
}
from
'../core/data-table-view'
import
{
DataTablePagination
}
from
'../core/pagination'
import
{
DataTablePagination
}
from
'../core/pagination'
import
{
DataTableToolbar
}
from
'../toolbar/toolbar'
import
{
DataTableViewModeToggle
}
from
'../toolbar/view-mode-toggle'
import
{
import
{
DATA_TABLE_VIEW_MODES
,
DATA_TABLE_VIEW_MODES
,
useDataTableViewMode
,
useDataTableViewMode
,
type
DataTableViewMode
,
type
DataTableViewMode
,
}
from
'../hooks/use-data-table-view-mode'
}
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
{
DataTableCardGrid
}
from
'./card-grid'
import
{
MobileCardList
}
from
'./mobile-card-list'
/**
/**
* Pass-through configuration for the default {@link DataTableToolbar}.
* Pass-through configuration for the default {@link DataTableToolbar}.
...
@@ -376,7 +378,9 @@ function renderToolbar<TData>(
...
@@ -376,7 +378,9 @@ function renderToolbar<TData>(
function
renderPagination
<
TData
>
(
function
renderPagination
<
TData
>
(
props
:
DataTablePageProps
<
TData
>
props
:
DataTablePageProps
<
TData
>
):
React
.
ReactNode
{
):
React
.
ReactNode
{
if
(
props
.
showPagination
===
false
)
return
null
if
(
props
.
showPagination
===
false
)
{
return
null
}
const
pagination
=
<
DataTablePagination
table=
{
props
.
table
}
/>
const
pagination
=
<
DataTablePagination
table=
{
props
.
table
}
/>
...
@@ -391,7 +395,9 @@ function renderMobile<TData>(
...
@@ -391,7 +395,9 @@ function renderMobile<TData>(
props
:
DataTablePageProps
<
TData
>
,
props
:
DataTablePageProps
<
TData
>
,
showMobile
:
boolean
showMobile
:
boolean
):
React
.
ReactNode
{
):
React
.
ReactNode
{
if
(
!
showMobile
)
return
null
if
(
!
showMobile
)
{
return
null
}
const
ownGetRowClassName
=
props
.
getRowClassName
const
ownGetRowClassName
=
props
.
getRowClassName
const
mobileGetRowClassName
=
const
mobileGetRowClassName
=
...
@@ -436,7 +442,9 @@ function renderDesktop<TData>(
...
@@ -436,7 +442,9 @@ function renderDesktop<TData>(
cardViewActive
:
boolean
,
cardViewActive
:
boolean
,
viewMode
:
DataTableViewMode
viewMode
:
DataTableViewMode
):
React
.
ReactNode
{
):
React
.
ReactNode
{
if
(
showMobile
)
return
null
if
(
showMobile
)
{
return
null
}
const
isFetchingOnly
=
props
.
isFetching
&&
!
props
.
isLoading
const
isFetchingOnly
=
props
.
isFetching
&&
!
props
.
isLoading
const
fixedHeight
=
props
.
fixedHeight
!==
false
const
fixedHeight
=
props
.
fixedHeight
!==
false
...
@@ -481,8 +489,7 @@ function renderDesktop<TData>(
...
@@ -481,8 +489,7 @@ function renderDesktop<TData>(
splitHeader=
{
fixedHeight
}
splitHeader=
{
fixedHeight
}
tableContainerClassName=
{
fixedHeight
?
'h-full min-h-0'
:
undefined
}
tableContainerClassName=
{
fixedHeight
?
'h-full min-h-0'
:
undefined
}
tableHeaderClassName=
{
cn
(
tableHeaderClassName=
{
cn
(
fixedHeight
&&
fixedHeight
&&
'[background-color:var(--table-header)]'
,
'[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]'
,
props
.
tableHeaderClassName
props
.
tableHeaderClassName
)
}
)
}
getColumnClassName=
{
props
.
getColumnClassName
}
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -17,10 +19,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,10 +19,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
*
as
React
from
'react'
import
*
as
React
from
'react'
import
type
{
Row
,
Table
}
from
'@tanstack/react-table'
import
{
Database
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
import
{
Empty
,
Empty
,
EmptyDescription
,
EmptyDescription
,
...
@@ -29,6 +29,8 @@ import {
...
@@ -29,6 +29,8 @@ import {
EmptyTitle
,
EmptyTitle
,
}
from
'@/components/ui/empty'
}
from
'@/components/ui/empty'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
cn
}
from
'@/lib/utils'
import
{
tableHasCompactMeta
}
from
'./card-cell-utils'
import
{
tableHasCompactMeta
}
from
'./card-cell-utils'
import
{
CardRowContent
}
from
'./card-row-content'
import
{
CardRowContent
}
from
'./card-row-content'
...
@@ -143,7 +145,10 @@ export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
...
@@ -143,7 +145,10 @@ export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
return
(
return
(
<
div
<
div
key=
{
key
}
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
}
/>
<
CardRowContent
row=
{
row
}
compact=
{
hasCompactMeta
}
/>
</
div
>
</
div
>
...
...
web/default/src/components/data-table/static/static-data-table-classnames.ts
View file @
50b8f2a2
...
@@ -23,7 +23,7 @@ export const staticDataTableClassNames = {
...
@@ -23,7 +23,7 @@ export const staticDataTableClassNames = {
compactTable
:
'text-sm'
,
compactTable
:
'text-sm'
,
compactHeaderRow
:
'hover:bg-transparent'
,
compactHeaderRow
:
'hover:bg-transparent'
,
mutedHeaderRow
:
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
:
compactHeaderCell
:
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
,
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
,
compactHeaderCellRight
:
compactHeaderCellRight
:
...
...
web/default/src/components/status-badge.tsx
View file @
50b8f2a2
import
type
{
LucideIcon
}
from
'lucide-react'
/*
/*
Copyright (C) 2023-2026 QuantumNous
Copyright (C) 2023-2026 QuantumNous
...
@@ -18,10 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -18,10 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
*/
*/
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable react-refresh/only-export-components */
import
*
as
React
from
'react'
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
{
stringToColor
}
from
'@/lib/colors'
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
{
useCopyToClipboard
}
from
'@/hooks/use-copy-to-clipboard'
export
const
dotColorMap
=
{
export
const
dotColorMap
=
{
success
:
'bg-success'
,
success
:
'bg-success'
,
...
@@ -37,7 +38,7 @@ export const dotColorMap = {
...
@@ -37,7 +38,7 @@ export const dotColorMap = {
grey
:
'bg-neutral'
,
grey
:
'bg-neutral'
,
indigo
:
'bg-chart-1'
,
indigo
:
'bg-chart-1'
,
'light-blue'
:
'bg-info'
,
'light-blue'
:
'bg-info'
,
'light-green'
:
'bg-
success
'
,
'light-green'
:
'bg-
emerald-400
'
,
lime
:
'bg-chart-3'
,
lime
:
'bg-chart-3'
,
orange
:
'bg-warning'
,
orange
:
'bg-warning'
,
pink
:
'bg-chart-5'
,
pink
:
'bg-chart-5'
,
...
@@ -61,7 +62,7 @@ export const textColorMap = {
...
@@ -61,7 +62,7 @@ export const textColorMap = {
grey
:
'text-muted-foreground'
,
grey
:
'text-muted-foreground'
,
indigo
:
'text-chart-1'
,
indigo
:
'text-chart-1'
,
'light-blue'
:
'text-info'
,
'light-blue'
:
'text-info'
,
'light-green'
:
'text-
success
'
,
'light-green'
:
'text-
emerald-500 dark:text-emerald-300
'
,
lime
:
'text-chart-3'
,
lime
:
'text-chart-3'
,
orange
:
'text-warning'
,
orange
:
'text-warning'
,
pink
:
'text-chart-5'
,
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -18,18 +28,24 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -18,18 +28,24 @@ For commercial licensing, please contact support@quantumnous.com
*/
*/
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable react-refresh/only-export-components */
import
{
useState
}
from
'react'
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
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
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
{
import
{
formatCurrencyFromUSD
,
formatCurrencyFromUSD
,
formatQuotaWithCurrency
,
formatQuotaWithCurrency
,
...
@@ -40,21 +56,7 @@ import {
...
@@ -40,21 +56,7 @@ import {
formatQuota
as
formatQuotaValue
,
formatQuota
as
formatQuotaValue
,
}
from
'@/lib/format'
}
from
'@/lib/format'
import
{
truncateText
}
from
'@/lib/utils'
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
{
getCodexUsage
}
from
'../api'
import
{
CHANNEL_STATUS_CONFIG
,
MODEL_FETCHABLE_TYPES
}
from
'../constants'
import
{
CHANNEL_STATUS_CONFIG
,
MODEL_FETCHABLE_TYPES
}
from
'../constants'
import
{
import
{
...
@@ -90,7 +92,9 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | {
...
@@ -90,7 +92,9 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | {
source
?:
string
source
?:
string
deployment_id
?:
string
deployment_id
?:
string
}
{
}
{
if
(
!
otherInfo
)
return
null
if
(
!
otherInfo
)
{
return
null
}
try
{
try
{
const
parsed
=
JSON
.
parse
(
otherInfo
)
const
parsed
=
JSON
.
parse
(
otherInfo
)
if
(
parsed
&&
typeof
parsed
===
'object'
)
{
if
(
parsed
&&
typeof
parsed
===
'object'
)
{
...
@@ -107,14 +111,20 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | {
...
@@ -107,14 +111,20 @@ function parseIonetMeta(otherInfo: string | null | undefined): null | {
*/
*/
function
UpstreamUpdateTags
({
channel
}:
{
channel
:
Channel
})
{
function
UpstreamUpdateTags
({
channel
}:
{
channel
:
Channel
})
{
const
{
upstream
,
setCurrentRow
}
=
useChannels
()
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
)
const
meta
=
parseUpstreamUpdateMeta
(
channel
.
settings
)
if
(
!
meta
.
enabled
)
return
null
if
(
!
meta
.
enabled
)
{
return
null
}
const
addCount
=
meta
.
pendingAddModels
.
length
const
addCount
=
meta
.
pendingAddModels
.
length
const
removeCount
=
meta
.
pendingRemoveModels
.
length
const
removeCount
=
meta
.
pendingRemoveModels
.
length
if
(
addCount
===
0
&&
removeCount
===
0
)
return
null
if
(
addCount
===
0
&&
removeCount
===
0
)
{
return
null
}
return
(
return
(
<
div
className=
'flex items-center gap-0.5'
>
<
div
className=
'flex items-center gap-0.5'
>
...
@@ -300,7 +310,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
...
@@ -300,7 +310,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
const
remainingFull
=
withSuffix
(
formatBalance
(
balance
))
const
remainingFull
=
withSuffix
(
formatBalance
(
balance
))
const
usedDisplay
=
const
usedDisplay
=
usedFull
.
length
>
MAX_INLINE_BALANCE_CHARS
usedFull
.
length
>
MAX_INLINE_BALANCE_CHARS
?
withSuffix
(
formatQuotaWithCurrency
(
usedQuota
,
{
compact
:
true
,
locale
}))
?
withSuffix
(
formatQuotaWithCurrency
(
usedQuota
,
{
compact
:
true
,
locale
})
)
:
usedFull
:
usedFull
const
remainingDisplay
=
const
remainingDisplay
=
remainingFull
.
length
>
MAX_INLINE_BALANCE_CHARS
remainingFull
.
length
>
MAX_INLINE_BALANCE_CHARS
...
@@ -338,7 +350,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
...
@@ -338,7 +350,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
const
variant
=
getBalanceVariant
(
balance
)
const
variant
=
getBalanceVariant
(
balance
)
const
handleClickUpdate
=
async
()
=>
{
const
handleClickUpdate
=
async
()
=>
{
if
(
isUpdating
)
return
if
(
isUpdating
)
{
return
}
setIsUpdating
(
true
)
setIsUpdating
(
true
)
if
(
channel
.
type
===
57
)
{
if
(
channel
.
type
===
57
)
{
...
@@ -362,6 +376,18 @@ function BalanceCell({ channel }: { channel: Channel }) {
...
@@ -362,6 +376,18 @@ function BalanceCell({ channel }: { channel: Channel }) {
await
handleUpdateChannelBalance
(
channel
.
id
,
queryClient
)
await
handleUpdateChannelBalance
(
channel
.
id
,
queryClient
)
setIsUpdating
(
false
)
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
(
return
(
<
TooltipProvider
>
<
TooltipProvider
>
...
@@ -387,20 +413,8 @@ function BalanceCell({ channel }: { channel: Channel }) {
...
@@ -387,20 +413,8 @@ function BalanceCell({ channel }: { channel: Channel }) {
<
TooltipTrigger
<
TooltipTrigger
render=
{
render=
{
<
StatusBadge
<
StatusBadge
label=
{
label=
{
remainingBadgeLabel
}
isUpdating
variant=
{
remainingBadgeVariant
}
?
t
(
'Updating...'
)
:
channel
.
type
===
57
?
t
(
'Account Info'
)
:
remainingDisplay
}
variant=
{
channel
.
type
===
57
?
'info'
:
isUpdating
?
'neutral'
:
variant
}
size=
'sm'
size=
'sm'
copyable=
{
false
}
copyable=
{
false
}
showDot=
{
false
}
showDot=
{
false
}
...
@@ -427,7 +441,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
...
@@ -427,7 +441,9 @@ function BalanceCell({ channel }: { channel: Channel }) {
channelId=
{
channel
.
id
}
channelId=
{
channel
.
id
}
response=
{
codexUsageResponse
}
response=
{
codexUsageResponse
}
onRefresh=
{
async
()
=>
{
onRefresh=
{
async
()
=>
{
if
(
isUpdating
)
return
if
(
isUpdating
)
{
return
}
setIsUpdating
(
true
)
setIsUpdating
(
true
)
try
{
try
{
const
res
=
await
getCodexUsage
(
channel
.
id
)
const
res
=
await
getCodexUsage
(
channel
.
id
)
...
@@ -565,7 +581,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -565,7 +581,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
render=
{
render=
{
<
AlertTriangle
className=
'h-3.5 w-3.5 flex-shrink-0 text-amber-500'
/>
<
AlertTriangle
className=
'h-3.5 w-3.5 flex-shrink-0 text-amber-500'
/>
}
}
></
TooltipTrigger
>
/
>
<
TooltipContent
side=
'top'
>
<
TooltipContent
side=
'top'
>
{
t
(
{
t
(
'Request body pass-through is enabled. The request body will be sent directly to the upstream without any conversion.'
'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>[] {
...
@@ -581,7 +597,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
render=
{
render=
{
<
SlidersHorizontal
className=
'text-info h-3.5 w-3.5 flex-shrink-0'
/>
<
SlidersHorizontal
className=
'text-info h-3.5 w-3.5 flex-shrink-0'
/>
}
}
></
TooltipTrigger
>
/
>
<
TooltipContent
side=
'top'
>
<
TooltipContent
side=
'top'
>
{
t
(
'Override request parameters'
)
}
{
t
(
'Override request parameters'
)
}
</
TooltipContent
>
</
TooltipContent
>
...
@@ -698,7 +714,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -698,7 +714,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
className=
'flex cursor-pointer items-center gap-1.5 text-xs font-medium'
className=
'flex cursor-pointer items-center gap-1.5 text-xs font-medium'
onClick=
{
(
e
)
=>
{
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
()
e
.
stopPropagation
()
if
(
!
deploymentId
)
return
if
(
!
deploymentId
)
{
return
}
const
targetUrl
=
`/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
const
targetUrl
=
`/models/deployments?dFilter=${encodeURIComponent(String(deploymentId))}`
window
.
open
(
targetUrl
,
'_blank'
,
'noopener'
)
window
.
open
(
targetUrl
,
'_blank'
,
'noopener'
)
}
}
}
}
...
@@ -735,7 +753,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -735,7 +753,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
)
)
},
},
filterFn
:
(
row
,
id
,
value
)
=>
{
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
)))
return
value
.
includes
(
String
(
row
.
getValue
(
id
)))
},
},
size
:
220
,
size
:
220
,
...
@@ -856,10 +876,16 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -856,10 +876,16 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
)
)
},
},
filterFn
:
(
row
,
id
,
value
)
=>
{
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
const
status
=
row
.
getValue
(
id
)
as
number
if
(
value
.
includes
(
'enabled'
))
return
status
===
1
if
(
value
.
includes
(
'enabled'
))
{
if
(
value
.
includes
(
'disabled'
))
return
status
!==
1
return
status
===
1
}
if
(
value
.
includes
(
'disabled'
))
{
return
status
!==
1
}
return
false
return
false
},
},
size
:
120
,
size
:
120
,
...
@@ -876,9 +902,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -876,9 +902,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
const
modelArray
=
parseModelsList
(
models
)
const
modelArray
=
parseModelsList
(
models
)
return
(
return
(
<
BadgeListCell
<
BadgeListCell
items=
{
modelArray
.
map
((
model
,
idx
)
=>
(
items=
{
modelArray
.
map
((
model
)
=>
(
<
StatusBadge
<
StatusBadge
key=
{
idx
}
key=
{
model
}
label=
{
model
}
label=
{
model
}
autoColor=
{
model
}
autoColor=
{
model
}
size=
'sm'
size=
'sm'
...
@@ -909,7 +935,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -909,7 +935,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
)
)
},
},
filterFn
:
(
row
,
id
,
value
)
=>
{
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
group
=
row
.
getValue
(
id
)
as
string
const
groupArray
=
parseGroupsList
(
group
)
const
groupArray
=
parseGroupsList
(
group
)
return
groupArray
.
some
((
g
)
=>
value
.
includes
(
g
))
return
groupArray
.
some
((
g
)
=>
value
.
includes
(
g
))
...
@@ -925,8 +953,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -925,8 +953,9 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
meta
:
{
mobileHidden
:
true
},
meta
:
{
mobileHidden
:
true
},
cell
:
({
row
})
=>
{
cell
:
({
row
})
=>
{
const
tag
=
row
.
getValue
(
'tag'
)
as
string
|
null
const
tag
=
row
.
getValue
(
'tag'
)
as
string
|
null
if
(
!
tag
)
if
(
!
tag
)
{
return
<
span
className=
'text-muted-foreground text-xs'
>
-
</
span
>
return
<
span
className=
'text-muted-foreground text-xs'
>
-
</
span
>
}
return
(
return
(
<
StatusBadge
<
StatusBadge
...
@@ -1012,11 +1041,15 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
...
@@ -1012,11 +1041,15 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
<
Tooltip
>
<
Tooltip
>
<
TooltipTrigger
<
TooltipTrigger
render=
{
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'
>
<
TooltipContent
side=
'top'
>
<
p
className=
'font-mono text-sm'
>
{
fullDate
}
</
p
>
<
p
className=
'font-mono text-sm'
>
{
fullDate
}
</
p
>
</
TooltipContent
>
</
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -17,20 +26,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,20 +26,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
type
ReactNode
,
useCallback
,
useMemo
,
useState
}
from
'react'
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
{
useTranslation
}
from
'react-i18next'
import
dayjs
from
'@/lib/dayjs'
import
{
formatDateTimeStr
,
formatTimestampToDate
}
from
'@/lib/format
'
import
{
ConfirmDialog
}
from
'@/components/confirm-dialog
'
import
{
cn
}
from
'@/lib/utils
'
import
{
Dialog
}
from
'@/components/dialog
'
import
{
useCopyToClipboard
}
from
'@/hooks/use-copy-to-clipboard
'
import
{
StatusBadge
,
type
StatusBadgeProps
}
from
'@/components/status-badge
'
import
{
Alert
,
AlertDescription
,
AlertTitle
}
from
'@/components/ui/alert'
import
{
Alert
,
AlertDescription
,
AlertTitle
}
from
'@/components/ui/alert'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
import
{
...
@@ -55,9 +55,11 @@ import {
...
@@ -55,9 +55,11 @@ import {
import
{
Progress
}
from
'@/components/ui/progress'
import
{
Progress
}
from
'@/components/ui/progress'
import
{
ScrollArea
}
from
'@/components/ui/scroll-area'
import
{
ScrollArea
}
from
'@/components/ui/scroll-area'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
ConfirmDialog
}
from
'@/components/confirm-dialog'
import
{
useCopyToClipboard
}
from
'@/hooks/use-copy-to-clipboard'
import
{
Dialog
}
from
'@/components/dialog'
import
dayjs
from
'@/lib/dayjs'
import
{
StatusBadge
,
type
StatusBadgeProps
}
from
'@/components/status-badge'
import
{
formatDateTimeStr
,
formatTimestampToDate
}
from
'@/lib/format'
import
{
cn
}
from
'@/lib/utils'
import
{
import
{
getCodexResetCredits
,
getCodexResetCredits
,
resetCodexUsage
,
resetCodexUsage
,
...
@@ -153,9 +155,13 @@ function formatUnixSeconds(unixSeconds: unknown): string {
...
@@ -153,9 +155,13 @@ function formatUnixSeconds(unixSeconds: unknown): string {
}
}
function
formatIsoTimestamp
(
value
:
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
)
const
d
=
dayjs
(
value
)
if
(
!
d
.
isValid
())
return
value
if
(
!
d
.
isValid
())
{
return
value
}
return
formatDateTimeStr
(
d
.
toDate
())
return
formatDateTimeStr
(
d
.
toDate
())
}
}
...
@@ -164,15 +170,21 @@ function formatDurationSeconds(
...
@@ -164,15 +170,21 @@ function formatDurationSeconds(
t
:
(
key
:
string
)
=>
string
t
:
(
key
:
string
)
=>
string
):
string
{
):
string
{
const
s
=
Number
(
seconds
)
const
s
=
Number
(
seconds
)
if
(
!
Number
.
isFinite
(
s
)
||
s
<=
0
)
return
'-'
if
(
!
Number
.
isFinite
(
s
)
||
s
<=
0
)
{
return
'-'
}
const
total
=
Math
.
floor
(
s
)
const
total
=
Math
.
floor
(
s
)
const
hours
=
Math
.
floor
(
total
/
3600
)
const
hours
=
Math
.
floor
(
total
/
3600
)
const
minutes
=
Math
.
floor
((
total
%
3600
)
/
60
)
const
minutes
=
Math
.
floor
((
total
%
3600
)
/
60
)
const
secs
=
total
%
60
const
secs
=
total
%
60
if
(
hours
>
0
)
return
`
${
hours
}${
t
(
'h'
)}
${
minutes
}${
t
(
'm'
)}
`
if
(
hours
>
0
)
{
if
(
minutes
>
0
)
return
`
${
minutes
}${
t
(
'm'
)}
${
secs
}${
t
(
's'
)}
`
return
`
${
hours
}${
t
(
'h'
)}
${
minutes
}${
t
(
'm'
)}
`
}
if
(
minutes
>
0
)
{
return
`
${
minutes
}${
t
(
'm'
)}
${
secs
}${
t
(
's'
)}
`
}
return
`
${
secs
}${
t
(
's'
)}
`
return
`
${
secs
}${
t
(
's'
)}
`
}
}
...
@@ -180,12 +192,18 @@ function formatTimeLeftUntil(
...
@@ -180,12 +192,18 @@ function formatTimeLeftUntil(
value
:
unknown
,
value
:
unknown
,
t
:
(
key
:
string
)
=>
string
t
:
(
key
:
string
)
=>
string
):
string
{
):
string
{
if
(
typeof
value
!==
'string'
||
value
.
trim
()
===
''
)
return
'-'
if
(
typeof
value
!==
'string'
||
value
.
trim
()
===
''
)
{
return
'-'
}
const
expiresAt
=
dayjs
(
value
)
const
expiresAt
=
dayjs
(
value
)
if
(
!
expiresAt
.
isValid
())
return
'-'
if
(
!
expiresAt
.
isValid
())
{
return
'-'
}
const
secondsLeft
=
expiresAt
.
diff
(
dayjs
(),
'second'
)
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
days
=
Math
.
floor
(
secondsLeft
/
(
24
*
60
*
60
))
const
remainingSeconds
=
secondsLeft
%
(
24
*
60
*
60
)
const
remainingSeconds
=
secondsLeft
%
(
24
*
60
*
60
)
...
@@ -198,7 +216,9 @@ function formatTimeLeftUntil(
...
@@ -198,7 +216,9 @@ function formatTimeLeftUntil(
}
}
function
normalizePlanType
(
value
:
unknown
):
string
{
function
normalizePlanType
(
value
:
unknown
):
string
{
if
(
value
==
null
)
return
''
if
(
value
==
null
)
{
return
''
}
return
String
(
value
).
trim
().
toLowerCase
()
return
String
(
value
).
trim
().
toLowerCase
()
}
}
...
@@ -220,15 +240,21 @@ function sortResetCredits(credits: CodexResetCredit[]): CodexResetCredit[] {
...
@@ -220,15 +240,21 @@ function sortResetCredits(credits: CodexResetCredit[]): CodexResetCredit[] {
return
[...
credits
].
sort
((
a
,
b
)
=>
{
return
[...
credits
].
sort
((
a
,
b
)
=>
{
const
aAvailable
=
normalizeResetCreditStatus
(
a
.
status
)
===
'available'
const
aAvailable
=
normalizeResetCreditStatus
(
a
.
status
)
===
'available'
const
bAvailable
=
normalizeResetCreditStatus
(
b
.
status
)
===
'available'
const
bAvailable
=
normalizeResetCreditStatus
(
b
.
status
)
===
'available'
if
(
aAvailable
!==
bAvailable
)
return
aAvailable
?
-
1
:
1
if
(
aAvailable
!==
bAvailable
)
{
return
aAvailable
?
-
1
:
1
}
const
expiresDiff
=
const
expiresDiff
=
parseTimeValue
(
a
.
expires_at
)
-
parseTimeValue
(
b
.
expires_at
)
parseTimeValue
(
a
.
expires_at
)
-
parseTimeValue
(
b
.
expires_at
)
if
(
expiresDiff
!==
0
)
return
expiresDiff
if
(
expiresDiff
!==
0
)
{
return
expiresDiff
}
const
grantedDiff
=
const
grantedDiff
=
parseTimeValue
(
a
.
granted_at
)
-
parseTimeValue
(
b
.
granted_at
)
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
||
''
))
return
String
(
a
.
id
||
''
).
localeCompare
(
String
(
b
.
id
||
''
))
})
})
...
@@ -238,7 +264,9 @@ function classifyWindowByDuration(
...
@@ -238,7 +264,9 @@ function classifyWindowByDuration(
windowData
?:
CodexRateLimitWindow
|
null
windowData
?:
CodexRateLimitWindow
|
null
):
'weekly'
|
'fiveHour'
|
null
{
):
'weekly'
|
'fiveHour'
|
null
{
const
seconds
=
Number
(
windowData
?.
limit_window_seconds
)
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'
return
seconds
>=
24
*
60
*
60
?
'weekly'
:
'fiveHour'
}
}
...
@@ -272,7 +300,9 @@ function resolveRateLimitWindows(data: RateLimitSource | null): {
...
@@ -272,7 +300,9 @@ function resolveRateLimitWindows(data: RateLimitSource | null): {
}
}
if
(
planType
===
'free'
)
{
if
(
planType
===
'free'
)
{
if
(
!
weeklyWindow
)
weeklyWindow
=
primary
??
secondary
??
null
if
(
!
weeklyWindow
)
{
weeklyWindow
=
primary
??
secondary
??
null
}
return
{
fiveHourWindow
:
null
,
weeklyWindow
}
return
{
fiveHourWindow
:
null
,
weeklyWindow
}
}
}
...
@@ -338,8 +368,12 @@ function getResetCreditStatusBadge(
...
@@ -338,8 +368,12 @@ function getResetCreditStatusBadge(
function
windowLabel
(
windowData
?:
CodexRateLimitWindow
|
null
)
{
function
windowLabel
(
windowData
?:
CodexRateLimitWindow
|
null
)
{
const
percent
=
clampPercent
(
windowData
?.
used_percent
)
const
percent
=
clampPercent
(
windowData
?.
used_percent
)
const
variant
:
StatusBadgeProps
[
'variant'
]
=
let
variant
:
StatusBadgeProps
[
'variant'
]
=
'info'
percent
>=
95
?
'danger'
:
percent
>=
80
?
'warning'
:
'info'
if
(
percent
>=
95
)
{
variant
=
'danger'
}
else
if
(
percent
>=
80
)
{
variant
=
'warning'
}
return
{
percent
,
variant
}
return
{
percent
,
variant
}
}
}
...
@@ -381,7 +415,7 @@ const percentTextClassName: Record<
...
@@ -381,7 +415,7 @@ const percentTextClassName: Record<
grey
:
'text-muted-foreground'
,
grey
:
'text-muted-foreground'
,
indigo
:
'text-chart-1'
,
indigo
:
'text-chart-1'
,
'light-blue'
:
'text-info'
,
'light-blue'
:
'text-info'
,
'light-green'
:
'text-
success
'
,
'light-green'
:
'text-
emerald-500 dark:text-emerald-300
'
,
lime
:
'text-chart-3'
,
lime
:
'text-chart-3'
,
orange
:
'text-warning'
,
orange
:
'text-warning'
,
pink
:
'text-chart-5'
,
pink
:
'text-chart-5'
,
...
@@ -708,6 +742,51 @@ function ResetCreditsPanel(props: {
...
@@ -708,6 +742,51 @@ function ResetCreditsPanel(props: {
?
String
(
props
.
payload
?.
total_earned_count
)
?
String
(
props
.
payload
?.
total_earned_count
)
:
'-'
:
'-'
const
canReset
=
Number
(
availableCount
)
>
0
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
(
return
(
<
div
className=
'flex flex-col gap-3 p-3'
>
<
div
className=
'flex flex-col gap-3 p-3'
>
...
@@ -796,35 +875,7 @@ function ResetCreditsPanel(props: {
...
@@ -796,35 +875,7 @@ function ResetCreditsPanel(props: {
</
Alert
>
</
Alert
>
)
:
null
}
)
:
null
}
{
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
>
)
:
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
>
)
}
</
div
>
</
div
>
)
)
}
}
...
@@ -853,13 +904,17 @@ export function CodexUsageDialog({
...
@@ -853,13 +904,17 @@ export function CodexUsageDialog({
const
payload
:
CodexUsagePayload
|
null
=
useMemo
(()
=>
{
const
payload
:
CodexUsagePayload
|
null
=
useMemo
(()
=>
{
const
raw
=
response
?.
data
const
raw
=
response
?.
data
if
(
!
raw
||
typeof
raw
!==
'object'
)
return
null
if
(
!
raw
||
typeof
raw
!==
'object'
)
{
return
null
}
return
raw
as
CodexUsagePayload
return
raw
as
CodexUsagePayload
},
[
response
?.
data
])
},
[
response
?.
data
])
const
resetCreditsPayload
:
CodexResetCreditsPayload
|
null
=
useMemo
(()
=>
{
const
resetCreditsPayload
:
CodexResetCreditsPayload
|
null
=
useMemo
(()
=>
{
const
raw
=
resetCreditsResponse
?.
data
const
raw
=
resetCreditsResponse
?.
data
if
(
!
raw
||
typeof
raw
!==
'object'
)
return
null
if
(
!
raw
||
typeof
raw
!==
'object'
)
{
return
null
}
return
raw
as
CodexResetCreditsPayload
return
raw
as
CodexResetCreditsPayload
},
[
resetCreditsResponse
?.
data
])
},
[
resetCreditsResponse
?.
data
])
...
@@ -892,7 +947,9 @@ export function CodexUsageDialog({
...
@@ -892,7 +947,9 @@ export function CodexUsageDialog({
setResetCreditsError
(
t
(
'Channel ID is required'
))
setResetCreditsError
(
t
(
'Channel ID is required'
))
return
return
}
}
if
(
isLoadingResetCredits
||
(
!
force
&&
resetCreditsResponse
))
return
if
(
isLoadingResetCredits
||
(
!
force
&&
resetCreditsResponse
))
{
return
}
setIsLoadingResetCredits
(
true
)
setIsLoadingResetCredits
(
true
)
setResetCreditsError
(
''
)
setResetCreditsError
(
''
)
...
@@ -940,7 +997,9 @@ export function CodexUsageDialog({
...
@@ -940,7 +997,9 @@ export function CodexUsageDialog({
}
}
const
handleConfirmReset
=
async
()
=>
{
const
handleConfirmReset
=
async
()
=>
{
if
(
!
channelId
||
isResetting
||
!
canResetCodexUsage
)
return
if
(
!
channelId
||
isResetting
||
!
canResetCodexUsage
)
{
return
}
setIsResetting
(
true
)
setIsResetting
(
true
)
setResetActionError
(
''
)
setResetActionError
(
''
)
...
@@ -975,7 +1034,9 @@ export function CodexUsageDialog({
...
@@ -975,7 +1034,9 @@ export function CodexUsageDialog({
}
}
const
rawJsonText
=
useMemo
(()
=>
{
const
rawJsonText
=
useMemo
(()
=>
{
if
(
!
response
)
return
''
if
(
!
response
)
{
return
''
}
try
{
try
{
return
JSON
.
stringify
(
return
JSON
.
stringify
(
{
{
...
@@ -1002,15 +1063,13 @@ export function CodexUsageDialog({
...
@@ -1002,15 +1063,13 @@ export function CodexUsageDialog({
contentHeight=
'auto'
contentHeight=
'auto'
bodyClassName=
'flex flex-col gap-4'
bodyClassName=
'flex flex-col gap-4'
footer=
{
footer=
{
<>
<
Button
<
Button
type=
'button'
type=
'button'
variant=
'outline'
variant=
'outline'
onClick=
{
()
=>
handleDialogOpenChange
(
false
)
}
onClick=
{
()
=>
handleDialogOpenChange
(
false
)
}
>
>
{
t
(
'Close'
)
}
{
t
(
'Close'
)
}
</
Button
>
</
Button
>
</>
}
}
>
>
<
div
className=
'flex flex-col gap-4'
>
<
div
className=
'flex flex-col gap-4'
>
...
@@ -1074,11 +1133,7 @@ export function CodexUsageDialog({
...
@@ -1074,11 +1133,7 @@ export function CodexUsageDialog({
)
:
null
}
)
:
null
}
</
div
>
</
div
>
<
div
className=
'mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'
>
<
div
className=
'mt-4 grid grid-cols-1 gap-3 md:grid-cols-2'
>
<
InfoField
<
InfoField
label=
{
t
(
'Email'
)
}
value=
{
payload
?.
email
}
copyable
/>
label=
{
t
(
'Email'
)
}
value=
{
payload
?.
email
}
copyable=
{
true
}
/>
<
InfoField
<
InfoField
label=
{
t
(
'Channel'
)
}
label=
{
t
(
'Channel'
)
}
value=
{
channelLabel
}
value=
{
channelLabel
}
...
@@ -1171,14 +1226,14 @@ export function CodexUsageDialog({
...
@@ -1171,14 +1226,14 @@ export function CodexUsageDialog({
)
}
)
}
/>
/>
<
div
className=
'flex flex-col gap-3'
>
<
div
className=
'flex flex-col gap-3'
>
{
additionalRateLimits
.
map
((
item
,
index
)
=>
{
{
additionalRateLimits
.
map
((
item
)
=>
{
const
limitName
=
const
limitName
=
item
.
limit_name
||
item
.
limit_name
||
item
.
metered_feature
||
item
.
metered_feature
||
`${t('Additional Limit')} ${index + 1}`
t
(
'Additional Limit'
)
return
(
return
(
<
RateLimitGroupSection
<
RateLimitGroupSection
key=
{
`${limitName}-${item.metered_feature ?? ''}-${i
ndex
}`
}
key=
{
`${limitName}-${item.metered_feature ?? ''}-${i
tem.plan_type ?? ''
}`
}
title=
{
limitName
}
title=
{
limitName
}
description=
{
t
(
'Additional metered capability'
)
}
description=
{
t
(
'Additional metered capability'
)
}
source=
{
item
}
source=
{
item
}
...
...
web/default/src/features/channels/constants.ts
View file @
50b8f2a2
...
@@ -80,8 +80,8 @@ export const CHANNEL_TYPES = {
...
@@ -80,8 +80,8 @@ export const CHANNEL_TYPES = {
}
as
const
}
as
const
const
CHANNEL_TYPE_DISPLAY_ORDER
:
number
[]
=
[
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
,
1
,
14
,
33
,
24
,
43
,
3
,
41
,
48
,
58
,
42
,
34
,
20
,
4
,
40
,
27
,
25
,
17
,
26
,
15
,
46
,
18
,
45
,
31
,
35
,
49
,
19
,
47
,
37
,
38
,
39
,
11
,
8
,
57
,
22
,
21
,
44
,
2
,
5
,
36
,
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
,
50
,
51
,
52
,
53
,
54
,
55
,
56
,
]
]
...
@@ -322,7 +322,7 @@ export const RESPONSE_TIME_THRESHOLDS = {
...
@@ -322,7 +322,7 @@ export const RESPONSE_TIME_THRESHOLDS = {
export
const
RESPONSE_TIME_CONFIG
=
{
export
const
RESPONSE_TIME_CONFIG
=
{
EXCELLENT
:
{
variant
:
'success'
as
const
,
label
:
'Excellent'
},
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'
},
FAIR
:
{
variant
:
'warning'
as
const
,
label
:
'Fair'
},
POOR
:
{
variant
:
'danger'
as
const
,
label
:
'Poor'
},
POOR
:
{
variant
:
'danger'
as
const
,
label
:
'Poor'
},
UNKNOWN
:
{
variant
:
'neutral'
as
const
,
label
:
'Not tested'
},
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
...
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
formatCurrencyFromUSD
,
formatQuotaWithCurrency
}
from
'@/lib/currency'
import
{
formatCurrencyFromUSD
,
formatQuotaWithCurrency
}
from
'@/lib/currency'
import
{
formatTimestampToDate
}
from
'@/lib/format'
import
{
formatTimestampToDate
}
from
'@/lib/format'
import
{
import
{
CHANNEL_STATUS_CONFIG
,
CHANNEL_STATUS_CONFIG
,
CHANNEL_TYPES
,
CHANNEL_TYPES
,
...
@@ -170,7 +171,9 @@ export function formatChannelKey(
...
@@ -170,7 +171,9 @@ export function formatChannelKey(
key
:
string
,
key
:
string
,
isMultiKey
:
boolean
=
false
isMultiKey
:
boolean
=
false
):
string
{
):
string
{
if
(
!
key
)
return
''
if
(
!
key
)
{
return
''
}
if
(
isMultiKey
)
{
if
(
isMultiKey
)
{
const
keys
=
key
.
split
(
'\n'
).
filter
((
k
)
=>
k
.
trim
())
const
keys
=
key
.
split
(
'\n'
).
filter
((
k
)
=>
k
.
trim
())
...
@@ -190,8 +193,12 @@ export function formatChannelKey(
...
@@ -190,8 +193,12 @@ export function formatChannelKey(
* Format key preview for multi-key display
* Format key preview for multi-key display
*/
*/
export
function
formatKeyPreview
(
key
:
string
,
maxLength
:
number
=
10
):
string
{
export
function
formatKeyPreview
(
key
:
string
,
maxLength
:
number
=
10
):
string
{
if
(
!
key
)
return
''
if
(
!
key
)
{
if
(
key
.
length
<=
maxLength
)
return
key
return
''
}
if
(
key
.
length
<=
maxLength
)
{
return
key
}
return
`
${
key
.
slice
(
0
,
maxLength
)}
...`
return
`
${
key
.
slice
(
0
,
maxLength
)}
...`
}
}
...
@@ -199,7 +206,9 @@ export function formatKeyPreview(key: string, maxLength: number = 10): string {
...
@@ -199,7 +206,9 @@ export function formatKeyPreview(key: string, maxLength: number = 10): string {
* Count keys in multi-key string
* Count keys in multi-key string
*/
*/
export
function
countKeys
(
key
:
string
):
number
{
export
function
countKeys
(
key
:
string
):
number
{
if
(
!
key
)
return
0
if
(
!
key
)
{
return
0
}
return
key
.
split
(
'\n'
).
filter
((
k
)
=>
k
.
trim
()).
length
return
key
.
split
(
'\n'
).
filter
((
k
)
=>
k
.
trim
()).
length
}
}
...
@@ -211,7 +220,9 @@ export function countKeys(key: string): number {
...
@@ -211,7 +220,9 @@ export function countKeys(key: string): number {
* Parse comma-separated models list
* Parse comma-separated models list
*/
*/
export
function
parseModelsList
(
models
:
string
):
string
[]
{
export
function
parseModelsList
(
models
:
string
):
string
[]
{
if
(
!
models
)
return
[]
if
(
!
models
)
{
return
[]
}
return
models
return
models
.
split
(
','
)
.
split
(
','
)
.
map
((
m
)
=>
m
.
trim
())
.
map
((
m
)
=>
m
.
trim
())
...
@@ -223,14 +234,20 @@ export function parseModelsList(models: string): string[] {
...
@@ -223,14 +234,20 @@ export function parseModelsList(models: string): string[] {
* Sorts with 'default' group first, then locale-sorted alphabetically.
* Sorts with 'default' group first, then locale-sorted alphabetically.
*/
*/
export
function
parseGroupsList
(
groups
:
string
):
string
[]
{
export
function
parseGroupsList
(
groups
:
string
):
string
[]
{
if
(
!
groups
)
return
[]
if
(
!
groups
)
{
return
[]
}
const
list
=
groups
const
list
=
groups
.
split
(
','
)
.
split
(
','
)
.
map
((
g
)
=>
g
.
trim
())
.
map
((
g
)
=>
g
.
trim
())
.
filter
((
g
)
=>
g
.
length
>
0
)
.
filter
((
g
)
=>
g
.
length
>
0
)
return
list
.
sort
((
a
,
b
)
=>
{
return
list
.
sort
((
a
,
b
)
=>
{
if
(
a
===
'default'
)
return
-
1
if
(
a
===
'default'
)
{
if
(
b
===
'default'
)
return
1
return
-
1
}
if
(
b
===
'default'
)
{
return
1
}
return
a
.
localeCompare
(
b
)
return
a
.
localeCompare
(
b
)
})
})
}
}
...
@@ -259,7 +276,9 @@ export function formatGroupsString(groups: string[]): string {
...
@@ -259,7 +276,9 @@ export function formatGroupsString(groups: string[]): string {
export
function
parseChannelSettings
(
export
function
parseChannelSettings
(
settingStr
:
string
|
null
|
undefined
settingStr
:
string
|
null
|
undefined
):
ChannelSettings
{
):
ChannelSettings
{
if
(
!
settingStr
)
return
{}
if
(
!
settingStr
)
{
return
{}
}
try
{
try
{
return
JSON
.
parse
(
settingStr
)
as
ChannelSettings
return
JSON
.
parse
(
settingStr
)
as
ChannelSettings
}
catch
{
}
catch
{
...
@@ -273,7 +292,9 @@ export function parseChannelSettings(
...
@@ -273,7 +292,9 @@ export function parseChannelSettings(
export
function
parseChannelOtherSettings
(
export
function
parseChannelOtherSettings
(
settingsStr
:
string
|
null
|
undefined
settingsStr
:
string
|
null
|
undefined
):
ChannelOtherSettings
{
):
ChannelOtherSettings
{
if
(
!
settingsStr
||
settingsStr
===
'{}'
)
return
{}
if
(
!
settingsStr
||
settingsStr
===
'{}'
)
{
return
{}
}
try
{
try
{
return
JSON
.
parse
(
settingsStr
)
as
ChannelOtherSettings
return
JSON
.
parse
(
settingsStr
)
as
ChannelOtherSettings
}
catch
{
}
catch
{
...
@@ -285,7 +306,9 @@ export function parseChannelOtherSettings(
...
@@ -285,7 +306,9 @@ export function parseChannelOtherSettings(
* Validate JSON string
* Validate JSON string
*/
*/
export
function
validateChannelSettings
(
settings
:
string
):
boolean
{
export
function
validateChannelSettings
(
settings
:
string
):
boolean
{
if
(
!
settings
||
settings
.
trim
()
===
''
)
return
true
if
(
!
settings
||
settings
.
trim
()
===
''
)
{
return
true
}
try
{
try
{
JSON
.
parse
(
settings
)
JSON
.
parse
(
settings
)
return
true
return
true
...
@@ -302,7 +325,9 @@ export function validateChannelSettings(settings: string): boolean {
...
@@ -302,7 +325,9 @@ export function validateChannelSettings(settings: string): boolean {
* Format balance with currency symbol
* Format balance with currency symbol
*/
*/
export
function
formatBalance
(
balance
:
number
|
null
|
undefined
):
string
{
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
,
{
return
formatCurrencyFromUSD
(
balance
,
{
digitsLarge
:
2
,
digitsLarge
:
2
,
digitsSmall
:
4
,
digitsSmall
:
4
,
...
@@ -316,9 +341,15 @@ export function formatBalance(balance: number | null | undefined): string {
...
@@ -316,9 +341,15 @@ export function formatBalance(balance: number | null | undefined): string {
export
function
getBalanceVariant
(
export
function
getBalanceVariant
(
balance
:
number
balance
:
number
):
'success'
|
'warning'
|
'danger'
|
'neutral'
{
):
'success'
|
'warning'
|
'danger'
|
'neutral'
{
if
(
balance
===
0
)
return
'neutral'
if
(
balance
===
0
)
{
if
(
balance
<
1
)
return
'danger'
return
'neutral'
if
(
balance
<
10
)
return
'warning'
}
if
(
balance
<
1
)
{
return
'danger'
}
if
(
balance
<
10
)
{
return
'warning'
}
return
'success'
return
'success'
}
}
...
@@ -334,9 +365,12 @@ type TFunction = (key: string, options?: { value?: number | string }) => string
...
@@ -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").
* Pass `t` from useTranslation() for i18n (e.g. "Not tested", "{{value}}ms", "{{value}}s").
*/
*/
export
function
formatResponseTime
(
timeMs
:
number
,
t
?:
TFunction
):
string
{
export
function
formatResponseTime
(
timeMs
:
number
,
t
?:
TFunction
):
string
{
if
(
timeMs
===
0
)
return
t
?
t
(
'Not tested'
)
:
'Not tested'
if
(
timeMs
===
0
)
{
if
(
timeMs
<
1000
)
return
t
?
t
(
'Not tested'
)
:
'Not tested'
}
if
(
timeMs
<
1000
)
{
return
t
?
t
(
'{{value}}ms'
,
{
value
:
timeMs
})
:
`
${
timeMs
}
ms`
return
t
?
t
(
'{{value}}ms'
,
{
value
:
timeMs
})
:
`
${
timeMs
}
ms`
}
return
t
return
t
?
t
(
'{{value}}s'
,
{
value
:
(
timeMs
/
1000
).
toFixed
(
2
)
})
?
t
(
'{{value}}s'
,
{
value
:
(
timeMs
/
1000
).
toFixed
(
2
)
})
:
`
${(
timeMs
/
1000
).
toFixed
(
2
)}
s`
:
`
${(
timeMs
/
1000
).
toFixed
(
2
)}
s`
...
@@ -346,12 +380,21 @@ export function formatResponseTime(timeMs: number, t?: TFunction): string {
...
@@ -346,12 +380,21 @@ export function formatResponseTime(timeMs: number, t?: TFunction): string {
* Get response time performance rating
* Get response time performance rating
*/
*/
export
function
getResponseTimeConfig
(
timeMs
:
number
)
{
export
function
getResponseTimeConfig
(
timeMs
:
number
)
{
if
(
timeMs
===
0
)
return
RESPONSE_TIME_CONFIG
.
UNKNOWN
if
(
timeMs
===
0
)
{
if
(
timeMs
<=
RESPONSE_TIME_THRESHOLDS
.
EXCELLENT
)
return
RESPONSE_TIME_CONFIG
.
UNKNOWN
}
if
(
timeMs
<=
RESPONSE_TIME_THRESHOLDS
.
EXCELLENT
)
{
return
RESPONSE_TIME_CONFIG
.
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
.
GOOD
)
{
if
(
timeMs
<=
RESPONSE_TIME_THRESHOLDS
.
POOR
)
return
RESPONSE_TIME_CONFIG
.
POOR
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
return
RESPONSE_TIME_CONFIG
.
POOR
}
}
...
@@ -362,14 +405,16 @@ export function getResponseTimeConfig(timeMs: number) {
...
@@ -362,14 +405,16 @@ export function getResponseTimeConfig(timeMs: number) {
/**
/**
* Format a Unix timestamp (seconds) as a compact, locale-aware relative time.
* Format a Unix timestamp (seconds) as a compact, locale-aware relative time.
* Uses `Intl.RelativeTimeFormat` with the `narrow` style so the label stays
* 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".
* instead of the verbose "4 hours ago".
*/
*/
export
function
formatRelativeTime
(
export
function
formatRelativeTime
(
timestamp
:
number
,
timestamp
:
number
,
locale
?:
Intl
.
LocalesArgument
locale
?:
Intl
.
LocalesArgument
):
string
{
):
string
{
if
(
!
timestamp
||
timestamp
===
0
)
return
'Never'
if
(
!
timestamp
||
timestamp
===
0
)
{
return
'Never'
}
try
{
try
{
const
diffSec
=
timestamp
-
Date
.
now
()
/
1000
const
diffSec
=
timestamp
-
Date
.
now
()
/
1000
...
@@ -385,12 +430,35 @@ export function formatRelativeTime(
...
@@ -385,12 +430,35 @@ export function formatRelativeTime(
const
MONTH
=
30
*
DAY
const
MONTH
=
30
*
DAY
const
YEAR
=
365
*
DAY
const
YEAR
=
365
*
DAY
if
(
absSec
<
MINUTE
)
return
rtf
.
format
(
Math
.
round
(
diffSec
),
'second'
)
let
value
:
number
if
(
absSec
<
HOUR
)
return
rtf
.
format
(
Math
.
round
(
diffSec
/
MINUTE
),
'minute'
)
let
unit
:
Intl
.
RelativeTimeFormatUnit
if
(
absSec
<
DAY
)
return
rtf
.
format
(
Math
.
round
(
diffSec
/
HOUR
),
'hour'
)
if
(
absSec
<
MINUTE
)
{
if
(
absSec
<
MONTH
)
return
rtf
.
format
(
Math
.
round
(
diffSec
/
DAY
),
'day'
)
value
=
Math
.
round
(
diffSec
)
if
(
absSec
<
YEAR
)
return
rtf
.
format
(
Math
.
round
(
diffSec
/
MONTH
),
'month'
)
unit
=
'second'
return
rtf
.
format
(
Math
.
round
(
diffSec
/
YEAR
),
'year'
)
}
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
{
}
catch
{
return
'Unknown'
return
'Unknown'
}
}
...
@@ -400,7 +468,9 @@ export function formatRelativeTime(
...
@@ -400,7 +468,9 @@ export function formatRelativeTime(
* Format Unix timestamp to date string
* Format Unix timestamp to date string
*/
*/
export
function
formatTimestamp
(
timestamp
:
number
):
string
{
export
function
formatTimestamp
(
timestamp
:
number
):
string
{
if
(
!
timestamp
||
timestamp
===
0
)
return
'N/A'
if
(
!
timestamp
||
timestamp
===
0
)
{
return
'N/A'
}
try
{
try
{
return
formatTimestampToDate
(
timestamp
)
return
formatTimestampToDate
(
timestamp
)
...
@@ -432,7 +502,9 @@ export function formatQuota(quota: number): string {
...
@@ -432,7 +502,9 @@ export function formatQuota(quota: number): string {
export
function
getPriorityDisplay
(
export
function
getPriorityDisplay
(
priority
:
number
|
null
|
undefined
priority
:
number
|
null
|
undefined
):
string
{
):
string
{
if
(
priority
===
null
||
priority
===
undefined
)
return
'0'
if
(
priority
===
null
||
priority
===
undefined
)
{
return
'0'
}
return
String
(
priority
)
return
String
(
priority
)
}
}
...
@@ -440,7 +512,9 @@ export function getPriorityDisplay(
...
@@ -440,7 +512,9 @@ export function getPriorityDisplay(
* Get weight display value
* Get weight display value
*/
*/
export
function
getWeightDisplay
(
weight
:
number
|
null
|
undefined
):
string
{
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
)
return
String
(
weight
)
}
}
...
@@ -481,10 +555,14 @@ export function validateGroups(groups: string): boolean {
...
@@ -481,10 +555,14 @@ export function validateGroups(groups: string): boolean {
*/
*/
export
function
channelNeedsAttention
(
channel
:
Channel
):
boolean
{
export
function
channelNeedsAttention
(
channel
:
Channel
):
boolean
{
// Auto-disabled
// Auto-disabled
if
(
channel
.
status
===
3
)
return
true
if
(
channel
.
status
===
3
)
{
return
true
}
// Low balance (less than $1)
// 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
// Multi-key channel with all keys disabled
if
(
if
(
...
@@ -503,8 +581,12 @@ export function channelNeedsAttention(channel: Channel): boolean {
...
@@ -503,8 +581,12 @@ export function channelNeedsAttention(channel: Channel): boolean {
* Get attention reason for channel
* Get attention reason for channel
*/
*/
export
function
getAttentionReason
(
channel
:
Channel
):
string
|
null
{
export
function
getAttentionReason
(
channel
:
Channel
):
string
|
null
{
if
(
channel
.
status
===
3
)
return
'Auto-disabled'
if
(
channel
.
status
===
3
)
{
if
(
channel
.
balance
>
0
&&
channel
.
balance
<
1
)
return
'Low balance'
return
'Auto-disabled'
}
if
(
channel
.
balance
>
0
&&
channel
.
balance
<
1
)
{
return
'Low balance'
}
if
(
if
(
channel
.
channel_info
?.
is_multi_key
&&
channel
.
channel_info
?.
is_multi_key
&&
channel
.
channel_info
.
multi_key_status_list
&&
channel
.
channel_info
.
multi_key_status_list
&&
...
@@ -553,7 +635,7 @@ export function aggregateChannelsByTag(
...
@@ -553,7 +635,7 @@ export function aggregateChannelsByTag(
...
channel
,
...
channel
,
key
:
tag
,
key
:
tag
,
id
:
tag
as
unknown
as
number
,
id
:
tag
as
unknown
as
number
,
tag
:
tag
,
tag
,
name
:
tag
,
name
:
tag
,
type
:
0
,
type
:
0
,
status
:
undefined
as
unknown
as
number
,
status
:
undefined
as
unknown
as
number
,
...
@@ -573,7 +655,10 @@ export function aggregateChannelsByTag(
...
@@ -573,7 +655,10 @@ export function aggregateChannelsByTag(
result
.
push
(
tagRow
)
result
.
push
(
tagRow
)
}
}
const
tagRow
=
tagMap
.
get
(
tag
)
!
const
tagRow
=
tagMap
.
get
(
tag
)
if
(
!
tagRow
)
{
continue
}
// Add to children
// Add to children
tagRow
.
children
.
push
(
channel
)
tagRow
.
children
.
push
(
channel
)
...
@@ -609,7 +694,7 @@ export function aggregateChannelsByTag(
...
@@ -609,7 +694,7 @@ export function aggregateChannelsByTag(
const
newGroups
=
channel
.
group
.
split
(
','
).
filter
(
Boolean
)
const
newGroups
=
channel
.
group
.
split
(
','
).
filter
(
Boolean
)
newGroups
.
forEach
((
g
)
=>
{
newGroups
.
forEach
((
g
)
=>
{
if
(
!
existingGroups
.
has
(
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
Copyright (C) 2023-2026 QuantumNous
...
@@ -17,10 +19,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,10 +19,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
useMemo
}
from
'react'
import
{
useMemo
}
from
'react'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Copy
,
ExternalLink
,
Loader2
,
RefreshCcw
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
toast
}
from
'sonner'
import
{
Dialog
}
from
'@/components/dialog'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
import
{
Collapsible
,
Collapsible
,
...
@@ -28,7 +30,7 @@ import {
...
@@ -28,7 +30,7 @@ import {
CollapsibleTrigger
,
CollapsibleTrigger
,
}
from
'@/components/ui/collapsible'
}
from
'@/components/ui/collapsible'
import
{
Separator
}
from
'@/components/ui/separator'
import
{
Separator
}
from
'@/components/ui/separator'
import
{
Dialog
}
from
'@/components/dialog'
import
{
getDeployment
,
listDeploymentContainers
}
from
'../../api'
import
{
getDeployment
,
listDeploymentContainers
}
from
'../../api'
export
function
ViewDetailsDialog
({
export
function
ViewDetailsDialog
({
...
@@ -73,10 +75,14 @@ export function ViewDetailsDialog({
...
@@ -73,10 +75,14 @@ export function ViewDetailsDialog({
const
locations
=
useMemo
(()
=>
{
const
locations
=
useMemo
(()
=>
{
const
items
=
details
?.
locations
const
items
=
details
?.
locations
if
(
!
Array
.
isArray
(
items
))
return
[]
if
(
!
Array
.
isArray
(
items
))
{
return
[]
}
return
items
return
items
.
map
((
x
)
=>
{
.
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
name
=
(
x
as
Record
<
string
,
unknown
>
)?.
name
const
iso2
=
(
x
as
Record
<
string
,
unknown
>
)?.
iso2
const
iso2
=
(
x
as
Record
<
string
,
unknown
>
)?.
iso2
const
id
=
(
x
as
Record
<
string
,
unknown
>
)?.
id
const
id
=
(
x
as
Record
<
string
,
unknown
>
)?.
id
...
@@ -86,7 +92,9 @@ export function ViewDetailsDialog({
...
@@ -86,7 +92,9 @@ export function ViewDetailsDialog({
}, [details])
}, [details])
const handleCopyId = async () => {
const handleCopyId = async () => {
if (deploymentId === null || deploymentId === undefined) return
if (deploymentId === null || deploymentId === undefined) {
return
}
try {
try {
await navigator.clipboard.writeText(String(deploymentId))
await navigator.clipboard.writeText(String(deploymentId))
toast.success(t('Copied'))
toast.success(t('Copied'))
...
@@ -108,6 +116,9 @@ export function ViewDetailsDialog({
...
@@ -108,6 +116,9 @@ export function ViewDetailsDialog({
return ''
return ''
}
}
}, [details])
}, [details])
const isDetailsLoading = isLoadingDetails || isLoadingContainers
const showDetailsError = !isDetailsLoading && !detailsRes?.success
const showDetailsContent = !isDetailsLoading && detailsRes?.success
return (
return (
<Dialog
<Dialog
...
@@ -118,15 +129,13 @@ export function ViewDetailsDialog({
...
@@ -118,15 +129,13 @@ export function ViewDetailsDialog({
contentHeight='auto'
contentHeight='auto'
bodyClassName='space-y-4'
bodyClassName='space-y-4'
footer={
footer={
<>
<Button
<Button
variant='outline'
variant='outline'
onClick={() => onOpenChange(false)}
onClick={() => onOpenChange(false)}
className='w-full sm:w-auto'
className='w-full sm:w-auto'
>
>
{t('Close')}
{t('Close')}
</Button>
</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'>
<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({
...
@@ -158,15 +167,17 @@ export function ViewDetailsDialog({
<Separator />
<Separator />
{is
LoadingDetails || isLoadingContainers
? (
{is
DetailsLoading
? (
<div className='flex items-center justify-center py-10'>
<div className='flex items-center justify-center py-10'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
</div>
</div>
) : !detailsRes?.success ? (
) : null}
{showDetailsError ? (
<div className='text-muted-foreground py-10 text-center text-sm'>
<div className='text-muted-foreground py-10 text-center text-sm'>
{detailsRes?.message || t('Failed to fetch deployment details')}
{detailsRes?.message || t('Failed to fetch deployment details')}
</div>
</div>
) : (
) : null}
{showDetailsContent ? (
<>
<>
<div className='grid gap-3 sm:grid-cols-2'>
<div className='grid gap-3 sm:grid-cols-2'>
<div className='rounded-lg border p-3'>
<div className='rounded-lg border p-3'>
...
@@ -225,7 +236,9 @@ export function ViewDetailsDialog({
...
@@ -225,7 +236,9 @@ export function ViewDetailsDialog({
<div className='space-y-2'>
<div className='space-y-2'>
{containers.map((c) => {
{containers.map((c) => {
const id = c?.container_id
const id = c?.container_id
if (typeof id !== 'string' || !id) return null
if (typeof id !== 'string' || !id) {
return null
}
const status =
const status =
typeof c?.status === 'string' ? c.status : undefined
typeof c?.status === 'string' ? c.status : undefined
const url =
const url =
...
@@ -263,13 +276,13 @@ export function ViewDetailsDialog({
...
@@ -263,13 +276,13 @@ export function ViewDetailsDialog({
{t('Raw JSON')}
{t('Raw JSON')}
</CollapsibleTrigger>
</CollapsibleTrigger>
<CollapsibleContent>
<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 || '-'}
{payloadJson || '-'}
</pre>
</pre>
</CollapsibleContent>
</CollapsibleContent>
</Collapsible>
</Collapsible>
</>
</>
)}
)
: null
}
</div>
</div>
</Dialog>
</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
Copyright (C) 2023-2026 QuantumNous
...
@@ -16,10 +18,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,10 +18,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
import
{
type
ReactNode
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Download
,
Loader2
,
RefreshCcw
,
Terminal
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
Dialog
}
from
'@/components/dialog'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
import
{
Select
,
Select
,
...
@@ -30,7 +32,7 @@ import {
...
@@ -30,7 +32,7 @@ import {
SelectValue
,
SelectValue
,
}
from
'@/components/ui/select'
}
from
'@/components/ui/select'
import
{
Switch
}
from
'@/components/ui/switch'
import
{
Switch
}
from
'@/components/ui/switch'
import
{
Dialog
}
from
'@/components/dialog'
import
{
getDeploymentLogs
,
listDeploymentContainers
}
from
'../../api'
import
{
getDeploymentLogs
,
listDeploymentContainers
}
from
'../../api'
interface
ViewLogsDialogProps
{
interface
ViewLogsDialogProps
{
...
@@ -114,9 +116,17 @@ export function ViewLogsDialog({
...
@@ -114,9 +116,17 @@ export function ViewLogsDialog({
},
[
logsData
?.
data
])
},
[
logsData
?.
data
])
const
logLines
=
useMemo
(()
=>
{
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'
)
:
[]
return
normalized
?
normalized
.
split
(
'\n'
)
:
[]
},
[
logsText
])
},
[
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
// Auto-scroll to bottom
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -135,6 +145,44 @@ export function ViewLogsDialog({
...
@@ -135,6 +145,44 @@ export function ViewLogsDialog({
a
.
click
()
a
.
click
()
URL
.
revokeObjectURL
(
url
)
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
(
return
(
<
Dialog
<
Dialog
...
@@ -191,47 +239,39 @@ export function ViewLogsDialog({
...
@@ -191,47 +239,39 @@ export function ViewLogsDialog({
<
div
className=
'space-y-1'
>
<
div
className=
'space-y-1'
>
<
div
className=
'text-muted-foreground text-xs'
>
{
t
(
'Container'
)
}
</
div
>
<
div
className=
'text-muted-foreground text-xs'
>
{
t
(
'Container'
)
}
</
div
>
<
Select
<
Select
items=
{
[
items=
{
containers
.
flatMap
((
c
)
=>
{
...
containers
.
flatMap
((
c
)
=>
{
const
id
=
c
?.
container_id
const
id
=
c
?.
container_id
if
(
typeof
id
!==
'string'
||
!
id
)
return
[]
if
(
typeof
id
!==
'string'
||
!
id
)
return
[]
const
status
=
const
status
=
typeof
c
?.
status
===
'string'
&&
c
.
status
typeof
c
?.
status
===
'string'
&&
c
.
status
?
` (${c.status})`
?
` (${c.status})`
:
''
:
''
return
[
return
[
{
{
value
:
id
,
value
:
id
,
label
:
(
label
:
(
<>
<>
{
id
}
{
id
}
{
status
}
{
status
}
</>
</>
),
),
},
},
]
]
})
}
}),
]
}
value=
{
containerId
}
value=
{
containerId
}
onValueChange=
{
(
v
)
=>
v
!==
null
&&
setContainerId
(
v
)
}
onValueChange=
{
(
v
)
=>
v
!==
null
&&
setContainerId
(
v
)
}
disabled=
{
isLoadingContainers
||
containers
.
length
===
0
}
disabled=
{
isLoadingContainers
||
containers
.
length
===
0
}
>
>
<
SelectTrigger
>
<
SelectTrigger
>
<
SelectValue
<
SelectValue
placeholder=
{
containerPlaceholder
}
/>
placeholder=
{
isLoadingContainers
?
t
(
'Loading...'
)
:
containers
.
length
===
0
?
t
(
'No containers'
)
:
t
(
'Select'
)
}
/>
</
SelectTrigger
>
</
SelectTrigger
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectContent
alignItemWithTrigger=
{
false
}
>
<
SelectGroup
>
<
SelectGroup
>
{
containers
.
map
((
c
)
=>
{
{
containers
.
map
((
c
)
=>
{
const
id
=
c
?.
container_id
const
id
=
c
?.
container_id
if
(
typeof
id
!==
'string'
||
!
id
)
return
null
if
(
typeof
id
!==
'string'
||
!
id
)
{
return
null
}
const
status
=
const
status
=
typeof
c
?.
status
===
'string'
&&
c
.
status
typeof
c
?.
status
===
'string'
&&
c
.
status
?
` (${c.status})`
?
` (${c.status})`
...
@@ -279,7 +319,7 @@ export function ViewLogsDialog({
...
@@ -279,7 +319,7 @@ export function ViewLogsDialog({
</
div
>
</
div
>
<
div
<
div
ref=
{
scrollRef
}
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
)
=>
{
onScroll=
{
(
e
)
=>
{
const
target
=
e
.
target
as
HTMLDivElement
const
target
=
e
.
target
as
HTMLDivElement
const
isAtBottom
=
const
isAtBottom
=
...
@@ -287,29 +327,7 @@ export function ViewLogsDialog({
...
@@ -287,29 +327,7 @@ export function ViewLogsDialog({
setAutoScroll
(
isAtBottom
)
setAutoScroll
(
isAtBottom
)
}
}
}
}
>
>
{
isLoadingContainers
||
isLoadingLogs
?
(
{
logsContent
}
<
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
>
)
}
</
div
>
</
div
>
</
Dialog
>
</
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
...
@@ -18,14 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
Route
}
from
'lucide-react'
import
{
Route
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
cn
}
from
'@/lib/utils
'
import
{
StatusBadge
}
from
'@/components/status-badge
'
import
{
import
{
Popover
,
Popover
,
PopoverContent
,
PopoverContent
,
PopoverTrigger
,
PopoverTrigger
,
}
from
'@/components/ui/popover'
}
from
'@/components/ui/popover'
import
{
StatusBadge
}
from
'@/components/status-badge'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
cn
}
from
'@/lib/utils'
interface
ModelBadgeProps
{
interface
ModelBadgeProps
{
modelName
:
string
modelName
:
string
...
@@ -109,11 +110,11 @@ function ModelBadgeContent(props: ModelBadgeProps) {
...
@@ -109,11 +110,11 @@ function ModelBadgeContent(props: ModelBadgeProps) {
<
span
className=
'flex max-w-none items-center gap-1.5'
>
<
span
className=
'flex max-w-none items-center gap-1.5'
>
{
provider
&&
(
{
provider
&&
(
<
span
<
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
}
title=
{
provider
.
label
}
aria
-
label=
{
provider
.
label
}
aria
-
label=
{
provider
.
label
}
>
>
{
getLobeIcon
(
provider
.
icon
,
1
4
)
}
{
getLobeIcon
(
provider
.
icon
,
1
8
)
}
</
span
>
</
span
>
)
}
)
}
<
span
className=
'whitespace-nowrap'
>
{
props
.
modelName
}
</
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(
...
@@ -25,8 +25,8 @@ export function showSubmittedData(
toast
.
message
(
title
,
{
toast
.
message
(
title
,
{
description
:
(
description
:
(
// w-[340px]
// w-[340px]
<
pre
className=
'
mt-2 w-full overflow-x-auto rounded-md bg-slate-950
p-4'
>
<
pre
className=
'
bg-muted text-foreground mt-2 w-full overflow-x-auto rounded-md
p-4'
>
<
code
className=
'text-white'
>
{
JSON
.
stringify
(
data
,
null
,
2
)
}
</
code
>
<
code
>
{
JSON
.
stringify
(
data
,
null
,
2
)
}
</
code
>
</
pre
>
</
pre
>
),
),
})
})
...
...
web/default/src/styles/theme-presets.css
View file @
50b8f2a2
...
@@ -353,32 +353,32 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -353,32 +353,32 @@ For commercial licensing, please contact support@quantumnous.com
--spacing
:
0.3rem
;
--spacing
:
0.3rem
;
}
}
.dark
[
data-theme-preset
=
'simple-large'
]
{
.dark
[
data-theme-preset
=
'simple-large'
]
{
--background
:
oklch
(
0.
12
0
0
);
--background
:
oklch
(
0.
215
0
0
);
--foreground
:
oklch
(
0.9
8
0
0
);
--foreground
:
oklch
(
0.9
7
0
0
);
--card
:
oklch
(
0.
18
0
0
);
--card
:
oklch
(
0.
27
0
0
);
--card-foreground
:
oklch
(
0.9
8
0
0
);
--card-foreground
:
oklch
(
0.9
7
0
0
);
--popover
:
oklch
(
0.2
0
0
);
--popover
:
oklch
(
0.2
9
0
0
);
--popover-foreground
:
oklch
(
0.9
8
0
0
);
--popover-foreground
:
oklch
(
0.9
7
0
0
);
--primary
:
oklch
(
0.94
0
0
);
--primary
:
oklch
(
0.94
0
0
);
--primary-foreground
:
oklch
(
0.1
2
0
0
);
--primary-foreground
:
oklch
(
0.1
45
0
0
);
--secondary
:
oklch
(
0.
24
0
0
);
--secondary
:
oklch
(
0.
315
0
0
);
--secondary-foreground
:
oklch
(
0.9
8
0
0
);
--secondary-foreground
:
oklch
(
0.9
7
0
0
);
--muted
:
oklch
(
0.
24
0
0
);
--muted
:
oklch
(
0.
315
0
0
);
--muted-foreground
:
oklch
(
0.82
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
);
--accent-foreground
:
oklch
(
0.98
0
0
);
--destructive
:
oklch
(
0.68
0.19
25
);
--destructive
:
oklch
(
0.68
0.19
25
);
--destructive-foreground
:
oklch
(
0.98
0
0
);
--destructive-foreground
:
oklch
(
0.98
0
0
);
--success
:
oklch
(
0.72
0.13
145
);
--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
:
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
:
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
:
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%
);
--border
:
oklch
(
1
0
0
/
18%
);
--input
:
oklch
(
1
0
0
/
24%
);
--input
:
oklch
(
1
0
0
/
24%
);
...
@@ -390,17 +390,17 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -390,17 +390,17 @@ For commercial licensing, please contact support@quantumnous.com
--chart-4
:
oklch
(
0.82
0.13
75
);
--chart-4
:
oklch
(
0.82
0.13
75
);
--chart-5
:
oklch
(
0.68
0.19
25
);
--chart-5
:
oklch
(
0.68
0.19
25
);
--sidebar
:
oklch
(
0.
16
0
0
);
--sidebar
:
oklch
(
0.
205
0
0
);
--sidebar-foreground
:
oklch
(
0.9
8
0
0
);
--sidebar-foreground
:
oklch
(
0.9
7
0
0
);
--sidebar-primary
:
oklch
(
0.94
0
0
);
--sidebar-primary
:
oklch
(
0.94
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
2
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
45
0
0
);
--sidebar-accent
:
oklch
(
0.
28
0
0
);
--sidebar-accent
:
oklch
(
0.
34
0
0
);
--sidebar-accent-foreground
:
oklch
(
0.98
0
0
);
--sidebar-accent-foreground
:
oklch
(
0.98
0
0
);
--sidebar-border
:
oklch
(
1
0
0
/
18%
);
--sidebar-border
:
oklch
(
1
0
0
/
18%
);
--sidebar-ring
:
oklch
(
0.94
0
0
);
--sidebar-ring
:
oklch
(
0.94
0
0
);
--skeleton-base
:
oklch
(
0.
24
0
0
);
--skeleton-base
:
oklch
(
0.
315
0
0
);
--skeleton-highlight
:
oklch
(
0.
34
0
0
);
--skeleton-highlight
:
oklch
(
0.
415
0
0
);
}
}
/* ── Semantic surface bridge ──────────────────────────────────────────── */
/* ── Semantic surface bridge ──────────────────────────────────────────── */
...
@@ -552,21 +552,21 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -552,21 +552,21 @@ For commercial licensing, please contact support@quantumnous.com
.dark
[
data-theme-preset
=
'anthropic'
]
{
.dark
[
data-theme-preset
=
'anthropic'
]
{
/* Warm near-black product surfaces, not pure black — keeps the editorial
/* Warm near-black product surfaces, not pure black — keeps the editorial
* personality even when inverted. Coral lifts slightly for legibility. */
* personality even when inverted. Coral lifts slightly for legibility. */
--background
:
oklch
(
0.2
05
0.004
60
);
/* ≈ #181715 */
--background
:
oklch
(
0.2
15
0.004
60
);
--foreground
:
oklch
(
0.965
0.005
92
);
/* ≈ #faf9f5 */
--foreground
:
oklch
(
0.965
0.005
92
);
--card
:
oklch
(
0.2
4
5
0.004
60
);
--card
:
oklch
(
0.2
5
5
0.004
60
);
--card-foreground
:
oklch
(
0.965
0.005
92
);
--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
);
--popover-foreground
:
oklch
(
0.965
0.005
92
);
--primary
:
oklch
(
0.72
0.135
40
);
--primary
:
oklch
(
0.72
0.135
40
);
--primary-foreground
:
oklch
(
0.18
0.005
60
);
--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
);
--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
);
--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
);
--accent-foreground
:
oklch
(
0.985
0.005
92
);
--destructive
:
oklch
(
0.7
0.19
22
);
--destructive
:
oklch
(
0.7
0.19
22
);
...
@@ -590,17 +590,17 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -590,17 +590,17 @@ For commercial licensing, please contact support@quantumnous.com
--chart-4
:
oklch
(
0.78
0.13
0
);
--chart-4
:
oklch
(
0.78
0.13
0
);
--chart-5
:
oklch
(
0.83
0.027
175
);
--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-foreground
:
oklch
(
0.95
0.005
92
);
--sidebar-primary
:
oklch
(
0.72
0.135
40
);
--sidebar-primary
:
oklch
(
0.72
0.135
40
);
--sidebar-primary-foreground
:
oklch
(
0.18
0.005
60
);
--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-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
);
--sidebar-ring
:
oklch
(
0.72
0.135
40
);
--skeleton-base
:
oklch
(
0.
29
5
0.004
60
);
--skeleton-base
:
oklch
(
0.
30
5
0.004
60
);
--skeleton-highlight
:
oklch
(
0.4
0.004
60
);
--skeleton-highlight
:
oklch
(
0.4
1
0.004
60
);
}
}
/* ── Font axis ────────────────────────────────────────────────────────────
/* ── Font axis ────────────────────────────────────────────────────────────
...
...
web/default/src/styles/theme.css
View file @
50b8f2a2
...
@@ -142,22 +142,48 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -142,22 +142,48 @@ For commercial licensing, please contact support@quantumnous.com
--sidebar-ring
:
oklch
(
0.708
0
0
);
--sidebar-ring
:
oklch
(
0.708
0
0
);
--skeleton-base
:
oklch
(
0.97
0
0
);
--skeleton-base
:
oklch
(
0.97
0
0
);
--skeleton-highlight
:
oklch
(
0.985
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
{
.dark
{
/* OpenAI-like dark mode with a
softer
charcoal canvas. */
/* OpenAI-like dark mode with a
crisp
charcoal canvas. */
--background
:
oklch
(
0.2
2
5
0
0
);
--background
:
oklch
(
0.2
3
5
0
0
);
--foreground
:
oklch
(
0.965
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
);
--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
);
--popover-foreground
:
oklch
(
0.965
0
0
);
--primary
:
oklch
(
0.965
0
0
);
--primary
:
oklch
(
0.965
0
0
);
--primary-foreground
:
oklch
(
0.1
4
5
0
0
);
--primary-foreground
:
oklch
(
0.1
5
5
0
0
);
--secondary
:
oklch
(
0.3
2
5
0
0
);
--secondary
:
oklch
(
0.3
3
5
0
0
);
--secondary-foreground
:
oklch
(
0.965
0
0
);
--secondary-foreground
:
oklch
(
0.965
0
0
);
--muted
:
oklch
(
0.
29
5
0
0
);
--muted
:
oklch
(
0.
30
5
0
0
);
--muted-foreground
:
oklch
(
0.7
6
0
0
);
--muted-foreground
:
oklch
(
0.7
8
0
0
);
--accent
:
oklch
(
0.365
0
0
);
--accent
:
oklch
(
0.365
0
0
);
--accent-foreground
:
oklch
(
0.985
0
0
);
--accent-foreground
:
oklch
(
0.985
0
0
);
--destructive
:
oklch
(
0.704
0.191
22.216
);
--destructive
:
oklch
(
0.704
0.191
22.216
);
...
@@ -169,23 +195,49 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -169,23 +195,49 @@ For commercial licensing, please contact support@quantumnous.com
--info
:
oklch
(
0.68
0.17
237.323
);
--info
:
oklch
(
0.68
0.17
237.323
);
--info-foreground
:
oklch
(
0.145
0
0
);
--info-foreground
:
oklch
(
0.145
0
0
);
--neutral
:
oklch
(
0.76
0
0
);
--neutral
:
oklch
(
0.76
0
0
);
--neutral-foreground
:
oklch
(
0.1
4
5
0
0
);
--neutral-foreground
:
oklch
(
0.1
5
5
0
0
);
--border
:
oklch
(
1
0
0
/
9
%
);
--border
:
oklch
(
1
0
0
/
10
%
);
--input
:
oklch
(
1
0
0
/
1
6
%
);
--input
:
oklch
(
1
0
0
/
1
7
%
);
--ring
:
oklch
(
0.68
0
0
);
--ring
:
oklch
(
0.68
0
0
);
--chart-1
:
oklch
(
0.488
0.243
264.376
);
--chart-1
:
oklch
(
0.488
0.243
264.376
);
--chart-2
:
oklch
(
0.696
0.17
162.48
);
--chart-2
:
oklch
(
0.696
0.17
162.48
);
--chart-3
:
oklch
(
0.769
0.188
70.08
);
--chart-3
:
oklch
(
0.769
0.188
70.08
);
--chart-4
:
oklch
(
0.627
0.265
303.9
);
--chart-4
:
oklch
(
0.627
0.265
303.9
);
--chart-5
:
oklch
(
0.645
0.246
16.439
);
--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-foreground
:
oklch
(
0.95
0
0
);
--sidebar-primary
:
oklch
(
0.965
0
0
);
--sidebar-primary
:
oklch
(
0.965
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
4
5
0
0
);
--sidebar-primary-foreground
:
oklch
(
0.1
5
5
0
0
);
--sidebar-accent
:
oklch
(
0.35
0
0
);
--sidebar-accent
:
oklch
(
0.35
5
0
0
);
--sidebar-accent-foreground
:
oklch
(
0.985
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
);
--sidebar-ring
:
oklch
(
0.68
0
0
);
--skeleton-base
:
oklch
(
0.325
0
0
);
--skeleton-base
:
oklch
(
0.335
0
0
);
--skeleton-highlight
:
oklch
(
0.43
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