From b08febaa3c41b7ea1da5118744f661073a585ba5 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Mon, 25 May 2026 00:34:26 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20refactor:=20system=20settings=20UI?= =?UTF-8?q?=20for=20consistent,=20compact=20layouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesign the system settings interface to align with the rest of the console experience by using fixed header actions, removing redundant subtitles, respecting global content width, and standardizing responsive form layouts. Introduce reusable settings layout primitives for forms, switch rows, grouped controls, nested control sections, title status indicators, and page action portals. Replace duplicated card-style switch markup with explicit compact components, improve nested switch readability, and reduce visual noise across authentication, billing, content, integrations, maintenance, models, and request-limit settings. Also complete missing i18n translations, remove obsolete subtitle translation keys, refine i18n sync reporting, fix sidebar truncation for long labels, and verify the frontend with type checking and lint diagnostics. --- web/default/scripts/sync-i18n.mjs | 106 ++++ .../layout/components/chat-presets-item.tsx | 12 +- .../layout/components/nav-group.tsx | 22 +- .../layout/components/section-page-layout.tsx | 10 +- web/default/src/features/channels/index.tsx | 3 - web/default/src/features/dashboard/index.tsx | 11 +- .../features/dashboard/section-registry.tsx | 3 - web/default/src/features/keys/index.tsx | 3 - web/default/src/features/models/index.tsx | 10 +- .../src/features/models/section-registry.tsx | 2 - .../src/features/redemption-codes/index.tsx | 3 - .../src/features/subscriptions/index.tsx | 3 - .../auth/basic-auth-section.tsx | 96 ++-- .../auth/bot-protection-section.tsx | 42 +- .../components/preset-selector.tsx | 5 +- .../components/provider-form-dialog.tsx | 21 +- .../custom-oauth/custom-oauth-section.tsx | 14 +- .../features/system-settings/auth/index.tsx | 2 + .../system-settings/auth/oauth-section.tsx | 128 +++-- .../system-settings/auth/passkey-section.tsx | 46 +- .../system-settings/auth/section-registry.tsx | 6 +- .../system-settings/billing/index.tsx | 2 + .../billing/section-registry.tsx | 9 +- .../components/form-dirty-indicator.tsx | 20 +- .../components/settings-accordion.tsx | 8 +- .../components/settings-form-layout.tsx | 182 +++++++ .../components/settings-page-context.tsx | 146 ++++++ .../components/settings-page.tsx | 99 +++- .../components/settings-section.tsx | 37 +- .../content/announcements-section.tsx | 19 +- .../content/api-info-section.tsx | 19 +- .../content/chat-settings-section.tsx | 21 +- .../content/dashboard-section.tsx | 36 +- .../content/drawing-settings-section.tsx | 38 +- .../system-settings/content/faq-section.tsx | 21 +- .../system-settings/content/index.tsx | 117 ++--- .../content/json-toggle-section.tsx | 36 +- .../content/section-registry.tsx | 8 +- .../content/uptime-kuma-section.tsx | 21 +- .../general/channel-affinity/index.tsx | 45 +- .../channel-affinity/rule-editor-dialog.tsx | 65 ++- .../general/checkin-settings-section.tsx | 47 +- .../general/pricing-section.tsx | 65 +-- .../general/quota-settings-section.tsx | 343 ++++++------- .../general/system-behavior-section.tsx | 60 ++- .../general/system-info-section.tsx | 463 +++++++++--------- .../src/features/system-settings/index.tsx | 9 +- .../integrations/email-settings-section.tsx | 53 +- .../ionet-deployment-settings-section.tsx | 46 +- .../monitoring-settings-section.tsx | 65 ++- .../integrations/payment-settings-section.tsx | 419 +++------------- .../waffo-pancake-settings-section.tsx | 25 +- .../integrations/waffo-settings-section.tsx | 45 +- .../integrations/worker-settings-section.tsx | 45 +- .../maintenance/header-navigation-section.tsx | 99 ++-- .../maintenance/log-settings-section.tsx | 42 +- .../maintenance/notice-section.tsx | 23 +- .../maintenance/performance-section.tsx | 51 +- .../maintenance/sidebar-modules-section.tsx | 71 ++- .../maintenance/update-checker-section.tsx | 5 +- .../models/claude-settings-card.tsx | 43 +- .../models/gemini-settings-card.tsx | 65 ++- .../models/global-settings-card.tsx | 49 +- .../models/grok-settings-card.tsx | 44 +- .../models/group-ratio-form.tsx | 52 +- .../features/system-settings/models/index.tsx | 2 + .../models/model-pricing-sheet.tsx | 40 +- .../models/model-ratio-form.tsx | 81 ++- .../models/ratio-settings-card.tsx | 4 +- .../models/section-registry.tsx | 7 +- .../system-settings/operations/index.tsx | 56 +-- .../operations/section-registry.tsx | 8 +- .../request-limits/rate-limit-section.tsx | 38 +- .../sensitive-words-section.tsx | 51 +- .../request-limits/ssrf-section.tsx | 61 ++- .../system-settings/security/index.tsx | 2 + .../security/section-registry.tsx | 4 +- .../features/system-settings/site/index.tsx | 2 + .../system-settings/site/section-registry.tsx | 5 +- .../system-settings/utils/section-registry.ts | 8 +- web/default/src/features/usage-logs/index.tsx | 11 +- .../features/usage-logs/section-registry.tsx | 3 - web/default/src/features/users/index.tsx | 3 - web/default/src/features/wallet/index.tsx | 3 - .../i18n/locales/_reports/_sync-report.json | 10 +- .../locales/_reports/fr.untranslated.json | 23 - .../locales/_reports/ja.untranslated.json | 122 ----- .../locales/_reports/ru.untranslated.json | 137 ------ .../locales/_reports/vi.untranslated.json | 25 - .../locales/_reports/zh.untranslated.json | 101 ---- web/default/src/i18n/locales/en.json | 52 -- web/default/src/i18n/locales/fr.json | 167 +++---- web/default/src/i18n/locales/ja.json | 175 ++++--- web/default/src/i18n/locales/ru.json | 175 ++++--- web/default/src/i18n/locales/vi.json | 175 ++++--- web/default/src/i18n/locales/zh.json | 172 +++---- web/default/src/i18n/static-keys.ts | 3 - 97 files changed, 2420 insertions(+), 3032 deletions(-) create mode 100644 web/default/src/features/system-settings/components/settings-form-layout.tsx create mode 100644 web/default/src/features/system-settings/components/settings-page-context.tsx delete mode 100644 web/default/src/i18n/locales/_reports/fr.untranslated.json delete mode 100644 web/default/src/i18n/locales/_reports/ja.untranslated.json delete mode 100644 web/default/src/i18n/locales/_reports/ru.untranslated.json delete mode 100644 web/default/src/i18n/locales/_reports/vi.untranslated.json delete mode 100644 web/default/src/i18n/locales/_reports/zh.untranslated.json diff --git a/web/default/scripts/sync-i18n.mjs b/web/default/scripts/sync-i18n.mjs index cb93b748..a4e5da6d 100644 --- a/web/default/scripts/sync-i18n.mjs +++ b/web/default/scripts/sync-i18n.mjs @@ -29,6 +29,90 @@ const OBFUSCATED_KEYS = [ }, ] +const BRAND_AND_LITERAL_KEYS = new Set([ + 'AI Proxy', + 'AIGC2D', + 'Alipay', + 'Anthropic', + 'API URL', + 'API2GPT', + 'AccessKey / SecretAccessKey', + 'AZURE_OPENAI_ENDPOINT *', + 'Baidu V2', + 'ChatGPT', + 'Claude', + 'Client ID', + 'Client Secret', + 'Cloudflare', + 'Cohere', + 'DeepSeek', + 'Discord', + 'DoubaoVideo', + 'FastGPT', + 'Gemini', + 'Gemini Image 4K', + 'GitHub', + 'Jimeng', + 'JustSong', + 'LingYiWanWu', + 'LinuxDO', + 'Midjourney', + 'MidjourneyPlus', + 'Midjourney-Proxy', + 'MiniMax', + 'Mistral', + 'MokaAI', + 'Moonshot', + 'New API', + 'New API <noreply@example.com>', + 'NewAPI', + 'OAuth Client Secret', + 'OhMyGPT', + 'Ollama', + 'One API', + 'OpenAI', + 'OpenAIMax', + 'OpenRouter', + 'Pancake', + 'Passkey', + 'Perplexity', + 'QuantumNous', + 'Quota:', + 'Replicate', + 'SiliconFlow', + 'Stripe', + 'Submodel', + 'SunoAPI', + 'Telegram', + 'Tencent', + 'TTFT P50', + 'TTFT P95', + 'TTFT P99', + 'Uptime Kuma', + 'Uptime Kuma URL', + 'Vertex AI', + 'VolcEngine', + 'Waffo Pancake Dashboard', + 'Waffo Pancake MoR', + 'WeChat', + 'WeChat Pay', + 'Webhook URL', + 'Webhook URL:', + 'Well-Known URL', + 'Worker URL', + 'Xinference', + 'Xunfei', + 'Zhipu V4', + '"default": "us-central1", "claude-3-5-sonnet-20240620": "europe-west1"', + 'edit_this', + 'footer.columns.related.links.midjourney', + 'footer.columns.related.links.newApiKeyTool', + 'my-status', + 'new-api-key-tool', + 'price_xxx', + 'whsec_xxx', +]) + function isPlainObject(v) { return typeof v === 'object' && v !== null && !Array.isArray(v) } @@ -97,6 +181,24 @@ function isLikelyUntranslated({ locale, baseValue, value }) { // Skip short tokens / acronyms / ids const s = baseValue.trim() + if (BRAND_AND_LITERAL_KEYS.has(s)) return false + if ( + /^https?:\/\//.test(s) || + /^\/[\w/-]+/.test(s) || + /^[\w.-]+@[\w.-]+$/.test(s) || + /^smtp\./i.test(s) || + /^socks5:/i.test(s) || + /^org-/.test(s) || + /^gpt-/i.test(s) || + /^checkout\./.test(s) || + /^footer\./.test(s) || + /^[A-Z0-9_ *./:-]+$/.test(s) || + s.startsWith('{') || + s.startsWith('[') || + s.includes(' ') + ) { + return false + } if (s.length < 6) return false if (!/[A-Za-z]{3,}/.test(s)) return false @@ -187,6 +289,8 @@ async function main() { if (Object.keys(extras).length > 0) { await fs.writeFile(path.join(extrasDir, `${locale}.extras.json`), stableStringify(extras), 'utf8') + } else { + await fs.rm(path.join(extrasDir, `${locale}.extras.json`), { force: true }) } if (Object.keys(untranslated).length > 0) { await fs.writeFile( @@ -194,6 +298,8 @@ async function main() { stableStringify(untranslated), 'utf8', ) + } else { + await fs.rm(path.join(reportsDir, `${locale}.untranslated.json`), { force: true }) } // Rewrite locale file in base order (even for en to normalize formatting) diff --git a/web/default/src/components/layout/components/chat-presets-item.tsx b/web/default/src/components/layout/components/chat-presets-item.tsx index f82c78b2..c954113d 100644 --- a/web/default/src/components/layout/components/chat-presets-item.tsx +++ b/web/default/src/components/layout/components/chat-presets-item.tsx @@ -231,9 +231,9 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) { } > - {item.icon && } - {item.title} - + {item.icon && } + {item.title} + {visiblePresets.map((preset) => ( @@ -261,9 +261,9 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) { className='group/collapsible-trigger' render={} > - {item.icon && } - {item.title} - + {item.icon && } + {item.title} + diff --git a/web/default/src/components/layout/components/nav-group.tsx b/web/default/src/components/layout/components/nav-group.tsx index c1688acf..23845e54 100644 --- a/web/default/src/components/layout/components/nav-group.tsx +++ b/web/default/src/components/layout/components/nav-group.tsx @@ -112,7 +112,7 @@ export function NavGroup({ title, items }: NavGroupProps) { * Navigation badge component */ function NavBadge({ children }: { children: ReactNode }) { - return {children} + return {children} } /** @@ -127,8 +127,8 @@ function SidebarMenuLink({ item, href }: { item: NavLink; href: string }) { tooltip={item.title} render={ setOpenMobile(false)} />} > - {item.icon && } - {item.title} + {item.icon && } + {item.title} {item.badge && {item.badge}} @@ -170,10 +170,10 @@ function SidebarMenuCollapsible({ className='group/collapsible-trigger' render={} > - {item.icon && } - {item.title} + {item.icon && } + {item.title} {item.badge && {item.badge}} - + @@ -185,8 +185,8 @@ function SidebarMenuCollapsible({ setOpenMobile(false)} /> } > - {subItem.icon && } - {subItem.title} + {subItem.icon && } + {subItem.title} {subItem.badge && {subItem.badge}} @@ -219,10 +219,10 @@ function SidebarMenuCollapsedDropdown({ /> } > - {item.icon && } - {item.title} + {item.icon && } + {item.title} {item.badge && {item.badge}} - + 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 c3a7e462..35c381f0 100644 --- a/web/default/src/components/layout/components/section-page-layout.tsx +++ b/web/default/src/components/layout/components/section-page-layout.tsx @@ -33,11 +33,6 @@ function SectionPageLayoutTitle(_props: SlotProps) { } SectionPageLayoutTitle.displayName = 'SectionPageLayout.Title' -function SectionPageLayoutDescription(_props: SlotProps) { - return null -} -SectionPageLayoutDescription.displayName = 'SectionPageLayout.Description' - function SectionPageLayoutActions(_props: SlotProps) { return null } @@ -87,13 +82,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
{breadcrumb}
)}
-
+

