+1
-1
@@ -131,7 +131,7 @@ export function StatusBadge({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex w-fit max-w-full shrink-0 items-center rounded-full font-medium tracking-normal whitespace-nowrap transition-colors',
|
'inline-flex w-fit max-w-full shrink-0 items-center rounded-4xl font-medium tracking-normal whitespace-nowrap transition-colors',
|
||||||
sizeMap[size ?? 'sm'],
|
sizeMap[size ?? 'sm'],
|
||||||
textColorMap[computedVariant],
|
textColorMap[computedVariant],
|
||||||
pulse && 'animate-pulse',
|
pulse && 'animate-pulse',
|
||||||
|
|||||||
+31
-24
@@ -19,6 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { useMediaQuery } from '@/hooks'
|
||||||
import { Select as SelectPrimitive } from '@base-ui/react/select'
|
import { Select as SelectPrimitive } from '@base-ui/react/select'
|
||||||
import {
|
import {
|
||||||
UnfoldMoreIcon,
|
UnfoldMoreIcon,
|
||||||
@@ -97,32 +98,38 @@ function SelectContent({
|
|||||||
SelectPrimitive.Positioner.Props,
|
SelectPrimitive.Positioner.Props,
|
||||||
'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger'
|
'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger'
|
||||||
>) {
|
>) {
|
||||||
return (
|
const isMobile = useMediaQuery('(max-width: 640px)')
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Positioner
|
const content = (
|
||||||
side={side}
|
<SelectPrimitive.Positioner
|
||||||
sideOffset={sideOffset}
|
side={side}
|
||||||
align={align}
|
sideOffset={sideOffset}
|
||||||
alignOffset={alignOffset}
|
align={align}
|
||||||
alignItemWithTrigger={alignItemWithTrigger}
|
alignOffset={alignOffset}
|
||||||
className='isolate z-50'
|
alignItemWithTrigger={alignItemWithTrigger}
|
||||||
|
className='isolate z-50'
|
||||||
|
>
|
||||||
|
<SelectPrimitive.Popup
|
||||||
|
data-slot='select-content'
|
||||||
|
data-align-trigger={alignItemWithTrigger}
|
||||||
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.Popup
|
<SelectScrollUpButton />
|
||||||
data-slot='select-content'
|
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
||||||
data-align-trigger={alignItemWithTrigger}
|
<SelectScrollDownButton />
|
||||||
className={cn(
|
</SelectPrimitive.Popup>
|
||||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
|
</SelectPrimitive.Positioner>
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Popup>
|
|
||||||
</SelectPrimitive.Positioner>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SelectPrimitive.Portal>{content}</SelectPrimitive.Portal>
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLabel({
|
function SelectLabel({
|
||||||
|
|||||||
+13
-10
@@ -40,6 +40,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
@@ -418,7 +419,7 @@ function SidebarGroupLabel({
|
|||||||
props: mergeProps<'div'>(
|
props: mergeProps<'div'>(
|
||||||
{
|
{
|
||||||
className: cn(
|
className: cn(
|
||||||
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
className
|
className
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -556,15 +557,17 @@ function SidebarMenuButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<TooltipProvider delay={0}>
|
||||||
{comp}
|
<Tooltip>
|
||||||
<TooltipContent
|
{comp}
|
||||||
side='right'
|
<TooltipContent
|
||||||
align='center'
|
side='right'
|
||||||
hidden={state !== 'collapsed' || isMobile}
|
align='center'
|
||||||
{...tooltip}
|
hidden={state !== 'collapsed' || isMobile}
|
||||||
/>
|
{...tooltip}
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ListOrdered,
|
ListOrdered,
|
||||||
Shuffle,
|
Shuffle,
|
||||||
|
SlidersHorizontal,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -301,15 +302,18 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
|||||||
|
|
||||||
const usedDisplay = withSuffix(formatQuotaValue(usedQuota))
|
const usedDisplay = withSuffix(formatQuotaValue(usedQuota))
|
||||||
const remainingDisplay = withSuffix(formatBalance(balance))
|
const remainingDisplay = withSuffix(formatBalance(balance))
|
||||||
|
const usedLabel = `${t('Used:')} ${usedDisplay}`
|
||||||
|
const remainingLabel = `${t('Remaining:')} ${remainingDisplay}`
|
||||||
|
|
||||||
// Tag row: only show cumulative used quota
|
// Tag row: only show cumulative used quota
|
||||||
if (isTagRow) {
|
if (isTagRow) {
|
||||||
return (
|
return (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={`Used: ${usedDisplay}`}
|
label={usedLabel}
|
||||||
variant='neutral'
|
variant='neutral'
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
|
showDot={false}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -354,14 +358,13 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
|||||||
variant='neutral'
|
variant='neutral'
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
|
showDot={false}
|
||||||
className='cursor-help'
|
className='cursor-help'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>{usedLabel}</p>
|
||||||
{t('Used:')} {usedDisplay}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -384,6 +387,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
|||||||
}
|
}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
|
showDot={false}
|
||||||
className='cursor-pointer'
|
className='cursor-pointer'
|
||||||
onClick={handleClickUpdate}
|
onClick={handleClickUpdate}
|
||||||
/>
|
/>
|
||||||
@@ -393,7 +397,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
|
|||||||
<p>
|
<p>
|
||||||
{channel.type === 57
|
{channel.type === 57
|
||||||
? t('Click to view Codex usage')
|
? t('Click to view Codex usage')
|
||||||
: `${t('Remaining:')} ${remainingDisplay}`}
|
: remainingLabel}
|
||||||
</p>
|
</p>
|
||||||
{channel.type !== 57 && <p>{t('Click to update balance')}</p>}
|
{channel.type !== 57 && <p>{t('Click to update balance')}</p>}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -494,7 +498,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
|||||||
const isTagRow = isTagAggregateRow(row.original)
|
const isTagRow = isTagAggregateRow(row.original)
|
||||||
const name = row.getValue('name') as string
|
const name = row.getValue('name') as string
|
||||||
const channel = row.original
|
const channel = row.original
|
||||||
const isMultiKey = isMultiKeyChannel(channel)
|
|
||||||
|
|
||||||
// Tag row with expand/collapse
|
// Tag row with expand/collapse
|
||||||
if (isTagRow) {
|
if (isTagRow) {
|
||||||
@@ -531,6 +534,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
|||||||
// Regular channel row
|
// Regular channel row
|
||||||
const settings = parseChannelSettings(channel.setting)
|
const settings = parseChannelSettings(channel.setting)
|
||||||
const isPassThrough = settings.pass_through_body_enabled === true
|
const isPassThrough = settings.pass_through_body_enabled === true
|
||||||
|
const hasParamOverride = Boolean(channel.param_override?.trim())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
@@ -557,13 +561,19 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
{isMultiKey && (
|
{hasParamOverride && (
|
||||||
<StatusBadge
|
<TooltipProvider delay={100}>
|
||||||
label={`${channel.channel_info.multi_key_size} keys`}
|
<Tooltip>
|
||||||
variant='purple'
|
<TooltipTrigger
|
||||||
size='sm'
|
render={
|
||||||
copyable={false}
|
<SlidersHorizontal className='text-info h-3.5 w-3.5 flex-shrink-0' />
|
||||||
/>
|
}
|
||||||
|
></TooltipTrigger>
|
||||||
|
<TooltipContent side='top'>
|
||||||
|
{t('Override request parameters')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
<UpstreamUpdateTags channel={channel} />
|
<UpstreamUpdateTags channel={channel} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-13
@@ -2754,23 +2754,15 @@ export function ChannelMutateDrawer({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<JsonEditor
|
<Textarea
|
||||||
value={field.value || ''}
|
value={field.value || ''}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
keyPlaceholder='temperature'
|
rows={8}
|
||||||
valuePlaceholder='0.7'
|
placeholder={t(
|
||||||
keyLabel='Parameter'
|
'Override request parameters. Cannot override stream parameter.'
|
||||||
valueLabel='Value'
|
|
||||||
emptyMessage={t(
|
|
||||||
'No parameter overrides configured.'
|
|
||||||
)}
|
)}
|
||||||
template={{
|
className='max-h-72 min-h-40 resize-y overflow-auto font-mono text-xs'
|
||||||
temperature: 0.7,
|
|
||||||
max_tokens: 2000,
|
|
||||||
top_p: 1,
|
|
||||||
}}
|
|
||||||
valueType='any'
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
+50
-35
@@ -26,6 +26,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Circle,
|
Circle,
|
||||||
|
Copy,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
FileText,
|
FileText,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
@@ -38,13 +39,14 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { motion, useReducedMotion } from 'motion/react'
|
import { motion, useReducedMotion } from 'motion/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { toast } from 'sonner'
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
import { getUserModels } from '@/lib/api'
|
import { getUserModels } from '@/lib/api'
|
||||||
import { MOTION_TRANSITION } from '@/lib/motion'
|
import { MOTION_TRANSITION } from '@/lib/motion'
|
||||||
import { ROLE } from '@/lib/roles'
|
import { ROLE } from '@/lib/roles'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { CopyButton } from '@/components/copy-button'
|
|
||||||
import {
|
import {
|
||||||
CardStaggerContainer,
|
CardStaggerContainer,
|
||||||
CardStaggerItem,
|
CardStaggerItem,
|
||||||
@@ -104,8 +106,8 @@ interface RequestExample {
|
|||||||
endpoint: string
|
endpoint: string
|
||||||
model: string
|
model: string
|
||||||
keyName: string
|
keyName: string
|
||||||
|
keyId?: number
|
||||||
displayKey: string
|
displayKey: string
|
||||||
curl: string
|
|
||||||
ready: boolean
|
ready: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +181,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute inset-0 bg-[linear-gradient(112deg,oklch(0.97_0.04_250/.92)_0%,oklch(0.95_0.08_315/.82)_38%,oklch(0.96_0.12_92/.78)_74%,oklch(0.94_0.1_132/.62)_100%)] dark:opacity-25',
|
'pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_48%_120%_at_78%_0%,color-mix(in_oklch,var(--primary)_8%,transparent)_0%,transparent_62%),linear-gradient(112deg,color-mix(in_oklch,var(--card)_98%,var(--primary)_2%)_0%,color-mix(in_oklch,var(--card)_94%,var(--muted)_6%)_48%,color-mix(in_oklch,var(--background)_92%,var(--accent)_8%)_100%)] dark:opacity-65',
|
||||||
props.compact
|
props.compact
|
||||||
? '[mask-image:linear-gradient(90deg,black_0%,black_48%,transparent_74%)] opacity-55'
|
? '[mask-image:linear-gradient(90deg,black_0%,black_48%,transparent_74%)] opacity-55'
|
||||||
: 'opacity-85'
|
: 'opacity-85'
|
||||||
@@ -188,7 +190,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono text-lime-100/75 sm:block dark:text-lime-200/25',
|
'text-foreground/5 pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono sm:block dark:text-foreground/8',
|
||||||
props.compact ? 'w-1/2 opacity-45' : 'w-[58%] opacity-75'
|
props.compact ? 'w-1/2 opacity-45' : 'w-[58%] opacity-75'
|
||||||
)}
|
)}
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
@@ -275,12 +277,41 @@ function RequestPreview(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const shouldReduceMotion = useReducedMotion()
|
const shouldReduceMotion = useReducedMotion()
|
||||||
const previewLines = props.example.curl.split('\n').map((line) => {
|
const [isCopying, setIsCopying] = useState(false)
|
||||||
if (line.includes('Authorization: Bearer')) {
|
const { copyToClipboard } = useCopyToClipboard({ notify: false })
|
||||||
return ` -H "Authorization: Bearer ${props.example.displayKey}" \\`
|
const previewCurl = buildCurlCommand({
|
||||||
}
|
endpoint: props.example.endpoint,
|
||||||
return line
|
apiKey: props.example.displayKey,
|
||||||
|
model: props.example.model,
|
||||||
})
|
})
|
||||||
|
const previewLines = previewCurl.split('\n')
|
||||||
|
const handleCopyRequest = async () => {
|
||||||
|
if (!props.example.keyId || isCopying) return
|
||||||
|
|
||||||
|
setIsCopying(true)
|
||||||
|
try {
|
||||||
|
const result = await fetchTokenKey(props.example.keyId)
|
||||||
|
const key = result.success && result.data?.key ? result.data.key : ''
|
||||||
|
if (!key) {
|
||||||
|
toast.error(result.message || t('Failed to copy to clipboard'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const realCurl = buildCurlCommand({
|
||||||
|
endpoint: props.example.endpoint,
|
||||||
|
apiKey: `sk-${key}`,
|
||||||
|
model: props.example.model,
|
||||||
|
})
|
||||||
|
const copied = await copyToClipboard(realCurl)
|
||||||
|
if (copied) {
|
||||||
|
toast.success(t('Copied to clipboard'))
|
||||||
|
} else {
|
||||||
|
toast.error(t('Failed to copy to clipboard'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsCopying(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -315,17 +346,17 @@ function RequestPreview(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.example.ready ? (
|
{props.example.ready ? (
|
||||||
<CopyButton
|
<Button
|
||||||
value={props.example.curl}
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-7 gap-1.5 px-2 text-xs'
|
className='h-7 gap-1.5 px-2 text-xs'
|
||||||
tooltip={t('Copy ready-to-run curl')}
|
disabled={isCopying}
|
||||||
successTooltip={t('Copied!')}
|
onClick={handleCopyRequest}
|
||||||
aria-label={t('Copy ready-to-run curl')}
|
aria-label={t('Copy ready-to-run curl')}
|
||||||
>
|
>
|
||||||
{t('Copy')}
|
<Copy data-icon='inline-start' />
|
||||||
</CopyButton>
|
{isCopying ? t('Loading') : t('Copy')}
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button size='sm' variant='outline' render={<Link to='/keys' />}>
|
<Button size='sm' variant='outline' render={<Link to='/keys' />}>
|
||||||
{t('Create API Key')}
|
{t('Create API Key')}
|
||||||
@@ -463,17 +494,6 @@ export function OverviewDashboard() {
|
|||||||
[apiKeysQuery.data]
|
[apiKeysQuery.data]
|
||||||
)
|
)
|
||||||
|
|
||||||
const realKeyQuery = useQuery({
|
|
||||||
queryKey: ['dashboard', 'overview', 'token-key', preferredKey?.id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!preferredKey?.id) return ''
|
|
||||||
const result = await fetchTokenKey(preferredKey.id)
|
|
||||||
return result.success && result.data?.key ? `sk-${result.data.key}` : ''
|
|
||||||
},
|
|
||||||
enabled: Boolean(preferredKey?.id),
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
const startSteps = useMemo<StartStep[]>(
|
const startSteps = useMemo<StartStep[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -561,23 +581,18 @@ export function OverviewDashboard() {
|
|||||||
const requestExample = useMemo<RequestExample>(() => {
|
const requestExample = useMemo<RequestExample>(() => {
|
||||||
const endpoint = normalizeEndpoint(apiInfoItems[0]?.url)
|
const endpoint = normalizeEndpoint(apiInfoItems[0]?.url)
|
||||||
const model = modelsQuery.data?.[0] ?? 'gpt-4o-mini'
|
const model = modelsQuery.data?.[0] ?? 'gpt-4o-mini'
|
||||||
const apiKey = realKeyQuery.data ?? ''
|
|
||||||
const keyName = preferredKey?.name ?? t('No API key yet')
|
const keyName = preferredKey?.name ?? t('No API key yet')
|
||||||
const ready = Boolean(apiKey && model)
|
const ready = Boolean(preferredKey?.id && model)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
endpoint,
|
endpoint,
|
||||||
model,
|
model,
|
||||||
keyName,
|
keyName,
|
||||||
displayKey: formatDisplayKey(apiKey),
|
keyId: preferredKey?.id,
|
||||||
|
displayKey: preferredKey ? formatDisplayKey(`sk-${preferredKey.key}`) : 'sk-...',
|
||||||
ready,
|
ready,
|
||||||
curl: buildCurlCommand({
|
|
||||||
endpoint,
|
|
||||||
apiKey: apiKey || 'sk-...',
|
|
||||||
model,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}, [apiInfoItems, modelsQuery.data, preferredKey, realKeyQuery.data, t])
|
}, [apiInfoItems, modelsQuery.data, preferredKey, t])
|
||||||
|
|
||||||
const completedStepCount = startSteps.filter((step) => step.completed).length
|
const completedStepCount = startSteps.filter((step) => step.completed).length
|
||||||
const setupComplete = completedStepCount === startSteps.length
|
const setupComplete = completedStepCount === startSteps.length
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export function createDurationColumn<T>(config: {
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn('font-mono', durationBgMap[variant])}
|
className={cn('rounded-md font-mono', durationBgMap[variant])}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
+29
-11
@@ -90,6 +90,12 @@ function getGroupRatioText(other: LogOtherData | null): string | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitQuotaDisplay(value: string): { prefix: string; amount: string } {
|
||||||
|
const match = value.match(/^([^0-9+\-.,\s]+)(.+)$/)
|
||||||
|
if (!match) return { prefix: '', amount: value }
|
||||||
|
return { prefix: match[1], amount: match[2] }
|
||||||
|
}
|
||||||
|
|
||||||
function buildDetailSegments(
|
function buildDetailSegments(
|
||||||
log: UsageLog,
|
log: UsageLog,
|
||||||
other: LogOtherData | null,
|
other: LogOtherData | null,
|
||||||
@@ -277,6 +283,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
variant={config.color as StatusBadgeProps['variant']}
|
variant={config.color as StatusBadgeProps['variant']}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
|
className='!text-xs [&_span]:!text-xs'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -295,6 +302,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
columns.push(
|
columns.push(
|
||||||
{
|
{
|
||||||
id: 'channel',
|
id: 'channel',
|
||||||
|
accessorFn: (row) => row.channel,
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title={t('Channel')} />
|
<DataTableColumnHeader column={column} title={t('Channel')} />
|
||||||
),
|
),
|
||||||
@@ -395,10 +403,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('Channel'), mobileHidden: true },
|
meta: { label: t('Channel') },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'user',
|
id: 'user',
|
||||||
|
accessorFn: (row) => row.username,
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title={t('User')} />
|
<DataTableColumnHeader column={column} title={t('User')} />
|
||||||
),
|
),
|
||||||
@@ -419,7 +428,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
setUserInfoDialogOpen(true)
|
setUserInfoDialogOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar className='ring-border/60 size-6 ring-1'>
|
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] font-semibold',
|
'text-[11px] font-semibold',
|
||||||
@@ -451,7 +460,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('User'), mobileHidden: true },
|
meta: { label: t('User') },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -555,7 +564,9 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
? log.completion_tokens / useTime
|
? log.completion_tokens / useTime
|
||||||
: null
|
: null
|
||||||
const timeVariant = getResponseTimeColor(useTime, log.completion_tokens)
|
const timeVariant = getResponseTimeColor(useTime, log.completion_tokens)
|
||||||
const frtVariant = frt ? getFirstResponseTimeColor(frt / 1000) : 'neutral'
|
const frtVariant = frt
|
||||||
|
? getFirstResponseTimeColor(frt / 1000)
|
||||||
|
: 'neutral'
|
||||||
|
|
||||||
const timingBgMap: Record<string, string> = {
|
const timingBgMap: Record<string, string> = {
|
||||||
success:
|
success:
|
||||||
@@ -576,7 +587,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
variant={timeVariant as StatusBadgeProps['variant']}
|
variant={timeVariant as StatusBadgeProps['variant']}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn('font-mono', timingBgMap[timeVariant])}
|
className={cn('rounded-md font-mono', timingBgMap[timeVariant])}
|
||||||
/>
|
/>
|
||||||
{log.is_stream &&
|
{log.is_stream &&
|
||||||
(frt != null && frt > 0 ? (
|
(frt != null && frt > 0 ? (
|
||||||
@@ -586,7 +597,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
size='sm'
|
size='sm'
|
||||||
showDot={false}
|
showDot={false}
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={cn('font-mono', timingBgMap[frtVariant])}
|
className={cn(
|
||||||
|
'rounded-md font-mono',
|
||||||
|
timingBgMap[frtVariant]
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -595,7 +609,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
size='sm'
|
size='sm'
|
||||||
showDot={false}
|
showDot={false}
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className={timingBgMap.neutral}
|
className={cn('rounded-md font-mono', timingBgMap.neutral)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -641,7 +655,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('Timing'), mobileHidden: true },
|
meta: { label: t('Timing') },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -692,7 +706,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: 'Tokens', mobileHidden: true },
|
meta: { label: 'Tokens' },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -734,11 +748,15 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const quotaStr = formatLogQuota(quota)
|
const quotaStr = formatLogQuota(quota)
|
||||||
|
const quotaDisplay = splitQuotaDisplay(quotaStr)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-0.5'>
|
<div className='flex flex-col gap-0.5'>
|
||||||
<span className='border-border/80 bg-muted/60 inline-flex w-fit items-center rounded-md border px-1.5 py-0.5 [font-family:var(--font-body)] font-semibold tabular-nums'>
|
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 text-sm leading-none [font-family:var(--font-body)] font-semibold tabular-nums'>
|
||||||
{quotaStr}
|
{quotaDisplay.prefix && (
|
||||||
|
<span className='mr-1'>{quotaDisplay.prefix}</span>
|
||||||
|
)}
|
||||||
|
<span>{quotaDisplay.amount}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -231,7 +231,7 @@ export function useDrawingLogsColumns(
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('Image'), mobileHidden: true },
|
meta: { label: t('Image') },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'prompt',
|
accessorKey: 'prompt',
|
||||||
@@ -268,7 +268,7 @@ export function useDrawingLogsColumns(
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('Prompt'), mobileHidden: true },
|
meta: { label: t('Prompt') },
|
||||||
size: 200,
|
size: 200,
|
||||||
maxSize: 220,
|
maxSize: 220,
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-2
@@ -123,6 +123,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
columns.push(createChannelColumn<TaskLog>({ headerLabel: t('Channel') }), {
|
columns.push(createChannelColumn<TaskLog>({ headerLabel: t('Channel') }), {
|
||||||
id: 'user',
|
id: 'user',
|
||||||
|
accessorFn: (row) => row.username || row.user_id,
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title={t('User')} />
|
<DataTableColumnHeader column={column} title={t('User')} />
|
||||||
),
|
),
|
||||||
@@ -142,7 +143,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
|||||||
setUserInfoDialogOpen(true)
|
setUserInfoDialogOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar className='ring-border/60 size-6 ring-1'>
|
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] font-semibold',
|
'text-[11px] font-semibold',
|
||||||
@@ -161,7 +162,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('User'), mobileHidden: true },
|
meta: { label: t('User') },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ function ModelBadgeContent(props: ModelBadgeProps) {
|
|||||||
showDot={!provider}
|
showDot={!provider}
|
||||||
autoColor={provider ? undefined : props.modelName}
|
autoColor={provider ? undefined : props.modelName}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-border/60 bg-muted/30 h-auto min-h-6 gap-1.5 rounded-md border px-2 py-0.5 [font-family:var(--font-body)] break-all whitespace-normal',
|
'border-border/60 bg-muted/30 h-6 max-w-full gap-1.5 rounded-md border px-2 [font-family:var(--font-body)]',
|
||||||
provider && 'text-foreground',
|
provider && 'text-foreground',
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className='flex min-w-0 items-center gap-1.5'>
|
<span className='flex max-w-full min-w-0 items-center gap-1.5'>
|
||||||
{provider && (
|
{provider && (
|
||||||
<span
|
<span
|
||||||
className='flex size-3.5 shrink-0 items-center justify-center'
|
className='flex size-3.5 shrink-0 items-center justify-center'
|
||||||
@@ -116,7 +116,7 @@ function ModelBadgeContent(props: ModelBadgeProps) {
|
|||||||
{getLobeIcon(provider.icon, 14)}
|
{getLobeIcon(provider.icon, 14)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span>{props.modelName}</span>
|
<span className='truncate'>{props.modelName}</span>
|
||||||
</span>
|
</span>
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2023-2026 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
type Cell,
|
||||||
|
type Table,
|
||||||
|
} from '@tanstack/react-table'
|
||||||
|
import { Database } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { formatTimestampToDate } from '@/lib/format'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from '@/components/ui/empty'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { dotColorMap, textColorMap, type StatusVariant } from '@/components/status-badge'
|
||||||
|
import type { LogCategory } from '../types'
|
||||||
|
import { LOG_TYPE_ENUM } from '../constants'
|
||||||
|
import { getLogTypeConfig } from '../lib/utils'
|
||||||
|
|
||||||
|
const logTypeRowTint: Record<number, string> = {
|
||||||
|
[LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20 border-rose-200/50 dark:border-rose-900/30',
|
||||||
|
[LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15 border-blue-200/50 dark:border-blue-900/30',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsageLogsMobileListProps<TData> {
|
||||||
|
table: Table<TData>
|
||||||
|
isLoading?: boolean
|
||||||
|
emptyTitle?: string
|
||||||
|
emptyDescription?: string
|
||||||
|
logCategory: LogCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsageLogsMobileSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className='overflow-hidden rounded-lg border border-border/50 bg-card'>
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className='space-y-2.5 border-b border-border/40 p-3 last:border-b-0'
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-3'>
|
||||||
|
<Skeleton className='h-5 w-40 rounded-md' />
|
||||||
|
<Skeleton className='h-5 w-16 rounded-md' />
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-2 gap-x-4 gap-y-2'>
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((j) => (
|
||||||
|
<div key={j} className='min-w-0 space-y-1'>
|
||||||
|
<Skeleton className='h-3 w-10 rounded' />
|
||||||
|
<Skeleton className='h-4 w-full rounded' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompactCell<TData>({
|
||||||
|
cell,
|
||||||
|
fallback = '-',
|
||||||
|
className,
|
||||||
|
primaryOnly = false,
|
||||||
|
}: {
|
||||||
|
cell?: Cell<TData, unknown>
|
||||||
|
fallback?: string
|
||||||
|
className?: string
|
||||||
|
primaryOnly?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 overflow-hidden leading-tight [&_button]:max-w-full [&_span]:max-w-full',
|
||||||
|
primaryOnly &&
|
||||||
|
'[&_.flex-col>*:not(:first-child)]:hidden [&_.flex-col]:min-w-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cell ? (
|
||||||
|
flexRender(cell.column.columnDef.cell, cell.getContext())
|
||||||
|
) : (
|
||||||
|
<span className='text-muted-foreground/50'>{fallback}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryField<TData>({
|
||||||
|
label,
|
||||||
|
cell,
|
||||||
|
className,
|
||||||
|
valueClassName,
|
||||||
|
primaryOnly = false,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
cell?: Cell<TData, unknown>
|
||||||
|
className?: string
|
||||||
|
valueClassName?: string
|
||||||
|
primaryOnly?: boolean
|
||||||
|
}) {
|
||||||
|
if (!cell) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 rounded-md bg-muted/20 px-2 py-1.5',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<CompactCell
|
||||||
|
cell={cell}
|
||||||
|
primaryOnly={primaryOnly}
|
||||||
|
className={valueClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileLogTimeStatus({
|
||||||
|
createdAt,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
createdAt: unknown
|
||||||
|
type: unknown
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const timestamp = typeof createdAt === 'number' ? createdAt : undefined
|
||||||
|
const logType = typeof type === 'number' ? type : undefined
|
||||||
|
const config = getLogTypeConfig(logType ?? LOG_TYPE_ENUM.UNKNOWN)
|
||||||
|
const variant = config.color as StatusVariant
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<div className='font-mono text-xs leading-tight tabular-nums'>
|
||||||
|
{formatTimestampToDate(timestamp)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1 text-xs leading-none font-medium',
|
||||||
|
textColorMap[variant]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn('size-1.5 shrink-0 rounded-full', dotColorMap[variant])}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
<span>{t(config.label)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommonLogsCard<TData>({
|
||||||
|
cells,
|
||||||
|
}: {
|
||||||
|
cells: Map<string, Cell<TData, unknown>>
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const modelCell = cells.get('model_name')
|
||||||
|
const quotaCell = cells.get('quota')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-2.5'>
|
||||||
|
<div className='flex min-w-0 items-start justify-between gap-3'>
|
||||||
|
<CompactCell cell={modelCell} className='flex-1' />
|
||||||
|
<CompactCell
|
||||||
|
cell={quotaCell}
|
||||||
|
className='shrink-0 text-right [&_span]:!h-6 [&_span]:!px-2 [&_span]:!text-sm [&_span]:!leading-none'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)] gap-1.5'>
|
||||||
|
<div className='min-w-0 rounded-md bg-muted/20 px-2 py-1.5'>
|
||||||
|
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
|
||||||
|
{t('Time')}
|
||||||
|
</div>
|
||||||
|
<MobileLogTimeStatus
|
||||||
|
createdAt={cells.get('created_at')?.row.original?.created_at}
|
||||||
|
type={cells.get('created_at')?.row.original?.type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Channel')}
|
||||||
|
cell={cells.get('channel')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField label={t('User')} cell={cells.get('user')} primaryOnly />
|
||||||
|
<SummaryField
|
||||||
|
label={t('Token')}
|
||||||
|
cell={cells.get('token_name')}
|
||||||
|
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-[11px] [&_.flex-col>*:not(:first-child)]:leading-none'
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Timing')}
|
||||||
|
cell={cells.get('use_time')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Tokens')}
|
||||||
|
cell={cells.get('prompt_tokens')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Details')}
|
||||||
|
cell={cells.get('content')}
|
||||||
|
className='col-span-2 bg-transparent px-0 py-0'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskLogsCard<TData>({
|
||||||
|
cells,
|
||||||
|
}: {
|
||||||
|
cells: Map<string, Cell<TData, unknown>>
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const taskIdCell = cells.get('task_id')
|
||||||
|
const statusCell = cells.get('status')
|
||||||
|
const submitTimeCell = cells.get('submit_time')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-2.5'>
|
||||||
|
<div className='flex min-w-0 items-start justify-between gap-3'>
|
||||||
|
<CompactCell cell={taskIdCell} className='flex-1' />
|
||||||
|
<CompactCell cell={statusCell} className='shrink-0 text-right' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-2 gap-1.5'>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Submit Time')}
|
||||||
|
cell={submitTimeCell}
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('User')}
|
||||||
|
cell={cells.get('user')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Result')}
|
||||||
|
cell={cells.get('fail_reason')}
|
||||||
|
className='col-span-2 bg-transparent px-0 py-0'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawingLogsCard<TData>({
|
||||||
|
cells,
|
||||||
|
}: {
|
||||||
|
cells: Map<string, Cell<TData, unknown>>
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const actionCell = cells.get('action')
|
||||||
|
const codeCell = cells.get('code')
|
||||||
|
const submitTimeCell = cells.get('submit_time')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-2.5'>
|
||||||
|
<div className='flex min-w-0 items-start justify-between gap-3'>
|
||||||
|
<CompactCell cell={actionCell} className='flex-1' />
|
||||||
|
<CompactCell cell={codeCell} className='shrink-0 text-right' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-2 gap-1.5'>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Submit Time')}
|
||||||
|
cell={submitTimeCell}
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Channel')}
|
||||||
|
cell={cells.get('channel')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Task ID')}
|
||||||
|
cell={cells.get('mj_id')}
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Duration')}
|
||||||
|
cell={cells.get('duration')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Image')}
|
||||||
|
cell={cells.get('image_url')}
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Prompt')}
|
||||||
|
cell={cells.get('prompt')}
|
||||||
|
primaryOnly
|
||||||
|
/>
|
||||||
|
<SummaryField
|
||||||
|
label={t('Fail Reason')}
|
||||||
|
cell={cells.get('fail_reason')}
|
||||||
|
className='col-span-2 bg-transparent px-0 py-0'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UsageLogsMobileList<TData>({
|
||||||
|
table,
|
||||||
|
isLoading = false,
|
||||||
|
emptyTitle,
|
||||||
|
emptyDescription,
|
||||||
|
logCategory,
|
||||||
|
}: UsageLogsMobileListProps<TData>) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const resolvedEmptyTitle = emptyTitle ?? t('No Logs Found')
|
||||||
|
const resolvedEmptyDescription =
|
||||||
|
emptyDescription ??
|
||||||
|
t('No usage logs available. Logs will appear here once API calls are made.')
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <UsageLogsMobileSkeleton />
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = table.getRowModel().rows
|
||||||
|
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border p-6">
|
||||||
|
<Empty className="border-none p-0">
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia variant="icon">
|
||||||
|
<Database className="size-6" />
|
||||||
|
</EmptyMedia>
|
||||||
|
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
|
||||||
|
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='overflow-hidden rounded-lg border border-border/50 bg-card'>
|
||||||
|
{rows.map((row) => {
|
||||||
|
const cells = new Map(
|
||||||
|
row.getVisibleCells().map((cell) => [cell.column.id, cell])
|
||||||
|
)
|
||||||
|
|
||||||
|
const logType = (row.original as Record<string, unknown>).type as
|
||||||
|
| number
|
||||||
|
| undefined
|
||||||
|
const tintClass = logType != null ? (logTypeRowTint[logType] ?? '') : ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={row.id}
|
||||||
|
className={cn(
|
||||||
|
'border-l-2 border-l-transparent border-b border-border/40 p-3 transition-colors last:border-b-0',
|
||||||
|
tintClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{logCategory === 'common' && (
|
||||||
|
<CommonLogsCard cells={cells} />
|
||||||
|
)}
|
||||||
|
{logCategory === 'task' && (
|
||||||
|
<TaskLogsCard cells={cells} />
|
||||||
|
)}
|
||||||
|
{logCategory === 'drawing' && (
|
||||||
|
<DrawingLogsCard cells={cells} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import { fetchLogsByCategory } from '../lib/utils'
|
|||||||
import type { LogCategory } from '../types'
|
import type { LogCategory } from '../types'
|
||||||
import { CommonLogsFilterBar } from './common-logs-filter-bar'
|
import { CommonLogsFilterBar } from './common-logs-filter-bar'
|
||||||
import { TaskLogsFilterBar } from './task-logs-filter-bar'
|
import { TaskLogsFilterBar } from './task-logs-filter-bar'
|
||||||
|
import { UsageLogsMobileList } from './usage-logs-mobile-card'
|
||||||
|
|
||||||
const route = getRouteApi('/_authenticated/usage-logs/$section')
|
const route = getRouteApi('/_authenticated/usage-logs/$section')
|
||||||
|
|
||||||
@@ -190,6 +191,13 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
|
|||||||
'[&_[data-slot=table]]:text-[13px] [&_[data-slot=table]_td]:text-[13px] [&_[data-slot=table]_td_*]:text-[13px] [&_[data-slot=table]_th]:text-[13px] [&_[data-slot=table]_th_*]:text-[13px]'
|
'[&_[data-slot=table]]:text-[13px] [&_[data-slot=table]_td]:text-[13px] [&_[data-slot=table]_td_*]:text-[13px] [&_[data-slot=table]_th]:text-[13px] [&_[data-slot=table]_th_*]:text-[13px]'
|
||||||
)}
|
)}
|
||||||
tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
|
tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
|
||||||
|
mobile={
|
||||||
|
<UsageLogsMobileList
|
||||||
|
table={table}
|
||||||
|
isLoading={isLoadingData}
|
||||||
|
logCategory={logCategory}
|
||||||
|
/>
|
||||||
|
}
|
||||||
toolbar={
|
toolbar={
|
||||||
isCommon ? (
|
isCommon ? (
|
||||||
<CommonLogsFilterBar table={table} />
|
<CommonLogsFilterBar table={table} />
|
||||||
|
|||||||
Vendored
+1
@@ -2778,6 +2778,7 @@
|
|||||||
"Override auto-discovered endpoint": "Override auto-discovered endpoint",
|
"Override auto-discovered endpoint": "Override auto-discovered endpoint",
|
||||||
"Override request headers": "Override request headers",
|
"Override request headers": "Override request headers",
|
||||||
"Override request headers (JSON format)": "Override request headers (JSON format)",
|
"Override request headers (JSON format)": "Override request headers (JSON format)",
|
||||||
|
"Override request parameters": "Override request parameters",
|
||||||
"Override request parameters (JSON format)": "Override request parameters (JSON format)",
|
"Override request parameters (JSON format)": "Override request parameters (JSON format)",
|
||||||
"Override request parameters. Cannot override": "Override request parameters. Cannot override",
|
"Override request parameters. Cannot override": "Override request parameters. Cannot override",
|
||||||
"Override request parameters. Cannot override stream parameter.": "Override request parameters. Cannot override stream parameter.",
|
"Override request parameters. Cannot override stream parameter.": "Override request parameters. Cannot override stream parameter.",
|
||||||
|
|||||||
Vendored
+1
@@ -2778,6 +2778,7 @@
|
|||||||
"Override auto-discovered endpoint": "Remplacer le point de terminaison auto-découvert",
|
"Override auto-discovered endpoint": "Remplacer le point de terminaison auto-découvert",
|
||||||
"Override request headers": "Remplacer les en-têtes de requête",
|
"Override request headers": "Remplacer les en-têtes de requête",
|
||||||
"Override request headers (JSON format)": "Surcharge des en-têtes de requête (format JSON)",
|
"Override request headers (JSON format)": "Surcharge des en-têtes de requête (format JSON)",
|
||||||
|
"Override request parameters": "Remplacer les paramètres de requête",
|
||||||
"Override request parameters (JSON format)": "Remplacer les paramètres de requête (format JSON)",
|
"Override request parameters (JSON format)": "Remplacer les paramètres de requête (format JSON)",
|
||||||
"Override request parameters. Cannot override": "Remplacer les paramètres de requête. Impossible de remplacer",
|
"Override request parameters. Cannot override": "Remplacer les paramètres de requête. Impossible de remplacer",
|
||||||
"Override request parameters. Cannot override stream parameter.": "Remplace les paramètres de requête. Impossible de remplacer le paramètre stream.",
|
"Override request parameters. Cannot override stream parameter.": "Remplace les paramètres de requête. Impossible de remplacer le paramètre stream.",
|
||||||
|
|||||||
Vendored
+1
@@ -2778,6 +2778,7 @@
|
|||||||
"Override auto-discovered endpoint": "自動検出されたエンドポイントを上書きする",
|
"Override auto-discovered endpoint": "自動検出されたエンドポイントを上書きする",
|
||||||
"Override request headers": "リクエストヘッダーを上書きする",
|
"Override request headers": "リクエストヘッダーを上書きする",
|
||||||
"Override request headers (JSON format)": "リクエストヘッダーのオーバーライド (JSON 形式)",
|
"Override request headers (JSON format)": "リクエストヘッダーのオーバーライド (JSON 形式)",
|
||||||
|
"Override request parameters": "リクエストパラメータを上書き",
|
||||||
"Override request parameters (JSON format)": "リクエストパラメータの上書き (JSON形式)",
|
"Override request parameters (JSON format)": "リクエストパラメータの上書き (JSON形式)",
|
||||||
"Override request parameters. Cannot override": "リクエストパラメーターを上書きします。上書きできません",
|
"Override request parameters. Cannot override": "リクエストパラメーターを上書きします。上書きできません",
|
||||||
"Override request parameters. Cannot override stream parameter.": "リクエストパラメータを上書きします。stream パラメータは上書きできません。",
|
"Override request parameters. Cannot override stream parameter.": "リクエストパラメータを上書きします。stream パラメータは上書きできません。",
|
||||||
|
|||||||
Vendored
+1
@@ -2778,6 +2778,7 @@
|
|||||||
"Override auto-discovered endpoint": "Переопределить автоматически обнаруженную конечную точку",
|
"Override auto-discovered endpoint": "Переопределить автоматически обнаруженную конечную точку",
|
||||||
"Override request headers": "Переопределить заголовки запроса",
|
"Override request headers": "Переопределить заголовки запроса",
|
||||||
"Override request headers (JSON format)": "Переопределение заголовков запроса (формат JSON)",
|
"Override request headers (JSON format)": "Переопределение заголовков запроса (формат JSON)",
|
||||||
|
"Override request parameters": "Переопределить параметры запроса",
|
||||||
"Override request parameters (JSON format)": "Переопределить параметры запроса (формат JSON)",
|
"Override request parameters (JSON format)": "Переопределить параметры запроса (формат JSON)",
|
||||||
"Override request parameters. Cannot override": "Переопределить параметры запроса. Невозможно переопределить",
|
"Override request parameters. Cannot override": "Переопределить параметры запроса. Невозможно переопределить",
|
||||||
"Override request parameters. Cannot override stream parameter.": "Переопределяет параметры запроса. Параметр stream переопределить нельзя.",
|
"Override request parameters. Cannot override stream parameter.": "Переопределяет параметры запроса. Параметр stream переопределить нельзя.",
|
||||||
|
|||||||
Vendored
+1
@@ -2778,6 +2778,7 @@
|
|||||||
"Override auto-discovered endpoint": "Ghi đè điểm cuối tự động phát hiện",
|
"Override auto-discovered endpoint": "Ghi đè điểm cuối tự động phát hiện",
|
||||||
"Override request headers": "Ghi đè tiêu đề yêu cầu",
|
"Override request headers": "Ghi đè tiêu đề yêu cầu",
|
||||||
"Override request headers (JSON format)": "Ghi đè tiêu đề yêu cầu (định dạng JSON)",
|
"Override request headers (JSON format)": "Ghi đè tiêu đề yêu cầu (định dạng JSON)",
|
||||||
|
"Override request parameters": "Ghi đè tham số yêu cầu",
|
||||||
"Override request parameters (JSON format)": "Ghi đè tham số yêu cầu (định dạng JSON)",
|
"Override request parameters (JSON format)": "Ghi đè tham số yêu cầu (định dạng JSON)",
|
||||||
"Override request parameters. Cannot override": "Ghi đè tham số yêu cầu. Không thể ghi đè",
|
"Override request parameters. Cannot override": "Ghi đè tham số yêu cầu. Không thể ghi đè",
|
||||||
"Override request parameters. Cannot override stream parameter.": "Ghi đè tham số yêu cầu. Không thể ghi đè tham số stream.",
|
"Override request parameters. Cannot override stream parameter.": "Ghi đè tham số yêu cầu. Không thể ghi đè tham số stream.",
|
||||||
|
|||||||
Vendored
+1
@@ -2778,6 +2778,7 @@
|
|||||||
"Override auto-discovered endpoint": "覆盖自动发现的端点",
|
"Override auto-discovered endpoint": "覆盖自动发现的端点",
|
||||||
"Override request headers": "覆盖请求标头",
|
"Override request headers": "覆盖请求标头",
|
||||||
"Override request headers (JSON format)": "覆盖请求头(JSON 格式)",
|
"Override request headers (JSON format)": "覆盖请求头(JSON 格式)",
|
||||||
|
"Override request parameters": "覆盖请求参数",
|
||||||
"Override request parameters (JSON format)": "覆盖请求参数 (JSON 格式)",
|
"Override request parameters (JSON format)": "覆盖请求参数 (JSON 格式)",
|
||||||
"Override request parameters. Cannot override": "覆盖请求参数。无法覆盖",
|
"Override request parameters. Cannot override": "覆盖请求参数。无法覆盖",
|
||||||
"Override request parameters. Cannot override stream parameter.": "覆盖请求参数。无法覆盖 stream 参数。",
|
"Override request parameters. Cannot override stream parameter.": "覆盖请求参数。无法覆盖 stream 参数。",
|
||||||
|
|||||||
Vendored
+1
@@ -315,6 +315,7 @@ export const STATIC_I18N_KEYS = [
|
|||||||
'Regex Replace',
|
'Regex Replace',
|
||||||
'Return Error',
|
'Return Error',
|
||||||
'Param Override',
|
'Param Override',
|
||||||
|
'Override request parameters',
|
||||||
|
|
||||||
// Profile / 2FA
|
// Profile / 2FA
|
||||||
'Backed up',
|
'Backed up',
|
||||||
|
|||||||
Vendored
+1
-1
@@ -292,7 +292,7 @@ function formatCurrencyValue(
|
|||||||
maximumFractionDigits: digits,
|
maximumFractionDigits: digits,
|
||||||
}).format(adjustedValue)
|
}).format(adjustedValue)
|
||||||
|
|
||||||
return `${meta.symbol}${decimal}`
|
return `${meta.symbol} ${decimal}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user