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
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
406 additions
and
247 deletions
+406
-247
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
+58
-14
web/default/src/features/profile/components/profile-header.tsx
+83
-72
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
+33
-45
No files found.
web/default/src/components/data-table/faceted-filter.tsx
View file @
22ae14f0
...
...
@@ -28,6 +28,8 @@ type DataTableFacetedFilterProps<TData, TValue> = {
label
:
string
value
:
string
icon
?:
React
.
ComponentType
<
{
className
?:
string
}
>
iconNode
?:
React
.
ReactNode
count
?:
number
}[]
/** Enable single select mode (only one option can be selected at a time) */
singleSelect
?:
boolean
...
...
@@ -130,15 +132,25 @@ export function DataTableFacetedFilter<TData, TValue>({
>
<
CheckIcon
className=
{
cn
(
'text-background h-4 w-4'
)
}
/>
</
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'
/>
)
}
<
span
>
{
t
(
option
.
label
)
}
</
span
>
{
facets
?.
get
(
option
.
value
)
&&
(
)
:
null
}
<
span
className=
'min-w-0 flex-1 truncate'
>
{
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'
>
{
facets
.
get
(
option
.
value
)
}
</
span
>
)
}
)
:
null
}
</
CommandItem
>
)
})
}
...
...
web/default/src/components/data-table/toolbar.tsx
View file @
22ae14f0
...
...
@@ -20,6 +20,8 @@ type DataTableToolbarProps<TData> = {
label
:
string
value
:
string
icon
?:
React
.
ComponentType
<
{
className
?:
string
}
>
iconNode
?:
React
.
ReactNode
count
?:
number
}[]
singleSelect
?:
boolean
}[]
...
...
web/default/src/features/channels/components/channels-table.tsx
View file @
22ae14f0
...
...
@@ -13,6 +13,7 @@ import {
}
from
'@tanstack/react-table'
import
{
useDebounce
,
useMediaQuery
}
from
'@/hooks'
import
{
useTranslation
}
from
'react-i18next'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
cn
}
from
'@/lib/utils'
import
{
useTableUrlState
}
from
'@/hooks/use-table-url-state'
import
{
Input
}
from
'@/components/ui/input'
...
...
@@ -37,12 +38,13 @@ import {
DEFAULT_PAGE_SIZE
,
CHANNEL_STATUS
,
CHANNEL_STATUS_OPTIONS
,
CHANNEL_TYPE_OPTIONS
,
}
from
'../constants'
import
{
channelsQueryKeys
,
aggregateChannelsByTag
,
isTagAggregateRow
,
getChannelTypeIcon
,
getChannelTypeLabel
,
}
from
'../lib'
import
type
{
Channel
}
from
'../types'
import
{
useChannelsColumns
}
from
'./channels-columns'
...
...
@@ -52,7 +54,9 @@ import { DataTableBulkActions } from './data-table-bulk-actions'
const
route
=
getRouteApi
(
'/_authenticated/channels/'
)
function
isDisabledChannelRow
(
channel
:
Channel
)
{
return
!
isTagAggregateRow
(
channel
)
&&
channel
.
status
!==
CHANNEL_STATUS
.
ENABLED
return
(
!
isTagAggregateRow
(
channel
)
&&
channel
.
status
!==
CHANNEL_STATUS
.
ENABLED
)
}
export
function
ChannelsTable
()
{
...
...
@@ -264,17 +268,57 @@ export function ChannelsTable() {
ensurePageInRange
(
pageCount
)
},
[
pageCount
,
ensurePageInRange
])
// Prepare filter options (option.label are i18n keys; faceted-filter uses t(option.label))
const
typeFilterOptions
=
[
{
label
:
`
${
t
(
'All Types'
)}${
typeCounts
?.
all
?
` (
${
typeCounts
.
all
}
)`
:
''
}
`,
value: 'all',
},
...CHANNEL_TYPE_OPTIONS.map((option) => ({
label: `
$
{
t
(
option
.
label
)}
$
{
typeCounts
?.[
option
.
value
]
?
` (
${
typeCounts
[
option
.
value
]}
)`
:
''
}
`,
value: String(option.value),
})),
]
// Prepare filter options from existing channel types only.
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
:
'All Types'
,
value
:
'all'
,
count
:
totalTypes
,
},
...
typeIds
.
map
((
item
)
=>
{
const
iconName
=
getChannelTypeIcon
(
item
.
type
)
return
{
label
:
getChannelTypeLabel
(
item
.
type
),
value
:
String
(
item
.
type
),
count
:
item
.
count
,
iconNode
:
getLobeIcon
(
`
${
iconName
}
.Color`
,
16
),
}
}),
]
},
[
t
,
typeCounts
,
typeFilter
])
const
groupFilterOptions
=
[
{
label
:
t
(
'All Groups'
),
value
:
'all'
},
...
...
@@ -375,7 +419,7 @@ export function ChannelsTable() {
data
-
state=
{
row
.
getIsSelected
()
&&
'selected'
}
className=
{
cn
(
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
)
=>
(
...
...
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
{
formatCompactNumber
,
formatQuota
}
from
'@/lib/format'
import
{
getRoleLabel
}
from
'@/lib/roles'
...
...
@@ -21,33 +22,34 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
if
(
loading
)
{
return
(
<
div
className=
'bg-card overflow-hidden rounded-2xl border'
>
<
div
className=
'p-5 sm:p-6'
>
<
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'
>
<
Skeleton
className=
'h-20 w-20 rounded-2xl'
/>
<
div
className=
'space-y-3'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
Skeleton
className=
'h-8 w-48'
/>
<
Skeleton
className=
'h-5 w-16'
/>
</
div
>
<
div
className=
'flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'
>
<
Skeleton
className=
'h-4 w-24'
/>
<
Skeleton
className=
'h-4 w-40'
/>
<
Skeleton
className=
'h-4 w-20'
/>
</
div
>
<
div
className=
'bg-card overflow-hidden rounded-lg border'
>
<
div
className=
'p-4 sm:p-5'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
Skeleton
className=
'h-16 w-16 rounded-2xl'
/>
<
div
className=
'space-y-3'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
Skeleton
className=
'h-8 w-48'
/>
<
Skeleton
className=
'h-5 w-16'
/>
</
div
>
<
div
className=
'flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'
>
<
Skeleton
className=
'h-4 w-24'
/>
<
Skeleton
className=
'h-4 w-40'
/>
<
Skeleton
className=
'h-4 w-20'
/>
</
div
>
</
div
>
<
div
className=
'grid gap-3 sm:grid-cols-3 lg:w-[480px]'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
i
}
className=
'rounded-xl border p-4'
>
<
Skeleton
className=
'mb-3 h-3 w-20'
/>
<
Skeleton
className=
'h-7 w-24'
/>
</
div
>
))
}
</
div
>
</
div
>
</
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
)
=>
(
<
div
key=
{
i
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
<
Skeleton
className=
'h-3.5 w-20'
/>
<
Skeleton
className=
'mt-2 h-7 w-28'
/>
<
Skeleton
className=
'mt-1.5 h-3.5 w-24'
/>
</
div
>
))
}
</
div
>
</
div
>
</
div
>
)
}
...
...
@@ -61,73 +63,82 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
{
label
:
t
(
'Current Balance'
),
value
:
formatQuota
(
profile
.
quota
),
description
:
t
(
'Remaining quota'
),
icon
:
WalletCards
,
},
{
label
:
t
(
'Total Usage'
),
value
:
formatQuota
(
profile
.
used_quota
),
description
:
t
(
'Total consumed quota'
),
icon
:
BarChart3
,
},
{
label
:
t
(
'API Requests'
),
value
:
formatCompactNumber
(
profile
.
request_count
),
description
:
t
(
'Total requests made'
),
icon
:
Activity
,
},
]
return
(
<
div
className=
'bg-card relative overflow-hidden rounded-2xl border'
>
<
div
className=
'relative p-5 sm:p-6'
>
<
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'
>
<
Avatar
className=
'ring-background h-20 w-20 rounded-2xl text-xl ring-4'
>
<
AvatarFallback
className=
'bg-primary/10 text-primary rounded-2xl'
>
{
initials
}
</
AvatarFallback
>
</
Avatar
>
<
div
className=
'bg-card overflow-hidden rounded-lg border'
>
<
div
className=
'p-4 sm:p-5'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
Avatar
className=
'ring-background h-16 w-16 rounded-2xl text-lg ring-4'
>
<
AvatarFallback
className=
'bg-primary/10 text-primary rounded-2xl'
>
{
initials
}
</
AvatarFallback
>
</
Avatar
>
<
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'
>
<
h1
className=
'text-2xl font-semibold tracking-tight sm:text-3xl
'
>
{
displayName
}
</
h1
>
<
StatusBadge
label=
{
roleLabel
}
variant=
'neutral'
copyable=
{
false
}
/>
</
div
>
<
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'
>
<
h1
className=
'text-2xl font-semibold tracking-tight
'
>
{
displayName
}
</
h1
>
<
StatusBadge
label=
{
roleLabel
}
variant=
'neutral'
copyable=
{
false
}
/>
</
div
>
<
div
className=
'text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-start sm:gap-4'
>
<
span
>
@
{
profile
.
username
}
</
span
>
{
profile
.
email
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
email
}
</
span
>
</>
)
}
{
profile
.
group
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
group
}
</
span
>
</>
)
}
</
div
>
<
div
className=
'text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-start sm:gap-4'
>
<
span
>
@
{
profile
.
username
}
</
span
>
{
profile
.
email
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
email
}
</
span
>
</>
)
}
{
profile
.
group
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
group
}
</
span
>
</>
)
}
</
div
>
</
div
>
<
div
className=
'grid gap-3 sm:grid-cols-3 lg:w-[480px]'
>
{
stats
.
map
((
item
)
=>
(
<
div
key=
{
item
.
label
}
className=
'bg-background/70 rounded-xl border p-4 backdrop-blur'
>
<
p
className=
'text-muted-foreground text-xs font-medium'
>
</
div
>
</
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'
>
{
stats
.
map
((
item
)
=>
(
<
div
key=
{
item
.
label
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
<
div
className=
'flex items-center gap-2'
>
<
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'
>
{
item
.
label
}
</
p
>
<
p
className=
'mt-2 truncate text-xl font-semibold tracking-tight'
>
{
item
.
value
}
</
p
>
</
div
>
</
div
>
))
}
</
div
>
<
div
className=
'text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'
>
{
item
.
value
}
</
div
>
<
div
className=
'text-muted-foreground/60 mt-1 hidden text-xs md:block'
>
{
item
.
description
}
</
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
{
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
{
formatBillingCurrencyFromUSD
}
from
'@/lib/currency'
import
{
...
...
@@ -11,11 +11,6 @@ import {
import
{
cn
}
from
'@/lib/utils'
import
{
Avatar
,
AvatarFallback
}
from
'@/components/ui/avatar'
import
{
Popover
,
PopoverContent
,
PopoverTrigger
,
}
from
'@/components/ui/popover'
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
...
...
@@ -25,8 +20,6 @@ import { DataTableColumnHeader } from '@/components/data-table'
import
{
StatusBadge
,
type
StatusBadgeProps
,
dotColorMap
,
textColorMap
,
}
from
'@/components/status-badge'
import
type
{
UsageLog
}
from
'../../data/schema'
import
{
...
...
@@ -47,6 +40,7 @@ import {
}
from
'../../lib/utils'
import
type
{
LogOtherData
}
from
'../../types'
import
{
DetailsDialog
}
from
'../dialogs/details-dialog'
import
{
ModelBadge
}
from
'../model-badge'
import
{
useUsageLogsContext
}
from
'../usage-logs-provider'
interface
DetailSegment
{
...
...
@@ -445,10 +439,20 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
const tokenName = log.token_name
if (!tokenName) return null
const other = parseLogOther(log.other)
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 (
<div className='
max-w-[120px]
'>
<div className='
flex max-w-[150px] flex-col gap-0.5
'>
<StatusBadge
label={displayName}
icon={KeyRound}
...
...
@@ -457,6 +461,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
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'
/>
{metaParts.length > 0 && (
<span className='text-muted-foreground/60 truncate text-[11px]'>
{metaParts.join(' · ')}
</span>
)}
</div>
)
},
...
...
@@ -471,81 +480,17 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<DataTableColumnHeader column={column} title={t('Model')} />
),
cell: function ModelCell({ row }) {
const { sensitiveVisible } = useUsageLogsContext()
const log = row.original
if (!isDisplayableLogType(log.type)) return null
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 (
<div className='flex max-w-[220px] flex-col gap-0.5'>
{modelBadge}
{metaParts.length > 0 && (
<span className='text-muted-foreground/60 truncate text-[11px]'>
{metaParts.join(' · ')}
</span>
)}
<ModelBadge
modelName={modelInfo.name}
actualModel={modelInfo.actualModel}
/>
</div>
)
},
...
...
@@ -576,11 +521,21 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
const pillBg: Record<string, string> = {
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:
'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:
'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 (
...
...
@@ -590,13 +545,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
className={cn(
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
pillBg[timeVariant],
textColorMap
[timeVariant]
pillText
[timeVariant]
)}
>
<span
className={cn(
'size-1.5 shrink-0 rounded-full',
dotColorMap
[timeVariant]
pillDot
[timeVariant]
)}
aria-hidden='true'
/>
...
...
@@ -607,7 +562,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
className={cn(
'inline-flex items-center rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
pillBg[frtVariant!],
textColorMap
[frtVariant!]
pillText
[frtVariant!]
)}
>
{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'
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
()
{
const
{
t
}
=
useTranslation
()
const
isAdmin
=
useIsAdmin
()
...
...
@@ -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
(
<
div
className=
'flex flex-wrap items-center gap-2'
>
<
span
className=
{
cn
(
tagClass
,
'border-blue-200/70 bg-blue-50 text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-300'
)
}
>
{
t
(
'Usage'
)
}
:
{
' '
}
{
sensitiveVisible
?
formatLogQuota
(
stats
?.
quota
||
0
)
:
'••••'
}
</
span
>
<
span
className=
{
cn
(
tagClass
,
'border-pink-200/70 bg-pink-50 text-pink-700 dark:border-pink-500/20 dark:bg-pink-500/10 dark:text-pink-300'
)
}
>
{
t
(
'RPM'
)
}
:
{
stats
?.
rpm
||
0
}
</
span
>
<
span
className=
{
cn
(
tagClass
,
'border-border bg-background text-muted-foreground'
)
}
>
{
t
(
'TPM'
)
}
:
{
stats
?.
tpm
||
0
}
</
span
>
<
StatBadge
label=
{
t
(
'Usage'
)
}
value=
{
sensitiveVisible
?
formatLogQuota
(
stats
?.
quota
||
0
)
:
'••••'
}
accent=
'bg-sky-500/70'
/>
<
StatBadge
label=
{
t
(
'RPM'
)
}
value=
{
stats
?.
rpm
||
0
}
accent=
'bg-rose-500/65'
/>
<
StatBadge
label=
{
t
(
'TPM'
)
}
value=
{
stats
?.
tpm
||
0
}
accent=
'bg-slate-400/70'
/>
</
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
{
useTranslation
}
from
'react-i18next'
import
{
formatQuota
}
from
'@/lib/format'
import
{
cn
}
from
'@/lib/utils'
import
{
Card
,
CardContent
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
type
{
UserWalletData
}
from
'../types'
...
...
@@ -15,26 +13,17 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
const
{
t
}
=
useTranslation
()
if
(
props
.
loading
)
{
return
(
<
Card
className=
'overflow-hidden'
>
<
CardContent
className=
'p-0'
>
<
div
className=
'grid grid-cols-1 sm:grid-cols-3'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
i
}
className=
{
cn
(
'flex items-center justify-center px-4 py-3 sm:px-5 sm:py-4'
,
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
>
</
CardContent
>
</
Card
>
<
div
className=
'overflow-hidden rounded-lg border'
>
<
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
)
=>
(
<
div
key=
{
i
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
<
Skeleton
className=
'h-3.5 w-20'
/>
<
Skeleton
className=
'mt-2 h-7 w-28'
/>
<
Skeleton
className=
'mt-1.5 h-3.5 w-24'
/>
</
div
>
))
}
</
div
>
</
div
>
)
}
...
...
@@ -42,45 +31,44 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
{
label
:
t
(
'Current Balance'
),
value
:
formatQuota
(
props
.
user
?.
quota
??
0
),
description
:
t
(
'Remaining quota'
),
icon
:
WalletCards
,
},
{
label
:
t
(
'Total Usage'
),
value
:
formatQuota
(
props
.
user
?.
used_quota
??
0
),
description
:
t
(
'Total consumed quota'
),
icon
:
BarChart3
,
},
{
label
:
t
(
'API Requests'
),
value
:
(
props
.
user
?.
request_count
??
0
).
toLocaleString
(),
description
:
t
(
'Total requests made'
),
icon
:
Activity
,
},
]
return
(
<
Card
className=
'overflow-hidden'
>
<
CardContent
className=
'p-0'
>
<
div
className=
'grid grid-cols-1 sm:grid-cols-3'
>
{
stats
.
map
((
item
,
index
)
=>
(
<
div
key=
{
item
.
label
}
className=
{
cn
(
'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
}
</
div
>
<
div
className=
'mt-1 text-xl leading-tight font-semibold tracking-tight break-all lg:text-2xl'
>
{
item
.
value
}
</
div
>
<
div
className=
'overflow-hidden rounded-lg border'
>
<
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
)
=>
(
<
div
key=
{
item
.
label
}
className=
'px-4 py-3.5 sm:px-5 sm:py-4'
>
<
div
className=
'flex items-center gap-2'
>
<
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'
>
{
item
.
label
}
</
div
>
</
div
>
))
}
</
div
>
</
CardContent
>
</
Card
>
<
div
className=
'text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'
>
{
item
.
value
}
</
div
>
<
div
className=
'text-muted-foreground/60 mt-1 hidden text-xs md:block'
>
{
item
.
description
}
</
div
>
</
div
>
))
}
</
div
>
</
div
>
)
}
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