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')} - -