From a7d019e3a9ff6e2f2661a38df00b35a2a58cfab5 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Thu, 7 May 2026 03:20:35 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(default):=20redesign=20dashboa?= =?UTF-8?q?rd=20overview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refresh the overview page with an actionable Get Started guide, live API request details, real usage sparklines, and OpenAI-inspired dashboard panels. Add collapsible setup state, role-aware quick actions, and localized copy so returning users can focus on platform health. --- .../layout/components/section-page-layout.tsx | 8 + .../features/auth/otp/components/otp-form.tsx | 6 +- .../models/models-chart-preferences.tsx | 11 +- .../models/models-filter-dialog.tsx | 13 +- .../overview/announcements-panel.tsx | 10 +- .../components/overview/api-info-panel.tsx | 8 +- .../components/overview/faq-panel.tsx | 8 +- .../overview/overview-dashboard.tsx | 722 ++++++++++++++++++ .../components/overview/summary-cards.tsx | 205 ++++- .../components/overview/uptime-panel.tsx | 8 +- .../dashboard/components/ui/panel-wrapper.tsx | 75 +- .../dashboard/components/ui/stat-card.tsx | 65 +- web/default/src/features/dashboard/index.tsx | 32 +- .../src/features/dashboard/lib/filters.ts | 3 +- .../pricing/components/model-perf-badge.tsx | 7 +- .../billing/section-registry.tsx | 24 +- .../general/quota-settings-section.tsx | 2 +- .../models/group-ratio-form.tsx | 16 +- .../models/group-ratio-visual-editor.tsx | 12 +- .../models/section-registry.tsx | 4 +- .../operations/section-registry.tsx | 7 +- .../security/section-registry.tsx | 4 +- .../system-settings/site/section-registry.tsx | 6 +- .../system-settings/utils/route-config.ts | 10 +- web/default/src/i18n/locales/en.json | 53 +- web/default/src/i18n/locales/fr.json | 53 +- web/default/src/i18n/locales/ja.json | 53 +- web/default/src/i18n/locales/ru.json | 53 +- web/default/src/i18n/locales/vi.json | 53 +- web/default/src/i18n/locales/zh.json | 53 +- 30 files changed, 1389 insertions(+), 195 deletions(-) create mode 100644 web/default/src/features/dashboard/components/overview/overview-dashboard.tsx diff --git a/web/default/src/components/layout/components/section-page-layout.tsx b/web/default/src/components/layout/components/section-page-layout.tsx index 600af58c..2e83fbdc 100644 --- a/web/default/src/components/layout/components/section-page-layout.tsx +++ b/web/default/src/components/layout/components/section-page-layout.tsx @@ -45,6 +45,7 @@ export function SectionPageLayout(props: SectionPageLayoutProps) { ) let title: ReactNode = null + let description: ReactNode = null let actions: ReactNode = null let content: ReactNode = null let breadcrumb: ReactNode = null @@ -53,6 +54,8 @@ export function SectionPageLayout(props: SectionPageLayoutProps) { if (!isValidElement(node)) return const child = node as ReactElement if (child.type === SectionPageLayoutTitle) title = child.props.children + else if (child.type === SectionPageLayoutDescription) + description = child.props.children else if (child.type === SectionPageLayoutActions) actions = child.props.children else if (child.type === SectionPageLayoutContent) @@ -73,6 +76,11 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {

{title}

+ {description != null && ( +

+ {description} +

+ )} {actions != null && (
diff --git a/web/default/src/features/auth/otp/components/otp-form.tsx b/web/default/src/features/auth/otp/components/otp-form.tsx index b8e7e8e5..dc6e528a 100644 --- a/web/default/src/features/auth/otp/components/otp-form.tsx +++ b/web/default/src/features/auth/otp/components/otp-form.tsx @@ -183,7 +183,11 @@ export function OtpForm({ className, ...props }: OtpFormProps) { )} /> - diff --git a/web/default/src/features/dashboard/components/models/models-chart-preferences.tsx b/web/default/src/features/dashboard/components/models/models-chart-preferences.tsx index 368df17d..428c0329 100644 --- a/web/default/src/features/dashboard/components/models/models-chart-preferences.tsx +++ b/web/default/src/features/dashboard/components/models/models-chart-preferences.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { Save, Settings2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import type { TimeGranularity } from '@/lib/time' @@ -45,9 +45,10 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { props.preferences ) - useEffect(() => { - if (open) setDraft(props.preferences) - }, [open, props.preferences]) + const handleOpenChange = (nextOpen: boolean) => { + if (nextOpen) setDraft(props.preferences) + setOpen(nextOpen) + } const handleSave = () => { props.onPreferencesChange(draft) @@ -55,7 +56,7 @@ export function ModelsChartPreferences(props: ModelsChartPreferencesProps) { } return ( - + }> {t('Preferences')} diff --git a/web/default/src/features/dashboard/components/models/models-filter-dialog.tsx b/web/default/src/features/dashboard/components/models/models-filter-dialog.tsx index be6e3bf7..784c57dc 100644 --- a/web/default/src/features/dashboard/components/models/models-filter-dialog.tsx +++ b/web/default/src/features/dashboard/components/models/models-filter-dialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { Filter, RotateCcw, Calendar, Search } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/stores/auth-store' @@ -73,10 +73,15 @@ export function ModelsFilter(props: ModelsFilterProps) { () => props.preferences.defaultTimeRangeDays ) - useEffect(() => { + const resetFiltersFromPreferences = () => { setFilters(buildDefaultDashboardFilters(props.preferences)) setSelectedRange(props.preferences.defaultTimeRangeDays) - }, [props.preferences]) + } + + const handleOpenChange = (nextOpen: boolean) => { + if (nextOpen) resetFiltersFromPreferences() + setOpen(nextOpen) + } const handleApply = () => { props.onFilterChange( @@ -121,7 +126,7 @@ export function ModelsFilter(props: ModelsFilterProps) { } return ( - + }> {t('Filter')} diff --git a/web/default/src/features/dashboard/components/overview/announcements-panel.tsx b/web/default/src/features/dashboard/components/overview/announcements-panel.tsx index 99d0a239..0aba3b79 100644 --- a/web/default/src/features/dashboard/components/overview/announcements-panel.tsx +++ b/web/default/src/features/dashboard/components/overview/announcements-panel.tsx @@ -44,13 +44,15 @@ export function AnnouncementsPanel() { {t('Announcements')} } + description={t('Latest platform updates and notices')} loading={loading} empty={!list.length} emptyMessage={t('No announcements at this time')} - height='h-56 sm:h-64' + height='h-72' + contentClassName='p-0' > - -
+ +
{list.map((item: AnnouncementItem, idx: number) => { const key = item.id ?? `announcement-${idx}` return ( @@ -65,7 +67,7 @@ export function AnnouncementsPanel() { >
-
+

{getPreviewText(item.content)}

diff --git a/web/default/src/features/dashboard/components/overview/api-info-panel.tsx b/web/default/src/features/dashboard/components/overview/api-info-panel.tsx index f434c27b..09e213c5 100644 --- a/web/default/src/features/dashboard/components/overview/api-info-panel.tsx +++ b/web/default/src/features/dashboard/components/overview/api-info-panel.tsx @@ -34,13 +34,15 @@ export function ApiInfoPanel() { {t('API Info')} } + description={t('Configured routes and latency checks')} loading={loading} empty={!list.length} emptyMessage={t('No API routes configured')} - height='h-56 sm:h-64' + height='h-72' + contentClassName='p-0' > - -
+ +
{list.map((item: ApiInfoItem, idx: number) => (
} + description={t('Answers for common access and billing questions')} loading={loading} empty={!list.length} emptyMessage={t('No FAQ entries available')} - height='h-64 sm:h-80' + height='h-80' + contentClassName='p-0' > - - + + {list.map((item: FAQItem, idx: number) => { const key = item.id ?? `faq-${idx}` const value = `item-${key}` diff --git a/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx b/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx new file mode 100644 index 00000000..748c617e --- /dev/null +++ b/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx @@ -0,0 +1,722 @@ +import { useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Link } from '@tanstack/react-router' +import { + ArrowRight, + BookOpen, + Check, + ChevronDown, + ChevronUp, + Circle, + CreditCard, + FileText, + KeyRound, + ListChecks, + Play, + RadioTower, + ShieldCheck, + TerminalSquare, + Timer, + type LucideIcon, +} from 'lucide-react' +import { motion, useReducedMotion } from 'motion/react' +import { useTranslation } from 'react-i18next' +import { useAuthStore } from '@/stores/auth-store' +import { getUserModels } from '@/lib/api' +import { MOTION_TRANSITION } from '@/lib/motion' +import { ROLE } from '@/lib/roles' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { CopyButton } from '@/components/copy-button' +import { + CardStaggerContainer, + CardStaggerItem, +} from '@/components/page-transition' +import { fetchTokenKey, getApiKeys } from '@/features/keys/api' +import type { ApiKey } from '@/features/keys/types' +import { useApiInfo } from '../../hooks/use-status-data' +import { AnnouncementsPanel } from './announcements-panel' +import { ApiInfoPanel } from './api-info-panel' +import { FAQPanel } from './faq-panel' +import { SummaryCards } from './summary-cards' +import { UptimePanel } from './uptime-panel' + +const SETUP_GUIDE_VISIBILITY_STORAGE_KEY = + 'dashboard_overview_setup_guide_expanded' + +const SETUP_GUIDE_CODE_PATTERN = [ + 'const request = await client.responses.create({', + " model: 'gpt-4.1-mini',", + " input: 'Start routing traffic',", + '})', + '', + 'if (request.output_text) {', + ' console.log(request.output_text)', + '}', +].join('\n') + +type DashboardActionPath = + | '/keys' + | '/wallet' + | '/playground' + | '/channels' + | '/usage-logs' + | '/pricing' + +interface StartStep { + title: string + description: string + to: DashboardActionPath + icon: LucideIcon + completed: boolean +} + +interface QuickAction { + title: string + description: string + to: DashboardActionPath + icon: LucideIcon + adminOnly?: boolean +} + +interface RequestExample { + endpoint: string + model: string + keyName: string + displayKey: string + curl: string + ready: boolean +} + +interface HeroSignal { + label: string + value: string + icon: LucideIcon +} + +function getSavedSetupGuideExpanded(): boolean | null { + if (typeof window === 'undefined') return null + const saved = window.localStorage.getItem(SETUP_GUIDE_VISIBILITY_STORAGE_KEY) + if (saved === 'expanded') return true + if (saved === 'collapsed') return false + return null +} + +function saveSetupGuideExpanded(expanded: boolean): void { + if (typeof window === 'undefined') return + window.localStorage.setItem( + SETUP_GUIDE_VISIBILITY_STORAGE_KEY, + expanded ? 'expanded' : 'collapsed' + ) +} + +function getCurrentOrigin(): string { + if (typeof window === 'undefined') return '' + return window.location.origin +} + +function normalizeEndpoint(sourceUrl?: string): string { + const fallback = `${getCurrentOrigin()}/v1/chat/completions` + const trimmed = sourceUrl?.trim() + if (!trimmed) return fallback + + const withoutTrailingSlash = trimmed.replace(/\/+$/, '') + if (withoutTrailingSlash.endsWith('/v1/chat/completions')) { + return withoutTrailingSlash + } + if (withoutTrailingSlash.endsWith('/v1')) { + return `${withoutTrailingSlash}/chat/completions` + } + return `${withoutTrailingSlash}/v1/chat/completions` +} + +function getPreferredKey(keys: ApiKey[]): ApiKey | null { + return keys.find((item) => item.status === 1) ?? keys[0] ?? null +} + +function formatDisplayKey(key?: string): string { + if (!key) return 'sk-...' + if (key.length <= 14) return key + return `${key.slice(0, 7)}...${key.slice(-4)}` +} + +function buildCurlCommand(args: { + endpoint: string + apiKey: string + model: string +}): string { + return [ + `curl ${args.endpoint} \\`, + ' -H "Content-Type: application/json" \\', + ` -H "Authorization: Bearer ${args.apiKey}" \\`, + ` -d '{"model":"${args.model}","messages":[{"role":"user","content":"Say hello in one sentence."}]}'`, + ].join('\n') +} + +function SetupGuideBackdrop(props: { compact?: boolean }) { + return ( + <> +