8b2b03d276
* 🎨 feat(web/default): add shadcn-style theme presets, radius prefs, and fix selection badges Integrate the qn-platform–style OKLCH color system into the default frontend while keeping the existing blue-tinted dark tokens for the default theme. Add [data-theme-preset] palettes for seven named presets plus the default zinc-like scale, define [data-theme-radius] overrides so user radius beats preset --radius, and align the Tailwind @custom-variant dark helper with .dark usage. Introduce ThemeCustomizationProvider to own preset and radius state, persist choices in cookies (theme-preset, theme-radius), and sync data-theme-preset / data-theme-radius on <html>. Wrap the tree in main.tsx. Extend ConfigDrawer with theme preset swatches (scoped data-theme-preset) and radius previews wired to context; refactor swatch/card markup so selected CircleCheck badges sit outside clipped rows (remove outer overflow-hidden that hid the centered checkmark). Add i18n keys for preset names, radius, and accessibility labels across en, zh, fr, ja, ru, vi. * 🎨 fix(web): align segmented controls with theme radius tokens - Replace hard-coded inner pill radii (rounded-[5px]) on dashboard chart toolbars with radius-md so the active state follows --radius when users change Radius in Theme Settings. - Use nested radii consistent with TabsList/TabsTrigger: outer rounded-lg (var(--radius)) and inner rounded-md (calc(var(--radius) - 2px)) so the track and active thumb stay concentric at small scales (e.g. 0.3rem) instead of a squared “focus” block inside a rounded shell. - Apply the same pattern to pricing SegmentedControl and the segmented groups in consumption-distribution-chart, model-charts, and user-charts. Verified: bun run typecheck (web/default) * ✨ feat(pricing): enrich model details with uptime sparkline and API documentation Add a compact 30-day uptime sparkline (OpenRouter-style bars + aggregate %) with per-day tooltips, surface it in a status row under quick stats and in the per-group performance table, and extend mock data so uptime series are stable and optionally scoped by group. Introduce an API tab with Shiki-highlighted code samples (cURL, Python, TypeScript, JavaScript), endpoint-type switching, authentication guidance, a supported-parameters table, and mock per-group RPM/TPM/RPD limits. Infer vendor, tokenizer, license, and data-retention hints for a provider & data privacy card on the Overview tab (capabilities/modalities stay with model identity; rate limits stay with the API tab). Update i18n for all new user-facing strings across en, zh, fr, ja, ru, and vi. * 🏆 feat(rankings): add comprehensive rankings dashboard Add a mock-data powered rankings experience with period tabs, model, app, and vendor leaderboards, market share and history charts, movers, new releases, and per-category sections while backend analytics are pending. Link ranked models to pricing details and ranked vendors to filtered pricing results, and include localized copy for all supported frontend locales. * fix(theme): correct theme preset selection state - update Base UI Radio selectors to use data-checked/data-unchecked states. - fix unchecked theme options still showing selected indicators. - isolate the default theme preview tokens to prevent preset changes from leaking into it. * fix(setup): correct usage mode radio state - use Base UI data-checked/data-unchecked states for RadioGroup styling. - hide radio indicators when options are unchecked to avoid setup page display issues. - drive usage mode card and icon selection styles from Base UI state. * fix(auth): submit sign-in and sign-up forms * 🎨 refactor: Align default theme with shadcn Base Nova and prune legacy customization Migrate shadcn UI to Base UI primitives via CLI (`base-nova` / `components.json`) and reinstall full component registry with `--overwrite`, including Hugeicons-backed widgets and newly added registry components. - Remove custom multi-preset/theme-radius system (`ThemeCustomizationProvider`, cookies, preset UI from config drawer); rely on official semantic CSS tokens + light/dark only. - Replace `theme.css` with shadcn’s documented neutral `:root`/`.dark` palette and `@theme inline` mappings (plus skeleton token vars for existing shimmer usage). - Update global styles for Base UI: collapsible animation uses `--collapsible-panel-height`; clarify scroll-lock override comment. Application compatibility: - Keep minimal shims where app code diverged from official APIs (popover collision props, combobox legacy `options` callers, Spinner prop typing). - Switch interactive styling from Radix-era `data-state` / `--radix-*` selectors to Base UI semantics (`data-open`, `data-popup-open`, `data-panel-open`, `--anchor-width`, etc.) Tooling / docs / build: - Rename Rsbuild vendor chunk grouping to `@base-ui` + transitive `@radix-ui`. - Refresh AGENTS.md / CLAUDE.md / classic→default sync skill for Base UI stack. - Bump `package.json` / lockfile for shadcn-postinstall deps (Hugeicons, chart stack, themes, etc.) Verified: `bun run typecheck` passes. Note: `bun run lint` still reports pre-existing hooks rule violations elsewhere; not addressed in this change. * 🎨 chore(web/default): unify table toolbar, relocate usage stats, refine filters - Refactor DataTableToolbar to a single wrapping flex row with a right-aligned action cluster (Reset / Search / View / Expand) for a cleaner Ant Design Pro–style filter bar; remove the dedicated stats row and the toolbar `stats` prop. - Move Common Logs summary badges (Usage / RPM / TPM) and the sensitive- data visibility toggle into the page header via CommonLogsHeaderActions and SectionPageLayout.Actions so the toolbar stays focused on filters. - Slim CommonLogsFilterBar props (no stats / preActions eye control). - Improve CompactDateTimeRangePicker: show minute-precision labels on the trigger (seconds omitted; aligns with datetime-local inputs); widen the trigger on sm+ breakpoints so the full range is visible without truncation; apply the same width in task logs filters. - Simplify DataTableViewOptions: text-only “View” trigger, no sliders icon. - Earlier layout tweak: extra top padding on SectionPageLayout scroll content so control focus rings are not clipped by overflow. * feat(web/default): Base UI migration and component foundation Migrate from Radix UI to Base UI, rewrite core UI primitives, update dependencies (recharts, date-fns, next-themes), add shadcn agent skill documentation, and refresh AI element components. This is the foundational work from the v2/localmain lineage that was not covered by the individual feature commits above. --------- Co-authored-by: t0ng7u <dev@aiass.cc> Co-authored-by: QuentinHsu <xuquentinyang@gmail.com>
342 lines
11 KiB
TypeScript
Vendored
342 lines
11 KiB
TypeScript
Vendored
import { useCallback, useMemo, useState } from 'react'
|
|
import { AlertTriangle, KeyRound, Loader2, ShieldAlert } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { toast } from 'sonner'
|
|
import dayjs from '@/lib/dayjs'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from '@/components/ui/alert-dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { StatusBadge } from '@/components/status-badge'
|
|
import { usePasskeyManagement } from '@/features/auth/passkey'
|
|
import {
|
|
SecureVerificationDialog,
|
|
useSecureVerification,
|
|
type VerificationMethod,
|
|
type VerificationMethods,
|
|
} from '@/features/auth/secure-verification'
|
|
|
|
interface PasskeyCardProps {
|
|
loading: boolean
|
|
}
|
|
|
|
export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
|
const { t } = useTranslation()
|
|
const [confirmOpen, setConfirmOpen] = useState(false)
|
|
const [restrictedMethod, setRestrictedMethod] =
|
|
useState<VerificationMethod | null>(null)
|
|
|
|
const {
|
|
status,
|
|
loading,
|
|
registering,
|
|
removing,
|
|
supported,
|
|
enabled,
|
|
lastUsed,
|
|
register,
|
|
remove,
|
|
} = usePasskeyManagement()
|
|
|
|
const {
|
|
open: verificationOpen,
|
|
setOpen: setVerificationOpen,
|
|
methods: verificationMethods,
|
|
state: verificationState,
|
|
startVerification,
|
|
executeVerification,
|
|
cancel: cancelVerification,
|
|
setCode,
|
|
switchMethod,
|
|
fetchVerificationMethods,
|
|
} = useSecureVerification({
|
|
onSuccess: () => {
|
|
setRestrictedMethod(null)
|
|
},
|
|
})
|
|
|
|
const dialogMethods = useMemo<VerificationMethods>(() => {
|
|
if (!restrictedMethod) return verificationMethods
|
|
return {
|
|
...verificationMethods,
|
|
has2FA: restrictedMethod === '2fa' && verificationMethods.has2FA,
|
|
hasPasskey:
|
|
restrictedMethod === 'passkey' && verificationMethods.hasPasskey,
|
|
}
|
|
}, [restrictedMethod, verificationMethods])
|
|
|
|
const handleRegister = useCallback(async () => {
|
|
if (!supported) {
|
|
toast.info(t('This device does not support Passkey'))
|
|
return
|
|
}
|
|
|
|
const methods = await fetchVerificationMethods()
|
|
if (!methods.has2FA) {
|
|
// Without 2FA enabled, register directly. The browser-level Passkey prompt
|
|
// is itself a strong proof of presence, so no extra verification is needed.
|
|
await register()
|
|
return
|
|
}
|
|
|
|
setRestrictedMethod('2fa')
|
|
await startVerification(register, {
|
|
preferredMethod: '2fa',
|
|
title: t('Security verification'),
|
|
description: t(
|
|
'Confirm your identity with Two-factor Authentication before registering a Passkey.'
|
|
),
|
|
})
|
|
}, [fetchVerificationMethods, register, startVerification, supported, t])
|
|
|
|
const handleRemove = useCallback(async () => {
|
|
const methods = await fetchVerificationMethods()
|
|
const required: VerificationMethod | null = methods.has2FA
|
|
? '2fa'
|
|
: methods.hasPasskey
|
|
? 'passkey'
|
|
: null
|
|
|
|
if (!required) {
|
|
toast.error(
|
|
t(
|
|
'Please enable Two-factor Authentication or Passkey before proceeding'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
if (required === 'passkey' && !methods.passkeySupported) {
|
|
toast.info(t('This device does not support Passkey'))
|
|
return
|
|
}
|
|
|
|
setConfirmOpen(false)
|
|
setRestrictedMethod(required)
|
|
await startVerification(remove, {
|
|
preferredMethod: required,
|
|
title: t('Security verification'),
|
|
description: t(
|
|
'Confirm your identity before removing this Passkey from your account.'
|
|
),
|
|
})
|
|
}, [fetchVerificationMethods, remove, startVerification, t])
|
|
|
|
const handleVerificationCancel = useCallback(() => {
|
|
setRestrictedMethod(null)
|
|
cancelVerification()
|
|
}, [cancelVerification])
|
|
|
|
const handleVerificationOpenChange = useCallback(
|
|
(next: boolean) => {
|
|
if (!next) {
|
|
setRestrictedMethod(null)
|
|
}
|
|
setVerificationOpen(next)
|
|
},
|
|
[setVerificationOpen]
|
|
)
|
|
|
|
// Adapt the hook's `Promise<unknown>` return into the dialog's
|
|
// `void | Promise<void>` signature without losing error propagation
|
|
// semantics (errors are surfaced via toast inside the hook).
|
|
const handleDialogVerify = useCallback(
|
|
async (method: VerificationMethod, code?: string) => {
|
|
try {
|
|
await executeVerification(method, code)
|
|
} catch {
|
|
// Errors are already surfaced by useSecureVerification via toast.
|
|
}
|
|
},
|
|
[executeVerification]
|
|
)
|
|
|
|
if (pageLoading || loading) {
|
|
return (
|
|
<Card className='gap-0 overflow-hidden py-0'>
|
|
<CardHeader className='p-3 sm:p-5'>
|
|
<Skeleton className='h-6 w-48' />
|
|
<Skeleton className='mt-2 h-4 w-64' />
|
|
</CardHeader>
|
|
<CardContent className='p-3 sm:p-5'>
|
|
<Skeleton className='h-20 w-full' />
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const formattedLastUsed =
|
|
lastUsed && !Number.isNaN(Date.parse(lastUsed))
|
|
? dayjs(lastUsed).fromNow()
|
|
: t('Not used yet')
|
|
|
|
const showUnsupportedNotice = !supported && !enabled
|
|
|
|
return (
|
|
<>
|
|
<Card className='gap-0 overflow-hidden py-0'>
|
|
<CardHeader className='p-3 sm:p-5'>
|
|
<CardTitle className='text-lg tracking-tight sm:text-xl'>
|
|
{t('Passkey Login')}
|
|
</CardTitle>
|
|
<CardDescription className='text-xs sm:text-sm'>
|
|
{t('Use Passkey to sign in without entering your password.')}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className='p-3 sm:p-5'>
|
|
<div className='space-y-6'>
|
|
<div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:flex-col 2xl:flex-row'>
|
|
<div className='flex items-start gap-4'>
|
|
<div className='bg-muted rounded-md p-2'>
|
|
<KeyRound className='h-5 w-5' />
|
|
</div>
|
|
<div className='space-y-1'>
|
|
<div className='flex flex-wrap items-center gap-2'>
|
|
<p className='font-medium'>{t('Passkey Authentication')}</p>
|
|
<StatusBadge
|
|
label={enabled ? t('Enabled') : t('Disabled')}
|
|
variant={enabled ? 'success' : 'neutral'}
|
|
showDot
|
|
copyable={false}
|
|
/>
|
|
{status?.backup_eligible !== undefined && (
|
|
<StatusBadge
|
|
label={
|
|
status.backup_eligible
|
|
? status.backup_state
|
|
? t('Backed up')
|
|
: t('Not backed up')
|
|
: t('No backup')
|
|
}
|
|
variant={
|
|
status.backup_eligible
|
|
? status.backup_state
|
|
? 'success'
|
|
: 'warning'
|
|
: 'neutral'
|
|
}
|
|
showDot
|
|
copyable={false}
|
|
/>
|
|
)}
|
|
</div>
|
|
<p className='text-muted-foreground text-sm'>
|
|
{t('Last used:')} {formattedLastUsed}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{!enabled && (
|
|
<Button
|
|
className='w-full sm:w-auto xl:w-full 2xl:w-auto'
|
|
onClick={handleRegister}
|
|
disabled={!supported || registering}
|
|
>
|
|
{registering && (
|
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
)}
|
|
{t('Enable Passkey')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{enabled && (
|
|
<div className='flex flex-col gap-3 border-t pt-6 sm:flex-row xl:flex-col 2xl:flex-row'>
|
|
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
<AlertDialogTrigger
|
|
render={
|
|
<Button
|
|
variant='destructive'
|
|
className='flex-1'
|
|
disabled={removing}
|
|
/>
|
|
}
|
|
>
|
|
{removing ? (
|
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
) : (
|
|
<AlertTriangle className='mr-2 h-4 w-4' />
|
|
)}
|
|
{t('Remove Passkey')}
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
{t('Remove Passkey?')}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{t(
|
|
'Removing Passkey will require you to sign in with your password next time. You can re-register anytime.'
|
|
)}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={removing}>
|
|
{t('Cancel')}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
disabled={removing}
|
|
onClick={(event) => {
|
|
event.preventDefault()
|
|
handleRemove()
|
|
}}
|
|
>
|
|
{t('Remove')}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
)}
|
|
|
|
{showUnsupportedNotice && (
|
|
<div className='bg-muted/60 text-muted-foreground flex items-start gap-3 rounded-md p-4 text-sm'>
|
|
<ShieldAlert className='mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500' />
|
|
<div>
|
|
<p className='text-foreground font-medium'>
|
|
{t('Passkey not supported on this device')}
|
|
</p>
|
|
<p>
|
|
{t(
|
|
'Use a compatible browser or device with biometric authentication or a security key to register a Passkey.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<SecureVerificationDialog
|
|
open={verificationOpen}
|
|
onOpenChange={handleVerificationOpenChange}
|
|
methods={dialogMethods}
|
|
state={verificationState}
|
|
onVerify={handleDialogVerify}
|
|
onCancel={handleVerificationCancel}
|
|
onCodeChange={setCode}
|
|
onMethodChange={switchMethod}
|
|
/>
|
|
</>
|
|
)
|
|
}
|