Просмотр исходного кода

feat(ui): refine default frontend layouts

CaIon 1 месяц назад
Родитель
Сommit
f982544825
28 измененных файлов с 925 добавлено и 586 удалено
  1. 6 2
      web/default/src/components/layout/lib/url-utils.ts
  2. 2 0
      web/default/src/components/layout/types.ts
  3. 37 2
      web/default/src/features/dashboard/index.tsx
  4. 59 21
      web/default/src/features/models/index.tsx
  5. 16 10
      web/default/src/features/profile/components/passkey-card.tsx
  6. 92 38
      web/default/src/features/profile/components/profile-header.tsx
  7. 27 14
      web/default/src/features/profile/components/profile-security-card.tsx
  8. 35 16
      web/default/src/features/profile/components/profile-settings-card.tsx
  9. 25 16
      web/default/src/features/profile/components/sidebar-modules-card.tsx
  10. 7 7
      web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
  11. 7 4
      web/default/src/features/profile/components/tabs/notification-tab.tsx
  12. 19 10
      web/default/src/features/profile/components/two-fa-card.tsx
  13. 12 12
      web/default/src/features/profile/index.tsx
  14. 80 20
      web/default/src/features/usage-logs/index.tsx
  15. 39 23
      web/default/src/features/wallet/components/affiliate-rewards-card.tsx
  16. 46 30
      web/default/src/features/wallet/components/recharge-form-card.tsx
  17. 257 241
      web/default/src/features/wallet/components/subscription-plans-card.tsx
  18. 55 37
      web/default/src/features/wallet/components/wallet-stats-card.tsx
  19. 48 46
      web/default/src/features/wallet/index.tsx
  20. 6 1
      web/default/src/hooks/use-sidebar-config.ts
  21. 17 9
      web/default/src/hooks/use-sidebar-data.ts
  22. 1 0
      web/default/src/i18n/locales/en.json
  23. 1 0
      web/default/src/i18n/locales/fr.json
  24. 1 0
      web/default/src/i18n/locales/ja.json
  25. 1 0
      web/default/src/i18n/locales/ru.json
  26. 1 0
      web/default/src/i18n/locales/vi.json
  27. 1 0
      web/default/src/i18n/locales/zh.json
  28. 27 27
      web/default/src/styles/theme.css

+ 6 - 2
web/default/src/components/layout/lib/url-utils.ts

