fix(web): improve channel and usage log UI

Fixes #5121
This commit is contained in:
CaIon
2026-05-26 20:28:28 +08:00
parent f8add4ca49
commit dc245ae764
21 changed files with 579 additions and 116 deletions
@@ -26,6 +26,7 @@ import {
ChevronDown,
ChevronUp,
Circle,
Copy,
CreditCard,
FileText,
KeyRound,
@@ -38,13 +39,14 @@ import {
} from 'lucide-react'
import { motion, useReducedMotion } from 'motion/react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useAuthStore } from '@/stores/auth-store'
import { getUserModels } from '@/lib/api'
import { MOTION_TRANSITION } from '@/lib/motion'
import { ROLE } from '@/lib/roles'
import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Button } from '@/components/ui/button'
import { CopyButton } from '@/components/copy-button'
import {
CardStaggerContainer,
CardStaggerItem,
@@ -104,8 +106,8 @@ interface RequestExample {
endpoint: string
model: string
keyName: string
keyId?: number
displayKey: string
curl: string
ready: boolean
}
@@ -179,7 +181,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
<>
<div
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
? '[mask-image:linear-gradient(90deg,black_0%,black_48%,transparent_74%)] opacity-55'
: 'opacity-85'
@@ -188,7 +190,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
/>
<div
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'
)}
aria-hidden='true'
@@ -275,12 +277,41 @@ function RequestPreview(props: {
}) {
const { t } = useTranslation()
const shouldReduceMotion = useReducedMotion()
const previewLines = props.example.curl.split('\n').map((line) => {
if (line.includes('Authorization: Bearer')) {
return ` -H "Authorization: Bearer ${props.example.displayKey}" \\`
}
return line
const [isCopying, setIsCopying] = useState(false)
const { copyToClipboard } = useCopyToClipboard({ notify: false })
const previewCurl = buildCurlCommand({
endpoint: props.example.endpoint,
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 (
<motion.div
@@ -315,17 +346,17 @@ function RequestPreview(props: {
</div>
</div>
{props.example.ready ? (
<CopyButton
value={props.example.curl}
<Button
variant='outline'
size='sm'
className='h-7 gap-1.5 px-2 text-xs'
tooltip={t('Copy ready-to-run curl')}
successTooltip={t('Copied!')}
disabled={isCopying}
onClick={handleCopyRequest}
aria-label={t('Copy ready-to-run curl')}
>
{t('Copy')}
</CopyButton>
<Copy data-icon='inline-start' />
{isCopying ? t('Loading') : t('Copy')}
</Button>
) : (
<Button size='sm' variant='outline' render={<Link to='/keys' />}>
{t('Create API Key')}
@@ -463,17 +494,6 @@ export function OverviewDashboard() {
[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[]>(
() => [
{
@@ -561,23 +581,18 @@ export function OverviewDashboard() {
const requestExample = useMemo<RequestExample>(() => {
const endpoint = normalizeEndpoint(apiInfoItems[0]?.url)
const model = modelsQuery.data?.[0] ?? 'gpt-4o-mini'
const apiKey = realKeyQuery.data ?? ''
const keyName = preferredKey?.name ?? t('No API key yet')
const ready = Boolean(apiKey && model)
const ready = Boolean(preferredKey?.id && model)
return {
endpoint,
model,
keyName,
displayKey: formatDisplayKey(apiKey),
keyId: preferredKey?.id,
displayKey: preferredKey ? formatDisplayKey(`sk-${preferredKey.key}`) : 'sk-...',
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 setupComplete = completedStepCount === startSteps.length