{title}

{actions != null && ( -
+
{actions}
)} @@ -114,7 +109,6 @@ export function SectionPageLayout(props: SectionPageLayoutProps) { } SectionPageLayout.Title = SectionPageLayoutTitle -SectionPageLayout.Description = SectionPageLayoutDescription SectionPageLayout.Actions = SectionPageLayoutActions SectionPageLayout.Content = SectionPageLayoutContent SectionPageLayout.Breadcrumb = SectionPageLayoutBreadcrumb diff --git a/web/default/src/features/channels/index.tsx b/web/default/src/features/channels/index.tsx index f3ea43b7..4099c105 100644 --- a/web/default/src/features/channels/index.tsx +++ b/web/default/src/features/channels/index.tsx @@ -29,9 +29,6 @@ export function Channels() { {t('Channels')} - - {t('Manage API channels and provider configurations')} - diff --git a/web/default/src/features/dashboard/index.tsx b/web/default/src/features/dashboard/index.tsx index 6f57e7a9..3c20ad71 100644 --- a/web/default/src/features/dashboard/index.tsx +++ b/web/default/src/features/dashboard/index.tsx @@ -130,21 +130,15 @@ function PerformanceOverviewFallback() { ) } -const SECTION_META: Record< - DashboardSectionId, - { titleKey: string; descriptionKey: string } -> = { +const SECTION_META: Record = { overview: { titleKey: 'Overview', - descriptionKey: 'View dashboard overview and statistics', }, models: { titleKey: 'Model Call Analytics', - descriptionKey: 'View model call count analytics and charts', }, users: { titleKey: 'User Analytics', - descriptionKey: 'View user consumption statistics and charts', }, } @@ -227,9 +221,6 @@ export function Dashboard() { return ( {t(meta.titleKey)} - - {t(meta.descriptionKey)} -
{activeSection !== 'overview' && ( diff --git a/web/default/src/features/dashboard/section-registry.tsx b/web/default/src/features/dashboard/section-registry.tsx index d258dde6..db670f0c 100644 --- a/web/default/src/features/dashboard/section-registry.tsx +++ b/web/default/src/features/dashboard/section-registry.tsx @@ -26,19 +26,16 @@ const DASHBOARD_SECTIONS = [ { id: 'overview', titleKey: 'Overview', - descriptionKey: 'View dashboard overview and statistics', build: () => null, }, { id: 'models', titleKey: 'Model Call Analytics', - descriptionKey: 'View model call count analytics and charts', build: () => null, }, { id: 'users', titleKey: 'User Analytics', - descriptionKey: 'View user consumption statistics and charts', adminOnly: true, build: () => null, }, diff --git a/web/default/src/features/keys/index.tsx b/web/default/src/features/keys/index.tsx index 9c980909..0414d456 100644 --- a/web/default/src/features/keys/index.tsx +++ b/web/default/src/features/keys/index.tsx @@ -29,9 +29,6 @@ export function ApiKeys() { {t('API Keys')} - - {t('Manage your API keys for accessing the service')} - diff --git a/web/default/src/features/models/index.tsx b/web/default/src/features/models/index.tsx index 71d96024..3afbff60 100644 --- a/web/default/src/features/models/index.tsx +++ b/web/default/src/features/models/index.tsx @@ -42,17 +42,12 @@ import { const route = getRouteApi('/_authenticated/models/$section') -const SECTION_META: Record< - ModelsSectionId, - { titleKey: string; descriptionKey: string } -> = { +const SECTION_META: Record = { metadata: { titleKey: 'Metadata', - descriptionKey: 'Manage model metadata and configuration', }, deployments: { titleKey: 'Deployments', - descriptionKey: 'Manage model deployments', }, } @@ -126,9 +121,6 @@ function ModelsContent() { <> {t(meta.titleKey)} - - {t(meta.descriptionKey)} - {activeSection === 'metadata' ? ( diff --git a/web/default/src/features/models/section-registry.tsx b/web/default/src/features/models/section-registry.tsx index f8c0e018..6f96ac0f 100644 --- a/web/default/src/features/models/section-registry.tsx +++ b/web/default/src/features/models/section-registry.tsx @@ -25,13 +25,11 @@ const MODELS_SECTIONS = [ { id: 'metadata', titleKey: 'Metadata', - descriptionKey: 'Manage model metadata and configuration', build: () => null, // Content is rendered directly in the page component }, { id: 'deployments', titleKey: 'Deployments', - descriptionKey: 'Manage model deployments', build: () => null, // Content is rendered directly in the page component }, ] as const diff --git a/web/default/src/features/redemption-codes/index.tsx b/web/default/src/features/redemption-codes/index.tsx index dc71ee35..9fd74a66 100644 --- a/web/default/src/features/redemption-codes/index.tsx +++ b/web/default/src/features/redemption-codes/index.tsx @@ -31,9 +31,6 @@ export function Redemptions() { {t('Redemption Codes')} - - {t('Manage redemption codes for quota top-up')} - diff --git a/web/default/src/features/subscriptions/index.tsx b/web/default/src/features/subscriptions/index.tsx index 322042ff..ddae9405 100644 --- a/web/default/src/features/subscriptions/index.tsx +++ b/web/default/src/features/subscriptions/index.tsx @@ -38,9 +38,6 @@ function SubscriptionsContent() { {t('Subscription Management')} - - {t('Manage subscription plan creation, pricing and status')} -
diff --git a/web/default/src/features/system-settings/auth/basic-auth-section.tsx b/web/default/src/features/system-settings/auth/basic-auth-section.tsx index ebf8e29d..8d53ea1a 100644 --- a/web/default/src/features/system-settings/auth/basic-auth-section.tsx +++ b/web/default/src/features/system-settings/auth/basic-auth-section.tsx @@ -21,7 +21,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -33,6 +32,12 @@ import { } from '@/components/ui/form' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useResetForm } from '../hooks/use-reset-form' import { useUpdateOption } from '../hooks/use-update-option' @@ -100,32 +105,31 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { } return ( - +
- + + ( - -
- - {t('Password Login')} - + + + {t('Password Login')} {t('Allow users to log in with password')} -
+ -
+ )} /> @@ -133,22 +137,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { control={form.control} name='RegisterEnabled' render={({ field }) => ( - -
- - {t('Registration Enabled')} - + + + {t('Registration Enabled')} {t('Allow new users to register')} -
+ -
+ )} /> @@ -156,22 +158,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { control={form.control} name='PasswordRegisterEnabled' render={({ field }) => ( - -
- - {t('Password Registration')} - + + + {t('Password Registration')} {t('Allow registration with password')} -
+ -
+ )} /> @@ -179,22 +179,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { control={form.control} name='EmailVerificationEnabled' render={({ field }) => ( - -
- - {t('Email Verification')} - + + + {t('Email Verification')} {t('Require email verification for new accounts')} -
+ -
+ )} /> @@ -202,22 +200,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { control={form.control} name='EmailDomainRestrictionEnabled' render={({ field }) => ( - -
- - {t('Email Domain Restriction')} - + + + {t('Email Domain Restriction')} {t('Only allow specific email domains')} -
+ -
+ )} /> @@ -225,22 +221,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { control={form.control} name='EmailAliasRestrictionEnabled' render={({ field }) => ( - -
- - {t('Email Alias Restriction')} - + + + {t('Email Alias Restriction')} {t('Block email aliases (e.g., user+alias@domain.com)')} -
+ -
+ )} /> @@ -266,11 +260,7 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { )} /> - - - +
) diff --git a/web/default/src/features/system-settings/auth/bot-protection-section.tsx b/web/default/src/features/system-settings/auth/bot-protection-section.tsx index 143d220e..44943705 100644 --- a/web/default/src/features/system-settings/auth/bot-protection-section.tsx +++ b/web/default/src/features/system-settings/auth/bot-protection-section.tsx @@ -21,7 +21,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -33,6 +32,12 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -75,40 +80,33 @@ export function BotProtectionSection({ } return ( - +
- + + ( - -
- - {t('Enable Turnstile')} - + + + {t('Enable Turnstile')} {t( 'Protect login and registration with Cloudflare Turnstile' )} -
+ -
+ )} /> @@ -148,11 +146,7 @@ export function BotProtectionSection({ )} /> - - - +
) diff --git a/web/default/src/features/system-settings/auth/custom-oauth/components/preset-selector.tsx b/web/default/src/features/system-settings/auth/custom-oauth/components/preset-selector.tsx index 3ab4bd47..70293102 100644 --- a/web/default/src/features/system-settings/auth/custom-oauth/components/preset-selector.tsx +++ b/web/default/src/features/system-settings/auth/custom-oauth/components/preset-selector.tsx @@ -29,6 +29,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { SettingsControlGroup } from '../../../components/settings-form-layout' import { OAUTH_PRESETS, type CustomOAuthFormValues } from '../types' type PresetSelectorProps = { @@ -102,7 +103,7 @@ export function PresetSelector(props: PresetSelectorProps) { } return ( -
+

{t('Quick Setup from Preset')}

@@ -140,6 +141,6 @@ export function PresetSelector(props: PresetSelectorProps) { />
-
+ ) } diff --git a/web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx b/web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx index 4280de5b..e34afe15 100644 --- a/web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx +++ b/web/default/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx @@ -50,6 +50,11 @@ import { import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../../../components/settings-form-layout' import { useCreateProvider, useUpdateProvider, @@ -185,7 +190,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
- + {/* Preset Selector (only for creating) */} {!isEditing && } @@ -197,22 +202,20 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { control={form.control} name='enabled' render={({ field }) => ( - -
- - {t('Enabled')} - + + + {t('Enabled')} {t('Allow users to sign in with this provider')} -
+ -
+ )} /> @@ -602,7 +605,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { : t('Create Provider')} - +
diff --git a/web/default/src/features/system-settings/auth/custom-oauth/custom-oauth-section.tsx b/web/default/src/features/system-settings/auth/custom-oauth/custom-oauth-section.tsx index 5fc4b575..39517cd9 100644 --- a/web/default/src/features/system-settings/auth/custom-oauth/custom-oauth-section.tsx +++ b/web/default/src/features/system-settings/auth/custom-oauth/custom-oauth-section.tsx @@ -50,12 +50,7 @@ export function CustomOAuthSection() { if (isLoading) { return ( - +
{t('Loading...')}
@@ -64,12 +59,7 @@ export function CustomOAuthSection() { } return ( - + ) } diff --git a/web/default/src/features/system-settings/auth/oauth-section.tsx b/web/default/src/features/system-settings/auth/oauth-section.tsx index e9d2d78c..31af1e15 100644 --- a/web/default/src/features/system-settings/auth/oauth-section.tsx +++ b/web/default/src/features/system-settings/auth/oauth-section.tsx @@ -21,10 +21,8 @@ import * as z from 'zod' import axios from 'axios' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' -import { RotateCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -39,6 +37,12 @@ import { Switch } from '@/components/ui/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { FormDirtyIndicator } from '../components/form-dirty-indicator' import { FormNavigationGuard } from '../components/form-navigation-guard' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -69,6 +73,9 @@ const oauthSchema = z.object({ WeChatAccountQRCodeImageURL: z.string().optional(), }) +const oauthTabContentClassName = + 'grid min-w-0 gap-x-5 gap-y-6 lg:grid-cols-2 [&>[data-slot=form-item]]:min-w-0 lg:[&>[data-slot=form-item]:has([data-slot=switch])]:col-span-2' + type OAuthFormValues = z.infer type OAuthSectionProps = { @@ -250,12 +257,15 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { <> - +
- + + @@ -268,27 +278,25 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { {t('WeChat')} - + ( - -
- - {t('Enable GitHub OAuth')} - + + + {t('Enable GitHub OAuth')} {t('Allow users to sign in with GitHub')} -
+ -
+ )} /> @@ -330,27 +338,25 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { />
- + ( - -
- - {t('Enable Discord OAuth')} - + + + {t('Enable Discord OAuth')} {t('Allow users to sign in with Discord')} -
+ -
+ )} /> @@ -393,27 +399,25 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { />
- + ( - -
- - {t('Enable OIDC')} - + + + {t('Enable OIDC')} {t('Allow users to sign in with OpenID Connect')} -
+ -
+ )} /> @@ -537,27 +541,28 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { />
- + ( - -
- - {t('Enable Telegram OAuth')} - + + + {t('Enable Telegram OAuth')} {t('Allow users to sign in with Telegram')} -
+ -
+ )} /> @@ -599,27 +604,25 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { />
- + ( - -
- - {t('Enable LinuxDO OAuth')} - + + + {t('Enable LinuxDO OAuth')} {t('Allow users to sign in with LinuxDO')} -
+ -
+ )} /> @@ -678,27 +681,25 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { />
- + ( - -
- - {t('Enable WeChat Auth')} - + + + {t('Enable WeChat Auth')} {t('Allow users to sign in with WeChat')} -
+ -
+ )} /> @@ -758,22 +759,7 @@ export function OAuthSection({ defaultValues }: OAuthSectionProps) { />
- -
- - -
- +
diff --git a/web/default/src/features/system-settings/auth/passkey-section.tsx b/web/default/src/features/system-settings/auth/passkey-section.tsx index d7897789..873302c8 100644 --- a/web/default/src/features/system-settings/auth/passkey-section.tsx +++ b/web/default/src/features/system-settings/auth/passkey-section.tsx @@ -21,7 +21,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -42,6 +41,12 @@ import { } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useResetForm } from '../hooks/use-reset-form' import { useUpdateOption } from '../hooks/use-update-option' @@ -156,34 +161,33 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) { } return ( - +
- + + ( - -
- - {t('Enable Passkey')} - + + + {t('Enable Passkey')} {t( 'Allow users to register and sign in with Passkey (WebAuthn)' )} -
+ -
+ )} /> @@ -323,24 +327,22 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) { control={form.control} name='passkey.allow_insecure_origin' render={({ field }) => ( - -
- - {t('Allow Insecure Origins')} - + + + {t('Allow Insecure Origins')} {t( 'Permit Passkey registration on non-HTTPS origins (only recommended for development)' )} -
+ -
+ )} /> @@ -367,9 +369,7 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) { )} /> - - - +
) diff --git a/web/default/src/features/system-settings/auth/section-registry.tsx b/web/default/src/features/system-settings/auth/section-registry.tsx index 89839d68..622eb1d3 100644 --- a/web/default/src/features/system-settings/auth/section-registry.tsx +++ b/web/default/src/features/system-settings/auth/section-registry.tsx @@ -28,7 +28,6 @@ const AUTH_SECTIONS = [ { id: 'basic-auth', titleKey: 'Basic Authentication', - descriptionKey: 'Configure password-based login and registration', build: (settings: AuthSettings) => ( ( ( ( , }, ] as const @@ -143,3 +138,4 @@ export const AUTH_SECTION_IDS = authRegistry.sectionIds export const AUTH_DEFAULT_SECTION = authRegistry.defaultSection export const getAuthSectionNavItems = authRegistry.getSectionNavItems export const getAuthSectionContent = authRegistry.getSectionContent +export const getAuthSectionMeta = authRegistry.getSectionMeta diff --git a/web/default/src/features/system-settings/billing/index.tsx b/web/default/src/features/system-settings/billing/index.tsx index 93817224..daad5066 100644 --- a/web/default/src/features/system-settings/billing/index.tsx +++ b/web/default/src/features/system-settings/billing/index.tsx @@ -21,6 +21,7 @@ import type { BillingSettings } from '../types' import { BILLING_DEFAULT_SECTION, getBillingSectionContent, + getBillingSectionMeta, } from './section-registry.tsx' const defaultBillingSettings: BillingSettings = { @@ -113,6 +114,7 @@ export function BillingSettings() { defaultSettings={defaultBillingSettings} defaultSection={BILLING_DEFAULT_SECTION} getSectionContent={getBillingSectionContent} + getSectionMeta={getBillingSectionMeta} /> ) } diff --git a/web/default/src/features/system-settings/billing/section-registry.tsx b/web/default/src/features/system-settings/billing/section-registry.tsx index 2e43d66e..1a1dc8a2 100644 --- a/web/default/src/features/system-settings/billing/section-registry.tsx +++ b/web/default/src/features/system-settings/billing/section-registry.tsx @@ -54,7 +54,6 @@ const BILLING_SECTIONS = [ { id: 'quota', titleKey: 'Quota Settings', - descriptionKey: 'Configure user quota allocation and rewards', build: (settings: BillingSettings) => ( ( ( ( ( ( . For commercial licensing, please contact support@quantumnous.com */ -import { Info } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { Alert, AlertDescription } from '@/components/ui/alert' +import { SettingsPageTitleStatusPortal } from './settings-page-context' type FormDirtyIndicatorProps = { isDirty: boolean @@ -26,7 +25,7 @@ type FormDirtyIndicatorProps = { } /** - * Visual indicator that the form has unsaved changes + * Compact page-title status indicator for unsaved form changes. * * @example * ```tsx @@ -41,14 +40,11 @@ export function FormDirtyIndicator({ if (!isDirty) return null return ( - - - - {message ?? t('You have unsaved changes')} - - + + + + {message ? t(message) : t('Unsaved changes')} + + ) } diff --git a/web/default/src/features/system-settings/components/settings-accordion.tsx b/web/default/src/features/system-settings/components/settings-accordion.tsx index 76ec9c91..31222b43 100644 --- a/web/default/src/features/system-settings/components/settings-accordion.tsx +++ b/web/default/src/features/system-settings/components/settings-accordion.tsx @@ -16,6 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ +import { cn } from '@/lib/utils' import { AccordionItem, AccordionTrigger, @@ -25,7 +26,6 @@ import { type SettingsAccordionProps = { value: string title: string - description?: string children: React.ReactNode className?: string } @@ -33,18 +33,14 @@ type SettingsAccordionProps = { export function SettingsAccordion({ value, title, - description, children, className, }: SettingsAccordionProps) { return ( - +
{title}
- {description && ( -
{description}
- )}
{children} diff --git a/web/default/src/features/system-settings/components/settings-form-layout.tsx b/web/default/src/features/system-settings/components/settings-form-layout.tsx new file mode 100644 index 00000000..070136b4 --- /dev/null +++ b/web/default/src/features/system-settings/components/settings-form-layout.tsx @@ -0,0 +1,182 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { ComponentProps, ReactNode } from 'react' +import { cn } from '@/lib/utils' +import { FormItem } from '@/components/ui/form' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' + +type SettingsFormGridProps = { + children: ReactNode + className?: string +} + +type SettingsFormGridItemProps = SettingsFormGridProps & { + span?: 'default' | 'full' +} + +type SettingsSwitchItemProps = ComponentProps +type SettingsSwitchRowProps = ComponentProps<'div'> +type SettingsControlGroupProps = ComponentProps<'div'> +type SettingsControlChildrenProps = ComponentProps<'div'> +type SettingsSwitchFieldProps = SettingsSwitchRowProps & { + checked: boolean + onCheckedChange: (checked: boolean) => void + label: ReactNode + description?: ReactNode + disabled?: boolean +} + +const settingsSwitchRowClassName = + 'flex min-w-0 flex-row items-center justify-between gap-4 border-b py-2.5 last:border-b-0' + +export function SettingsFormGrid(props: SettingsFormGridProps) { + return ( +
+ {props.children} +
+ ) +} + +export function SettingsFormGridItem(props: SettingsFormGridItemProps) { + return ( +
+ {props.children} +
+ ) +} + +export function SettingsSwitchItem({ + className, + ...props +}: SettingsSwitchItemProps) { + return ( + + ) +} + +export function SettingsSwitchRow({ + className, + ...props +}: SettingsSwitchRowProps) { + return ( +
+ ) +} + +export function SettingsSwitchField({ + checked, + onCheckedChange, + label, + description, + disabled, + className, + ...props +}: SettingsSwitchFieldProps) { + return ( + + + + {description ? ( +

{description}

+ ) : null} +
+ +
+ ) +} + +export function SettingsSwitchContent(props: SettingsFormGridProps) { + return ( +
+ {props.children} +
+ ) +} + +export function SettingsControlGroup({ + className, + ...props +}: SettingsControlGroupProps) { + return ( +
+ ) +} + +export function SettingsControlChildren({ + className, + ...props +}: SettingsControlChildrenProps) { + return ( +
+ ) +} + +export function SettingsForm({ className, ...props }: ComponentProps<'form'>) { + return ( +
*:not([data-slot=form-item])]:col-span-2', + 'lg:[&>[data-settings-form-span=full]]:col-span-2', + 'lg:[&>[data-slot=alert]]:col-span-2', + '[&>[data-slot=form-item]]:min-w-0', + 'lg:[&>[data-slot=form-item]:has(textarea)]:col-span-2', + 'lg:[&>[data-slot=form-item]:has([data-slot=switch])]:col-span-2', + className + )} + {...props} + /> + ) +} diff --git a/web/default/src/features/system-settings/components/settings-page-context.tsx b/web/default/src/features/system-settings/components/settings-page-context.tsx new file mode 100644 index 00000000..2cdb73e4 --- /dev/null +++ b/web/default/src/features/system-settings/components/settings-page-context.tsx @@ -0,0 +1,146 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { + createContext, + useContext, + type ComponentProps, + type ReactNode, + type RefObject, +} from 'react' +import { RotateCcw, Save } from 'lucide-react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import { Button } from '@/components/ui/button' + +type SettingsPageContextValue = { + actionsContainer: HTMLDivElement | null + titleStatusContainer: HTMLSpanElement | null + suppressSectionHeader: boolean +} + +const SettingsPageContext = createContext({ + actionsContainer: null, + titleStatusContainer: null, + suppressSectionHeader: false, +}) + +type SettingsPageProviderProps = { + actionsContainer: HTMLDivElement | null + titleStatusContainer?: HTMLSpanElement | null + children: ReactNode + suppressSectionHeader?: boolean +} + +export function SettingsPageProvider(props: SettingsPageProviderProps) { + return ( + + {props.children} + + ) +} + +export function useSuppressSettingsSectionHeader() { + return useContext(SettingsPageContext).suppressSectionHeader +} + +type SettingsPageTitleStatusPortalProps = { + children: ReactNode +} + +export function SettingsPageTitleStatusPortal( + props: SettingsPageTitleStatusPortalProps +) { + const { titleStatusContainer } = useContext(SettingsPageContext) + + if (!titleStatusContainer) return null + + return createPortal(props.children, titleStatusContainer) +} + +type SettingsPageActionsPortalProps = { + children: ReactNode +} + +export function SettingsPageActionsPortal( + props: SettingsPageActionsPortalProps +) { + const { actionsContainer } = useContext(SettingsPageContext) + + if (!actionsContainer) return null + + return createPortal( +
+ {props.children} +
, + actionsContainer + ) +} + +type SettingsPageFormActionsProps = { + onSave: () => void + onReset?: () => void + isSaving?: boolean + isSaveDisabled?: boolean + isResetDisabled?: boolean + saveLabel?: string + savingLabel?: string + resetLabel?: string + resetVariant?: ComponentProps['variant'] + saveButtonRef?: RefObject +} + +export function SettingsPageFormActions(props: SettingsPageFormActionsProps) { + const { t } = useTranslation() + const saveLabel = props.isSaving + ? (props.savingLabel ?? 'Saving...') + : (props.saveLabel ?? 'Save Changes') + + return ( + + {props.onReset && ( + + )} + + + ) +} diff --git a/web/default/src/features/system-settings/components/settings-page.tsx b/web/default/src/features/system-settings/components/settings-page.tsx index 1cf8bc5b..98e3bf1f 100644 --- a/web/default/src/features/system-settings/components/settings-page.tsx +++ b/web/default/src/features/system-settings/components/settings-page.tsx @@ -16,13 +16,18 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ +import { useMemo, useState, type ReactNode } from 'react' import { useParams } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' +import { SectionPageLayout } from '@/components/layout' import { useSystemOptions, getOptionValue } from '../hooks/use-system-options' +import type { SystemOption } from '../types' +import { SettingsPageProvider } from './settings-page-context' type SettingsPageProps< TSettings extends Record, TSectionId extends string, + TExtraArgs extends unknown[] = [], > = { routePath: string defaultSettings: TSettings @@ -30,9 +35,57 @@ type SettingsPageProps< getSectionContent: ( sectionId: TSectionId, settings: TSettings, - ...extraArgs: unknown[] - ) => React.ReactNode - extraArgs?: unknown[] + ...extraArgs: TExtraArgs + ) => ReactNode + getSectionMeta: (sectionId: TSectionId) => { + titleKey: string + } + extraArgs?: TExtraArgs + loadingMessage?: string + resolveSettings?: ( + settings: TSettings, + raw: SystemOption[] | undefined + ) => TSettings +} + +type SettingsPageFrameProps = { + title: ReactNode + children: ReactNode +} + +function SettingsPageFrame(props: SettingsPageFrameProps) { + const [actionsContainer, setActionsContainer] = + useState(null) + const [titleStatusContainer, setTitleStatusContainer] = + useState(null) + + return ( + + + + + {props.title} + + + + +
+ + +
{props.children}
+
+ + + ) } /** @@ -42,39 +95,53 @@ type SettingsPageProps< export function SettingsPage< TSettings extends Record, TSectionId extends string, + TExtraArgs extends unknown[] = [], >({ routePath, defaultSettings, defaultSection, getSectionContent, - extraArgs = [], -}: SettingsPageProps) { + getSectionMeta, + extraArgs, + loadingMessage = 'Loading settings...', + resolveSettings, +}: SettingsPageProps) { const { t } = useTranslation() const { data, isLoading } = useSystemOptions() // eslint-disable-next-line @typescript-eslint/no-explicit-any const params = useParams({ from: routePath as any }) + const activeSection = (params?.section ?? defaultSection) as TSectionId + const sectionMeta = getSectionMeta(activeSection) + + const settings = useMemo(() => { + const baseSettings = getOptionValue( + data?.data, + defaultSettings + ) as TSettings + return resolveSettings + ? resolveSettings(baseSettings, data?.data) + : baseSettings + }, [data?.data, defaultSettings, resolveSettings]) if (isLoading) { return ( -
-
{t('Loading settings...')}
-
+ +
+ {t(loadingMessage)} +
+
) } - const settings = getOptionValue(data?.data, defaultSettings) as TSettings - const activeSection = (params?.section ?? defaultSection) as TSectionId const sectionContent = getSectionContent( activeSection, settings, - ...extraArgs + ...((extraArgs ?? []) as TExtraArgs) ) return ( -
-
-
{sectionContent}
-
-
+ + {sectionContent} + ) } diff --git a/web/default/src/features/system-settings/components/settings-section.tsx b/web/default/src/features/system-settings/components/settings-section.tsx index 89cbe9e0..b0eb2cb9 100644 --- a/web/default/src/features/system-settings/components/settings-section.tsx +++ b/web/default/src/features/system-settings/components/settings-section.tsx @@ -16,10 +16,12 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ +import { cn } from '@/lib/utils' +import { useSuppressSettingsSectionHeader } from './settings-page-context' + type SettingsSectionProps = { title: string titleProps?: React.HTMLAttributes - description?: string children: React.ReactNode className?: string } @@ -27,32 +29,23 @@ type SettingsSectionProps = { export function SettingsSection({ title, titleProps, - description, children, className, }: SettingsSectionProps) { - const baseClassName = 'space-y-4' - const sectionClassName = className - ? `${baseClassName} ${className}` - : baseClassName + const suppressHeader = useSuppressSettingsSectionHeader() return ( -
-
-

- {title} -

- {description && ( -

{description}

- )} -
+
+ {!suppressHeader && ( +
+

+ {title} +

+
+ )} {children}
) diff --git a/web/default/src/features/system-settings/content/announcements-section.tsx b/web/default/src/features/system-settings/content/announcements-section.tsx index f48df817..560cb2ca 100644 --- a/web/default/src/features/system-settings/content/announcements-section.tsx +++ b/web/default/src/features/system-settings/content/announcements-section.tsx @@ -62,7 +62,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { Switch } from '@/components/ui/switch' import { Table, TableBody, @@ -74,6 +73,7 @@ import { import { Textarea } from '@/components/ui/textarea' import { DateTimePicker } from '@/components/datetime-picker' import { StatusBadge } from '@/components/status-badge' +import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -319,10 +319,7 @@ export function AnnouncementsSection({ } return ( - +
@@ -350,12 +347,12 @@ export function AnnouncementsSection({ {updateOption.isPending ? t('Saving...') : t('Save Settings')}
-
- - {t('Enabled')} - - -
+
diff --git a/web/default/src/features/system-settings/content/api-info-section.tsx b/web/default/src/features/system-settings/content/api-info-section.tsx index eb552584..3616449c 100644 --- a/web/default/src/features/system-settings/content/api-info-section.tsx +++ b/web/default/src/features/system-settings/content/api-info-section.tsx @@ -62,7 +62,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { Switch } from '@/components/ui/switch' import { Table, TableBody, @@ -72,6 +71,7 @@ import { TableRow, } from '@/components/ui/table' import { StatusBadge } from '@/components/status-badge' +import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -275,10 +275,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { const getColorClass = (color: string) => getBgColorClass(color) return ( - +
@@ -306,12 +303,12 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { {updateOption.isPending ? t('Saving...') : t('Save Settings')}
-
- - {t('Enabled')} - - -
+
diff --git a/web/default/src/features/system-settings/content/chat-settings-section.tsx b/web/default/src/features/system-settings/content/chat-settings-section.tsx index 2fa64f1b..bc030657 100644 --- a/web/default/src/features/system-settings/content/chat-settings-section.tsx +++ b/web/default/src/features/system-settings/content/chat-settings-section.tsx @@ -21,7 +21,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -33,6 +32,8 @@ import { } from '@/components/ui/form' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Textarea } from '@/components/ui/textarea' +import { SettingsForm } from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' import { ChatSettingsVisualEditor } from './chat-settings-visual-editor' @@ -125,13 +126,15 @@ export function ChatSettingsSection({ } return ( - + {/* eslint-disable-next-line react-hooks/refs */} - + + setEditMode(value as 'visual' | 'json')} @@ -186,11 +189,7 @@ export function ChatSettingsSection({ /> - - - + ) diff --git a/web/default/src/features/system-settings/content/dashboard-section.tsx b/web/default/src/features/system-settings/content/dashboard-section.tsx index 0d80685d..4206c9d7 100644 --- a/web/default/src/features/system-settings/content/dashboard-section.tsx +++ b/web/default/src/features/system-settings/content/dashboard-section.tsx @@ -21,7 +21,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -41,6 +40,12 @@ import { SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -89,29 +94,28 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) { const isEnabled = form.watch('DataExportEnabled') return ( - +
- + + ( - -
- - {t('Enable Data Dashboard')} - -
+ + + {t('Enable Data Dashboard')} + -
+ )} /> @@ -183,11 +187,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) { )} />
- - - + ) diff --git a/web/default/src/features/system-settings/content/drawing-settings-section.tsx b/web/default/src/features/system-settings/content/drawing-settings-section.tsx index 129aa5aa..eedf4138 100644 --- a/web/default/src/features/system-settings/content/drawing-settings-section.tsx +++ b/web/default/src/features/system-settings/content/drawing-settings-section.tsx @@ -21,17 +21,21 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, FormDescription, FormField, - FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Switch } from '@/components/ui/switch' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -124,12 +128,14 @@ export function DrawingSettingsSection({ ] return ( - +
- + +
{switches.map((item) => ( ( - -
- {item.label} + + + {item.label} {item.description} -
+ -
+ )} /> ))}
- - - +
) diff --git a/web/default/src/features/system-settings/content/faq-section.tsx b/web/default/src/features/system-settings/content/faq-section.tsx index 23df8e11..4ca07c40 100644 --- a/web/default/src/features/system-settings/content/faq-section.tsx +++ b/web/default/src/features/system-settings/content/faq-section.tsx @@ -53,7 +53,6 @@ import { FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' -import { Switch } from '@/components/ui/switch' import { Table, TableBody, @@ -63,6 +62,7 @@ import { TableRow, } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' +import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -238,12 +238,7 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { } return ( - +
@@ -271,12 +266,12 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { {updateOption.isPending ? t('Saving...') : t('Save Settings')}
-
- - {t('Enabled')} - - -
+
diff --git a/web/default/src/features/system-settings/content/index.tsx b/web/default/src/features/system-settings/content/index.tsx index 74709aff..147a11dd 100644 --- a/web/default/src/features/system-settings/content/index.tsx +++ b/web/default/src/features/system-settings/content/index.tsx @@ -16,14 +16,12 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useMemo } from 'react' -import { useParams } from '@tanstack/react-router' -import { useTranslation } from 'react-i18next' -import { getOptionValue, useSystemOptions } from '../hooks/use-system-options' -import type { ContentSettings } from '../types' +import { SettingsPage } from '../components/settings-page' +import type { ContentSettings, SystemOption } from '../types' import { CONTENT_DEFAULT_SECTION, getContentSectionContent, + getContentSectionMeta, } from './section-registry.tsx' const defaultContentSettings: ContentSettings = { @@ -47,84 +45,53 @@ const defaultContentSettings: ContentSettings = { MjActionCheckSuccessEnabled: false, } -export function ContentSettings() { - const { t } = useTranslation() - const { data, isLoading } = useSystemOptions() - const params = useParams({ - from: '/_authenticated/system-settings/content/$section', - }) +function resolveContentSettings( + settings: ContentSettings, + raw: SystemOption[] | undefined +): ContentSettings { + if (!raw || raw.length === 0) return settings - const settings = useMemo(() => { - const resolved = getOptionValue(data?.data, defaultContentSettings) + const optionMap = new Map(raw.map((item) => [item.key, item.value])) + const next = { ...settings } - const optionMap = new Map( - (data?.data ?? []).map((item) => [item.key, item.value]) - ) + const legacyMap = [ + { current: 'console_setting.announcements', legacy: 'Announcements' }, + { current: 'console_setting.api_info', legacy: 'ApiInfo' }, + { current: 'console_setting.faq', legacy: 'FAQ' }, + ] as const - if (!optionMap.has('console_setting.announcements')) { - const legacy = optionMap.get('Announcements') - if (legacy !== undefined) { - resolved['console_setting.announcements'] = legacy + for (const { current, legacy } of legacyMap) { + if (!optionMap.has(current)) { + const legacyValue = optionMap.get(legacy) + if (legacyValue !== undefined) { + next[current] = legacyValue } } - - if (!optionMap.has('console_setting.api_info')) { - const legacy = optionMap.get('ApiInfo') - if (legacy !== undefined) { - resolved['console_setting.api_info'] = legacy - } - } - - if (!optionMap.has('console_setting.faq')) { - const legacy = optionMap.get('FAQ') - if (legacy !== undefined) { - resolved['console_setting.faq'] = legacy - } - } - - if (!optionMap.has('console_setting.uptime_kuma_groups')) { - const legacyUrl = optionMap.get('UptimeKumaUrl') - const legacySlug = optionMap.get('UptimeKumaSlug') - if (legacyUrl && legacySlug) { - resolved['console_setting.uptime_kuma_groups'] = JSON.stringify([ - { - id: 1, - categoryName: 'Legacy', - url: legacyUrl, - slug: legacySlug, - }, - ]) - } - } - - return resolved - }, [data?.data]) - - if (isLoading) { - return ( -
-
- {t('Loading content settings...')} -
-
- ) } - const activeSection = (params?.section ?? CONTENT_DEFAULT_SECTION) as - | 'dashboard' - | 'announcements' - | 'api-info' - | 'faq' - | 'uptime-kuma' - | 'chat' - | 'drawing' - const sectionContent = getContentSectionContent(activeSection, settings) + if (!optionMap.has('console_setting.uptime_kuma_groups')) { + const legacyUrl = optionMap.get('UptimeKumaUrl') + const legacySlug = optionMap.get('UptimeKumaSlug') + if (legacyUrl && legacySlug) { + next['console_setting.uptime_kuma_groups'] = JSON.stringify([ + { id: 1, categoryName: 'Legacy', url: legacyUrl, slug: legacySlug }, + ]) + } + } + return next +} + +export function ContentSettings() { return ( -
-
-
{sectionContent}
-
-
+ ) } diff --git a/web/default/src/features/system-settings/content/json-toggle-section.tsx b/web/default/src/features/system-settings/content/json-toggle-section.tsx index c04da62a..4b79b1a6 100644 --- a/web/default/src/features/system-settings/content/json-toggle-section.tsx +++ b/web/default/src/features/system-settings/content/json-toggle-section.tsx @@ -21,7 +21,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -34,13 +33,18 @@ import { import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { SettingsAccordion } from '../components/settings-accordion' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { useUpdateOption } from '../hooks/use-update-option' import { formatJsonForEditor, normalizeJsonString } from './utils' type JsonToggleSectionProps = { value: string title: string - description?: string toggleDescription?: string optionKey: string enabledKey: string @@ -63,7 +67,6 @@ type JsonToggleFormValues = { export function JsonToggleSection({ value, title, - description, toggleDescription, optionKey, enabledKey, @@ -157,30 +160,33 @@ export function JsonToggleSection({ } return ( - +
{/* eslint-disable-next-line react-hooks/refs */} - + + ( - -
- - {t('Module availability')} - + + + {t('Module availability')} {toggleDescription && ( {t(toggleDescription)} )} -
+ -
+ )} /> @@ -203,11 +209,7 @@ export function JsonToggleSection({ )} /> - - - +
) diff --git a/web/default/src/features/system-settings/content/section-registry.tsx b/web/default/src/features/system-settings/content/section-registry.tsx index 172e690a..1de31657 100644 --- a/web/default/src/features/system-settings/content/section-registry.tsx +++ b/web/default/src/features/system-settings/content/section-registry.tsx @@ -41,7 +41,6 @@ const CONTENT_SECTIONS = [ { id: 'dashboard', titleKey: 'Data Dashboard', - descriptionKey: 'Configure data export settings for dashboard', build: (settings: ContentSettings) => ( ( ( ( ( ( ), @@ -109,7 +103,6 @@ const CONTENT_SECTIONS = [ { id: 'drawing', titleKey: 'Drawing', - descriptionKey: 'Configure drawing and Midjourney settings', build: (settings: ContentSettings) => ( +
@@ -280,12 +275,12 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { {updateOption.isPending ? t('Saving...') : t('Save Settings')}
-
- - {t('Enabled')} - - -
+
diff --git a/web/default/src/features/system-settings/general/channel-affinity/index.tsx b/web/default/src/features/system-settings/general/channel-affinity/index.tsx index b9a9cc60..6cd86de3 100644 --- a/web/default/src/features/system-settings/general/channel-affinity/index.tsx +++ b/web/default/src/features/system-settings/general/channel-affinity/index.tsx @@ -31,7 +31,6 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' -import { Switch } from '@/components/ui/switch' import { Table, TableBody, @@ -43,6 +42,8 @@ import { import { Textarea } from '@/components/ui/textarea' import { ConfirmDialog } from '@/components/confirm-dialog' import { StatusBadge } from '@/components/status-badge' +import { SettingsSwitchField } from '../../components/settings-form-layout' +import { SettingsPageActionsPortal } from '../../components/settings-page-context' import { SettingsSection } from '../../components/settings-section' import { useUpdateOption } from '../../hooks/use-update-option' import { getCacheStats, clearAllCache, clearRuleCache } from './api' @@ -333,12 +334,7 @@ export function ChannelAffinitySection(props: Props) { return ( <> - + {t( @@ -349,10 +345,12 @@ export function ChannelAffinitySection(props: Props) { {/* Basic Settings */}
-
- - -
+
-
- - - - {t( - 'If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.' - )} - -
+ - {/* Toolbar */} -
+
+ {/* Rules Table or JSON Editor */} {editMode === 'visual' ? ( diff --git a/web/default/src/features/system-settings/general/channel-affinity/rule-editor-dialog.tsx b/web/default/src/features/system-settings/general/channel-affinity/rule-editor-dialog.tsx index e5b7ff57..3224f16c 100644 --- a/web/default/src/features/system-settings/general/channel-affinity/rule-editor-dialog.tsx +++ b/web/default/src/features/system-settings/general/channel-affinity/rule-editor-dialog.tsx @@ -45,8 +45,8 @@ import { SelectValue, } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' -import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' +import { SettingsSwitchField } from '../../components/settings-form-layout' import { RULE_TEMPLATES } from './constants' import type { AffinityRule, KeySource } from './types' @@ -264,13 +264,11 @@ export function RuleEditorDialog(props: Props) {
-
- form.setValue('skip_retry_on_failure', v)} - /> - -
+ form.setValue('skip_retry_on_failure', v)} + label={t('Skip retry on failure')} + /> @@ -415,34 +413,29 @@ export function RuleEditorDialog(props: Props) { />
-
-
- - form.setValue('include_using_group', v) - } - /> - -
-
- - form.setValue('include_model_name', v) - } - /> - -
-
- - form.setValue('include_rule_name', v) - } - /> - -
+
+ + form.setValue('include_using_group', v) + } + label={t('Include Group')} + className='border-b-0 py-0' + /> + + form.setValue('include_model_name', v) + } + label={t('Include Model')} + className='border-b-0 py-0' + /> + form.setValue('include_rule_name', v)} + label={t('Include Rule Name')} + className='border-b-0 py-0' + />
diff --git a/web/default/src/features/system-settings/general/checkin-settings-section.tsx b/web/default/src/features/system-settings/general/checkin-settings-section.tsx index 2818f104..da70eaf2 100644 --- a/web/default/src/features/system-settings/general/checkin-settings-section.tsx +++ b/web/default/src/features/system-settings/general/checkin-settings-section.tsx @@ -21,7 +21,6 @@ import { useForm, type Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -33,6 +32,12 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' @@ -105,31 +110,28 @@ export function CheckinSettingsSection({ } return ( - +
- + + ( - -
- - {t('Enable check-in feature')} - + + + {t('Enable check-in feature')} {t( 'Allow users to check in daily for random quota rewards' )} -
+ -
+ )} /> @@ -188,16 +190,7 @@ export function CheckinSettingsSection({ />
)} - - - +
) diff --git a/web/default/src/features/system-settings/general/pricing-section.tsx b/web/default/src/features/system-settings/general/pricing-section.tsx index 744ef529..cdeb815d 100644 --- a/web/default/src/features/system-settings/general/pricing-section.tsx +++ b/web/default/src/features/system-settings/general/pricing-section.tsx @@ -19,10 +19,8 @@ For commercial licensing, please contact support@quantumnous.com import * as z from 'zod' import type { Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' -import { RotateCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { DEFAULT_CURRENCY_CONFIG } from '@/stores/system-config-store' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -44,6 +42,12 @@ import { import { Switch } from '@/components/ui/switch' import { FormDirtyIndicator } from '../components/form-dirty-indicator' import { FormNavigationGuard } from '../components/form-navigation-guard' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useSettingsForm } from '../hooks/use-settings-form' import { useUpdateOption } from '../hooks/use-update-option' @@ -141,12 +145,15 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { <> - +
- + + {showQuotaPerUnit && ( ( - -
- - {t('Display in Currency')} - + + + {t('Display in Currency')} {displayType === 'TOKENS' ? t( @@ -332,14 +337,14 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { ) : t('Show prices in currency instead of quota.')} -
+ -
+ )} /> )} @@ -348,43 +353,23 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { control={form.control} name='DisplayTokenStatEnabled' render={({ field }) => ( - -
- - {t('Display Token Statistics')} - + + + {t('Display Token Statistics')} {t('Show token usage statistics in the UI')} -
+ -
+ )} /> - -
- - -
- +
diff --git a/web/default/src/features/system-settings/general/quota-settings-section.tsx b/web/default/src/features/system-settings/general/quota-settings-section.tsx index 96a981dc..a8a83700 100644 --- a/web/default/src/features/system-settings/general/quota-settings-section.tsx +++ b/web/default/src/features/system-settings/general/quota-settings-section.tsx @@ -22,7 +22,6 @@ import type { Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' import { Alert, AlertDescription } from '@/components/ui/alert' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -36,6 +35,14 @@ import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { FormDirtyIndicator } from '../components/form-dirty-indicator' import { FormNavigationGuard } from '../components/form-navigation-guard' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, + SettingsFormGrid, + SettingsFormGridItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useSettingsForm } from '../hooks/use-settings-form' import { useUpdateOption } from '../hooks/use-update-option' @@ -94,10 +101,7 @@ export function QuotaSettingsSection({ }) return ( - + {!complianceConfirmed ? ( @@ -111,177 +115,176 @@ export function QuotaSettingsSection({ ) : null}
- + + - ( - - {t('New User Quota')} - - - - - {t('Initial quota given to new users')} - - - - )} - /> - - ( - - {t('Pre-Consumed Quota')} - - - - - {t('Quota consumed before charging users')} - - - - )} - /> - - ( - - {t('Inviter Reward')} - - - - - {t('Quota given to users who invite others')} - - - - )} - /> - - ( - - {t('Invitee Reward')} - - - - - {t('Quota given to invited users')} - - - - )} - /> - - ( - -
- - {t('Pre-Consume for Free Models')} - + + ( + + {t('New User Quota')} + + + - {t( - 'When enabled, zero-cost models also pre-consume quota before final settlement.' - )} + {t('Initial quota given to new users')} -
- - - -
- )} - /> + + + )} + /> - ( - - {t('Top-Up Link')} - - - - - {t('External link for users to purchase quota')} - - - - )} - /> + ( + + {t('Pre-Consumed Quota')} + + + + + {t('Quota consumed before charging users')} + + + + )} + /> - ( - - {t('Documentation Link')} - - - - - {t('Link to your documentation site')} - - - - )} - /> + ( + + {t('Inviter Reward')} + + + + + {t('Quota given to users who invite others')} + + + + )} + /> - - + ( + + {t('Invitee Reward')} + + + + + {t('Quota given to invited users')} + + + + )} + /> + + + ( + + + {t('Pre-Consume for Free Models')} + + {t( + 'When enabled, zero-cost models also pre-consume quota before final settlement.' + )} + + + + + + + )} + /> + + + ( + + {t('Top-Up Link')} + + + + + {t('External link for users to purchase quota')} + + + + )} + /> + + ( + + {t('Documentation Link')} + + + + + {t('Link to your documentation site')} + + + + )} + /> + +
) diff --git a/web/default/src/features/system-settings/general/system-behavior-section.tsx b/web/default/src/features/system-settings/general/system-behavior-section.tsx index 8ebfbe5b..e4232903 100644 --- a/web/default/src/features/system-settings/general/system-behavior-section.tsx +++ b/web/default/src/features/system-settings/general/system-behavior-section.tsx @@ -20,7 +20,6 @@ import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -32,6 +31,12 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' +import { + SettingsForm, + SettingsSwitchContent, + SettingsSwitchItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useResetForm } from '../hooks/use-reset-form' import { useUpdateOption } from '../hooks/use-update-option' @@ -73,12 +78,13 @@ export function SystemBehaviorSection({ } return ( - +
- + + ( - -
- - {t('Default Collapse Sidebar')} - + + + {t('Default Collapse Sidebar')} {t('Sidebar collapsed by default for new users')} -
+ -
+ )} /> @@ -132,22 +136,20 @@ export function SystemBehaviorSection({ control={form.control} name='DemoSiteEnabled' render={({ field }) => ( - -
- - {t('Demo Site Mode')} - + + + {t('Demo Site Mode')} {t('Enable demo mode with limited functionality')} -
+ -
+ )} /> @@ -155,29 +157,23 @@ export function SystemBehaviorSection({ control={form.control} name='SelfUseModeEnabled' render={({ field }) => ( - -
- - {t('Self-Use Mode')} - + + + {t('Self-Use Mode')} {t('Optimize system for self-hosted single-user usage')} -
+ -
+ )} /> - - - +
) diff --git a/web/default/src/features/system-settings/general/system-info-section.tsx b/web/default/src/features/system-settings/general/system-info-section.tsx index b1a85227..d1ecbc57 100644 --- a/web/default/src/features/system-settings/general/system-info-section.tsx +++ b/web/default/src/features/system-settings/general/system-info-section.tsx @@ -19,9 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import * as z from 'zod' import type { Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' -import { RotateCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -43,6 +41,12 @@ import { import { Textarea } from '@/components/ui/textarea' import { FormDirtyIndicator } from '../components/form-dirty-indicator' import { FormNavigationGuard } from '../components/form-navigation-guard' +import { + SettingsForm, + SettingsFormGrid, + SettingsFormGridItem, +} from '../components/settings-form-layout' +import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useSettingsForm } from '../hooks/use-settings-form' import { useUpdateOption } from '../hooks/use-update-option' @@ -139,250 +143,243 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) { <> - +
- + + - ( - - {t('Frontend Theme')} - + + + + + + + + + {t('Default (New Frontend)')} + + + {t('Classic (Legacy Frontend)')} + + + + + + {t( + 'Switch between the new frontend and the classic frontend. Changes take effect after page reload.' + )} + + + + )} + /> + + ( + + {t('System Name')} - - - + - - - - {t('Default (New Frontend)')} - - - {t('Classic (Legacy Frontend)')} - - - - - - {t( - 'Switch between the new frontend and the classic frontend. Changes take effect after page reload.' - )} - - - - )} - /> + + {t('The name displayed across the application')} + + + + )} + /> - ( - - {t('System Name')} - - - - - {t('The name displayed across the application')} - - - - )} - /> - - ( - - {t('Server Address')} - - - - - {t( - 'The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations' - )} - - - - )} - /> - - ( - - {t('Logo URL')} - - - - - {t('URL to your logo image (optional)')} - - - - )} - /> - - ( - - {t('Footer')} - -