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
22ae14f0
authored
Apr 29, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): enhance ChannelsTable and CommonLogs components with improved UI elements
parent
f9825448
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
345 additions
and
186 deletions
+345
-186
web/default/src/components/data-table/faceted-filter.tsx
+17
-5
web/default/src/components/data-table/toolbar.tsx
+2
-0
web/default/src/features/channels/components/channels-table.tsx
+54
-10
web/default/src/features/profile/components/profile-header.tsx
+36
-25
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
+38
-83
web/default/src/features/usage-logs/components/common-logs-stats.tsx
+31
-28
web/default/src/features/usage-logs/components/model-badge.tsx
+144
-0
web/default/src/features/wallet/components/wallet-stats-card.tsx
+23
-35
No files found.
web/default/src/components/data-table/faceted-filter.tsx
View file @
22ae14f0
...
@@ -28,6 +28,8 @@ type DataTableFacetedFilterProps<TData, TValue> = {
...
@@ -28,6 +28,8 @@ type DataTableFacetedFilterProps<TData, TValue> = {
label
:
string
label
:
string
value
:
string
value
:
string
icon
?:
React
.
ComponentType
<
{
className
?:
string
}
>
icon
?:
React
.
ComponentType
<
{
className
?:
string
}
>
iconNode
?:
React
.
ReactNode
count
?:
number
}[]
}[]
/** Enable single select mode (only one option can be selected at a time) */
/** Enable single select mode (only one option can be selected at a time) */
singleSelect
?:
boolean
singleSelect
?:
boolean
...
@@ -130,15 +132,25 @@ export function DataTableFacetedFilter<TData, TValue>({
...
@@ -130,15 +132,25 @@ export function DataTableFacetedFilter<TData, TValue>({
>
>
<
CheckIcon
className=
{
cn
(
'text-background h-4 w-4'
)
}
/>
<
CheckIcon
className=
{
cn
(
'text-background h-4 w-4'
)
}
/>
</
div
>
</
div
>
{
option
.
icon
&&
(
{
option
.
iconNode
?
(
<
span
className=
'text-muted-foreground flex size-4 items-center justify-center'
>
{
option
.
iconNode
}
</
span
>
)
:
option
.
icon
?
(
<
option
.
icon
className=
'text-muted-foreground size-4'
/>
<
option
.
icon
className=
'text-muted-foreground size-4'
/>
)
}
)
:
null
}
<
span
>
{
t
(
option
.
label
)
}
</
span
>
<
span
className=
'min-w-0 flex-1 truncate'
>
{
facets
?.
get
(
option
.
value
)
&&
(
{
t
(
option
.
label
)
}
</
span
>
{
typeof
option
.
count
===
'number'
?
(
<
span
className=
'text-muted-foreground ms-auto flex h-4 min-w-4 items-center justify-center font-mono text-xs'
>
{
option
.
count
}
</
span
>
)
:
facets
?.
get
(
option
.
value
)
?
(
<
span
className=
'ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs'
>
<
span
className=
'ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs'
>
{
facets
.
get
(
option
.
value
)
}
{
facets
.
get
(
option
.
value
)
}
</
span
>
</
span
>
)
}
)
:
null
}
</
CommandItem
>
</
CommandItem
>
)
)
})
}
})
}
...
...
web/default/src/components/data-table/toolbar.tsx
View file @
22ae14f0
...
@@ -20,6 +20,8 @@ type DataTableToolbarProps<TData> = {
...
@@ -20,6 +20,8 @@ type DataTableToolbarProps<TData> = {
label
:
string
label
:
string
value
:
string
value
:
string
icon
?:
React
.
ComponentType
<
{
className
?:
string
}
>
icon
?:
React
.
ComponentType
<
{
className
?:
string
}
>
iconNode
?:
React
.
ReactNode
count
?:
number
}[]
}[]
singleSelect
?:
boolean
singleSelect
?:
boolean
}[]
}[]
...
...
web/default/src/features/channels/components/channels-table.tsx
View file @
22ae14f0
...
@@ -13,6 +13,7 @@ import {
...
@@ -13,6 +13,7 @@ import {
}
from
'@tanstack/react-table'
}
from
'@tanstack/react-table'
import
{
useDebounce
,
useMediaQuery
}
from
'@/hooks'
import
{
useDebounce
,
useMediaQuery
}
from
'@/hooks'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
{
useTableUrlState
}
from
'@/hooks/use-table-url-state'
import
{
useTableUrlState
}
from
'@/hooks/use-table-url-state'
import
{
Input
}
from
'@/components/ui/input'
import
{
Input
}
from
'@/components/ui/input'
...
@@ -37,12 +38,13 @@ import {
...
@@ -37,12 +38,13 @@ import {
DEFAULT_PAGE_SIZE
,
DEFAULT_PAGE_SIZE
,
CHANNEL_STATUS
,
CHANNEL_STATUS
,
CHANNEL_STATUS_OPTIONS
,
CHANNEL_STATUS_OPTIONS
,
CHANNEL_TYPE_OPTIONS
,
}
from
'../constants'
}
from
'../constants'
import
{
import
{
channelsQueryKeys
,
channelsQueryKeys
,
aggregateChannelsByTag
,
aggregateChannelsByTag
,
isTagAggregateRow
,
isTagAggregateRow
,
getChannelTypeIcon
,
getChannelTypeLabel
,
}
from
'../lib'
}
from
'../lib'
import
type
{
Channel
}
from
'../types'
import
type
{
Channel
}
from
'../types'
import
{
useChannelsColumns
}
from
'./channels-columns'
import
{
useChannelsColumns
}
from
'./channels-columns'
...
@@ -52,7 +54,9 @@ import { DataTableBulkActions } from './data-table-bulk-actions'
...
@@ -52,7 +54,9 @@ import { DataTableBulkActions } from './data-table-bulk-actions'
const
route
=
getRouteApi
(
'/_authenticated/channels/'
)
const
route
=
getRouteApi
(
'/_authenticated/channels/'
)
function
isDisabledChannelRow
(
channel
:
Channel
)
{
function
isDisabledChannelRow
(
channel
:
Channel
)
{
return
!
isTagAggregateRow
(
channel
)
&&
channel
.
status
!==
CHANNEL_STATUS
.
ENABLED
return
(
!
isTagAggregateRow
(
channel
)
&&
channel
.
status
!==
CHANNEL_STATUS
.
ENABLED
)
}
}
export
function
ChannelsTable
()
{
export
function
ChannelsTable
()
{
...
@@ -264,17 +268,57 @@ export function ChannelsTable() {
...
@@ -264,17 +268,57 @@ export function ChannelsTable() {
ensurePageInRange
(
pageCount
)
ensurePageInRange
(
pageCount
)
},
[
pageCount
,
ensurePageInRange
])
},
[
pageCount
,
ensurePageInRange
])
// Prepare filter options (option.label are i18n keys; faceted-filter uses t(option.label))
// Prepare filter options from existing channel types only.
const
typeFilterOptions
=
[
const
typeFilterOptions
=
useMemo
(()
=>
{
const
counts
=
typeCounts
||
{}
const
typeIds
=
Object
.
entries
(
counts
)
.
map
(([
type
,
count
])
=>
({
type
:
Number
(
type
),
count
:
Number
(
count
)
||
0
,
}))
.
filter
((
item
)
=>
item
.
type
>
0
&&
item
.
count
>
0
)
.
sort
((
a
,
b
)
=>
{
const
labelA
=
t
(
getChannelTypeLabel
(
a
.
type
))
const
labelB
=
t
(
getChannelTypeLabel
(
b
.
type
))
return
labelA
.
localeCompare
(
labelB
)
})
const
selectedType
=
typeFilter
.
find
((
value
)
=>
value
!==
'all'
)
if
(
selectedType
)
{
const
selectedTypeId
=
Number
(
selectedType
)
const
alreadyIncluded
=
typeIds
.
some
(
(
item
)
=>
item
.
type
===
selectedTypeId
)
if
(
selectedTypeId
>
0
&&
!
alreadyIncluded
)
{
typeIds
.
push
({
type
:
selectedTypeId
,
count
:
Number
(
counts
[
selectedType
])
||
0
,
})
}
}
const
totalTypes
=
Object
.
values
(
counts
).
reduce
(
(
sum
,
count
)
=>
sum
+
(
Number
(
count
)
||
0
),
0
)
return
[
{
{
label
:
`
${
t
(
'All Types'
)}${
typeCounts
?.
all
?
` (
${
typeCounts
.
all
}
)`
:
''
}
`
,
label
:
'All Types'
,
value
:
'all'
,
value
:
'all'
,
count
:
totalTypes
,
},
},
...CHANNEL_TYPE_OPTIONS.map((option) => ({
...
typeIds
.
map
((
item
)
=>
{
label: `
$
{
t
(
option
.
label
)}
$
{
typeCounts
?.[
option
.
value
]
?
` (
${
typeCounts
[
option
.
value
]}
)`
:
''
}
`,
const
iconName
=
getChannelTypeIcon
(
item
.
type
)
value: String(option.value),
return
{
})),
label
:
getChannelTypeLabel
(
item
.
type
),
value
:
String
(
item
.
type
),
count
:
item
.
count
,
iconNode
:
getLobeIcon
(
`
${
iconName
}
.Color`
,
16
),
}
}),
]
]
},
[
t
,
typeCounts
,
typeFilter
])
const
groupFilterOptions
=
[
const
groupFilterOptions
=
[
{
label
:
t
(
'All Groups'
),
value
:
'all'
},
{
label
:
t
(
'All Groups'
),
value
:
'all'
},
...
@@ -375,7 +419,7 @@ export function ChannelsTable() {
...
@@ -375,7 +419,7 @@ export function ChannelsTable() {
data
-
state=
{
row
.
getIsSelected
()
&&
'selected'
}
data
-
state=
{
row
.
getIsSelected
()
&&
'selected'
}
className=
{
cn
(
className=
{
cn
(
isDisabledChannelRow
(
row
.
original
)
&&
isDisabledChannelRow
(
row
.
original
)
&&
'bg-muted/85 hover:bg-muted
dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:border-l-muted-foreground/35
[&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70'
'bg-muted/85 hover:bg-muted
[&>td:first-child]:border-l-muted-foreground/35 dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4
[&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70'
)
}
)
}
>
>
{
row
.
getVisibleCells
().
map
((
cell
)
=>
(
{
row
.
getVisibleCells
().
map
((
cell
)
=>
(
...
...
web/default/src/features/profile/components/profile-header.tsx
View file @
22ae14f0
import
{
Activity
,
BarChart3
,
WalletCards
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
formatCompactNumber
,
formatQuota
}
from
'@/lib/format'
import
{
formatCompactNumber
,
formatQuota
}
from
'@/lib/format'
import
{
getRoleLabel
}
from
'@/lib/roles'
import
{
getRoleLabel
}
from
'@/lib/roles'
...
@@ -21,11 +22,10 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
...
@@ -21,11 +22,10 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
if
(
loading
)
{
if
(
loading
)
{
return
(
return
(
<
div
className=
'bg-card overflow-hidden rounded-2xl border'
>
<
div
className=
'bg-card overflow-hidden rounded-lg border'
>
<
div
className=
'p-5 sm:p-6'
>
<
div
className=
'p-4 sm:p-5'
>
<
div
className=
'flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
Skeleton
className=
'h-20 w-20
rounded-2xl'
/>
<
Skeleton
className=
'h-16 w-16
rounded-2xl'
/>
<
div
className=
'space-y-3'
>
<
div
className=
'space-y-3'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
Skeleton
className=
'h-8 w-48'
/>
<
Skeleton
className=
'h-8 w-48'
/>
...
@@ -38,17 +38,19 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
...
@@ -38,17 +38,19 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
'grid gap-3 sm:grid-cols-3 lg:w-[480px]'
>
</
div
>
<
div
className=
'border-t'
>
<
div
className=
'divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
i
}
className=
'rounded-xl border p-4'
>
<
div
key=
{
i
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
<
Skeleton
className=
'mb-3 h-3 w-20'
/>
<
Skeleton
className=
'h-3.5 w-20'
/>
<
Skeleton
className=
'h-7 w-24'
/>
<
Skeleton
className=
'mt-2 h-7 w-28'
/>
<
Skeleton
className=
'mt-1.5 h-3.5 w-24'
/>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
)
)
}
}
...
@@ -61,23 +63,28 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
...
@@ -61,23 +63,28 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
{
{
label
:
t
(
'Current Balance'
),
label
:
t
(
'Current Balance'
),
value
:
formatQuota
(
profile
.
quota
),
value
:
formatQuota
(
profile
.
quota
),
description
:
t
(
'Remaining quota'
),
icon
:
WalletCards
,
},
},
{
{
label
:
t
(
'Total Usage'
),
label
:
t
(
'Total Usage'
),
value
:
formatQuota
(
profile
.
used_quota
),
value
:
formatQuota
(
profile
.
used_quota
),
description
:
t
(
'Total consumed quota'
),
icon
:
BarChart3
,
},
},
{
{
label
:
t
(
'API Requests'
),
label
:
t
(
'API Requests'
),
value
:
formatCompactNumber
(
profile
.
request_count
),
value
:
formatCompactNumber
(
profile
.
request_count
),
description
:
t
(
'Total requests made'
),
icon
:
Activity
,
},
},
]
]
return
(
return
(
<
div
className=
'bg-card relative overflow-hidden rounded-2xl border'
>
<
div
className=
'bg-card overflow-hidden rounded-lg border'
>
<
div
className=
'relative p-5 sm:p-6'
>
<
div
className=
'p-4 sm:p-5'
>
<
div
className=
'flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
Avatar
className=
'ring-background h-20 w-20 rounded-2xl text-xl
ring-4'
>
<
Avatar
className=
'ring-background h-16 w-16 rounded-2xl text-lg
ring-4'
>
<
AvatarFallback
className=
'bg-primary/10 text-primary rounded-2xl'
>
<
AvatarFallback
className=
'bg-primary/10 text-primary rounded-2xl'
>
{
initials
}
{
initials
}
</
AvatarFallback
>
</
AvatarFallback
>
...
@@ -85,7 +92,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
...
@@ -85,7 +92,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
<
div
className=
'min-w-0 flex-1 space-y-3'
>
<
div
className=
'min-w-0 flex-1 space-y-3'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
h1
className=
'text-2xl font-semibold tracking-tight sm:text-3xl
'
>
<
h1
className=
'text-2xl font-semibold tracking-tight
'
>
{
displayName
}
{
displayName
}
</
h1
>
</
h1
>
<
StatusBadge
<
StatusBadge
...
@@ -112,23 +119,27 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
...
@@ -112,23 +119,27 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
'grid gap-3 sm:grid-cols-3 lg:w-[480px]'
>
<
div
className=
'border-t'
>
<
div
className=
'divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'
>
{
stats
.
map
((
item
)
=>
(
{
stats
.
map
((
item
)
=>
(
<
div
<
div
key=
{
item
.
label
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
key=
{
item
.
label
}
<
div
className=
'flex items-center gap-2'
>
className=
'bg-background/70 rounded-xl border p-4 backdrop-blur'
<
item
.
icon
className=
'text-muted-foreground/60 size-3.5 shrink-0'
/>
>
<
div
className=
'text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'
>
<
p
className=
'text-muted-foreground text-xs font-medium'
>
{
item
.
label
}
{
item
.
label
}
</
p
>
</
div
>
<
p
className=
'mt-2 truncate text-xl font-semibold tracking-tight'
>
</
div
>
<
div
className=
'text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'
>
{
item
.
value
}
{
item
.
value
}
</
p
>
</
div
>
</
div
>
))
}
<
div
className=
'text-muted-foreground/60 mt-1 hidden text-xs md:block'
>
{
item
.
description
}
</
div
>
</
div
>
</
div
>
</
div
>
))
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
)
)
...
...
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
View file @
22ae14f0
import
{
useState
}
from
'react'
import
{
useState
}
from
'react'
import
{
type
ColumnDef
}
from
'@tanstack/react-table'
import
{
type
ColumnDef
}
from
'@tanstack/react-table'
import
{
Route
,
CircleAlert
,
Sparkles
,
KeyRound
}
from
'lucide-react'
import
{
CircleAlert
,
Sparkles
,
KeyRound
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
formatBillingCurrencyFromUSD
}
from
'@/lib/currency'
import
{
formatBillingCurrencyFromUSD
}
from
'@/lib/currency'
import
{
import
{
...
@@ -11,11 +11,6 @@ import {
...
@@ -11,11 +11,6 @@ import {
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
{
Avatar
,
AvatarFallback
}
from
'@/components/ui/avatar'
import
{
Avatar
,
AvatarFallback
}
from
'@/components/ui/avatar'
import
{
import
{
Popover
,
PopoverContent
,
PopoverTrigger
,
}
from
'@/components/ui/popover'
import
{
Tooltip
,
Tooltip
,
TooltipContent
,
TooltipContent
,
TooltipProvider
,
TooltipProvider
,
...
@@ -25,8 +20,6 @@ import { DataTableColumnHeader } from '@/components/data-table'
...
@@ -25,8 +20,6 @@ import { DataTableColumnHeader } from '@/components/data-table'
import
{
import
{
StatusBadge
,
StatusBadge
,
type
StatusBadgeProps
,
type
StatusBadgeProps
,
dotColorMap
,
textColorMap
,
}
from
'@/components/status-badge'
}
from
'@/components/status-badge'
import
type
{
UsageLog
}
from
'../../data/schema'
import
type
{
UsageLog
}
from
'../../data/schema'
import
{
import
{
...
@@ -47,6 +40,7 @@ import {
...
@@ -47,6 +40,7 @@ import {
}
from
'../../lib/utils'
}
from
'../../lib/utils'
import
type
{
LogOtherData
}
from
'../../types'
import
type
{
LogOtherData
}
from
'../../types'
import
{
DetailsDialog
}
from
'../dialogs/details-dialog'
import
{
DetailsDialog
}
from
'../dialogs/details-dialog'
import
{
ModelBadge
}
from
'../model-badge'
import
{
useUsageLogsContext
}
from
'../usage-logs-provider'
import
{
useUsageLogsContext
}
from
'../usage-logs-provider'
interface
DetailSegment
{
interface
DetailSegment
{
...
@@ -445,10 +439,20 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -445,10 +439,20 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
const tokenName = log.token_name
const tokenName = log.token_name
if (!tokenName) return null
if (!tokenName) return null
const other = parseLogOther(log.other)
const displayName = sensitiveVisible ? tokenName : '••••'
const displayName = sensitiveVisible ? tokenName : '••••'
let group = log.group
if (!group) group = other?.group || ''
const metaParts: string[] = []
const groupRatioText = getGroupRatioText(other)
if (group) {
metaParts.push(sensitiveVisible ? group : '••••')
}
if (groupRatioText) metaParts.push(groupRatioText)
return (
return (
<div className='
max-w-[120px]
'>
<div className='
flex max-w-[150px] flex-col gap-0.5
'>
<StatusBadge
<StatusBadge
label={displayName}
label={displayName}
icon={KeyRound}
icon={KeyRound}
...
@@ -457,6 +461,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -457,6 +461,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
showDot={false}
showDot={false}
className='max-w-full overflow-hidden rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono text-foreground'
className='max-w-full overflow-hidden rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono text-foreground'
/>
/>
{metaParts.length > 0 && (
<span className='text-muted-foreground/60 truncate text-[11px]'>
{metaParts.join(' · ')}
</span>
)}
</div>
</div>
)
)
},
},
...
@@ -471,81 +480,17 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -471,81 +480,17 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<DataTableColumnHeader column={column} title={t('Model')} />
<DataTableColumnHeader column={column} title={t('Model')} />
),
),
cell: function ModelCell({ row }) {
cell: function ModelCell({ row }) {
const { sensitiveVisible } = useUsageLogsContext()
const log = row.original
const log = row.original
if (!isDisplayableLogType(log.type)) return null
if (!isDisplayableLogType(log.type)) return null
const modelInfo = formatModelName(log)
const modelInfo = formatModelName(log)
const other = parseLogOther(log.other)
let group = log.group
if (!group) group = other?.group || ''
const badgeClass =
'truncate rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono'
const modelBadge = modelInfo.isMapped ? (
<Popover>
<PopoverTrigger asChild>
<button
type='button'
className='inline-flex items-center gap-1'
>
<StatusBadge
label={modelInfo.name}
autoColor={modelInfo.name}
copyText={modelInfo.name}
size='sm'
className={badgeClass}
/>
<Route className='text-muted-foreground size-3 shrink-0' />
</button>
</PopoverTrigger>
<PopoverContent className='w-72'>
<div className='space-y-2'>
<div className='flex items-start justify-between gap-3'>
<span className='text-muted-foreground text-xs'>
{t('Request Model:')}
</span>
<span className='truncate font-mono text-xs font-medium'>
{modelInfo.name}
</span>
</div>
<div className='flex items-start justify-between gap-3'>
<span className='text-muted-foreground text-xs'>
{t('Actual Model:')}
</span>
<span className='truncate font-mono text-xs font-medium'>
{modelInfo.actualModel}
</span>
</div>
</div>
</PopoverContent>
</Popover>
) : (
<StatusBadge
label={modelInfo.name}
autoColor={modelInfo.name}
copyText={modelInfo.name}
size='sm'
className={badgeClass}
/>
)
const metaParts: string[] = []
const groupRatioText = getGroupRatioText(other)
if (group) {
metaParts.push(sensitiveVisible ? group : '••••')
}
if (groupRatioText) metaParts.push(groupRatioText)
return (
return (
<div className='flex max-w-[220px] flex-col gap-0.5'>
<div className='flex max-w-[220px] flex-col gap-0.5'>
{modelBadge}
<ModelBadge
{metaParts.length > 0 && (
modelName={modelInfo.name}
<span className='text-muted-foreground/60 truncate text-[11px]'>
actualModel={modelInfo.actualModel}
{metaParts.join(' · ')}
/>
</span>
)}
</div>
</div>
)
)
},
},
...
@@ -576,11 +521,21 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -576,11 +521,21 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
const pillBg: Record<string, string> = {
const pillBg: Record<string, string> = {
success:
success:
'border border-emerald-200/
60 bg-emerald-50/50 dark:border-emerald-800/50 dark:bg-emerald-950/20
',
'border border-emerald-200/
40 bg-emerald-50/35 dark:border-emerald-900/40 dark:bg-emerald-950/15
',
warning:
warning:
'border border-amber-200/
60 bg-amber-50/50 dark:border-amber-800/50 dark:bg-amber-950/20
',
'border border-amber-200/
45 bg-amber-50/35 dark:border-amber-900/40 dark:bg-amber-950/15
',
danger:
danger:
'border border-rose-200/70 bg-rose-50/60 dark:border-rose-800/50 dark:bg-rose-950/25',
'border border-rose-200/50 bg-rose-50/35 dark:border-rose-900/40 dark:bg-rose-950/15',
}
const pillText: Record<string, string> = {
success: 'text-emerald-700/85 dark:text-emerald-400/85',
warning: 'text-amber-700/85 dark:text-amber-400/85',
danger: 'text-rose-700/85 dark:text-rose-400/85',
}
const pillDot: Record<string, string> = {
success: 'bg-emerald-500/80',
warning: 'bg-amber-500/80',
danger: 'bg-rose-500/80',
}
}
return (
return (
...
@@ -590,13 +545,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -590,13 +545,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
className={cn(
className={cn(
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
pillBg[timeVariant],
pillBg[timeVariant],
textColorMap
[timeVariant]
pillText
[timeVariant]
)}
)}
>
>
<span
<span
className={cn(
className={cn(
'size-1.5 shrink-0 rounded-full',
'size-1.5 shrink-0 rounded-full',
dotColorMap
[timeVariant]
pillDot
[timeVariant]
)}
)}
aria-hidden='true'
aria-hidden='true'
/>
/>
...
@@ -607,7 +562,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -607,7 +562,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
className={cn(
className={cn(
'inline-flex items-center rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
'inline-flex items-center rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
pillBg[frtVariant!],
pillBg[frtVariant!],
textColorMap
[frtVariant!]
pillText
[frtVariant!]
)}
)}
>
>
{formatUseTime(frt / 1000)}
{formatUseTime(frt / 1000)}
...
...
web/default/src/features/usage-logs/components/common-logs-stats.tsx
View file @
22ae14f0
...
@@ -12,6 +12,22 @@ import { useUsageLogsContext } from './usage-logs-provider'
...
@@ -12,6 +12,22 @@ import { useUsageLogsContext } from './usage-logs-provider'
const
route
=
getRouteApi
(
'/_authenticated/usage-logs/$section'
)
const
route
=
getRouteApi
(
'/_authenticated/usage-logs/$section'
)
function
StatBadge
(
props
:
{
label
:
string
value
:
string
|
number
accent
:
string
})
{
return
(
<
span
className=
'inline-flex h-7 items-center gap-2 rounded-md border border-border/60 bg-muted/25 px-2.5 text-xs shadow-xs'
>
<
span
className=
{
cn
(
'h-3.5 w-0.5 rounded-full'
,
props
.
accent
)
}
/>
<
span
className=
'text-muted-foreground'
>
{
props
.
label
}
</
span
>
<
span
className=
'font-mono font-semibold tabular-nums text-foreground/85'
>
{
props
.
value
}
</
span
>
</
span
>
)
}
export
function
CommonLogsStats
()
{
export
function
CommonLogsStats
()
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
isAdmin
=
useIsAdmin
()
const
isAdmin
=
useIsAdmin
()
...
@@ -50,36 +66,23 @@ export function CommonLogsStats() {
...
@@ -50,36 +66,23 @@ export function CommonLogsStats() {
)
)
}
}
const
tagClass
=
'inline-flex h-7 items-center rounded-md border px-3 py-1 text-xs font-medium shadow-xs'
return
(
return
(
<
div
className=
'flex flex-wrap items-center gap-2'
>
<
div
className=
'flex flex-wrap items-center gap-2'
>
<
span
<
StatBadge
className=
{
cn
(
label=
{
t
(
'Usage'
)
}
tagClass
,
value=
{
sensitiveVisible
?
formatLogQuota
(
stats
?.
quota
||
0
)
:
'••••'
}
'border-blue-200/70 bg-blue-50 text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-300'
accent=
'bg-sky-500/70'
)
}
/>
>
<
StatBadge
{
t
(
'Usage'
)
}
:
{
' '
}
label=
{
t
(
'RPM'
)
}
{
sensitiveVisible
?
formatLogQuota
(
stats
?.
quota
||
0
)
:
'••••'
}
value=
{
stats
?.
rpm
||
0
}
</
span
>
accent=
'bg-rose-500/65'
<
span
/>
className=
{
cn
(
<
StatBadge
tagClass
,
label=
{
t
(
'TPM'
)
}
'border-pink-200/70 bg-pink-50 text-pink-700 dark:border-pink-500/20 dark:bg-pink-500/10 dark:text-pink-300'
value=
{
stats
?.
tpm
||
0
}
)
}
accent=
'bg-slate-400/70'
>
/>
{
t
(
'RPM'
)
}
:
{
stats
?.
rpm
||
0
}
</
span
>
<
span
className=
{
cn
(
tagClass
,
'border-border bg-background text-muted-foreground'
)
}
>
{
t
(
'TPM'
)
}
:
{
stats
?.
tpm
||
0
}
</
span
>
</
div
>
</
div
>
)
)
}
}
web/default/src/features/usage-logs/components/model-badge.tsx
0 → 100644
View file @
22ae14f0
import
{
Route
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
cn
}
from
'@/lib/utils'
import
{
Popover
,
PopoverContent
,
PopoverTrigger
,
}
from
'@/components/ui/popover'
import
{
StatusBadge
}
from
'@/components/status-badge'
interface
ModelBadgeProps
{
modelName
:
string
actualModel
?:
string
className
?:
string
}
interface
ModelProvider
{
icon
:
string
label
:
string
}
function
resolveModelProvider
(
modelName
:
string
):
ModelProvider
|
null
{
const
model
=
modelName
.
toLowerCase
()
const
hasAny
=
(
keywords
:
string
[])
=>
keywords
.
some
((
keyword
)
=>
model
.
includes
(
keyword
))
if
(
hasAny
([
'gpt-'
,
'chatgpt-'
,
'text-embedding-'
,
'omni-moderation'
,
'dall-e'
,
'whisper'
,
'tts-'
,
])
||
/
\b
o
[
134
](?:
-|$
)
/
.
test
(
model
)
)
{
return
{
icon
:
'OpenAI.Color'
,
label
:
'OpenAI'
}
}
if
(
hasAny
([
'claude-'
,
'anthropic'
]))
{
return
{
icon
:
'Claude.Color'
,
label
:
'Claude'
}
}
if
(
hasAny
([
'gemini-'
,
'learnlm-'
]))
{
return
{
icon
:
'Gemini.Color'
,
label
:
'Gemini'
}
}
if
(
hasAny
([
'grok-'
,
'xai-'
]))
{
return
{
icon
:
'Grok.Color'
,
label
:
'Grok'
}
}
if
(
hasAny
([
'deepseek-'
]))
{
return
{
icon
:
'DeepSeek.Color'
,
label
:
'DeepSeek'
}
}
if
(
hasAny
([
'qwen'
,
'qwq-'
]))
{
return
{
icon
:
'Qwen.Color'
,
label
:
'Qwen'
}
}
if
(
hasAny
([
'doubao-'
,
'volcengine'
]))
{
return
{
icon
:
'Doubao.Color'
,
label
:
'Doubao'
}
}
if
(
hasAny
([
'moonshot-'
,
'kimi-'
]))
{
return
{
icon
:
'Moonshot.Color'
,
label
:
'Moonshot'
}
}
if
(
hasAny
([
'mistral-'
,
'mixtral-'
]))
{
return
{
icon
:
'Mistral.Color'
,
label
:
'Mistral'
}
}
if
(
hasAny
([
'llama-'
,
'meta-'
]))
{
return
{
icon
:
'Meta.Color'
,
label
:
'Meta'
}
}
if
(
hasAny
([
'command-'
,
'cohere-'
]))
{
return
{
icon
:
'Cohere.Color'
,
label
:
'Cohere'
}
}
return
null
}
function
ModelBadgeContent
(
props
:
ModelBadgeProps
)
{
const
provider
=
resolveModelProvider
(
props
.
modelName
)
return
(
<
StatusBadge
copyText=
{
props
.
modelName
}
size=
'sm'
showDot=
{
!
provider
}
autoColor=
{
provider
?
undefined
:
props
.
modelName
}
className=
{
cn
(
'rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono'
,
provider
&&
'text-foreground'
,
props
.
className
)
}
>
<
span
className=
'flex items-center gap-1.5'
>
{
provider
&&
(
<
span
className=
'flex size-3.5 shrink-0 items-center justify-center'
title=
{
provider
.
label
}
aria
-
label=
{
provider
.
label
}
>
{
getLobeIcon
(
provider
.
icon
,
14
)
}
</
span
>
)
}
<
span
>
{
props
.
modelName
}
</
span
>
</
span
>
</
StatusBadge
>
)
}
export
function
ModelBadge
(
props
:
ModelBadgeProps
)
{
const
{
t
}
=
useTranslation
()
if
(
!
props
.
actualModel
)
{
return
<
ModelBadgeContent
{
...
props
}
/>
}
return
(
<
Popover
>
<
PopoverTrigger
asChild
>
<
button
type=
'button'
className=
'inline-flex items-center gap-1'
>
<
ModelBadgeContent
{
...
props
}
/>
<
Route
className=
'text-muted-foreground size-3 shrink-0'
/>
</
button
>
</
PopoverTrigger
>
<
PopoverContent
className=
'w-72'
>
<
div
className=
'space-y-2'
>
<
div
className=
'flex items-start justify-between gap-3'
>
<
span
className=
'text-muted-foreground text-xs'
>
{
t
(
'Request Model:'
)
}
</
span
>
<
span
className=
'truncate font-mono text-xs font-medium'
>
{
props
.
modelName
}
</
span
>
</
div
>
<
div
className=
'flex items-start justify-between gap-3'
>
<
span
className=
'text-muted-foreground text-xs'
>
{
t
(
'Actual Model:'
)
}
</
span
>
<
span
className=
'truncate font-mono text-xs font-medium'
>
{
props
.
actualModel
}
</
span
>
</
div
>
</
div
>
</
PopoverContent
>
</
Popover
>
)
}
web/default/src/features/wallet/components/wallet-stats-card.tsx
View file @
22ae14f0
import
{
Activity
,
BarChart3
,
WalletCards
}
from
'lucide-react'
import
{
Activity
,
BarChart3
,
WalletCards
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
formatQuota
}
from
'@/lib/format'
import
{
formatQuota
}
from
'@/lib/format'
import
{
cn
}
from
'@/lib/utils'
import
{
Card
,
CardContent
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
type
{
UserWalletData
}
from
'../types'
import
type
{
UserWalletData
}
from
'../types'
...
@@ -15,26 +13,17 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
...
@@ -15,26 +13,17 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
if
(
props
.
loading
)
{
if
(
props
.
loading
)
{
return
(
return
(
<
Card
className=
'overflow-hidden'
>
<
div
className=
'overflow-hidden rounded-lg border'
>
<
CardContent
className=
'p-0'
>
<
div
className=
'divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'
>
<
div
className=
'grid grid-cols-1 sm:grid-cols-3'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
<
div
key=
{
i
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
key=
{
i
}
<
Skeleton
className=
'h-3.5 w-20'
/>
className=
{
cn
(
<
Skeleton
className=
'mt-2 h-7 w-28'
/>
'flex items-center justify-center px-4 py-3 sm:px-5 sm:py-4'
,
<
Skeleton
className=
'mt-1.5 h-3.5 w-24'
/>
i
>
0
&&
'border-t sm:border-t-0 sm:border-l'
)
}
>
<
div
className=
'w-full max-w-44'
>
<
Skeleton
className=
'h-4 w-24'
/>
<
Skeleton
className=
'mt-2 h-7 w-32'
/>
</
div
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
</
CardContent
>
</
div
>
</
Card
>
)
)
}
}
...
@@ -42,45 +31,44 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
...
@@ -42,45 +31,44 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
{
{
label
:
t
(
'Current Balance'
),
label
:
t
(
'Current Balance'
),
value
:
formatQuota
(
props
.
user
?.
quota
??
0
),
value
:
formatQuota
(
props
.
user
?.
quota
??
0
),
description
:
t
(
'Remaining quota'
),
icon
:
WalletCards
,
icon
:
WalletCards
,
},
},
{
{
label
:
t
(
'Total Usage'
),
label
:
t
(
'Total Usage'
),
value
:
formatQuota
(
props
.
user
?.
used_quota
??
0
),
value
:
formatQuota
(
props
.
user
?.
used_quota
??
0
),
description
:
t
(
'Total consumed quota'
),
icon
:
BarChart3
,
icon
:
BarChart3
,
},
},
{
{
label
:
t
(
'API Requests'
),
label
:
t
(
'API Requests'
),
value
:
(
props
.
user
?.
request_count
??
0
).
toLocaleString
(),
value
:
(
props
.
user
?.
request_count
??
0
).
toLocaleString
(),
description
:
t
(
'Total requests made'
),
icon
:
Activity
,
icon
:
Activity
,
},
},
]
]
return
(
return
(
<
Card
className=
'overflow-hidden'
>
<
div
className=
'overflow-hidden rounded-lg border'
>
<
CardContent
className=
'p-0'
>
<
div
className=
'divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'
>
<
div
className=
'grid grid-cols-1 sm:grid-cols-3'
>
{
stats
.
map
((
item
)
=>
(
{
stats
.
map
((
item
,
index
)
=>
(
<
div
key=
{
item
.
label
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
<
div
<
div
className=
'flex items-center gap-2'
>
key=
{
item
.
label
}
<
item
.
icon
className=
'text-muted-foreground/60 size-3.5 shrink-0'
/>
className=
{
cn
(
<
div
className=
'text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'
>
'flex min-w-0 justify-center px-4 py-3 sm:px-5 sm:py-4'
,
index
>
0
&&
'border-t sm:border-t-0 sm:border-l'
)
}
>
<
div
className=
'min-w-0 text-center'
>
<
div
className=
'text-muted-foreground flex items-center justify-center gap-1.5 text-xs font-medium'
>
<
item
.
icon
className=
'h-3.5 w-3.5'
/>
{
item
.
label
}
{
item
.
label
}
</
div
>
</
div
>
<
div
className=
'mt-1 text-xl leading-tight font-semibold tracking-tight break-all lg:text-2xl'
>
</
div
>
<
div
className=
'text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'
>
{
item
.
value
}
{
item
.
value
}
</
div
>
</
div
>
<
div
className=
'text-muted-foreground/60 mt-1 hidden text-xs md:block'
>
{
item
.
description
}
</
div
>
</
div
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
</
CardContent
>
</
div
>
</
Card
>
)
)
}
}
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