@@ -40,11 +40,16 @@ export function checkIsActive(
   item: NavItem,
   mainNav = false
 ): boolean {
+  const hrefWithoutQuery = href.split('?')[0]
+
+  if (item.activeUrls?.some((url) => urlToString(url) === hrefWithoutQuery)) {
+    return true
+  }
+
   // For collapsible items (NavCollapsible), check sub-items first
   if ('items' in item && item.items) {
     const collapsibleItem = item as NavCollapsible
     const items = collapsibleItem.items
-    const hrefWithoutQuery = href.split('?')[0]
 
     // Check if any sub-item matches
     if (
@@ -76,7 +81,6 @@ export function checkIsActive(
   // Exact match
   if (href === itemUrl) return true
 
-  const hrefWithoutQuery = href.split('?')[0]
   const itemUrlWithoutQuery = itemUrl.split('?')[0]
   const itemUrlHasQuery = itemUrl.includes('?')
 

+ 2 - 0
web/default/src/components/layout/types.ts

@@ -18,6 +18,8 @@ type BaseNavItem = {
   title: string
   badge?: string
   icon?: React.ElementType
+  activeUrls?: (LinkProps['to'] | (string & {}))[]
+  configUrls?: (LinkProps['to'] | (string & {}))[]
 }
 
 /**

+ 37 - 2
web/default/src/features/dashboard/index.tsx

@@ -1,7 +1,10 @@
-import { useState, useCallback, lazy, Suspense } from 'react'
-import { getRouteApi } from '@tanstack/react-router'
+import { useState, useCallback, useMemo, lazy, Suspense } from 'react'
+import { getRouteApi, useNavigate } from '@tanstack/react-router'
 import { useTranslation } from 'react-i18next'
+import { useAuthStore } from '@/stores/auth-store'
+import { ROLE } from '@/lib/roles'
 import { Skeleton } from '@/components/ui/skeleton'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { SectionPageLayout } from '@/components/layout'
 import {
   CardStaggerContainer,
@@ -18,6 +21,7 @@ import { DEFAULT_TIME_GRANULARITY } from './constants'
 import {
   type DashboardSectionId,
   DASHBOARD_DEFAULT_SECTION,
+  DASHBOARD_SECTION_IDS,
 } from './section-registry'
 import { type DashboardFilters, type QuotaDataItem } from './types'
 
@@ -97,7 +101,9 @@ const SECTION_META: Record<
 
 export function Dashboard() {
   const { t } = useTranslation()
+  const navigate = useNavigate()
   const params = route.useParams()
+  const userRole = useAuthStore((state) => state.auth.user?.role)
   const activeSection = (params.section ??
     DASHBOARD_DEFAULT_SECTION) as DashboardSectionId
 
@@ -122,6 +128,24 @@ export function Dashboard() {
   )
 
   const meta = SECTION_META[activeSection] ?? SECTION_META.overview
+  const isAdmin = Boolean(userRole && userRole >= ROLE.ADMIN)
+  const visibleSections = useMemo(
+    () =>
+      DASHBOARD_SECTION_IDS.filter(
+        (section) => section !== 'overview' && (section !== 'users' || isAdmin)
+      ),
+    [isAdmin]
+  )
+  const handleSectionChange = useCallback(
+    (section: string) => {
+      void navigate({
+        to: '/dashboard/$section',
+        params: { section: section as DashboardSectionId },
+      })
+    },
+    [navigate]
+  )
+  const showSectionTabs = activeSection !== 'overview' && visibleSections.length > 1
 
   return (
     <SectionPageLayout>
@@ -139,6 +163,17 @@ export function Dashboard() {
       )}
       <SectionPageLayout.Content>
         <div className='space-y-4'>
+          {showSectionTabs && (
+            <Tabs value={activeSection} onValueChange={handleSectionChange}>
+              <TabsList className='h-auto max-w-full flex-wrap justify-start'>
+                {visibleSections.map((section) => (
+                  <TabsTrigger key={section} value={section}>
+                    {t(SECTION_META[section].titleKey)}
+                  </TabsTrigger>
+                ))}
+              </TabsList>
+            </Tabs>
+          )}
           {activeSection === 'overview' && (
             <>
               <SummaryCards />

+ 59 - 21
web/default/src/features/models/index.tsx

@@ -1,9 +1,10 @@
-import { useEffect, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
 import { useQueryClient } from '@tanstack/react-query'
-import { getRouteApi } from '@tanstack/react-router'
+import { getRouteApi, useNavigate } from '@tanstack/react-router'
 import { Plus } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { Button } from '@/components/ui/button'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { SectionPageLayout } from '@/components/layout'
 import { listDeployments } from './api'
 import { DeploymentAccessGuard } from './components/deployment-access-guard'
@@ -18,12 +19,28 @@ import { deploymentsQueryKeys } from './lib'
 import {
   type ModelsSectionId,
   MODELS_DEFAULT_SECTION,
+  MODELS_SECTION_IDS,
 } from './section-registry'
 
 const route = getRouteApi('/_authenticated/models/$section')
 
+const SECTION_META: Record<
+  ModelsSectionId,
+  { titleKey: string; descriptionKey: string }
+> = {
+  metadata: {
+    titleKey: 'Metadata',
+    descriptionKey: 'Manage model metadata and configuration',
+  },
+  deployments: {
+    titleKey: 'Deployments',
+    descriptionKey: 'Manage model deployments',
+  },
+}
+
 function ModelsContent() {
   const { t } = useTranslation()
+  const navigate = useNavigate()
   const queryClient = useQueryClient()
   const { tabCategory, setTabCategory } = useModels()
   const params = route.useParams()
@@ -75,16 +92,26 @@ function ModelsContent() {
     }
   }, [activeSection, isIoNetEnabled, loadingPhase, queryClient])
 
+  const handleSectionChange = useCallback(
+    (section: string) => {
+      void navigate({
+        to: '/models/$section',
+        params: { section: section as ModelsSectionId },
+      })
+    },
+    [navigate]
+  )
+
+  const meta = SECTION_META[activeSection] ?? SECTION_META.metadata
+
   return (
     <>
       <SectionPageLayout>
         <SectionPageLayout.Title>
-          {activeSection === 'metadata' ? t('Metadata') : t('Deployments')}
+          {t(meta.titleKey)}
         </SectionPageLayout.Title>
         <SectionPageLayout.Description>
-          {activeSection === 'metadata'
-            ? t('Manage model metadata and configuration')
-            : t('Manage model deployments')}
+          {t(meta.descriptionKey)}
         </SectionPageLayout.Description>
         <SectionPageLayout.Actions>
           {activeSection === 'metadata' ? (
@@ -97,21 +124,32 @@ function ModelsContent() {
           )}
         </SectionPageLayout.Actions>
         <SectionPageLayout.Content>
-          {activeSection === 'metadata' ? (
-            <ModelsTable />
-          ) : (
-            <DeploymentAccessGuard
-              loading={deploymentLoading}
-              loadingPhase={loadingPhase}
-              isEnabled={isIoNetEnabled}
-              connectionLoading={connectionLoading}
-              connectionOk={connectionOk}
-              connectionError={connectionError}
-              onRetry={testConnection}
-            >
-              <DeploymentsTable />
-            </DeploymentAccessGuard>
-          )}
+          <div className='space-y-4'>
+            <Tabs value={activeSection} onValueChange={handleSectionChange}>
+              <TabsList className='h-auto max-w-full flex-wrap justify-start'>
+                {MODELS_SECTION_IDS.map((section) => (
+                  <TabsTrigger key={section} value={section}>
+                    {t(SECTION_META[section].titleKey)}
+                  </TabsTrigger>
+                ))}
+              </TabsList>
+            </Tabs>
+            {activeSection === 'metadata' ? (
+              <ModelsTable />
+            ) : (
+              <DeploymentAccessGuard
+                loading={deploymentLoading}
+                loadingPhase={loadingPhase}
+                isEnabled={isIoNetEnabled}
+                connectionLoading={connectionLoading}
+                connectionOk={connectionOk}
+                connectionError={connectionError}
+                onRetry={testConnection}
+              >
+                <DeploymentsTable />
+              </DeploymentAccessGuard>
+            )}
+          </div>
         </SectionPageLayout.Content>
       </SectionPageLayout>
 

+ 16 - 10
web/default/src/features/profile/components/passkey-card.tsx

@@ -15,7 +15,13 @@ import {
   AlertDialogTrigger,
 } from '@/components/ui/alert-dialog'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
+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'
@@ -163,7 +169,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
 
   if (pageLoading || loading) {
     return (
-      <Card>
+      <Card className='overflow-hidden'>
         <CardHeader>
           <Skeleton className='h-6 w-48' />
           <Skeleton className='mt-2 h-4 w-64' />
@@ -185,18 +191,18 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
 
   return (
     <>
-      <Card>
+      <Card className='overflow-hidden'>
         <CardHeader>
-          <h3 className='text-xl font-semibold tracking-tight'>
+          <CardTitle className='text-xl tracking-tight'>
             {t('Passkey Login')}
-          </h3>
-          <p className='text-muted-foreground mt-2 text-sm'>
+          </CardTitle>
+          <CardDescription>
             {t('Use Passkey to sign in without entering your password.')}
-          </p>
+          </CardDescription>
         </CardHeader>
 
         <CardContent className='space-y-6'>
-          <div className='flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
+          <div className='flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between xl:flex-col xl:items-stretch 2xl:flex-row 2xl:items-center'>
             <div className='flex items-start gap-3'>
               <div className='bg-muted rounded-md p-2'>
                 <KeyRound className='h-5 w-5' />
@@ -239,7 +245,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
 
             {!enabled ? (
               <Button
-                className='w-full sm:w-auto'
+                className='w-full sm:w-auto xl:w-full 2xl:w-auto'
                 onClick={handleRegister}
                 disabled={!supported || registering}
               >
@@ -253,7 +259,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
                 <AlertDialogTrigger asChild>
                   <Button
                     variant='outline'
-                    className='w-full sm:w-auto'
+                    className='w-full sm:w-auto xl:w-full 2xl:w-auto'
                     disabled={removing}
                   >
                     {t('Remove Passkey')}

+ 92 - 38
web/default/src/features/profile/components/profile-header.tsx

@@ -1,3 +1,5 @@
+import { useTranslation } from 'react-i18next'
+import { formatCompactNumber, formatQuota } from '@/lib/format'
 import { getRoleLabel } from '@/lib/roles'
 import { Avatar, AvatarFallback } from '@/components/ui/avatar'
 import { Skeleton } from '@/components/ui/skeleton'
@@ -15,20 +17,34 @@ interface ProfileHeaderProps {
 }
 
 export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
+  const { t } = useTranslation()
+
   if (loading) {
     return (
-      <div className='space-y-6'>
-        <div className='flex flex-col items-center gap-4 text-center lg:flex-row lg:items-center lg:gap-6 lg:text-left'>
-          <Skeleton className='h-20 w-20 rounded-full' />
-          <div className='flex-1 space-y-3 lg:space-y-2'>
-            <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-center lg:justify-start'>
-              <Skeleton className='h-8 w-48' />
-              <Skeleton className='h-5 w-16' />
+      <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>
             </div>
-            <div className='flex flex-col items-center gap-1 sm:flex-row sm:justify-center sm:gap-4 lg:justify-start'>
-              <Skeleton className='h-4 w-24' />
-              <Skeleton className='h-4 w-40' />
-              <Skeleton className='h-4 w-20' />
+            <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>
@@ -41,38 +57,76 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
   const displayName = getDisplayName(profile)
   const initials = getUserInitials(profile)
   const roleLabel = getRoleLabel(profile.role)
+  const stats = [
+    {
+      label: t('Current Balance'),
+      value: formatQuota(profile.quota),
+    },
+    {
+      label: t('Total Usage'),
+      value: formatQuota(profile.used_quota),
+    },
+    {
+      label: t('API Requests'),
+      value: formatCompactNumber(profile.request_count),
+    },
+  ]
 
   return (
-    <div className='space-y-6'>
-      <div className='flex flex-col items-center gap-4 text-center lg:flex-row lg:items-center lg:gap-6 lg:text-left'>
-        <Avatar className='h-20 w-20 text-xl'>
-          <AvatarFallback className='bg-primary/10 text-primary'>
-            {initials}
-          </AvatarFallback>
-        </Avatar>
+    <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='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='flex-1 space-y-3 lg:space-y-2'>
-          <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-center lg:justify-start'>
-            <h1 className='text-3xl font-semibold tracking-tight'>
-              {displayName}
-            </h1>
-            <StatusBadge label={roleLabel} variant='neutral' copyable={false} />
+              <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>
 
-          <div className='text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-center sm:gap-4 lg:justify-start'>
-            <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 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'>
+                  {item.label}
+                </p>
+                <p className='mt-2 truncate text-xl font-semibold tracking-tight'>
+                  {item.value}
+                </p>
+              </div>
+            ))}
           </div>
         </div>
       </div>

+ 27 - 14
web/default/src/features/profile/components/profile-security-card.tsx

@@ -1,7 +1,13 @@
 import { Shield, Key, Trash2 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useDialogs } from '@/hooks/use-dialog'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
 import type { UserProfile } from '../types'
 import { AccessTokenDialog } from './dialogs/access-token-dialog'
@@ -28,12 +34,12 @@ export function ProfileSecurityCard({
 
   if (loading) {
     return (
-      <Card>
-        <CardHeader>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
-        <CardContent className='space-y-3'>
+        <CardContent className='space-y-3 pt-6'>
           {Array.from({ length: 3 }).map((_, i) => (
             <Skeleton key={i} className='h-16 w-full' />
           ))}
@@ -70,18 +76,25 @@ export function ProfileSecurityCard({
 
   return (
     <>
-      <Card>
-        <CardHeader>
-          <h3 className='text-xl font-semibold tracking-tight'>
-            {t('Security')}
-          </h3>
-          <p className='text-muted-foreground mt-2 text-sm'>
-            {t('Manage your security settings and account access')}
-          </p>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
+          <div className='flex items-center gap-3'>
+            <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+              <Shield className='h-4 w-4' />
+            </div>
+            <div className='min-w-0'>
+              <CardTitle className='text-xl tracking-tight'>
+                {t('Security')}
+              </CardTitle>
+              <CardDescription>
+                {t('Manage your security settings and account access')}
+              </CardDescription>
+            </div>
+          </div>
         </CardHeader>
 
-        <CardContent>
-          <div className='grid grid-cols-1 gap-3 sm:grid-cols-3'>
+        <CardContent className='pt-6'>
+          <div className='grid grid-cols-1 gap-3 md:grid-cols-3'>
             {securityActions.map((item) => (
               <button
                 key={item.title}

+ 35 - 16
web/default/src/features/profile/components/profile-settings-card.tsx

@@ -1,7 +1,13 @@
 import { useState } from 'react'
 import { Link2, Settings } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import type { UserProfile } from '../types'
@@ -28,12 +34,12 @@ export function ProfileSettingsCard({
 
   if (loading) {
     return (
-      <Card>
-        <CardHeader>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
-        <CardContent className='space-y-4'>
+        <CardContent className='space-y-4 pt-6'>
           <Skeleton className='h-10 w-full' />
           {Array.from({ length: 3 }).map((_, i) => (
             <Skeleton key={i} className='h-20 w-full' />
@@ -44,25 +50,38 @@ export function ProfileSettingsCard({
   }
 
   return (
-    <Card>
-      <CardHeader>
-        <h3 className='text-xl font-semibold tracking-tight'>
-          {t('Settings')}
-        </h3>
-        <p className='text-muted-foreground mt-2 text-sm'>
-          {t('Configure your account preferences and integrations')}
-        </p>
+    <Card className='overflow-hidden'>
+      <CardHeader className='border-b'>
+        <div className='flex items-center gap-3'>
+          <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+            <Settings className='h-4 w-4' />
+          </div>
+          <div className='min-w-0'>
+            <CardTitle className='text-xl tracking-tight'>
+              {t('Settings')}
+            </CardTitle>
+            <CardDescription>
+              {t('Configure your account preferences and integrations')}
+            </CardDescription>
+          </div>
+        </div>
       </CardHeader>
 
-      <CardContent>
+      <CardContent className='pt-6'>
         <Tabs value={activeTab} onValueChange={setActiveTab}>
-          <TabsList className='grid w-full grid-cols-2'>
-            <TabsTrigger value='bindings' className='gap-2'>
+          <TabsList className='grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'>
+            <TabsTrigger
+              value='bindings'
+              className='h-auto gap-2 rounded-lg px-3 py-2.5'
+            >
               <Link2 className='h-4 w-4' />
               <span className='hidden sm:inline'>{t('Account Bindings')}</span>
               <span className='sm:hidden'>{t('Bindings')}</span>
             </TabsTrigger>
-            <TabsTrigger value='settings' className='gap-2'>
+            <TabsTrigger
+              value='settings'
+              className='h-auto gap-2 rounded-lg px-3 py-2.5'
+            >
               <Settings className='h-4 w-4' />
               <span className='hidden sm:inline'>
                 {t('Settings & Preferences')}

+ 25 - 16
web/default/src/features/profile/components/sidebar-modules-card.tsx

@@ -182,23 +182,32 @@ export function SidebarModulesCard() {
   }
 
   return (
-    <Card>
-      <CardHeader>
-        <CardTitle className='flex items-center gap-2'>
-          <LayoutDashboard className='h-4 w-4' />
-          {t('Sidebar Personal Settings')}
-        </CardTitle>
-        <CardDescription>
-          {t('Customize sidebar display content')}
-        </CardDescription>
+    <Card className='overflow-hidden'>
+      <CardHeader className='border-b'>
+        <div className='flex items-center gap-3'>
+          <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+            <LayoutDashboard className='h-4 w-4' />
+          </div>
+          <div className='min-w-0'>
+            <CardTitle className='text-xl tracking-tight'>
+              {t('Sidebar Personal Settings')}
+            </CardTitle>
+            <CardDescription>
+              {t('Customize sidebar display content')}
+            </CardDescription>
+          </div>
+        </div>
       </CardHeader>
-      <CardContent className='space-y-6'>
+      <CardContent className='space-y-5 pt-6'>
         {sectionDefs.map((section) => {
           const sectionEnabled = config[section.key]?.enabled !== false
           return (
-            <div key={section.key} className='space-y-3'>
-              <div className='bg-muted/50 flex items-center justify-between rounded-lg border p-3'>
-                <div>
+            <div
+              key={section.key}
+              className='bg-background/60 rounded-xl border p-3'
+            >
+              <div className='flex items-start justify-between gap-3'>
+                <div className='min-w-0'>
                   <p className='text-sm font-medium'>{section.title}</p>
                   <p className='text-muted-foreground text-xs'>
                     {section.description}
@@ -209,11 +218,11 @@ export function SidebarModulesCard() {
                   onCheckedChange={(v) => toggleSection(section.key, v)}
                 />
               </div>
-              <div className='grid grid-cols-2 gap-2 sm:grid-cols-3'>
+              <div className='mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-1'>
                 {section.modules.map((mod) => (
                   <div
                     key={mod.key}
-                    className={`flex items-center justify-between rounded-lg border p-3 transition-opacity ${
+                    className={`flex min-h-16 items-center justify-between rounded-lg border p-3 transition-opacity ${
                       sectionEnabled ? '' : 'opacity-50'
                     }`}
                   >
@@ -239,7 +248,7 @@ export function SidebarModulesCard() {
           )
         })}
 
-        <div className='flex justify-end gap-2 border-t pt-4'>
+        <div className='flex flex-col-reverse gap-2 border-t pt-4 sm:flex-row sm:justify-end'>
           <Button variant='outline' onClick={handleReset}>
             {t('Reset to Default')}
           </Button>

+ 7 - 7
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx

@@ -249,9 +249,9 @@ export function AccountBindingsTab({
         {bindings.map((binding) => (
           <div
             key={binding.id}
-            className='flex items-center justify-between rounded-lg border p-3'
+            className='flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between'
           >
-            <div className='flex items-center gap-3'>
+            <div className='flex min-w-0 items-center gap-3'>
               <div className='bg-muted shrink-0 rounded-md p-2'>
                 <binding.icon className='h-4 w-4' />
               </div>
@@ -274,7 +274,7 @@ export function AccountBindingsTab({
             <Button
               variant='outline'
               size='sm'
-              className='ml-2 h-7 shrink-0 px-2.5 text-xs'
+              className='h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto'
               onClick={binding.onBind}
               disabled={binding.isBound && binding.id !== 'email'}
             >
@@ -304,9 +304,9 @@ export function AccountBindingsTab({
               return (
                 <div
                   key={provider.id}
-                  className='flex items-center justify-between rounded-lg border p-3'
+                  className='flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between'
                 >
-                  <div className='flex items-center gap-3'>
+                  <div className='flex min-w-0 items-center gap-3'>
                     <div className='bg-muted shrink-0 rounded-md p-2'>
                       <Link2 className='h-4 w-4' />
                     </div>
@@ -332,7 +332,7 @@ export function AccountBindingsTab({
                     <Button
                       variant='ghost'
                       size='sm'
-                      className='text-destructive hover:text-destructive ml-2 h-7 shrink-0 px-2.5 text-xs'
+                      className='text-destructive hover:text-destructive h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto'
                       onClick={() => setUnbindTarget(binding)}
                     >
                       <Unlink className='mr-1 h-3 w-3' />
@@ -342,7 +342,7 @@ export function AccountBindingsTab({
                     <Button
                       variant='outline'
                       size='sm'
-                      className='ml-2 h-7 shrink-0 px-2.5 text-xs'
+                      className='h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto'
                       onClick={() => handleBindCustomOAuth(provider)}
                     >
                       {t('Bind')}

+ 7 - 4
web/default/src/features/profile/components/tabs/notification-tab.tsx

@@ -132,7 +132,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
                   className='sr-only'
                 />
                 <Icon className='h-5 w-5' />
-                <span className='text-sm font-medium'>{method.label}</span>
+                <span className='text-sm font-medium'>{t(method.label)}</span>
               </Label>
             )
           })}
@@ -297,7 +297,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
 
         {/* Receive Upstream Model Update Notifications (admin only) */}
         {isAdmin && (
-          <div className='flex items-center justify-between rounded-lg border p-4'>
+          <div className='flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
             <div className='space-y-0.5'>
               <Label htmlFor='upstreamModelUpdateNotify'>
                 {t('Receive Upstream Model Update Notifications')}
@@ -310,6 +310,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
             </div>
             <Switch
               id='upstreamModelUpdateNotify'
+              className='shrink-0'
               checked={settings.upstream_model_update_notify_enabled}
               onCheckedChange={(checked) =>
                 updateField('upstream_model_update_notify_enabled', checked)
@@ -319,7 +320,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
         )}
 
         {/* Accept Unset Model Price */}
-        <div className='flex items-center justify-between rounded-lg border p-4'>
+        <div className='flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
           <div className='space-y-0.5'>
             <Label htmlFor='acceptUnsetPrice'>
               {t('Accept Unpriced Models')}
@@ -330,6 +331,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
           </div>
           <Switch
             id='acceptUnsetPrice'
+            className='shrink-0'
             checked={settings.accept_unset_model_ratio_model}
             onCheckedChange={(checked) =>
               updateField('accept_unset_model_ratio_model', checked)
@@ -338,7 +340,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
         </div>
 
         {/* Record IP Log */}
-        <div className='flex items-center justify-between rounded-lg border p-4'>
+        <div className='flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'>
           <div className='space-y-0.5'>
             <Label htmlFor='recordIp'>{t('Record IP Address')}</Label>
             <p className='text-muted-foreground text-sm'>
@@ -347,6 +349,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
           </div>
           <Switch
             id='recordIp'
+            className='shrink-0'
             checked={settings.record_ip_log}
             onCheckedChange={(checked) => updateField('record_ip_log', checked)}
           />

+ 19 - 10
web/default/src/features/profile/components/two-fa-card.tsx

@@ -2,7 +2,13 @@ import { Shield, AlertTriangle, RefreshCw } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useDialogs } from '@/hooks/use-dialog'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
 import { StatusBadge } from '@/components/status-badge'
 import { useTwoFA } from '../hooks'
@@ -27,7 +33,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
 
   if (pageLoading || loading) {
     return (
-      <Card>
+      <Card className='overflow-hidden'>
         <CardHeader>
           <Skeleton className='h-6 w-48' />
           <Skeleton className='mt-2 h-4 w-64' />
@@ -41,20 +47,20 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
 
   return (
     <>
-      <Card>
+      <Card className='overflow-hidden'>
         <CardHeader>
-          <h3 className='text-xl font-semibold tracking-tight'>
+          <CardTitle className='text-xl tracking-tight'>
             {t('Two-Factor Authentication')}
-          </h3>
-          <p className='text-muted-foreground mt-2 text-sm'>
+          </CardTitle>
+          <CardDescription>
             {t('Add an extra layer of security to your account')}
-          </p>
+          </CardDescription>
         </CardHeader>
 
         <CardContent>
           <div className='space-y-6'>
             {/* Status Section */}
-            <div className='flex items-start justify-between'>
+            <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'>
                   <Shield className='h-5 w-5' />
@@ -97,7 +103,10 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
               </div>
 
               {!status.enabled && (
-                <Button onClick={() => dialogs.open('setup')}>
+                <Button
+                  className='w-full sm:w-auto xl:w-full 2xl:w-auto'
+                  onClick={() => dialogs.open('setup')}
+                >
                   {t('Enable')}
                 </Button>
               )}
@@ -105,7 +114,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
 
             {/* Actions Section - Only show when enabled */}
             {status.enabled && (
-              <div className='flex flex-col gap-3 border-t pt-6 sm:flex-row'>
+              <div className='flex flex-col gap-3 border-t pt-6 sm:flex-row xl:flex-col 2xl:flex-row'>
                 <Button
                   variant='outline'
                   className='flex-1'

+ 12 - 12
web/default/src/features/profile/index.tsx

@@ -30,21 +30,24 @@ export function Profile() {
     <>
       <AppHeader />
       <Main>
-        <div className='min-h-0 flex-1 overflow-auto px-4 py-6'>
-          <CardStaggerContainer className='space-y-8'>
+        <div className='min-h-0 flex-1 overflow-auto px-4 py-4 sm:py-6'>
+          <CardStaggerContainer className='mx-auto flex w-full max-w-7xl flex-col gap-5 sm:gap-6'>
             <CardStaggerItem>
               <ProfileHeader profile={profile} loading={loading} />
             </CardStaggerItem>
 
             <CardStaggerItem>
-              <div className='grid gap-6 lg:grid-cols-2 lg:items-start'>
-                <div className='space-y-6'>
+              <div className='grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.46fr)] xl:items-start'>
+                <div className='space-y-5 sm:space-y-6'>
+                  <ProfileSettingsCard
+                    profile={profile}
+                    loading={loading}
+                    onProfileUpdate={refreshProfile}
+                  />
                   <ProfileSecurityCard profile={profile} loading={loading} />
-                  <PasskeyCard loading={loading} />
-                  <TwoFACard loading={loading} />
                 </div>
 
-                <div className='space-y-6'>
+                <div className='space-y-5 sm:space-y-6 xl:sticky xl:top-6'>
                   {checkinEnabled && (
                     <CheckinCalendarCard
                       checkinEnabled={checkinEnabled}
@@ -52,12 +55,9 @@ export function Profile() {
                       turnstileSiteKey={turnstileSiteKey}
                     />
                   )}
-                  <ProfileSettingsCard
-                    profile={profile}
-                    loading={loading}
-                    onProfileUpdate={refreshProfile}
-                  />
                   {canConfigureSidebar && <SidebarModulesCard />}
+                  <PasskeyCard loading={loading} />
+                  <TwoFACard loading={loading} />
                 </div>
               </div>
             </CardStaggerItem>

+ 80 - 20
web/default/src/features/usage-logs/index.tsx

@@ -1,6 +1,10 @@
-import { getRouteApi } from '@tanstack/react-router'
+import { useCallback, useMemo } from 'react'
+import { getRouteApi, useNavigate } from '@tanstack/react-router'
 import { useTranslation } from 'react-i18next'
+import { useSidebarConfig } from '@/hooks/use-sidebar-config'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { SectionPageLayout } from '@/components/layout'
+import type { NavGroup } from '@/components/layout/types'
 import { CacheStatsDialog } from '@/features/system-settings/general/channel-affinity/cache-stats-dialog'
 import { UserInfoDialog } from './components/dialogs/user-info-dialog'
 import { UsageLogsPrimaryButtons } from './components/usage-logs-primary-buttons'
@@ -16,9 +20,29 @@ import {
 } from './section-registry'
 
 const route = getRouteApi('/_authenticated/usage-logs/$section')
+const TASK_LOG_SECTIONS = ['drawing', 'task'] as const
+
+const SECTION_META: Record<
+  UsageLogsSectionId,
+  { titleKey: string; descriptionKey: string }
+> = {
+  common: {
+    titleKey: 'Common Logs',
+    descriptionKey: 'View and manage your API usage logs',
+  },
+  drawing: {
+    titleKey: 'Drawing Logs',
+    descriptionKey: 'View and manage your drawing logs',
+  },
+  task: {
+    titleKey: 'Task Logs',
+    descriptionKey: 'View and manage your task logs',
+  },
+}
 
 function UsageLogsContent() {
   const { t } = useTranslation()
+  const navigate = useNavigate()
   const params = route.useParams()
   const activeCategory: UsageLogsSectionId =
     params.section && isUsageLogsSectionId(params.section)
@@ -32,31 +56,54 @@ function UsageLogsContent() {
     affinityDialogOpen,
     setAffinityDialogOpen,
   } = useUsageLogsContext()
+  const tabNavGroups = useMemo<NavGroup[]>(
+    () => [
+      {
+        title: 'Task Logs',
+        items: TASK_LOG_SECTIONS.map((section) => ({
+          title: SECTION_META[section].titleKey,
+          url: `/usage-logs/${section}`,
+        })),
+      },
+    ],
+    []
+  )
+  const filteredTabGroups = useSidebarConfig(tabNavGroups)
+  const visibleSections = useMemo(
+    () =>
+      (filteredTabGroups[0]?.items ?? [])
+        .map((item) => {
+          if (!('url' in item) || typeof item.url !== 'string') return null
+          return item.url.split('/').pop() ?? null
+        })
+        .filter(
+          (section): section is UsageLogsSectionId =>
+            Boolean(section && isUsageLogsSectionId(section))
+        ),
+    [filteredTabGroups]
+  )
 
-  const title =
-    activeCategory === 'common'
-      ? t('Common Logs')
-      : activeCategory === 'drawing'
-        ? t('Drawing Logs')
-        : activeCategory === 'task'
-          ? t('Task Logs')
-          : t('Usage Logs')
+  const handleSectionChange = useCallback(
+    (section: string) => {
+      void navigate({
+        to: '/usage-logs/$section',
+        params: { section: section as UsageLogsSectionId },
+      })
+    },
+    [navigate]
+  )
 
-  const description =
-    activeCategory === 'common'
-      ? t('View and manage your API usage logs')
-      : activeCategory === 'drawing'
-        ? t('View and manage your drawing logs')
-        : activeCategory === 'task'
-          ? t('View and manage your task logs')
-          : t('View and manage your API usage logs')
+  const pageMeta =
+    activeCategory === 'common' ? SECTION_META.common : SECTION_META.task
+  const showTaskSwitcher =
+    activeCategory !== 'common' && visibleSections.length > 1
 
   return (
     <>
       <SectionPageLayout>
-        <SectionPageLayout.Title>{title}</SectionPageLayout.Title>
+        <SectionPageLayout.Title>{t(pageMeta.titleKey)}</SectionPageLayout.Title>
         <SectionPageLayout.Description>
-          {description}
+          {t(pageMeta.descriptionKey)}
         </SectionPageLayout.Description>
         <SectionPageLayout.Actions>
           {activeCategory !== 'common' && (
@@ -64,7 +111,20 @@ function UsageLogsContent() {
           )}
         </SectionPageLayout.Actions>
         <SectionPageLayout.Content>
-          <UsageLogsTable logCategory={activeCategory} />
+          <div className='space-y-4'>
+            {showTaskSwitcher && (
+              <Tabs value={activeCategory} onValueChange={handleSectionChange}>
+                <TabsList className='h-auto max-w-full flex-wrap justify-start'>
+                  {visibleSections.map((section) => (
+                    <TabsTrigger key={section} value={section}>
+                      {t(SECTION_META[section].titleKey)}
+                    </TabsTrigger>
+                  ))}
+                </TabsList>
+              </Tabs>
+            )}
+            <UsageLogsTable logCategory={activeCategory} />
+          </div>
         </SectionPageLayout.Content>
       </SectionPageLayout>
 

+ 39 - 23
web/default/src/features/wallet/components/affiliate-rewards-card.tsx

@@ -1,7 +1,14 @@
+import { Share2 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { formatQuota } from '@/lib/format'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/components/ui/card'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Skeleton } from '@/components/ui/skeleton'
@@ -24,18 +31,18 @@ export function AffiliateRewardsCard({
   const { t } = useTranslation()
   if (loading) {
     return (
-      <Card>
-        <CardHeader>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
-        <CardContent className='space-y-8'>
+        <CardContent className='space-y-6 pt-6'>
           {/* Statistics Skeleton */}
-          <div className='grid grid-cols-1 gap-4 sm:grid-cols-3 sm:gap-6'>
+          <div className='grid grid-cols-1 gap-3'>
             {Array.from({ length: 3 }).map((_, i) => (
-              <div key={i} className='space-y-2'>
+              <div key={i} className='rounded-lg border p-3'>
                 <Skeleton className='h-3 w-16' />
-                <Skeleton className='h-8 w-24' />
+                <Skeleton className='mt-2 h-8 w-24' />
               </div>
             ))}
           </div>
@@ -59,41 +66,50 @@ export function AffiliateRewardsCard({
   const hasRewards = (user?.aff_quota ?? 0) > 0
 
   return (
-    <Card>
-      <CardHeader>
-        <h3 className='text-xl font-semibold tracking-tight'>
-          {t('Referral Program')}
-        </h3>
-        <p className='text-muted-foreground mt-2 text-sm'>
-          {t('Share your link and earn rewards')}
-        </p>
+    <Card className='overflow-hidden'>
+      <CardHeader className='border-b'>
+        <div className='flex items-center gap-3'>
+          <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+            <Share2 className='h-4 w-4' />
+          </div>
+          <div className='min-w-0'>
+            <CardTitle className='text-xl tracking-tight'>
+              {t('Referral Program')}
+            </CardTitle>
+            <CardDescription>
+              {t('Share your link and earn rewards')}
+            </CardDescription>
+          </div>
+        </div>
       </CardHeader>
-      <CardContent className='space-y-8'>
+      <CardContent className='space-y-6 pt-6'>
         {/* Statistics */}
-        <div className='grid grid-cols-1 gap-4 sm:grid-cols-3 sm:gap-6'>
-          <div className='space-y-2'>
+        <div className='grid grid-cols-1 gap-3 sm:grid-cols-3 xl:grid-cols-1'>
+          <div className='rounded-lg border p-3'>
             <div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
               {t('Pending')}
             </div>
-            <div className='text-2xl font-semibold'>
+            <div className='mt-2 text-2xl font-semibold break-all'>
               {formatQuota(user?.aff_quota ?? 0)}
             </div>
           </div>
 
-          <div className='space-y-2'>
+          <div className='rounded-lg border p-3'>
             <div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
               {t('Total Earned')}
             </div>
-            <div className='text-2xl font-semibold'>
+            <div className='mt-2 text-2xl font-semibold break-all'>
               {formatQuota(user?.aff_history_quota ?? 0)}
             </div>
           </div>
 
-          <div className='space-y-2'>
+          <div className='rounded-lg border p-3'>
             <div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
               {t('Invites')}
             </div>
-            <div className='text-2xl font-semibold'>{user?.aff_count ?? 0}</div>
+            <div className='mt-2 text-2xl font-semibold'>
+              {user?.aff_count ?? 0}
+            </div>
           </div>
         </div>
 

+ 46 - 30
web/default/src/features/wallet/components/recharge-form-card.tsx

@@ -1,11 +1,17 @@
 import { useState, useEffect } from 'react'
-import { Gift, ExternalLink, Loader2, Receipt } from 'lucide-react'
+import { Gift, ExternalLink, Loader2, Receipt, WalletCards } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { formatNumber } from '@/lib/format'
 import { cn } from '@/lib/utils'
 import { Alert, AlertDescription } from '@/components/ui/alert'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/components/ui/card'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Skeleton } from '@/components/ui/skeleton'
@@ -119,12 +125,12 @@ export function RechargeFormCard({
 
   if (loading) {
     return (
-      <Card>
-        <CardHeader>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
           <Skeleton className='h-6 w-32' />
           <Skeleton className='mt-2 h-4 w-48' />
         </CardHeader>
-        <CardContent className='space-y-8'>
+        <CardContent className='space-y-6 pt-6'>
           <div className='space-y-6'>
             {/* Preset Amounts Skeleton */}
             <div className='space-y-3'>
@@ -167,31 +173,36 @@ export function RechargeFormCard({
   }
 
   return (
-    <Card>
-      <CardHeader>
-        <div className='flex items-center justify-between'>
-          <div>
-            <h3 className='text-xl font-semibold tracking-tight'>
-              {t('Add Funds')}
-            </h3>
-            <p className='text-muted-foreground mt-2 text-sm'>
-              {t('Choose an amount and payment method')}
-            </p>
+    <Card className='overflow-hidden'>
+      <CardHeader className='border-b'>
+        <div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
+          <div className='flex min-w-0 items-center gap-3'>
+            <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+              <WalletCards className='h-4 w-4' />
+            </div>
+            <div className='min-w-0'>
+              <CardTitle className='text-xl tracking-tight'>
+                {t('Add Funds')}
+              </CardTitle>
+              <CardDescription>
+                {t('Choose an amount and payment method')}
+              </CardDescription>
+            </div>
           </div>
           {onOpenBilling && (
             <Button
               variant='outline'
               size='sm'
               onClick={onOpenBilling}
-              className='gap-2'
+              className='w-full gap-2 sm:w-auto'
             >
               <Receipt className='h-4 w-4' />
-              {t('Billing')}
+              {t('Order History')}
             </Button>
           )}
         </div>
       </CardHeader>
-      <CardContent className='space-y-8'>
+      <CardContent className='space-y-6 pt-6'>
         {/* Online Topup Section */}
         {hasAnyTopup ? (
           <div className='space-y-6'>
@@ -202,7 +213,7 @@ export function RechargeFormCard({
                     <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                       {t('Amount')}
                     </Label>
-                    <div className='grid grid-cols-2 gap-3 sm:grid-cols-4'>
+                    <div className='grid grid-cols-2 gap-3 md:grid-cols-4'>
                       {presetAmounts.map((preset, index) => {
                         const discount =
                           preset.discount ||
@@ -224,7 +235,7 @@ export function RechargeFormCard({
                             key={index}
                             variant='outline'
                             className={cn(
-                              'hover:border-foreground h-auto rounded-lg p-4 text-left whitespace-normal',
+                              'hover:border-foreground flex h-auto flex-col items-start rounded-lg p-4 text-left whitespace-normal',
                               selectedPreset === preset.value
                                 ? 'border-foreground bg-foreground/5'
                                 : 'border-muted'
@@ -264,7 +275,7 @@ export function RechargeFormCard({
                   >
                     {t('Custom Amount')}
                   </Label>
-                  <div className='relative'>
+                  <div className='grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center'>
                     <Input
                       id='topup-amount'
                       type='number'
@@ -272,9 +283,9 @@ export function RechargeFormCard({
                       onChange={(e) => handleAmountChange(e.target.value)}
                       min={minTopup}
                       placeholder={`Minimum ${minTopup}`}
-                      className='pr-32 text-lg'
+                      className='text-lg'
                     />
-                    <div className='absolute end-3 top-1/2 flex -translate-y-1/2 items-center gap-2'>
+                    <div className='bg-muted/30 flex min-h-10 items-center justify-between gap-3 rounded-md border px-3 lg:min-w-52'>
                       <span className='text-muted-foreground text-xs'>
                         {t('Amount to pay:')}
                       </span>
@@ -294,7 +305,7 @@ export function RechargeFormCard({
                     {t('Payment Method')}
                   </Label>
                   {hasStandardPaymentMethods ? (
-                    <div className='flex flex-wrap gap-3'>
+                    <div className='grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3'>
                       {topupInfo?.pay_methods?.map((method) => {
                         const minTopup = method.min_topup || 0
                         const disabled = minTopup > topupAmount
@@ -305,7 +316,7 @@ export function RechargeFormCard({
                             variant='outline'
                             onClick={() => onPaymentMethodSelect(method)}
                             disabled={disabled || !!paymentLoading}
-                            className='gap-2 rounded-lg'
+                            className='justify-start gap-2 rounded-lg'
                           >
                             {paymentLoading === method.type ? (
                               <Loader2 className='h-4 w-4 animate-spin' />
@@ -355,7 +366,7 @@ export function RechargeFormCard({
                       <Label className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
                         {t('Waffo Payment')}
                       </Label>
-                      <div className='flex flex-wrap gap-3'>
+                      <div className='grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3'>
                         {waffoPayMethods?.map((method, index) => {
                           const loadingKey = `waffo-${index}`
                           const waffoMin = waffoMinTopup || 0
@@ -367,7 +378,7 @@ export function RechargeFormCard({
                               variant='outline'
                               onClick={() => onWaffoMethodSelect(method, index)}
                               disabled={belowMin || !!paymentLoading}
-                              className='gap-2 rounded-lg'
+                              className='justify-start gap-2 rounded-lg'
                             >
                               {paymentLoading === loadingKey ? (
                                 <Loader2 className='h-4 w-4 animate-spin' />
@@ -434,7 +445,7 @@ export function RechargeFormCard({
           )}
 
         {/* Redemption Code Section */}
-        <div className='space-y-3 border-t pt-8'>
+        <div className='space-y-3 border-t pt-6'>
           <div className='flex items-center gap-2'>
             <Gift className='text-muted-foreground h-4 w-4' />
             <Label
@@ -444,7 +455,7 @@ export function RechargeFormCard({
               {t('Have a Code?')}
             </Label>
           </div>
-          <div className='flex gap-2'>
+          <div className='flex flex-col gap-2 sm:flex-row'>
             <Input
               id='redemption-code'
               value={redemptionCode}
@@ -452,7 +463,12 @@ export function RechargeFormCard({
               placeholder={t('Enter your redemption code')}
               className='flex-1'
             />
-            <Button onClick={onRedeem} disabled={redeeming} variant='outline'>
+            <Button
+              onClick={onRedeem}
+              disabled={redeeming}
+              variant='outline'
+              className='sm:w-auto'
+            >
               {redeeming && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
               {t('Redeem')}
             </Button>

+ 257 - 241
web/default/src/features/wallet/components/subscription-plans-card.tsx

@@ -6,7 +6,13 @@ import { formatQuota } from '@/lib/format'
 import { cn } from '@/lib/utils'
 import { useStatus } from '@/hooks/use-status'
 import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/components/ui/card'
 import { Progress } from '@/components/ui/progress'
 import {
   Select,
@@ -185,11 +191,11 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
 
   if (loading) {
     return (
-      <Card>
-        <CardHeader>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
           <Skeleton className='h-6 w-32' />
         </CardHeader>
-        <CardContent className='space-y-4'>
+        <CardContent className='space-y-4 pt-6'>
           <Skeleton className='h-20 w-full' />
           <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3'>
             {Array.from({ length: 3 }).map((_, i) => (
@@ -207,237 +213,242 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
 
   return (
     <>
-      <Card>
-        <CardHeader>
-          <CardTitle className='flex items-center gap-2 text-base'>
-            <Crown className='h-4 w-4' />
-            {t('Subscription Plans')}
-          </CardTitle>
+      <Card className='overflow-hidden'>
+        <CardHeader className='border-b'>
+          <div className='flex items-center gap-3'>
+            <div className='bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'>
+              <Crown className='h-4 w-4' />
+            </div>
+            <div className='min-w-0'>
+              <CardTitle className='text-xl tracking-tight'>
+                {t('Subscription Plans')}
+              </CardTitle>
+              <CardDescription>
+                {t('Purchase a plan to enjoy model benefits')}
+              </CardDescription>
+            </div>
+          </div>
         </CardHeader>
-        <CardContent className='space-y-5'>
+        <CardContent className='space-y-5 pt-6'>
           {/* My subscriptions & billing preference */}
-          <Card className='bg-muted/50'>
-            <CardContent className='p-4'>
-              <div className='flex flex-wrap items-center justify-between gap-3'>
-                <div className='flex items-center gap-2'>
-                  <span className='text-sm font-medium'>
-                    {t('My Subscriptions')}
-                  </span>
-                  <span className='flex items-center gap-1.5 text-xs font-medium'>
-                    <span
-                      className={cn(
-                        'size-1.5 shrink-0 rounded-full',
-                        hasActive ? dotColorMap.success : dotColorMap.neutral
-                      )}
-                      aria-hidden='true'
-                    />
-                    {hasActive ? (
-                      <span className={cn(textColorMap.success)}>
-                        {activeSubscriptions.length} {t('active')}
-                      </span>
-                    ) : (
+          <div className='rounded-xl border p-4'>
+            <div className='flex flex-wrap items-center justify-between gap-3'>
+              <div className='flex items-center gap-2'>
+                <span className='text-sm font-medium'>
+                  {t('My Subscriptions')}
+                </span>
+                <span className='flex items-center gap-1.5 text-xs font-medium'>
+                  <span
+                    className={cn(
+                      'size-1.5 shrink-0 rounded-full',
+                      hasActive ? dotColorMap.success : dotColorMap.neutral
+                    )}
+                    aria-hidden='true'
+                  />
+                  {hasActive ? (
+                    <span className={cn(textColorMap.success)}>
+                      {activeSubscriptions.length} {t('active')}
+                    </span>
+                  ) : (
+                    <span className='text-muted-foreground'>
+                      {t('No Active')}
+                    </span>
+                  )}
+                  {allSubscriptions.length > activeSubscriptions.length && (
+                    <>
+                      <span className='text-muted-foreground/30'>·</span>
                       <span className='text-muted-foreground'>
-                        {t('No Active')}
+                        {allSubscriptions.length - activeSubscriptions.length}{' '}
+                        {t('expired')}
                       </span>
-                    )}
-                    {allSubscriptions.length > activeSubscriptions.length && (
-                      <>
-                        <span className='text-muted-foreground/30'>·</span>
-                        <span className='text-muted-foreground'>
-                          {allSubscriptions.length - activeSubscriptions.length}{' '}
-                          {t('expired')}
-                        </span>
-                      </>
-                    )}
-                  </span>
-                </div>
-                <div className='flex items-center gap-2'>
-                  <Select
-                    value={displayPref}
-                    onValueChange={handlePreferenceChange}
-                  >
-                    <SelectTrigger className='h-8 w-[140px] text-xs'>
-                      <SelectValue />
-                    </SelectTrigger>
-                    <SelectContent>
-                      <SelectItem
-                        value='subscription_first'
-                        disabled={disablePref}
-                      >
-                        {t('Subscription First')}
-                        {disablePref ? ` (${t('No Active')})` : ''}
-                      </SelectItem>
-                      <SelectItem value='wallet_first'>
-                        {t('Wallet First')}
-                      </SelectItem>
-                      <SelectItem
-                        value='subscription_only'
-                        disabled={disablePref}
-                      >
-                        {t('Subscription Only')}
-                        {disablePref ? ` (${t('No Active')})` : ''}
-                      </SelectItem>
-                      <SelectItem value='wallet_only'>
-                        {t('Wallet Only')}
-                      </SelectItem>
-                    </SelectContent>
-                  </Select>
-                  <Button
-                    variant='ghost'
-                    size='icon'
-                    className='h-8 w-8'
-                    onClick={handleRefresh}
-                    disabled={refreshing}
-                  >
-                    <RefreshCw
-                      className={`h-3.5 w-3.5 ${refreshing ? 'animate-spin' : ''}`}
-                    />
-                  </Button>
-                </div>
+                    </>
+                  )}
+                </span>
               </div>
+              <div className='flex items-center gap-2'>
+                <Select
+                  value={displayPref}
+                  onValueChange={handlePreferenceChange}
+                >
+                  <SelectTrigger className='h-8 w-[140px] text-xs'>
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem
+                      value='subscription_first'
+                      disabled={disablePref}
+                    >
+                      {t('Subscription First')}
+                      {disablePref ? ` (${t('No Active')})` : ''}
+                    </SelectItem>
+                    <SelectItem value='wallet_first'>
+                      {t('Wallet First')}
+                    </SelectItem>
+                    <SelectItem
+                      value='subscription_only'
+                      disabled={disablePref}
+                    >
+                      {t('Subscription Only')}
+                      {disablePref ? ` (${t('No Active')})` : ''}
+                    </SelectItem>
+                    <SelectItem value='wallet_only'>
+                      {t('Wallet Only')}
+                    </SelectItem>
+                  </SelectContent>
+                </Select>
+                <Button
+                  variant='ghost'
+                  size='icon'
+                  className='h-8 w-8'
+                  onClick={handleRefresh}
+                  disabled={refreshing}
+                >
+                  <RefreshCw
+                    className={`h-3.5 w-3.5 ${refreshing ? 'animate-spin' : ''}`}
+                  />
+                </Button>
+              </div>
+            </div>
 
-              {disablePref && isSubPref && (
-                <p className='text-muted-foreground mt-2 text-xs'>
-                  {t(
-                    'Preference saved as {{pref}}, but no active subscription. Wallet will be used automatically.',
-                    {
-                      pref:
-                        billingPreference === 'subscription_only'
-                          ? t('Subscription Only')
-                          : t('Subscription First'),
-                    }
-                  )}
-                </p>
-              )}
+            {disablePref && isSubPref && (
+              <p className='text-muted-foreground mt-2 text-xs'>
+                {t(
+                  'Preference saved as {{pref}}, but no active subscription. Wallet will be used automatically.',
+                  {
+                    pref:
+                      billingPreference === 'subscription_only'
+                        ? t('Subscription Only')
+                        : t('Subscription First'),
+                  }
+                )}
+              </p>
+            )}
 
-              {hasAny && (
-                <>
-                  <Separator className='my-3' />
-                  <div className='max-h-64 space-y-3 overflow-y-auto pr-1'>
-                    {allSubscriptions.map((sub) => {
-                      const subscription = sub.subscription
-                      const totalAmount = Number(
-                        subscription?.amount_total || 0
-                      )
-                      const usedAmount = Number(subscription?.amount_used || 0)
-                      const remainAmount =
-                        totalAmount > 0
-                          ? Math.max(0, totalAmount - usedAmount)
-                          : 0
-                      const planTitle =
-                        planTitleMap.get(subscription?.plan_id) || ''
-                      const remainDays = getRemainingDays(sub)
-                      const usagePercent = getUsagePercent(sub)
-                      const now = Date.now() / 1000
-                      const isExpired = (subscription?.end_time || 0) < now
-                      const isCancelled = subscription?.status === 'cancelled'
-                      const isActive =
-                        subscription?.status === 'active' && !isExpired
+            {hasAny && (
+              <>
+                <Separator className='my-3' />
+                <div className='max-h-64 space-y-3 overflow-y-auto pr-1'>
+                  {allSubscriptions.map((sub) => {
+                    const subscription = sub.subscription
+                    const totalAmount = Number(subscription?.amount_total || 0)
+                    const usedAmount = Number(subscription?.amount_used || 0)
+                    const remainAmount =
+                      totalAmount > 0
+                        ? Math.max(0, totalAmount - usedAmount)
+                        : 0
+                    const planTitle =
+                      planTitleMap.get(subscription?.plan_id) || ''
+                    const remainDays = getRemainingDays(sub)
+                    const usagePercent = getUsagePercent(sub)
+                    const now = Date.now() / 1000
+                    const isExpired = (subscription?.end_time || 0) < now
+                    const isCancelled = subscription?.status === 'cancelled'
+                    const isActive =
+                      subscription?.status === 'active' && !isExpired
 
-                      return (
-                        <div
-                          key={subscription?.id}
-                          className='bg-background rounded-md border p-3 text-xs'
-                        >
-                          <div className='flex items-center justify-between'>
-                            <div className='flex items-center gap-2'>
-                              <span className='font-medium'>
-                                {planTitle
-                                  ? `${planTitle} · ${t('Subscription')} #${subscription?.id}`
-                                  : `${t('Subscription')} #${subscription?.id}`}
-                              </span>
-                              {isActive ? (
-                                <StatusBadge
-                                  label={t('Active')}
-                                  variant='success'
-                                  copyable={false}
-                                />
-                              ) : isCancelled ? (
-                                <StatusBadge
-                                  label={t('Cancelled')}
-                                  variant='neutral'
-                                  copyable={false}
-                                />
-                              ) : (
-                                <StatusBadge
-                                  label={t('Expired')}
-                                  variant='neutral'
-                                  copyable={false}
-                                />
-                              )}
-                            </div>
-                            {isActive && (
-                              <span className='text-muted-foreground'>
-                                {t('{{count}} days remaining', {
-                                  count: remainDays,
-                                })}
-                              </span>
-                            )}
-                          </div>
-                          <div className='text-muted-foreground mt-1.5'>
-                            {isActive
-                              ? t('Until')
-                              : isCancelled
-                                ? t('Cancelled at')
-                                : t('Expired at')}{' '}
-                            {new Date(
-                              (subscription?.end_time || 0) * 1000
-                            ).toLocaleString()}
-                          </div>
-                          {isActive &&
-                            (subscription?.next_reset_time ?? 0) > 0 && (
-                              <div className='text-muted-foreground mt-1'>
-                                {t('Next reset')}:{' '}
-                                {new Date(
-                                  subscription!.next_reset_time! * 1000
-                                ).toLocaleString()}
-                              </div>
-                            )}
-                          <div className='text-muted-foreground mt-1'>
-                            {t('Total Quota')}:{' '}
-                            {totalAmount > 0 ? (
-                              <Tooltip>
-                                <TooltipTrigger asChild>
-                                  <span className='cursor-help'>
-                                    {formatQuota(usedAmount)}/
-                                    {formatQuota(totalAmount)} ·{' '}
-                                    {t('Remaining')} {formatQuota(remainAmount)}
-                                  </span>
-                                </TooltipTrigger>
-                                <TooltipContent>
-                                  {t('Raw Quota')}: {usedAmount}/{totalAmount} ·{' '}
-                                  {t('Remaining')} {remainAmount}
-                                </TooltipContent>
-                              </Tooltip>
+                    return (
+                      <div
+                        key={subscription?.id}
+                        className='bg-background rounded-md border p-3 text-xs'
+                      >
+                        <div className='flex items-center justify-between'>
+                          <div className='flex items-center gap-2'>
+                            <span className='font-medium'>
+                              {planTitle
+                                ? `${planTitle} · ${t('Subscription')} #${subscription?.id}`
+                                : `${t('Subscription')} #${subscription?.id}`}
+                            </span>
+                            {isActive ? (
+                              <StatusBadge
+                                label={t('Active')}
+                                variant='success'
+                                copyable={false}
+                              />
+                            ) : isCancelled ? (
+                              <StatusBadge
+                                label={t('Cancelled')}
+                                variant='neutral'
+                                copyable={false}
+                              />
                             ) : (
-                              t('Unlimited')
-                            )}
-                            {totalAmount > 0 && (
-                              <span className='ml-2'>
-                                {t('Used')} {usagePercent}%
-                              </span>
+                              <StatusBadge
+                                label={t('Expired')}
+                                variant='neutral'
+                                copyable={false}
+                              />
                             )}
                           </div>
-                          {totalAmount > 0 && isActive && (
-                            <Progress
-                              value={usagePercent}
-                              className='mt-2 h-1.5'
-                            />
+                          {isActive && (
+                            <span className='text-muted-foreground'>
+                              {t('{{count}} days remaining', {
+                                count: remainDays,
+                              })}
+                            </span>
+                          )}
+                        </div>
+                        <div className='text-muted-foreground mt-1.5'>
+                          {isActive
+                            ? t('Until')
+                            : isCancelled
+                              ? t('Cancelled at')
+                              : t('Expired at')}{' '}
+                          {new Date(
+                            (subscription?.end_time || 0) * 1000
+                          ).toLocaleString()}
+                        </div>
+                        {isActive &&
+                          (subscription?.next_reset_time ?? 0) > 0 && (
+                            <div className='text-muted-foreground mt-1'>
+                              {t('Next reset')}:{' '}
+                              {new Date(
+                                subscription!.next_reset_time! * 1000
+                              ).toLocaleString()}
+                            </div>
+                          )}
+                        <div className='text-muted-foreground mt-1'>
+                          {t('Total Quota')}:{' '}
+                          {totalAmount > 0 ? (
+                            <Tooltip>
+                              <TooltipTrigger asChild>
+                                <span className='cursor-help'>
+                                  {formatQuota(usedAmount)}/
+                                  {formatQuota(totalAmount)} · {t('Remaining')}{' '}
+                                  {formatQuota(remainAmount)}
+                                </span>
+                              </TooltipTrigger>
+                              <TooltipContent>
+                                {t('Raw Quota')}: {usedAmount}/{totalAmount} ·{' '}
+                                {t('Remaining')} {remainAmount}
+                              </TooltipContent>
+                            </Tooltip>
+                          ) : (
+                            t('Unlimited')
+                          )}
+                          {totalAmount > 0 && (
+                            <span className='ml-2'>
+                              {t('Used')} {usagePercent}%
+                            </span>
                           )}
                         </div>
-                      )
-                    })}
-                  </div>
-                </>
-              )}
+                        {totalAmount > 0 && isActive && (
+                          <Progress
+                            value={usagePercent}
+                            className='mt-2 h-1.5'
+                          />
+                        )}
+                      </div>
+                    )
+                  })}
+                </div>
+              </>
+            )}
 
-              {!hasAny && (
-                <p className='text-muted-foreground mt-2 text-xs'>
-                  {t('Purchase a plan to enjoy model benefits')}
-                </p>
-              )}
-            </CardContent>
-          </Card>
+            {!hasAny && (
+              <p className='text-muted-foreground mt-2 text-xs'>
+                {t('Purchase a plan to enjoy model benefits')}
+              </p>
+            )}
+          </div>
 
           {/* Available plans grid */}
           {plans.length > 0 ? (
@@ -469,27 +480,32 @@ export function SubscriptionPlansCard(props: SubscriptionPlansCardProps) {
                 return (
                   <Card
                     key={plan.id}
-                    className={`relative transition-shadow hover:shadow-md ${
-                      isPopular ? 'ring-primary ring-2' : ''
-                    }`}
-                  >
-                    {isPopular && (
-                      <div className='absolute -top-2.5 left-3'>
-                        <StatusBadge variant='info' copyable={false}>
-                          <Sparkles className='h-3 w-3' />
-                          {t('Recommended')}
-                        </StatusBadge>
-                      </div>
+                    className={cn(
+                      'transition-shadow hover:shadow-md',
+                      isPopular && 'border-primary/70 shadow-sm'
                     )}
-                    <CardContent className='flex h-full flex-col p-4 pt-5'>
-                      <div className='mb-2'>
-                        <h4 className='truncate font-semibold'>
-                          {plan.title || t('Subscription Plans')}
-                        </h4>
-                        {plan.subtitle && (
-                          <p className='text-muted-foreground truncate text-xs'>
-                            {plan.subtitle}
-                          </p>
+                  >
+                    <CardContent className='flex h-full flex-col p-4'>
+                      <div className='mb-2 flex items-start justify-between gap-3'>
+                        <div className='min-w-0'>
+                          <h4 className='truncate font-semibold'>
+                            {plan.title || t('Subscription Plans')}
+                          </h4>
+                          {plan.subtitle && (
+                            <p className='text-muted-foreground truncate text-xs'>
+                              {plan.subtitle}
+                            </p>
+                          )}
+                        </div>
+                        {isPopular && (
+                          <StatusBadge
+                            variant='info'
+                            copyable={false}
+                            className='shrink-0'
+                          >
+                            <Sparkles className='h-3 w-3' />
+                            {t('Recommended')}
+                          </StatusBadge>
                         )}
                       </div>
 

+ 55 - 37
web/default/src/features/wallet/components/wallet-stats-card.tsx

@@ -1,5 +1,7 @@
+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'
@@ -13,13 +15,21 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
   const { t } = useTranslation()
   if (props.loading) {
     return (
-      <Card>
-        <CardContent>
-          <div className='grid grid-cols-1 gap-6 sm:grid-cols-3 sm:gap-8'>
+      <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='space-y-2'>
-                <Skeleton className='h-5 w-28' />
-                <Skeleton className='h-11 w-32' />
+              <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>
@@ -28,39 +38,47 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
     )
   }
 
-  return (
-    <Card>
-      <CardContent>
-        <div className='grid grid-cols-1 gap-6 sm:grid-cols-3 sm:gap-8'>
-          {/* Current Balance */}
-          <div className='min-w-0 space-y-2'>
-            <div className='text-muted-foreground text-sm font-medium'>
-              {t('Current Balance')}
-            </div>
-            <div className='text-3xl leading-tight font-semibold tracking-tight break-all lg:text-4xl'>
-              {formatQuota(props.user?.quota ?? 0)}
-            </div>
-          </div>
-
-          {/* Total Usage */}
-          <div className='min-w-0 space-y-2'>
-            <div className='text-muted-foreground text-sm font-medium'>
-              {t('Total Usage')}
-            </div>
-            <div className='text-3xl leading-tight font-semibold tracking-tight break-all lg:text-4xl'>
-              {formatQuota(props.user?.used_quota ?? 0)}
-            </div>
-          </div>
+  const stats = [
+    {
+      label: t('Current Balance'),
+      value: formatQuota(props.user?.quota ?? 0),
+      icon: WalletCards,
+    },
+    {
+      label: t('Total Usage'),
+      value: formatQuota(props.user?.used_quota ?? 0),
+      icon: BarChart3,
+    },
+    {
+      label: t('API Requests'),
+      value: (props.user?.request_count ?? 0).toLocaleString(),
+      icon: Activity,
+    },
+  ]
 
-          {/* Request Count */}
-          <div className='min-w-0 space-y-2'>
-            <div className='text-muted-foreground text-sm font-medium'>
-              {t('API Requests')}
-            </div>
-            <div className='text-3xl leading-tight font-semibold tracking-tight break-all lg:text-4xl'>
-              {(props.user?.request_count ?? 0).toLocaleString()}
+  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>
             </div>
-          </div>
+          ))}
         </div>
       </CardContent>
     </Card>

+ 48 - 46
web/default/src/features/wallet/index.tsx

@@ -239,54 +239,56 @@ export function Wallet(props: WalletProps) {
           {t('Manage your balance and payment methods')}
         </SectionPageLayout.Description>
         <SectionPageLayout.Content>
-          <div className='grid gap-6 lg:grid-cols-3'>
-            {/* Left Column - Stats & Recharge */}
-            <div className='space-y-6 lg:col-span-2'>
-              <WalletStatsCard user={user} loading={userLoading} />
-              <RechargeFormCard
-                topupInfo={topupInfo}
-                presetAmounts={presetAmounts}
-                selectedPreset={selectedPreset}
-                onSelectPreset={handleSelectPreset}
-                topupAmount={topupAmount}
-                onTopupAmountChange={handleTopupAmountChange}
-                paymentAmount={paymentAmount}
-                calculating={calculating}
-                onPaymentMethodSelect={handlePaymentMethodSelect}
-                paymentLoading={paymentLoading}
-                redemptionCode={redemptionCode}
-                onRedemptionCodeChange={setRedemptionCode}
-                onRedeem={handleRedeem}
-                redeeming={redeeming}
-                topupLink={topupInfo?.topup_link}
-                loading={topupLoading}
-                priceRatio={(status?.price as number) || 1}
-                usdExchangeRate={effectiveUsdExchangeRate}
-                onOpenBilling={() => setBillingDialogOpen(true)}
-                creemProducts={topupInfo?.creem_products}
-                enableCreemTopup={topupInfo?.enable_creem_topup}
-                onCreemProductSelect={handleCreemProductSelect}
-                enableWaffoTopup={topupInfo?.enable_waffo_topup}
-                waffoPayMethods={topupInfo?.waffo_pay_methods}
-                waffoMinTopup={topupInfo?.waffo_min_topup}
-                onWaffoMethodSelect={handleWaffoMethodSelect}
-                enableWaffoPancakeTopup={topupInfo?.enable_waffo_pancake_topup}
-              />
-            </div>
-
-            {/* Right Column - Affiliate & Subscriptions */}
-            <div className='space-y-6 lg:col-span-1'>
-              <AffiliateRewardsCard
-                user={user}
-                affiliateLink={affiliateLink}
-                onTransfer={() => setTransferDialogOpen(true)}
-                loading={affiliateLoading}
-              />
+          <div className='mx-auto flex w-full max-w-7xl flex-col gap-4'>
+            <WalletStatsCard user={user} loading={userLoading} />
+
+            <SubscriptionPlansCard topupInfo={topupInfo} />
+
+            <div className='grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(340px,0.4fr)] xl:items-start'>
+              <div className='min-w-0'>
+                <RechargeFormCard
+                  topupInfo={topupInfo}
+                  presetAmounts={presetAmounts}
+                  selectedPreset={selectedPreset}
+                  onSelectPreset={handleSelectPreset}
+                  topupAmount={topupAmount}
+                  onTopupAmountChange={handleTopupAmountChange}
+                  paymentAmount={paymentAmount}
+                  calculating={calculating}
+                  onPaymentMethodSelect={handlePaymentMethodSelect}
+                  paymentLoading={paymentLoading}
+                  redemptionCode={redemptionCode}
+                  onRedemptionCodeChange={setRedemptionCode}
+                  onRedeem={handleRedeem}
+                  redeeming={redeeming}
+                  topupLink={topupInfo?.topup_link}
+                  loading={topupLoading}
+                  priceRatio={(status?.price as number) || 1}
+                  usdExchangeRate={effectiveUsdExchangeRate}
+                  onOpenBilling={() => setBillingDialogOpen(true)}
+                  creemProducts={topupInfo?.creem_products}
+                  enableCreemTopup={topupInfo?.enable_creem_topup}
+                  onCreemProductSelect={handleCreemProductSelect}
+                  enableWaffoTopup={topupInfo?.enable_waffo_topup}
+                  waffoPayMethods={topupInfo?.waffo_pay_methods}
+                  waffoMinTopup={topupInfo?.waffo_min_topup}
+                  onWaffoMethodSelect={handleWaffoMethodSelect}
+                  enableWaffoPancakeTopup={
+                    topupInfo?.enable_waffo_pancake_topup
+                  }
+                />
+              </div>
+
+              <div className='xl:sticky xl:top-6'>
+                <AffiliateRewardsCard
+                  user={user}
+                  affiliateLink={affiliateLink}
+                  onTransfer={() => setTransferDialogOpen(true)}
+                  loading={affiliateLoading}
+                />
+              </div>
             </div>
           </div>
-
-          {/* Subscription Plans */}
-          <SubscriptionPlansCard topupInfo={topupInfo} />
         </SectionPageLayout.Content>
       </SectionPageLayout>
 

+ 6 - 1
web/default/src/hooks/use-sidebar-config.ts

@@ -55,7 +55,9 @@ const URL_TO_CONFIG_MAP: Record<string, { section: string; module: string }> = {
   '/dashboard': { section: 'console', module: 'detail' },
   '/dashboard/overview': { section: 'console', module: 'detail' },
   '/dashboard/models': { section: 'console', module: 'detail' },
+  '/dashboard/users': { section: 'console', module: 'detail' },
   '/keys': { section: 'console', module: 'token' },
+  '/usage-logs': { section: 'console', module: 'log' },
   '/usage-logs/common': { section: 'console', module: 'log' },
   '/usage-logs/drawing': { section: 'console', module: 'midjourney' },
   '/usage-logs/task': { section: 'console', module: 'task' },
@@ -173,7 +175,10 @@ function isNavItemVisible(
 
   // Handle direct link type
   if ('url' in item && item.url) {
-    return isModuleEnabled(item.url as string, adminConfig, userConfig)
+    const configUrls = item.configUrls ?? [item.url]
+    return configUrls.some((url) =>
+      isModuleEnabled(url as string, adminConfig, userConfig)
+    )
   }
 
   // Handle collapsible type (with sub-items)

+ 17 - 9
web/default/src/hooks/use-sidebar-data.ts

@@ -1,5 +1,6 @@
 import {
   LayoutDashboard,
+  Activity,
   Key,
   FileText,
   Wallet,
@@ -12,19 +13,14 @@ import {
   FlaskConical,
   MessageSquare,
   CreditCard,
+  ListTodo,
 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
-import { useAuthStore } from '@/stores/auth-store'
 import { WORKSPACE_IDS } from '@/components/layout/lib/workspace-registry'
 import { type SidebarData } from '@/components/layout/types'
-import { getDashboardSectionNavItems } from '@/features/dashboard/section-registry'
-import { getModelsSectionNavItems } from '@/features/models/section-registry'
-import { getUsageLogsSectionNavItems } from '@/features/usage-logs/section-registry'
 
 export function useSidebarData(): SidebarData {
   const { t } = useTranslation()
-  const user = useAuthStore((s) => s.auth.user)
-  const isAdmin = Boolean(user?.role && user.role >= 10)
 
   return {
     workspaces: [
@@ -56,10 +52,15 @@ export function useSidebarData(): SidebarData {
         id: 'general',
         title: t('General'),
         items: [
+          {
+            title: t('Overview'),
+            url: '/dashboard/overview',
+            icon: Activity,
+          },
           {
             title: t('Dashboard'),
+            url: '/dashboard/models',
             icon: LayoutDashboard,
-            items: getDashboardSectionNavItems(t, { isAdmin }),
           },
           {
             title: t('API Keys'),
@@ -68,8 +69,15 @@ export function useSidebarData(): SidebarData {
           },
           {
             title: t('Usage Logs'),
+            url: '/usage-logs/common',
             icon: FileText,
-            items: getUsageLogsSectionNavItems(t),
+          },
+          {
+            title: t('Task Logs'),
+            url: '/usage-logs/task',
+            activeUrls: ['/usage-logs/drawing'],
+            configUrls: ['/usage-logs/drawing', '/usage-logs/task'],
+            icon: ListTodo,
           },
           {
             title: t('Wallet'),
@@ -94,8 +102,8 @@ export function useSidebarData(): SidebarData {
           },
           {
             title: t('Models'),
+            url: '/models/metadata',
             icon: Box,
-            items: getModelsSectionNavItems(t),
           },
           {
             title: t('Users'),

+ 1 - 0
web/default/src/i18n/locales/en.json

@@ -2301,6 +2301,7 @@
     "Or continue with": "Or continue with",
     "Or enter this key manually:": "Or enter this key manually:",
     "Order completed successfully": "Order completed successfully",
+    "Order History": "Order History",
     "Order Payment Method": "Order Payment Method",
     "org-...": "org-...",
     "Original Model": "Original Model",

+ 1 - 0
web/default/src/i18n/locales/fr.json

@@ -2301,6 +2301,7 @@
     "Or continue with": "Ou continuer avec",
     "Or enter this key manually:": "Ou entrez cette clé manuellement :",
     "Order completed successfully": "Commande terminée avec succès",
+    "Order History": "Historique des commandes",
     "Order Payment Method": "Moyen de paiement (commande)",
     "org-...": "org-...",
     "Original Model": "Modèle Original",

+ 1 - 0
web/default/src/i18n/locales/ja.json

@@ -2301,6 +2301,7 @@
     "Or continue with": "または、以下で続行",
     "Or enter this key manually:": "または、このキーを手動で入力してください:",
     "Order completed successfully": "注文が正常に完了しました",
+    "Order History": "注文履歴",
     "Order Payment Method": "注文の支払い方法",
     "org-...": "org-...",
     "Original Model": "オリジナルモデル",

+ 1 - 0
web/default/src/i18n/locales/ru.json

@@ -2301,6 +2301,7 @@
     "Or continue with": "Или продолжить с",
     "Or enter this key manually:": "Или введите этот ключ вручную:",
     "Order completed successfully": "Заказ успешно завершен",
+    "Order History": "История заказов",
     "Order Payment Method": "Способ оплаты (заказа)",
     "org-...": "орг-...",
     "Original Model": "Оригинальная модель",

+ 1 - 0
web/default/src/i18n/locales/vi.json

@@ -2301,6 +2301,7 @@
     "Or continue with": "Hoặc tiếp tục với",
     "Or enter this key manually:": "Hoặc nhập khóa này thủ công:",
     "Order completed successfully": "Đơn hàng đã hoàn thành thành công",
+    "Order History": "Lịch sử đơn hàng",
     "Order Payment Method": "Phương thức thanh toán đơn hàng",
     "org-...": "org-...",
     "Original Model": "Nguyên mẫu",

+ 1 - 0
web/default/src/i18n/locales/zh.json

@@ -2301,6 +2301,7 @@
     "Or continue with": "或继续使用",
     "Or enter this key manually:": "或手动输入此密钥:",
     "Order completed successfully": "订单已成功完成",
+    "Order History": "订单历史",
     "Order Payment Method": "订单支付方式",
     "org-...": "org-...",
     "Original Model": "原始模型",

+ 27 - 27
web/default/src/styles/theme.css

@@ -38,39 +38,39 @@
 }
 
 .dark {
-  --background: oklch(0.165 0.012 258);
-  --foreground: oklch(0.92 0.008 247.858);
-  --card: oklch(0.205 0.012 258);
-  --card-foreground: oklch(0.92 0.008 247.858);
-  --popover: oklch(0.225 0.014 258);
-  --popover-foreground: oklch(0.92 0.008 247.858);
-  --primary: oklch(0.87 0.018 255.508);
-  --primary-foreground: oklch(0.235 0.042 265.755);
-  --secondary: oklch(0.255 0.012 258);
-  --secondary-foreground: oklch(0.92 0.008 247.858);
-  --muted: oklch(0.245 0.012 258);
-  --muted-foreground: oklch(0.68 0.014 257.417);
-  --accent: oklch(0.265 0.014 258);
-  --accent-foreground: oklch(0.92 0.008 247.858);
+  --background: oklch(0.245 0.018 265);
+  --foreground: oklch(0.88 0.014 252);
+  --card: oklch(0.275 0.017 265);
+  --card-foreground: oklch(0.88 0.014 252);
+  --popover: oklch(0.3 0.018 265);
+  --popover-foreground: oklch(0.88 0.014 252);
+  --primary: oklch(0.68 0.12 236);
+  --primary-foreground: oklch(0.985 0.004 247.858);
+  --secondary: oklch(0.32 0.016 265);
+  --secondary-foreground: oklch(0.88 0.014 252);
+  --muted: oklch(0.305 0.016 265);
+  --muted-foreground: oklch(0.72 0.018 252);
+  --accent: oklch(0.34 0.024 255);
+  --accent-foreground: oklch(0.9 0.012 252);
   --destructive: oklch(0.704 0.191 22.216);
-  --border: oklch(0.31 0.014 258);
-  --input: oklch(0.34 0.014 258);
-  --ring: oklch(0.58 0.025 256.788);
+  --border: oklch(0.38 0.018 265);
+  --input: oklch(0.405 0.018 265);
+  --ring: oklch(0.62 0.09 236);
   --chart-1: oklch(0.488 0.243 264.376);
   --chart-2: oklch(0.696 0.17 162.48);
   --chart-3: oklch(0.769 0.188 70.08);
   --chart-4: oklch(0.627 0.265 303.9);
   --chart-5: oklch(0.645 0.246 16.439);
-  --sidebar: oklch(0.185 0.012 258);
-  --sidebar-foreground: oklch(0.92 0.008 247.858);
-  --sidebar-primary: oklch(0.75 0.14 233);
-  --sidebar-primary-foreground: oklch(0.29 0.06 243);
-  --sidebar-accent: oklch(0.255 0.012 258);
-  --sidebar-accent-foreground: oklch(0.92 0.008 247.858);
-  --sidebar-border: oklch(0.3 0.014 258);
-  --sidebar-ring: oklch(0.52 0.02 256.788);
-  --skeleton-base: oklch(0.245 0.01 258);
-  --skeleton-highlight: oklch(0.32 0.014 258);
+  --sidebar: oklch(0.255 0.017 265);
+  --sidebar-foreground: oklch(0.86 0.014 252);
+  --sidebar-primary: var(--primary);
+  --sidebar-primary-foreground: var(--primary-foreground);
+  --sidebar-accent: oklch(0.325 0.018 265);
+  --sidebar-accent-foreground: oklch(0.9 0.012 252);
+  --sidebar-border: oklch(0.37 0.018 265);
+  --sidebar-ring: var(--ring);
+  --skeleton-base: oklch(0.31 0.014 265);
+  --skeleton-highlight: oklch(0.39 0.018 265);
 }
 
 @theme inline {