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 cb93b7484a..a4e5da6d6e 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 f82c78b261..c954113de8 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 c1688acf09..23845e5411 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 c3a7e462bf..35c381f04f 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 f3ea43b72f..4099c105c2 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 6f57e7a928..3c20ad7106 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 d258dde66f..db670f0c42 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 9c98090968..0414d456be 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 71d9602460..3afbff6093 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 f8c0e01800..6f96ac0fed 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 dc71ee35cb..9fd74a666e 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 322042ff5c..ddae94057c 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 ebf8e29d51..8d53ea1a4b 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 143d220e3c..4494370562 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 3ab4bd47aa..7029310265 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 4280de5b14..e34afe1516 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 5fc4b57579..39517cd9f4 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 e9d2d78cff..31af1e1596 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 d789778972..873302c8b0 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 89839d68f1..622eb1d351 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 9381722401..daad50668a 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 2e43d66e9b..1a1dc8a2f6 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 76ec9c9138..31222b4309 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 0000000000..070136b49b --- /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 0000000000..2cdb73e411 --- /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 1cf8bc5b68..98e3bf1f67 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 89cbe9e05b..b0eb2cb9e1 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 f48df817fc..560cb2cac0 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 eb552584f2..3616449cd6 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 2fa64f1b97..bc030657fd 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 0d80685d91..4206c9d701 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 129aa5aaa6..eedf41384c 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 23df8e1194..4ca07c4031 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 74709aff82..147a11dd3a 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 c04da62a25..4b79b1a600 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 172e690aee..1de31657b9 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 b9a9cc6014..6cd86de348 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 e5b7ff579c..3224f16cd3 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 2818f104aa..da70eaf209 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 744ef5290c..cdeb815d37 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 96a981dcbc..a8a8370040 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 8ebfbe5b11..e4232903a7 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 b1a85227a9..d1ecbc57eb 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')} - -