Commit b08febaa by t0ng7u

refactor: system settings UI for consistent, compact layouts

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.
parent 92a09594
...@@ -29,6 +29,90 @@ const OBFUSCATED_KEYS = [ ...@@ -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) { function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v) return typeof v === 'object' && v !== null && !Array.isArray(v)
} }
...@@ -97,6 +181,24 @@ function isLikelyUntranslated({ locale, baseValue, value }) { ...@@ -97,6 +181,24 @@ function isLikelyUntranslated({ locale, baseValue, value }) {
// Skip short tokens / acronyms / ids // Skip short tokens / acronyms / ids
const s = baseValue.trim() 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 (s.length < 6) return false
if (!/[A-Za-z]{3,}/.test(s)) return false if (!/[A-Za-z]{3,}/.test(s)) return false
...@@ -187,6 +289,8 @@ async function main() { ...@@ -187,6 +289,8 @@ async function main() {
if (Object.keys(extras).length > 0) { if (Object.keys(extras).length > 0) {
await fs.writeFile(path.join(extrasDir, `${locale}.extras.json`), stableStringify(extras), 'utf8') 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) { if (Object.keys(untranslated).length > 0) {
await fs.writeFile( await fs.writeFile(
...@@ -194,6 +298,8 @@ async function main() { ...@@ -194,6 +298,8 @@ async function main() {
stableStringify(untranslated), stableStringify(untranslated),
'utf8', '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) // Rewrite locale file in base order (even for en to normalize formatting)
......
...@@ -231,9 +231,9 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) { ...@@ -231,9 +231,9 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
<DropdownMenuTrigger <DropdownMenuTrigger
render={<SidebarMenuButton tooltip={item.title} />} render={<SidebarMenuButton tooltip={item.title} />}
> >
{item.icon && <item.icon className='h-4 w-4' />} {item.icon && <item.icon className='h-4 w-4 shrink-0' />}
<span>{item.title}</span> <span className='min-w-0 flex-1 truncate'>{item.title}</span>
<ChevronRight className='ms-auto h-4 w-4 opacity-70' /> <ChevronRight className='ms-auto h-4 w-4 shrink-0 opacity-70' />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align='start'> <DropdownMenuContent align='start'>
{visiblePresets.map((preset) => ( {visiblePresets.map((preset) => (
...@@ -261,9 +261,9 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) { ...@@ -261,9 +261,9 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
className='group/collapsible-trigger' className='group/collapsible-trigger'
render={<SidebarMenuButton />} render={<SidebarMenuButton />}
> >
{item.icon && <item.icon />} {item.icon && <item.icon className='shrink-0' />}
<span>{item.title}</span> <span className='min-w-0 flex-1 truncate'>{item.title}</span>
<ChevronRight className='ms-auto transition-transform duration-200 group-data-[panel-open]/collapsible-trigger:rotate-90' /> <ChevronRight className='ms-auto size-4 shrink-0 transition-transform duration-200 group-data-[panel-open]/collapsible-trigger:rotate-90' />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'> <CollapsibleContent className='CollapsibleContent'>
<SidebarMenuSub> <SidebarMenuSub>
......
...@@ -112,7 +112,7 @@ export function NavGroup({ title, items }: NavGroupProps) { ...@@ -112,7 +112,7 @@ export function NavGroup({ title, items }: NavGroupProps) {
* Navigation badge component * Navigation badge component
*/ */
function NavBadge({ children }: { children: ReactNode }) { function NavBadge({ children }: { children: ReactNode }) {
return <Badge className='px-1 py-0 text-xs'>{children}</Badge> return <Badge className='shrink-0 px-1 py-0 text-xs'>{children}</Badge>
} }
/** /**
...@@ -127,8 +127,8 @@ function SidebarMenuLink({ item, href }: { item: NavLink; href: string }) { ...@@ -127,8 +127,8 @@ function SidebarMenuLink({ item, href }: { item: NavLink; href: string }) {
tooltip={item.title} tooltip={item.title}
render={<Link to={item.url} onClick={() => setOpenMobile(false)} />} render={<Link to={item.url} onClick={() => setOpenMobile(false)} />}
> >
{item.icon && <item.icon />} {item.icon && <item.icon className='shrink-0' />}
<span>{item.title}</span> <span className='min-w-0 flex-1 truncate'>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>} {item.badge && <NavBadge>{item.badge}</NavBadge>}
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
...@@ -170,10 +170,10 @@ function SidebarMenuCollapsible({ ...@@ -170,10 +170,10 @@ function SidebarMenuCollapsible({
className='group/collapsible-trigger' className='group/collapsible-trigger'
render={<SidebarMenuButton tooltip={item.title} />} render={<SidebarMenuButton tooltip={item.title} />}
> >
{item.icon && <item.icon />} {item.icon && <item.icon className='shrink-0' />}
<span>{item.title}</span> <span className='min-w-0 flex-1 truncate'>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>} {item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ms-auto transition-transform duration-200 group-data-[panel-open]/collapsible-trigger:rotate-90' /> <ChevronRight className='ms-auto size-4 shrink-0 transition-transform duration-200 group-data-[panel-open]/collapsible-trigger:rotate-90' />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'> <CollapsibleContent className='CollapsibleContent'>
<SidebarMenuSub> <SidebarMenuSub>
...@@ -185,8 +185,8 @@ function SidebarMenuCollapsible({ ...@@ -185,8 +185,8 @@ function SidebarMenuCollapsible({
<Link to={subItem.url} onClick={() => setOpenMobile(false)} /> <Link to={subItem.url} onClick={() => setOpenMobile(false)} />
} }
> >
{subItem.icon && <subItem.icon />} {subItem.icon && <subItem.icon className='shrink-0' />}
<span>{subItem.title}</span> <span className='min-w-0 flex-1 truncate'>{subItem.title}</span>
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>} {subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
</SidebarMenuSubButton> </SidebarMenuSubButton>
</SidebarMenuSubItem> </SidebarMenuSubItem>
...@@ -219,10 +219,10 @@ function SidebarMenuCollapsedDropdown({ ...@@ -219,10 +219,10 @@ function SidebarMenuCollapsedDropdown({
/> />
} }
> >
{item.icon && <item.icon />} {item.icon && <item.icon className='shrink-0' />}
<span>{item.title}</span> <span className='min-w-0 flex-1 truncate'>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>} {item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ms-auto transition-transform duration-200 group-data-[popup-open]/dropdown-trigger:rotate-90' /> <ChevronRight className='ms-auto size-4 shrink-0 transition-transform duration-200 group-data-[popup-open]/dropdown-trigger:rotate-90' />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side='right' align='start' sideOffset={4}> <DropdownMenuContent side='right' align='start' sideOffset={4}>
<DropdownMenuGroup> <DropdownMenuGroup>
......
...@@ -33,11 +33,6 @@ function SectionPageLayoutTitle(_props: SlotProps) { ...@@ -33,11 +33,6 @@ function SectionPageLayoutTitle(_props: SlotProps) {
} }
SectionPageLayoutTitle.displayName = 'SectionPageLayout.Title' SectionPageLayoutTitle.displayName = 'SectionPageLayout.Title'
function SectionPageLayoutDescription(_props: SlotProps) {
return null
}
SectionPageLayoutDescription.displayName = 'SectionPageLayout.Description'
function SectionPageLayoutActions(_props: SlotProps) { function SectionPageLayoutActions(_props: SlotProps) {
return null return null
} }
...@@ -87,13 +82,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) { ...@@ -87,13 +82,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
<div className='mb-2 sm:mb-3'>{breadcrumb}</div> <div className='mb-2 sm:mb-3'>{breadcrumb}</div>
)} )}
<div className='flex flex-wrap items-center justify-between gap-x-3 gap-y-2 sm:gap-x-4'> <div className='flex flex-wrap items-center justify-between gap-x-3 gap-y-2 sm:gap-x-4'>
<div className='min-w-0'> <div className='min-w-0 flex-1'>
<h2 className='truncate text-base font-bold tracking-tight sm:text-lg'> <h2 className='truncate text-base font-bold tracking-tight sm:text-lg'>
{title} {title}
</h2> </h2>
</div> </div>
{actions != null && ( {actions != null && (
<div className='flex shrink-0 flex-wrap items-center gap-2 sm:gap-x-4'> <div className='flex shrink-0 flex-wrap items-center justify-end gap-2 sm:gap-x-4'>
{actions} {actions}
</div> </div>
)} )}
...@@ -114,7 +109,6 @@ export function SectionPageLayout(props: SectionPageLayoutProps) { ...@@ -114,7 +109,6 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
} }
SectionPageLayout.Title = SectionPageLayoutTitle SectionPageLayout.Title = SectionPageLayoutTitle
SectionPageLayout.Description = SectionPageLayoutDescription
SectionPageLayout.Actions = SectionPageLayoutActions SectionPageLayout.Actions = SectionPageLayoutActions
SectionPageLayout.Content = SectionPageLayoutContent SectionPageLayout.Content = SectionPageLayoutContent
SectionPageLayout.Breadcrumb = SectionPageLayoutBreadcrumb SectionPageLayout.Breadcrumb = SectionPageLayoutBreadcrumb
...@@ -29,9 +29,6 @@ export function Channels() { ...@@ -29,9 +29,6 @@ export function Channels() {
<ChannelsProvider> <ChannelsProvider>
<SectionPageLayout> <SectionPageLayout>
<SectionPageLayout.Title>{t('Channels')}</SectionPageLayout.Title> <SectionPageLayout.Title>{t('Channels')}</SectionPageLayout.Title>
<SectionPageLayout.Description>
{t('Manage API channels and provider configurations')}
</SectionPageLayout.Description>
<SectionPageLayout.Actions> <SectionPageLayout.Actions>
<ChannelsPrimaryButtons /> <ChannelsPrimaryButtons />
</SectionPageLayout.Actions> </SectionPageLayout.Actions>
......
...@@ -130,21 +130,15 @@ function PerformanceOverviewFallback() { ...@@ -130,21 +130,15 @@ function PerformanceOverviewFallback() {
) )
} }
const SECTION_META: Record< const SECTION_META: Record<DashboardSectionId, { titleKey: string }> = {
DashboardSectionId,
{ titleKey: string; descriptionKey: string }
> = {
overview: { overview: {
titleKey: 'Overview', titleKey: 'Overview',
descriptionKey: 'View dashboard overview and statistics',
}, },
models: { models: {
titleKey: 'Model Call Analytics', titleKey: 'Model Call Analytics',
descriptionKey: 'View model call count analytics and charts',
}, },
users: { users: {
titleKey: 'User Analytics', titleKey: 'User Analytics',
descriptionKey: 'View user consumption statistics and charts',
}, },
} }
...@@ -227,9 +221,6 @@ export function Dashboard() { ...@@ -227,9 +221,6 @@ export function Dashboard() {
return ( return (
<SectionPageLayout> <SectionPageLayout>
<SectionPageLayout.Title>{t(meta.titleKey)}</SectionPageLayout.Title> <SectionPageLayout.Title>{t(meta.titleKey)}</SectionPageLayout.Title>
<SectionPageLayout.Description>
{t(meta.descriptionKey)}
</SectionPageLayout.Description>
<SectionPageLayout.Content> <SectionPageLayout.Content>
<div className='space-y-3 sm:space-y-4'> <div className='space-y-3 sm:space-y-4'>
{activeSection !== 'overview' && ( {activeSection !== 'overview' && (
......
...@@ -26,19 +26,16 @@ const DASHBOARD_SECTIONS = [ ...@@ -26,19 +26,16 @@ const DASHBOARD_SECTIONS = [
{ {
id: 'overview', id: 'overview',
titleKey: 'Overview', titleKey: 'Overview',
descriptionKey: 'View dashboard overview and statistics',
build: () => null, build: () => null,
}, },
{ {
id: 'models', id: 'models',
titleKey: 'Model Call Analytics', titleKey: 'Model Call Analytics',
descriptionKey: 'View model call count analytics and charts',
build: () => null, build: () => null,
}, },
{ {
id: 'users', id: 'users',
titleKey: 'User Analytics', titleKey: 'User Analytics',
descriptionKey: 'View user consumption statistics and charts',
adminOnly: true, adminOnly: true,
build: () => null, build: () => null,
}, },
......
...@@ -29,9 +29,6 @@ export function ApiKeys() { ...@@ -29,9 +29,6 @@ export function ApiKeys() {
<ApiKeysProvider> <ApiKeysProvider>
<SectionPageLayout> <SectionPageLayout>
<SectionPageLayout.Title>{t('API Keys')}</SectionPageLayout.Title> <SectionPageLayout.Title>{t('API Keys')}</SectionPageLayout.Title>
<SectionPageLayout.Description>
{t('Manage your API keys for accessing the service')}
</SectionPageLayout.Description>
<SectionPageLayout.Actions> <SectionPageLayout.Actions>
<ApiKeysPrimaryButtons /> <ApiKeysPrimaryButtons />
</SectionPageLayout.Actions> </SectionPageLayout.Actions>
......
...@@ -42,17 +42,12 @@ import { ...@@ -42,17 +42,12 @@ import {
const route = getRouteApi('/_authenticated/models/$section') const route = getRouteApi('/_authenticated/models/$section')
const SECTION_META: Record< const SECTION_META: Record<ModelsSectionId, { titleKey: string }> = {
ModelsSectionId,
{ titleKey: string; descriptionKey: string }
> = {
metadata: { metadata: {
titleKey: 'Metadata', titleKey: 'Metadata',
descriptionKey: 'Manage model metadata and configuration',
}, },
deployments: { deployments: {
titleKey: 'Deployments', titleKey: 'Deployments',
descriptionKey: 'Manage model deployments',
}, },
} }
...@@ -126,9 +121,6 @@ function ModelsContent() { ...@@ -126,9 +121,6 @@ function ModelsContent() {
<> <>
<SectionPageLayout> <SectionPageLayout>
<SectionPageLayout.Title>{t(meta.titleKey)}</SectionPageLayout.Title> <SectionPageLayout.Title>{t(meta.titleKey)}</SectionPageLayout.Title>
<SectionPageLayout.Description>
{t(meta.descriptionKey)}
</SectionPageLayout.Description>
<SectionPageLayout.Actions> <SectionPageLayout.Actions>
{activeSection === 'metadata' ? ( {activeSection === 'metadata' ? (
<ModelsPrimaryButtons /> <ModelsPrimaryButtons />
......
...@@ -25,13 +25,11 @@ const MODELS_SECTIONS = [ ...@@ -25,13 +25,11 @@ const MODELS_SECTIONS = [
{ {
id: 'metadata', id: 'metadata',
titleKey: 'Metadata', titleKey: 'Metadata',
descriptionKey: 'Manage model metadata and configuration',
build: () => null, // Content is rendered directly in the page component build: () => null, // Content is rendered directly in the page component
}, },
{ {
id: 'deployments', id: 'deployments',
titleKey: 'Deployments', titleKey: 'Deployments',
descriptionKey: 'Manage model deployments',
build: () => null, // Content is rendered directly in the page component build: () => null, // Content is rendered directly in the page component
}, },
] as const ] as const
......
...@@ -31,9 +31,6 @@ export function Redemptions() { ...@@ -31,9 +31,6 @@ export function Redemptions() {
<SectionPageLayout.Title> <SectionPageLayout.Title>
{t('Redemption Codes')} {t('Redemption Codes')}
</SectionPageLayout.Title> </SectionPageLayout.Title>
<SectionPageLayout.Description>
{t('Manage redemption codes for quota top-up')}
</SectionPageLayout.Description>
<SectionPageLayout.Actions> <SectionPageLayout.Actions>
<RedemptionsPrimaryButtons /> <RedemptionsPrimaryButtons />
</SectionPageLayout.Actions> </SectionPageLayout.Actions>
......
...@@ -38,9 +38,6 @@ function SubscriptionsContent() { ...@@ -38,9 +38,6 @@ function SubscriptionsContent() {
<SectionPageLayout.Title> <SectionPageLayout.Title>
{t('Subscription Management')} {t('Subscription Management')}
</SectionPageLayout.Title> </SectionPageLayout.Title>
<SectionPageLayout.Description>
{t('Manage subscription plan creation, pricing and status')}
</SectionPageLayout.Description>
<SectionPageLayout.Actions> <SectionPageLayout.Actions>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Alert variant='default' className='hidden px-3 py-2 sm:flex'> <Alert variant='default' className='hidden px-3 py-2 sm:flex'>
......
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -33,6 +32,12 @@ import { ...@@ -33,6 +32,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -100,32 +105,31 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -100,32 +105,31 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('Basic Authentication')}>
title={t('Basic Authentication')}
description={t('Configure password-based login and registration')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='PasswordLoginEnabled' name='PasswordLoginEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Password Login')}</FormLabel>
{t('Password Login')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Allow users to log in with password')} {t('Allow users to log in with password')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -133,22 +137,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -133,22 +137,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
control={form.control} control={form.control}
name='RegisterEnabled' name='RegisterEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Registration Enabled')}</FormLabel>
{t('Registration Enabled')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Allow new users to register')} {t('Allow new users to register')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -156,22 +158,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -156,22 +158,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
control={form.control} control={form.control}
name='PasswordRegisterEnabled' name='PasswordRegisterEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Password Registration')}</FormLabel>
{t('Password Registration')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Allow registration with password')} {t('Allow registration with password')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -179,22 +179,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -179,22 +179,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
control={form.control} control={form.control}
name='EmailVerificationEnabled' name='EmailVerificationEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Email Verification')}</FormLabel>
{t('Email Verification')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Require email verification for new accounts')} {t('Require email verification for new accounts')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -202,22 +200,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -202,22 +200,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
control={form.control} control={form.control}
name='EmailDomainRestrictionEnabled' name='EmailDomainRestrictionEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Email Domain Restriction')}</FormLabel>
{t('Email Domain Restriction')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Only allow specific email domains')} {t('Only allow specific email domains')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -225,22 +221,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -225,22 +221,20 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
control={form.control} control={form.control}
name='EmailAliasRestrictionEnabled' name='EmailAliasRestrictionEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Email Alias Restriction')}</FormLabel>
{t('Email Alias Restriction')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Block email aliases (e.g., user+alias@domain.com)')} {t('Block email aliases (e.g., user+alias@domain.com)')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -266,11 +260,7 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) { ...@@ -266,11 +260,7 @@ export function BasicAuthSection({ defaultValues }: BasicAuthSectionProps) {
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -33,6 +32,12 @@ import { ...@@ -33,6 +32,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -75,40 +80,33 @@ export function BotProtectionSection({ ...@@ -75,40 +80,33 @@ export function BotProtectionSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Bot Protection')}>
title={t('Bot Protection')}
description={t(
'Protect login and registration with Cloudflare Turnstile'
)}
>
<Form {...form}> <Form {...form}>
<form <SettingsForm onSubmit={form.handleSubmit(onSubmit)} autoComplete='off'>
onSubmit={form.handleSubmit(onSubmit)} <SettingsPageFormActions
className='space-y-6' onSave={form.handleSubmit(onSubmit)}
autoComplete='off' isSaving={updateOption.isPending}
> />
<FormField <FormField
control={form.control} control={form.control}
name='TurnstileCheckEnabled' name='TurnstileCheckEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable Turnstile')}</FormLabel>
{t('Enable Turnstile')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Protect login and registration with Cloudflare Turnstile' 'Protect login and registration with Cloudflare Turnstile'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -148,11 +146,7 @@ export function BotProtectionSection({ ...@@ -148,11 +146,7 @@ export function BotProtectionSection({
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -29,6 +29,7 @@ import { ...@@ -29,6 +29,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { SettingsControlGroup } from '../../../components/settings-form-layout'
import { OAUTH_PRESETS, type CustomOAuthFormValues } from '../types' import { OAUTH_PRESETS, type CustomOAuthFormValues } from '../types'
type PresetSelectorProps = { type PresetSelectorProps = {
...@@ -102,7 +103,7 @@ export function PresetSelector(props: PresetSelectorProps) { ...@@ -102,7 +103,7 @@ export function PresetSelector(props: PresetSelectorProps) {
} }
return ( return (
<div className='space-y-3 rounded-lg border border-dashed p-4'> <SettingsControlGroup className='space-y-3 border-dashed'>
<p className='text-sm font-medium'>{t('Quick Setup from Preset')}</p> <p className='text-sm font-medium'>{t('Quick Setup from Preset')}</p>
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'> <div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
<div className='space-y-1.5'> <div className='space-y-1.5'>
...@@ -140,6 +141,6 @@ export function PresetSelector(props: PresetSelectorProps) { ...@@ -140,6 +141,6 @@ export function PresetSelector(props: PresetSelectorProps) {
/> />
</div> </div>
</div> </div>
</div> </SettingsControlGroup>
) )
} }
...@@ -51,6 +51,11 @@ import { Separator } from '@/components/ui/separator' ...@@ -51,6 +51,11 @@ import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../../../components/settings-form-layout'
import {
useCreateProvider, useCreateProvider,
useUpdateProvider, useUpdateProvider,
} from '../hooks/use-custom-oauth-mutations' } from '../hooks/use-custom-oauth-mutations'
...@@ -185,7 +190,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { ...@@ -185,7 +190,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
{/* Preset Selector (only for creating) */} {/* Preset Selector (only for creating) */}
{!isEditing && <PresetSelector form={form} />} {!isEditing && <PresetSelector form={form} />}
...@@ -197,22 +202,20 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { ...@@ -197,22 +202,20 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
control={form.control} control={form.control}
name='enabled' name='enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enabled')}</FormLabel>
{t('Enabled')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Allow users to sign in with this provider')} {t('Allow users to sign in with this provider')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -602,7 +605,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { ...@@ -602,7 +605,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
: t('Create Provider')} : t('Create Provider')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </SettingsForm>
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
......
...@@ -50,12 +50,7 @@ export function CustomOAuthSection() { ...@@ -50,12 +50,7 @@ export function CustomOAuthSection() {
if (isLoading) { if (isLoading) {
return ( return (
<SettingsSection <SettingsSection title={t('Custom OAuth Providers')}>
title={t('Custom OAuth Providers')}
description={t(
'Configure custom OAuth providers for user authentication'
)}
>
<div className='text-muted-foreground py-8 text-center text-sm'> <div className='text-muted-foreground py-8 text-center text-sm'>
{t('Loading...')} {t('Loading...')}
</div> </div>
...@@ -64,12 +59,7 @@ export function CustomOAuthSection() { ...@@ -64,12 +59,7 @@ export function CustomOAuthSection() {
} }
return ( return (
<SettingsSection <SettingsSection title={t('Custom OAuth Providers')}>
title={t('Custom OAuth Providers')}
description={t(
'Configure custom OAuth providers for user authentication'
)}
>
<ProviderTable <ProviderTable
providers={providers} providers={providers}
onEdit={handleEdit} onEdit={handleEdit}
......
...@@ -21,6 +21,7 @@ import type { AuthSettings } from '../types' ...@@ -21,6 +21,7 @@ import type { AuthSettings } from '../types'
import { import {
AUTH_DEFAULT_SECTION, AUTH_DEFAULT_SECTION,
getAuthSectionContent, getAuthSectionContent,
getAuthSectionMeta,
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultAuthSettings: AuthSettings = { const defaultAuthSettings: AuthSettings = {
...@@ -74,6 +75,7 @@ export function AuthSettings() { ...@@ -74,6 +75,7 @@ export function AuthSettings() {
defaultSettings={defaultAuthSettings} defaultSettings={defaultAuthSettings}
defaultSection={AUTH_DEFAULT_SECTION} defaultSection={AUTH_DEFAULT_SECTION}
getSectionContent={getAuthSectionContent} getSectionContent={getAuthSectionContent}
getSectionMeta={getAuthSectionMeta}
/> />
) )
} }
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -42,6 +41,12 @@ import { ...@@ -42,6 +41,12 @@ import {
} from '@/components/ui/select' } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -156,34 +161,33 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) { ...@@ -156,34 +161,33 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('Passkey Authentication')}>
title={t('Passkey Authentication')}
description={t('Configure Passkey (WebAuthn) login settings')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='passkey.enabled' name='passkey.enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable Passkey')}</FormLabel>
{t('Enable Passkey')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Allow users to register and sign in with Passkey (WebAuthn)' 'Allow users to register and sign in with Passkey (WebAuthn)'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -323,24 +327,22 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) { ...@@ -323,24 +327,22 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) {
control={form.control} control={form.control}
name='passkey.allow_insecure_origin' name='passkey.allow_insecure_origin'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Allow Insecure Origins')}</FormLabel>
{t('Allow Insecure Origins')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Permit Passkey registration on non-HTTPS origins (only recommended for development)' 'Permit Passkey registration on non-HTTPS origins (only recommended for development)'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -367,9 +369,7 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) { ...@@ -367,9 +369,7 @@ export function PasskeySection({ defaultValues }: PasskeySectionProps) {
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit'>{t('Save Changes')}</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -28,7 +28,6 @@ const AUTH_SECTIONS = [ ...@@ -28,7 +28,6 @@ const AUTH_SECTIONS = [
{ {
id: 'basic-auth', id: 'basic-auth',
titleKey: 'Basic Authentication', titleKey: 'Basic Authentication',
descriptionKey: 'Configure password-based login and registration',
build: (settings: AuthSettings) => ( build: (settings: AuthSettings) => (
<BasicAuthSection <BasicAuthSection
defaultValues={{ defaultValues={{
...@@ -46,7 +45,6 @@ const AUTH_SECTIONS = [ ...@@ -46,7 +45,6 @@ const AUTH_SECTIONS = [
{ {
id: 'oauth', id: 'oauth',
titleKey: 'OAuth Integrations', titleKey: 'OAuth Integrations',
descriptionKey: 'Configure third-party authentication providers',
build: (settings: AuthSettings) => ( build: (settings: AuthSettings) => (
<OAuthSection <OAuthSection
defaultValues={{ defaultValues={{
...@@ -82,7 +80,6 @@ const AUTH_SECTIONS = [ ...@@ -82,7 +80,6 @@ const AUTH_SECTIONS = [
{ {
id: 'passkey', id: 'passkey',
titleKey: 'Passkey Authentication', titleKey: 'Passkey Authentication',
descriptionKey: 'Configure Passkey (WebAuthn) login settings',
build: (settings: AuthSettings) => ( build: (settings: AuthSettings) => (
<PasskeySection <PasskeySection
defaultValues={{ defaultValues={{
...@@ -111,7 +108,6 @@ const AUTH_SECTIONS = [ ...@@ -111,7 +108,6 @@ const AUTH_SECTIONS = [
{ {
id: 'bot-protection', id: 'bot-protection',
titleKey: 'Bot Protection', titleKey: 'Bot Protection',
descriptionKey: 'Protect login and registration with Cloudflare Turnstile',
build: (settings: AuthSettings) => ( build: (settings: AuthSettings) => (
<BotProtectionSection <BotProtectionSection
defaultValues={{ defaultValues={{
...@@ -125,7 +121,6 @@ const AUTH_SECTIONS = [ ...@@ -125,7 +121,6 @@ const AUTH_SECTIONS = [
{ {
id: 'custom-oauth', id: 'custom-oauth',
titleKey: 'Custom OAuth', titleKey: 'Custom OAuth',
descriptionKey: 'Configure custom OAuth providers for user authentication',
build: () => <CustomOAuthSection />, build: () => <CustomOAuthSection />,
}, },
] as const ] as const
...@@ -143,3 +138,4 @@ export const AUTH_SECTION_IDS = authRegistry.sectionIds ...@@ -143,3 +138,4 @@ export const AUTH_SECTION_IDS = authRegistry.sectionIds
export const AUTH_DEFAULT_SECTION = authRegistry.defaultSection export const AUTH_DEFAULT_SECTION = authRegistry.defaultSection
export const getAuthSectionNavItems = authRegistry.getSectionNavItems export const getAuthSectionNavItems = authRegistry.getSectionNavItems
export const getAuthSectionContent = authRegistry.getSectionContent export const getAuthSectionContent = authRegistry.getSectionContent
export const getAuthSectionMeta = authRegistry.getSectionMeta
...@@ -21,6 +21,7 @@ import type { BillingSettings } from '../types' ...@@ -21,6 +21,7 @@ import type { BillingSettings } from '../types'
import { import {
BILLING_DEFAULT_SECTION, BILLING_DEFAULT_SECTION,
getBillingSectionContent, getBillingSectionContent,
getBillingSectionMeta,
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultBillingSettings: BillingSettings = { const defaultBillingSettings: BillingSettings = {
...@@ -113,6 +114,7 @@ export function BillingSettings() { ...@@ -113,6 +114,7 @@ export function BillingSettings() {
defaultSettings={defaultBillingSettings} defaultSettings={defaultBillingSettings}
defaultSection={BILLING_DEFAULT_SECTION} defaultSection={BILLING_DEFAULT_SECTION}
getSectionContent={getBillingSectionContent} getSectionContent={getBillingSectionContent}
getSectionMeta={getBillingSectionMeta}
/> />
) )
} }
...@@ -54,7 +54,6 @@ const BILLING_SECTIONS = [ ...@@ -54,7 +54,6 @@ const BILLING_SECTIONS = [
{ {
id: 'quota', id: 'quota',
titleKey: 'Quota Settings', titleKey: 'Quota Settings',
descriptionKey: 'Configure user quota allocation and rewards',
build: (settings: BillingSettings) => ( build: (settings: BillingSettings) => (
<QuotaSettingsSection <QuotaSettingsSection
defaultValues={{ defaultValues={{
...@@ -81,7 +80,6 @@ const BILLING_SECTIONS = [ ...@@ -81,7 +80,6 @@ const BILLING_SECTIONS = [
{ {
id: 'currency', id: 'currency',
titleKey: 'Currency & Display', titleKey: 'Currency & Display',
descriptionKey: 'Configure currency conversion and quota display options',
build: (settings: BillingSettings) => ( build: (settings: BillingSettings) => (
<PricingSection <PricingSection
defaultValues={{ defaultValues={{
...@@ -105,11 +103,9 @@ const BILLING_SECTIONS = [ ...@@ -105,11 +103,9 @@ const BILLING_SECTIONS = [
{ {
id: 'model-pricing', id: 'model-pricing',
titleKey: 'Model Pricing', titleKey: 'Model Pricing',
descriptionKey: 'Configure model pricing ratios and tool prices',
build: (settings: BillingSettings) => ( build: (settings: BillingSettings) => (
<RatioSettingsCard <RatioSettingsCard
titleKey='Model Pricing' titleKey='Model Pricing'
descriptionKey='Configure model pricing ratios and tool prices'
modelDefaults={getModelDefaults(settings)} modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)} groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']} toolPricesDefault={settings['tool_price_setting.prices']}
...@@ -120,11 +116,9 @@ const BILLING_SECTIONS = [ ...@@ -120,11 +116,9 @@ const BILLING_SECTIONS = [
{ {
id: 'group-pricing', id: 'group-pricing',
titleKey: 'Group Pricing', titleKey: 'Group Pricing',
descriptionKey: 'Configure group ratios and group-specific pricing rules',
build: (settings: BillingSettings) => ( build: (settings: BillingSettings) => (
<RatioSettingsCard <RatioSettingsCard
titleKey='Group Pricing' titleKey='Group Pricing'
descriptionKey='Configure group ratios and group-specific pricing rules'
modelDefaults={getModelDefaults(settings)} modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)} groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']} toolPricesDefault={settings['tool_price_setting.prices']}
...@@ -135,7 +129,6 @@ const BILLING_SECTIONS = [ ...@@ -135,7 +129,6 @@ const BILLING_SECTIONS = [
{ {
id: 'payment', id: 'payment',
titleKey: 'Payment Gateway', titleKey: 'Payment Gateway',
descriptionKey: 'Configure payment gateway integrations',
build: (settings: BillingSettings) => ( build: (settings: BillingSettings) => (
<PaymentSettingsSection <PaymentSettingsSection
defaultValues={{ defaultValues={{
...@@ -196,7 +189,6 @@ const BILLING_SECTIONS = [ ...@@ -196,7 +189,6 @@ const BILLING_SECTIONS = [
{ {
id: 'checkin', id: 'checkin',
titleKey: 'Check-in Rewards', titleKey: 'Check-in Rewards',
descriptionKey: 'Configure daily check-in rewards for users',
build: (settings: BillingSettings) => ( build: (settings: BillingSettings) => (
<CheckinSettingsSection <CheckinSettingsSection
defaultValues={{ defaultValues={{
...@@ -225,3 +217,4 @@ export const BILLING_SECTION_IDS = billingRegistry.sectionIds ...@@ -225,3 +217,4 @@ export const BILLING_SECTION_IDS = billingRegistry.sectionIds
export const BILLING_DEFAULT_SECTION = billingRegistry.defaultSection export const BILLING_DEFAULT_SECTION = billingRegistry.defaultSection
export const getBillingSectionNavItems = billingRegistry.getSectionNavItems export const getBillingSectionNavItems = billingRegistry.getSectionNavItems
export const getBillingSectionContent = billingRegistry.getSectionContent export const getBillingSectionContent = billingRegistry.getSectionContent
export const getBillingSectionMeta = billingRegistry.getSectionMeta
...@@ -16,9 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,9 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { Info } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert' import { SettingsPageTitleStatusPortal } from './settings-page-context'
type FormDirtyIndicatorProps = { type FormDirtyIndicatorProps = {
isDirty: boolean isDirty: boolean
...@@ -26,7 +25,7 @@ type FormDirtyIndicatorProps = { ...@@ -26,7 +25,7 @@ type FormDirtyIndicatorProps = {
} }
/** /**
* Visual indicator that the form has unsaved changes * Compact page-title status indicator for unsaved form changes.
* *
* @example * @example
* ```tsx * ```tsx
...@@ -41,14 +40,11 @@ export function FormDirtyIndicator({ ...@@ -41,14 +40,11 @@ export function FormDirtyIndicator({
if (!isDirty) return null if (!isDirty) return null
return ( return (
<Alert <SettingsPageTitleStatusPortal>
variant='default' <span className='inline-flex h-5 items-center gap-1.5 rounded-full bg-amber-500/10 px-2 text-[11px] font-medium whitespace-nowrap text-amber-700 ring-1 ring-amber-500/20 ring-inset dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20'>
className='border-orange-500/50 bg-orange-50 dark:bg-orange-950/20' <span className='size-1.5 rounded-full bg-amber-500 dark:bg-amber-300' />
> {message ? t(message) : t('Unsaved changes')}
<Info className='h-4 w-4 text-orange-600 dark:text-orange-500' /> </span>
<AlertDescription className='text-orange-800 dark:text-orange-400'> </SettingsPageTitleStatusPortal>
{message ?? t('You have unsaved changes')}
</AlertDescription>
</Alert>
) )
} }
...@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { cn } from '@/lib/utils'
import { import {
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
...@@ -25,7 +26,6 @@ import { ...@@ -25,7 +26,6 @@ import {
type SettingsAccordionProps = { type SettingsAccordionProps = {
value: string value: string
title: string title: string
description?: string
children: React.ReactNode children: React.ReactNode
className?: string className?: string
} }
...@@ -33,18 +33,14 @@ type SettingsAccordionProps = { ...@@ -33,18 +33,14 @@ type SettingsAccordionProps = {
export function SettingsAccordion({ export function SettingsAccordion({
value, value,
title, title,
description,
children, children,
className, className,
}: SettingsAccordionProps) { }: SettingsAccordionProps) {
return ( return (
<AccordionItem value={value} className={className}> <AccordionItem value={value} className={cn(className)}>
<AccordionTrigger className='hover:no-underline'> <AccordionTrigger className='hover:no-underline'>
<div className='flex flex-col gap-1 text-left'> <div className='flex flex-col gap-1 text-left'>
<div className='text-base font-semibold'>{title}</div> <div className='text-base font-semibold'>{title}</div>
{description && (
<div className='text-muted-foreground text-sm'>{description}</div>
)}
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className='pt-4'>{children}</AccordionContent> <AccordionContent className='pt-4'>{children}</AccordionContent>
......
/*
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 <https://www.gnu.org/licenses/>.
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<typeof FormItem>
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 (
<div
data-settings-form-span='full'
className={cn(
'grid min-w-0 gap-x-5 gap-y-6 lg:grid-cols-2',
props.className
)}
>
{props.children}
</div>
)
}
export function SettingsFormGridItem(props: SettingsFormGridItemProps) {
return (
<div
data-settings-form-span={props.span === 'full' ? 'full' : undefined}
className={cn(
'min-w-0',
props.span === 'full' && 'lg:col-span-2',
props.className
)}
>
{props.children}
</div>
)
}
export function SettingsSwitchItem({
className,
...props
}: SettingsSwitchItemProps) {
return (
<FormItem
data-settings-form-span='full'
className={cn(settingsSwitchRowClassName, className)}
{...props}
/>
)
}
export function SettingsSwitchRow({
className,
...props
}: SettingsSwitchRowProps) {
return (
<div
data-settings-form-span='full'
className={cn(settingsSwitchRowClassName, className)}
{...props}
/>
)
}
export function SettingsSwitchField({
checked,
onCheckedChange,
label,
description,
disabled,
className,
...props
}: SettingsSwitchFieldProps) {
return (
<SettingsSwitchRow className={className} {...props}>
<SettingsSwitchContent>
<Label className='text-sm font-medium'>{label}</Label>
{description ? (
<p className='text-muted-foreground text-xs'>{description}</p>
) : null}
</SettingsSwitchContent>
<Switch
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
/>
</SettingsSwitchRow>
)
}
export function SettingsSwitchContent(props: SettingsFormGridProps) {
return (
<div className={cn('min-w-0 space-y-0.5', props.className)}>
{props.children}
</div>
)
}
export function SettingsControlGroup({
className,
...props
}: SettingsControlGroupProps) {
return (
<div
data-settings-form-span='full'
className={cn(
'bg-muted/20 min-w-0 space-y-3 rounded-xl border px-3 py-2.5',
className
)}
{...props}
/>
)
}
export function SettingsControlChildren({
className,
...props
}: SettingsControlChildrenProps) {
return (
<div
className={cn('border-border/70 ml-2 min-w-0 border-l pl-3', className)}
{...props}
/>
)
}
export function SettingsForm({ className, ...props }: ComponentProps<'form'>) {
return (
<form
className={cn(
'grid min-w-0 gap-x-5 gap-y-6 lg:grid-cols-2',
'lg:[&>*: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}
/>
)
}
/*
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 <https://www.gnu.org/licenses/>.
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<SettingsPageContextValue>({
actionsContainer: null,
titleStatusContainer: null,
suppressSectionHeader: false,
})
type SettingsPageProviderProps = {
actionsContainer: HTMLDivElement | null
titleStatusContainer?: HTMLSpanElement | null
children: ReactNode
suppressSectionHeader?: boolean
}
export function SettingsPageProvider(props: SettingsPageProviderProps) {
return (
<SettingsPageContext.Provider
value={{
actionsContainer: props.actionsContainer,
titleStatusContainer: props.titleStatusContainer ?? null,
suppressSectionHeader: props.suppressSectionHeader ?? true,
}}
>
{props.children}
</SettingsPageContext.Provider>
)
}
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(
<div className='flex flex-wrap items-center justify-end gap-2'>
{props.children}
</div>,
actionsContainer
)
}
type SettingsPageFormActionsProps = {
onSave: () => void
onReset?: () => void
isSaving?: boolean
isSaveDisabled?: boolean
isResetDisabled?: boolean
saveLabel?: string
savingLabel?: string
resetLabel?: string
resetVariant?: ComponentProps<typeof Button>['variant']
saveButtonRef?: RefObject<HTMLButtonElement | null>
}
export function SettingsPageFormActions(props: SettingsPageFormActionsProps) {
const { t } = useTranslation()
const saveLabel = props.isSaving
? (props.savingLabel ?? 'Saving...')
: (props.saveLabel ?? 'Save Changes')
return (
<SettingsPageActionsPortal>
{props.onReset && (
<Button
type='button'
size='sm'
variant={props.resetVariant ?? 'outline'}
onClick={props.onReset}
disabled={props.isResetDisabled || props.isSaving}
>
<RotateCcw data-icon='inline-start' />
<span>{t(props.resetLabel ?? 'Reset')}</span>
</Button>
)}
<Button
ref={props.saveButtonRef}
type='button'
size='sm'
onClick={props.onSave}
disabled={props.isSaving || props.isSaveDisabled}
>
<Save data-icon='inline-start' />
<span>{t(saveLabel)}</span>
</Button>
</SettingsPageActionsPortal>
)
}
...@@ -16,13 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,13 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useMemo, useState, type ReactNode } from 'react'
import { useParams } from '@tanstack/react-router' import { useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SectionPageLayout } from '@/components/layout'
import { useSystemOptions, getOptionValue } from '../hooks/use-system-options' import { useSystemOptions, getOptionValue } from '../hooks/use-system-options'
import type { SystemOption } from '../types'
import { SettingsPageProvider } from './settings-page-context'
type SettingsPageProps< type SettingsPageProps<
TSettings extends Record<string, string | number | boolean | unknown[]>, TSettings extends Record<string, string | number | boolean | unknown[]>,
TSectionId extends string, TSectionId extends string,
TExtraArgs extends unknown[] = [],
> = { > = {
routePath: string routePath: string
defaultSettings: TSettings defaultSettings: TSettings
...@@ -30,9 +35,57 @@ type SettingsPageProps< ...@@ -30,9 +35,57 @@ type SettingsPageProps<
getSectionContent: ( getSectionContent: (
sectionId: TSectionId, sectionId: TSectionId,
settings: TSettings, settings: TSettings,
...extraArgs: unknown[] ...extraArgs: TExtraArgs
) => React.ReactNode ) => ReactNode
extraArgs?: unknown[] 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<HTMLDivElement | null>(null)
const [titleStatusContainer, setTitleStatusContainer] =
useState<HTMLSpanElement | null>(null)
return (
<SettingsPageProvider
actionsContainer={actionsContainer}
titleStatusContainer={titleStatusContainer}
>
<SectionPageLayout>
<SectionPageLayout.Title>
<span className='inline-flex max-w-full min-w-0 items-center gap-2 align-middle'>
<span className='truncate'>{props.title}</span>
<span
ref={setTitleStatusContainer}
className='inline-flex shrink-0'
/>
</span>
</SectionPageLayout.Title>
<SectionPageLayout.Actions>
<div
ref={setActionsContainer}
className='flex flex-wrap items-center justify-end gap-2'
/>
</SectionPageLayout.Actions>
<SectionPageLayout.Content>
<div className='flex w-full flex-col gap-4'>{props.children}</div>
</SectionPageLayout.Content>
</SectionPageLayout>
</SettingsPageProvider>
)
} }
/** /**
...@@ -42,39 +95,53 @@ type SettingsPageProps< ...@@ -42,39 +95,53 @@ type SettingsPageProps<
export function SettingsPage< export function SettingsPage<
TSettings extends Record<string, string | number | boolean | unknown[]>, TSettings extends Record<string, string | number | boolean | unknown[]>,
TSectionId extends string, TSectionId extends string,
TExtraArgs extends unknown[] = [],
>({ >({
routePath, routePath,
defaultSettings, defaultSettings,
defaultSection, defaultSection,
getSectionContent, getSectionContent,
extraArgs = [], getSectionMeta,
}: SettingsPageProps<TSettings, TSectionId>) { extraArgs,
loadingMessage = 'Loading settings...',
resolveSettings,
}: SettingsPageProps<TSettings, TSectionId, TExtraArgs>) {
const { t } = useTranslation() const { t } = useTranslation()
const { data, isLoading } = useSystemOptions() const { data, isLoading } = useSystemOptions()
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const params = useParams({ from: routePath as 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) { if (isLoading) {
return ( return (
<div className='flex items-center justify-center py-12'> <SettingsPageFrame title={t(sectionMeta.titleKey)}>
<div className='text-muted-foreground'>{t('Loading settings...')}</div> <div className='text-muted-foreground flex min-h-40 items-center justify-center text-sm'>
</div> {t(loadingMessage)}
</div>
</SettingsPageFrame>
) )
} }
const settings = getOptionValue(data?.data, defaultSettings) as TSettings
const activeSection = (params?.section ?? defaultSection) as TSectionId
const sectionContent = getSectionContent( const sectionContent = getSectionContent(
activeSection, activeSection,
settings, settings,
...extraArgs ...((extraArgs ?? []) as TExtraArgs)
) )
return ( return (
<div className='flex h-full w-full flex-1 flex-col'> <SettingsPageFrame title={t(sectionMeta.titleKey)}>
<div className='faded-bottom h-full w-full overflow-y-auto scroll-smooth pe-4 pb-12'> {sectionContent}
<div className='space-y-4'>{sectionContent}</div> </SettingsPageFrame>
</div>
</div>
) )
} }
...@@ -16,10 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,10 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { cn } from '@/lib/utils'
import { useSuppressSettingsSectionHeader } from './settings-page-context'
type SettingsSectionProps = { type SettingsSectionProps = {
title: string title: string
titleProps?: React.HTMLAttributes<HTMLHeadingElement> titleProps?: React.HTMLAttributes<HTMLHeadingElement>
description?: string
children: React.ReactNode children: React.ReactNode
className?: string className?: string
} }
...@@ -27,32 +29,23 @@ type SettingsSectionProps = { ...@@ -27,32 +29,23 @@ type SettingsSectionProps = {
export function SettingsSection({ export function SettingsSection({
title, title,
titleProps, titleProps,
description,
children, children,
className, className,
}: SettingsSectionProps) { }: SettingsSectionProps) {
const baseClassName = 'space-y-4' const suppressHeader = useSuppressSettingsSectionHeader()
const sectionClassName = className
? `${baseClassName} ${className}`
: baseClassName
return ( return (
<section className={sectionClassName}> <section className={cn('flex flex-col gap-4', className)}>
<div className='space-y-1'> {!suppressHeader && (
<h3 <div className='flex flex-col gap-1'>
{...titleProps} <h3
className={ {...titleProps}
titleProps?.className className={cn('text-base font-semibold', titleProps?.className)}
? `text-base font-semibold ${titleProps.className}` >
: 'text-base font-semibold' {title}
} </h3>
> </div>
{title} )}
</h3>
{description && (
<p className='text-muted-foreground text-sm'>{description}</p>
)}
</div>
{children} {children}
</section> </section>
) )
......
...@@ -62,7 +62,6 @@ import { ...@@ -62,7 +62,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -74,6 +73,7 @@ import { ...@@ -74,6 +73,7 @@ import {
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { DateTimePicker } from '@/components/datetime-picker' import { DateTimePicker } from '@/components/datetime-picker'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -319,10 +319,7 @@ export function AnnouncementsSection({ ...@@ -319,10 +319,7 @@ export function AnnouncementsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Announcements')}>
title={t('Announcements')}
description={t('Broadcast short system notices on the dashboard')}
>
<div className='space-y-4'> <div className='space-y-4'>
<div className='flex flex-wrap items-center justify-between gap-2'> <div className='flex flex-wrap items-center justify-between gap-2'>
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-wrap items-center gap-2'>
...@@ -350,12 +347,12 @@ export function AnnouncementsSection({ ...@@ -350,12 +347,12 @@ export function AnnouncementsSection({
{updateOption.isPending ? t('Saving...') : t('Save Settings')} {updateOption.isPending ? t('Saving...') : t('Save Settings')}
</Button> </Button>
</div> </div>
<div className='flex items-center gap-2'> <SettingsSwitchField
<span className='text-muted-foreground text-sm'> checked={isEnabled}
{t('Enabled')} onCheckedChange={handleToggleEnabled}
</span> label={t('Enabled')}
<Switch checked={isEnabled} onCheckedChange={handleToggleEnabled} /> className='border-b-0 py-0'
</div> />
</div> </div>
<div className='rounded-md border'> <div className='rounded-md border'>
......
...@@ -62,7 +62,6 @@ import { ...@@ -62,7 +62,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -72,6 +71,7 @@ import { ...@@ -72,6 +71,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -275,10 +275,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -275,10 +275,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
const getColorClass = (color: string) => getBgColorClass(color) const getColorClass = (color: string) => getBgColorClass(color)
return ( return (
<SettingsSection <SettingsSection title={t('API Addresses')}>
title={t('API Addresses')}
description={t('Curate quick links to your different Domains')}
>
<div className='space-y-4'> <div className='space-y-4'>
<div className='flex flex-wrap items-center justify-between gap-2'> <div className='flex flex-wrap items-center justify-between gap-2'>
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-wrap items-center gap-2'>
...@@ -306,12 +303,12 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -306,12 +303,12 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
{updateOption.isPending ? t('Saving...') : t('Save Settings')} {updateOption.isPending ? t('Saving...') : t('Save Settings')}
</Button> </Button>
</div> </div>
<div className='flex items-center gap-2'> <SettingsSwitchField
<span className='text-muted-foreground text-sm'> checked={isEnabled}
{t('Enabled')} onCheckedChange={handleToggleEnabled}
</span> label={t('Enabled')}
<Switch checked={isEnabled} onCheckedChange={handleToggleEnabled} /> className='border-b-0 py-0'
</div> />
</div> </div>
<div className='rounded-md border'> <div className='rounded-md border'>
......
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -33,6 +32,8 @@ import { ...@@ -33,6 +32,8 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea' 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 { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
import { ChatSettingsVisualEditor } from './chat-settings-visual-editor' import { ChatSettingsVisualEditor } from './chat-settings-visual-editor'
...@@ -125,13 +126,15 @@ export function ChatSettingsSection({ ...@@ -125,13 +126,15 @@ export function ChatSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Chat Presets')}>
title={t('Chat Presets')}
description={t('Configure predefined chat links surfaced to end users.')}
>
<Form {...form}> <Form {...form}>
{/* eslint-disable-next-line react-hooks/refs */} {/* eslint-disable-next-line react-hooks/refs */}
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save chat settings'
/>
<Tabs <Tabs
value={editMode} value={editMode}
onValueChange={(value) => setEditMode(value as 'visual' | 'json')} onValueChange={(value) => setEditMode(value as 'visual' | 'json')}
...@@ -186,11 +189,7 @@ export function ChatSettingsSection({ ...@@ -186,11 +189,7 @@ export function ChatSettingsSection({
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save chat settings')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -41,6 +40,12 @@ import { ...@@ -41,6 +40,12 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -89,29 +94,28 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) { ...@@ -89,29 +94,28 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
const isEnabled = form.watch('DataExportEnabled') const isEnabled = form.watch('DataExportEnabled')
return ( return (
<SettingsSection <SettingsSection title={t('Data Dashboard')}>
title={t('Data Dashboard')}
description={t('Configure experimental data export for the dashboard')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='DataExportEnabled' name='DataExportEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable Data Dashboard')}</FormLabel>
{t('Enable Data Dashboard')} </SettingsSwitchContent>
</FormLabel>
</div>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -183,11 +187,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) { ...@@ -183,11 +187,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
)} )}
/> />
</div> </div>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -21,17 +21,21 @@ import * as z from 'zod' ...@@ -21,17 +21,21 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -124,12 +128,14 @@ export function DrawingSettingsSection({ ...@@ -124,12 +128,14 @@ export function DrawingSettingsSection({
] ]
return ( return (
<SettingsSection <SettingsSection title={t('Drawing')}>
title={t('Drawing')}
description={t('Fine-tune Midjourney integration and guardrails.')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save drawing settings'
/>
<div className='space-y-4'> <div className='space-y-4'>
{switches.map((item) => ( {switches.map((item) => (
<FormField <FormField
...@@ -137,11 +143,11 @@ export function DrawingSettingsSection({ ...@@ -137,11 +143,11 @@ export function DrawingSettingsSection({
control={form.control} control={form.control}
name={item.name} name={item.name}
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-start justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5 pe-4'> <SettingsSwitchContent>
<FormLabel className='text-base'>{item.label}</FormLabel> <FormLabel>{item.label}</FormLabel>
<FormDescription>{item.description}</FormDescription> <FormDescription>{item.description}</FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
...@@ -149,18 +155,12 @@ export function DrawingSettingsSection({ ...@@ -149,18 +155,12 @@ export function DrawingSettingsSection({
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </SettingsSwitchItem>
)} )}
/> />
))} ))}
</div> </div>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending
? t('Saving...')
: t('Save drawing settings')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -53,7 +53,6 @@ import { ...@@ -53,7 +53,6 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -63,6 +62,7 @@ import { ...@@ -63,6 +62,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -238,12 +238,7 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { ...@@ -238,12 +238,7 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('FAQ')}>
title={t('FAQ')}
description={t(
'Maintain a list of common questions for the dashboard help panel'
)}
>
<div className='space-y-4'> <div className='space-y-4'>
<div className='flex flex-wrap items-center justify-between gap-2'> <div className='flex flex-wrap items-center justify-between gap-2'>
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-wrap items-center gap-2'>
...@@ -271,12 +266,12 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { ...@@ -271,12 +266,12 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
{updateOption.isPending ? t('Saving...') : t('Save Settings')} {updateOption.isPending ? t('Saving...') : t('Save Settings')}
</Button> </Button>
</div> </div>
<div className='flex items-center gap-2'> <SettingsSwitchField
<span className='text-muted-foreground text-sm'> checked={isEnabled}
{t('Enabled')} onCheckedChange={handleToggleEnabled}
</span> label={t('Enabled')}
<Switch checked={isEnabled} onCheckedChange={handleToggleEnabled} /> className='border-b-0 py-0'
</div> />
</div> </div>
<div className='rounded-md border'> <div className='rounded-md border'>
......
...@@ -16,14 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,14 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useMemo } from 'react' import { SettingsPage } from '../components/settings-page'
import { useParams } from '@tanstack/react-router' import type { ContentSettings, SystemOption } from '../types'
import { useTranslation } from 'react-i18next'
import { getOptionValue, useSystemOptions } from '../hooks/use-system-options'
import type { ContentSettings } from '../types'
import { import {
CONTENT_DEFAULT_SECTION, CONTENT_DEFAULT_SECTION,
getContentSectionContent, getContentSectionContent,
getContentSectionMeta,
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultContentSettings: ContentSettings = { const defaultContentSettings: ContentSettings = {
...@@ -47,84 +45,53 @@ const defaultContentSettings: ContentSettings = { ...@@ -47,84 +45,53 @@ const defaultContentSettings: ContentSettings = {
MjActionCheckSuccessEnabled: false, MjActionCheckSuccessEnabled: false,
} }
export function ContentSettings() { function resolveContentSettings(
const { t } = useTranslation() settings: ContentSettings,
const { data, isLoading } = useSystemOptions() raw: SystemOption[] | undefined
const params = useParams({ ): ContentSettings {
from: '/_authenticated/system-settings/content/$section', if (!raw || raw.length === 0) return settings
})
const optionMap = new Map(raw.map((item) => [item.key, item.value]))
const settings = useMemo(() => { const next = { ...settings }
const resolved = getOptionValue(data?.data, defaultContentSettings)
const legacyMap = [
const optionMap = new Map( { current: 'console_setting.announcements', legacy: 'Announcements' },
(data?.data ?? []).map((item) => [item.key, item.value]) { 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') for (const { current, legacy } of legacyMap) {
if (legacy !== undefined) { if (!optionMap.has(current)) {
resolved['console_setting.announcements'] = legacy 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')) { if (!optionMap.has('console_setting.uptime_kuma_groups')) {
const legacyUrl = optionMap.get('UptimeKumaUrl') const legacyUrl = optionMap.get('UptimeKumaUrl')
const legacySlug = optionMap.get('UptimeKumaSlug') const legacySlug = optionMap.get('UptimeKumaSlug')
if (legacyUrl && legacySlug) { if (legacyUrl && legacySlug) {
resolved['console_setting.uptime_kuma_groups'] = JSON.stringify([ next['console_setting.uptime_kuma_groups'] = JSON.stringify([
{ { id: 1, categoryName: 'Legacy', url: legacyUrl, slug: legacySlug },
id: 1, ])
categoryName: 'Legacy',
url: legacyUrl,
slug: legacySlug,
},
])
}
} }
return resolved
}, [data?.data])
if (isLoading) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-muted-foreground'>
{t('Loading content settings...')}
</div>
</div>
)
} }
const activeSection = (params?.section ?? CONTENT_DEFAULT_SECTION) as return next
| 'dashboard' }
| 'announcements'
| 'api-info'
| 'faq'
| 'uptime-kuma'
| 'chat'
| 'drawing'
const sectionContent = getContentSectionContent(activeSection, settings)
export function ContentSettings() {
return ( return (
<div className='flex h-full w-full flex-1 flex-col'> <SettingsPage
<div className='faded-bottom h-full w-full overflow-y-auto scroll-smooth pe-4 pb-12'> routePath='/_authenticated/system-settings/content/$section'
<div className='space-y-4'>{sectionContent}</div> defaultSettings={defaultContentSettings}
</div> defaultSection={CONTENT_DEFAULT_SECTION}
</div> getSectionContent={getContentSectionContent}
getSectionMeta={getContentSectionMeta}
loadingMessage='Loading content settings...'
resolveSettings={resolveContentSettings}
/>
) )
} }
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -34,13 +33,18 @@ import { ...@@ -34,13 +33,18 @@ import {
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { SettingsAccordion } from '../components/settings-accordion' 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 { useUpdateOption } from '../hooks/use-update-option'
import { formatJsonForEditor, normalizeJsonString } from './utils' import { formatJsonForEditor, normalizeJsonString } from './utils'
type JsonToggleSectionProps = { type JsonToggleSectionProps = {
value: string value: string
title: string title: string
description?: string
toggleDescription?: string toggleDescription?: string
optionKey: string optionKey: string
enabledKey: string enabledKey: string
...@@ -63,7 +67,6 @@ type JsonToggleFormValues = { ...@@ -63,7 +67,6 @@ type JsonToggleFormValues = {
export function JsonToggleSection({ export function JsonToggleSection({
value, value,
title, title,
description,
toggleDescription, toggleDescription,
optionKey, optionKey,
enabledKey, enabledKey,
...@@ -157,30 +160,33 @@ export function JsonToggleSection({ ...@@ -157,30 +160,33 @@ export function JsonToggleSection({
} }
return ( return (
<SettingsAccordion value={value} title={title} description={description}> <SettingsAccordion value={value} title={title}>
<Form {...form}> <Form {...form}>
{/* eslint-disable-next-line react-hooks/refs */} {/* eslint-disable-next-line react-hooks/refs */}
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel={submitLabel}
/>
<FormField <FormField
control={form.control} control={form.control}
name='enabled' name='enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Module availability')}</FormLabel>
{t('Module availability')}
</FormLabel>
{toggleDescription && ( {toggleDescription && (
<FormDescription>{t(toggleDescription)}</FormDescription> <FormDescription>{t(toggleDescription)}</FormDescription>
)} )}
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -203,11 +209,7 @@ export function JsonToggleSection({ ...@@ -203,11 +209,7 @@ export function JsonToggleSection({
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t(submitLabel)}
</Button>
</form>
</Form> </Form>
</SettingsAccordion> </SettingsAccordion>
) )
......
...@@ -41,7 +41,6 @@ const CONTENT_SECTIONS = [ ...@@ -41,7 +41,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'dashboard', id: 'dashboard',
titleKey: 'Data Dashboard', titleKey: 'Data Dashboard',
descriptionKey: 'Configure data export settings for dashboard',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<DashboardSection <DashboardSection
defaultValues={{ defaultValues={{
...@@ -57,7 +56,6 @@ const CONTENT_SECTIONS = [ ...@@ -57,7 +56,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'announcements', id: 'announcements',
titleKey: 'Announcements', titleKey: 'Announcements',
descriptionKey: 'Configure system announcements',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<AnnouncementsSection <AnnouncementsSection
enabled={settings['console_setting.announcements_enabled']} enabled={settings['console_setting.announcements_enabled']}
...@@ -68,7 +66,6 @@ const CONTENT_SECTIONS = [ ...@@ -68,7 +66,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'api-info', id: 'api-info',
titleKey: 'API Addresses', titleKey: 'API Addresses',
descriptionKey: 'Configure API information display',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<ApiInfoSection <ApiInfoSection
enabled={settings['console_setting.api_info_enabled']} enabled={settings['console_setting.api_info_enabled']}
...@@ -79,7 +76,6 @@ const CONTENT_SECTIONS = [ ...@@ -79,7 +76,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'faq', id: 'faq',
titleKey: 'FAQ', titleKey: 'FAQ',
descriptionKey: 'Configure frequently asked questions',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<FAQSection <FAQSection
enabled={settings['console_setting.faq_enabled']} enabled={settings['console_setting.faq_enabled']}
...@@ -90,7 +86,6 @@ const CONTENT_SECTIONS = [ ...@@ -90,7 +86,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'uptime-kuma', id: 'uptime-kuma',
titleKey: 'Uptime Kuma', titleKey: 'Uptime Kuma',
descriptionKey: 'Configure Uptime Kuma monitoring integration',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<UptimeKumaSection <UptimeKumaSection
enabled={settings['console_setting.uptime_kuma_enabled']} enabled={settings['console_setting.uptime_kuma_enabled']}
...@@ -101,7 +96,6 @@ const CONTENT_SECTIONS = [ ...@@ -101,7 +96,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'chat', id: 'chat',
titleKey: 'Chat Presets', titleKey: 'Chat Presets',
descriptionKey: 'Configure chat-related settings',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<ChatSettingsSection defaultValue={settings.Chats} /> <ChatSettingsSection defaultValue={settings.Chats} />
), ),
...@@ -109,7 +103,6 @@ const CONTENT_SECTIONS = [ ...@@ -109,7 +103,6 @@ const CONTENT_SECTIONS = [
{ {
id: 'drawing', id: 'drawing',
titleKey: 'Drawing', titleKey: 'Drawing',
descriptionKey: 'Configure drawing and Midjourney settings',
build: (settings: ContentSettings) => ( build: (settings: ContentSettings) => (
<DrawingSettingsSection <DrawingSettingsSection
defaultValues={{ defaultValues={{
...@@ -141,3 +134,4 @@ export const CONTENT_SECTION_IDS = contentRegistry.sectionIds ...@@ -141,3 +134,4 @@ export const CONTENT_SECTION_IDS = contentRegistry.sectionIds
export const CONTENT_DEFAULT_SECTION = contentRegistry.defaultSection export const CONTENT_DEFAULT_SECTION = contentRegistry.defaultSection
export const getContentSectionNavItems = contentRegistry.getSectionNavItems export const getContentSectionNavItems = contentRegistry.getSectionNavItems
export const getContentSectionContent = contentRegistry.getSectionContent export const getContentSectionContent = contentRegistry.getSectionContent
export const getContentSectionMeta = contentRegistry.getSectionMeta
...@@ -53,7 +53,6 @@ import { ...@@ -53,7 +53,6 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -62,6 +61,7 @@ import { ...@@ -62,6 +61,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -247,12 +247,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { ...@@ -247,12 +247,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('Uptime Kuma')}>
title={t('Uptime Kuma')}
description={t(
'Expose grouped Uptime Kuma status pages directly on the dashboard'
)}
>
<div className='space-y-4'> <div className='space-y-4'>
<div className='flex flex-wrap items-center justify-between gap-2'> <div className='flex flex-wrap items-center justify-between gap-2'>
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-wrap items-center gap-2'>
...@@ -280,12 +275,12 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { ...@@ -280,12 +275,12 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
{updateOption.isPending ? t('Saving...') : t('Save Settings')} {updateOption.isPending ? t('Saving...') : t('Save Settings')}
</Button> </Button>
</div> </div>
<div className='flex items-center gap-2'> <SettingsSwitchField
<span className='text-muted-foreground text-sm'> checked={isEnabled}
{t('Enabled')} onCheckedChange={handleToggleEnabled}
</span> label={t('Enabled')}
<Switch checked={isEnabled} onCheckedChange={handleToggleEnabled} /> className='border-b-0 py-0'
</div> />
</div> </div>
<div className='rounded-md border'> <div className='rounded-md border'>
......
...@@ -31,7 +31,6 @@ import { ...@@ -31,7 +31,6 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -43,6 +42,8 @@ import { ...@@ -43,6 +42,8 @@ import {
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { ConfirmDialog } from '@/components/confirm-dialog' import { ConfirmDialog } from '@/components/confirm-dialog'
import { StatusBadge } from '@/components/status-badge' 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 { SettingsSection } from '../../components/settings-section'
import { useUpdateOption } from '../../hooks/use-update-option' import { useUpdateOption } from '../../hooks/use-update-option'
import { getCacheStats, clearAllCache, clearRuleCache } from './api' import { getCacheStats, clearAllCache, clearRuleCache } from './api'
...@@ -333,12 +334,7 @@ export function ChannelAffinitySection(props: Props) { ...@@ -333,12 +334,7 @@ export function ChannelAffinitySection(props: Props) {
return ( return (
<> <>
<SettingsSection <SettingsSection title={t('Channel Affinity')}>
title={t('Channel Affinity')}
description={t(
'Prioritize reusing the last successful channel based on keys extracted from request context (sticky routing)'
)}
>
<Alert> <Alert>
<AlertDescription className='text-xs'> <AlertDescription className='text-xs'>
{t( {t(
...@@ -349,10 +345,12 @@ export function ChannelAffinitySection(props: Props) { ...@@ -349,10 +345,12 @@ export function ChannelAffinitySection(props: Props) {
{/* Basic Settings */} {/* Basic Settings */}
<div className='grid grid-cols-1 gap-4 md:grid-cols-3'> <div className='grid grid-cols-1 gap-4 md:grid-cols-3'>
<div className='flex items-center gap-2'> <SettingsSwitchField
<Switch checked={enabled} onCheckedChange={setEnabled} /> checked={enabled}
<Label>{t('Enable')}</Label> onCheckedChange={setEnabled}
</div> label={t('Enable')}
className='border-b-0 py-0'
/>
<div className='grid gap-1.5'> <div className='grid gap-1.5'>
<Label>{t('Max Entries')}</Label> <Label>{t('Max Entries')}</Label>
<Input <Input
...@@ -373,23 +371,18 @@ export function ChannelAffinitySection(props: Props) { ...@@ -373,23 +371,18 @@ export function ChannelAffinitySection(props: Props) {
</div> </div>
</div> </div>
<div className='flex items-center gap-2'> <SettingsSwitchField
<Switch checked={switchOnSuccess}
checked={switchOnSuccess} onCheckedChange={setSwitchOnSuccess}
onCheckedChange={setSwitchOnSuccess} label={t('Switch affinity on success')}
/> description={t(
<Label>{t('Switch affinity on success')}</Label> 'If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.'
<span className='text-muted-foreground text-xs'> )}
{t( />
'If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.'
)}
</span>
</div>
<Separator /> <Separator />
{/* Toolbar */} <SettingsPageActionsPortal>
<div className='flex flex-wrap items-center gap-2'>
<Button <Button
variant={editMode === 'visual' ? 'default' : 'outline'} variant={editMode === 'visual' ? 'default' : 'outline'}
size='sm' size='sm'
...@@ -472,7 +465,7 @@ export function ChannelAffinitySection(props: Props) { ...@@ -472,7 +465,7 @@ export function ChannelAffinitySection(props: Props) {
{cacheStats.cache_capacity} {cacheStats.cache_capacity}
</span> </span>
)} )}
</div> </SettingsPageActionsPortal>
{/* Rules Table or JSON Editor */} {/* Rules Table or JSON Editor */}
{editMode === 'visual' ? ( {editMode === 'visual' ? (
......
...@@ -45,8 +45,8 @@ import { ...@@ -45,8 +45,8 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { SettingsSwitchField } from '../../components/settings-form-layout'
import { RULE_TEMPLATES } from './constants' import { RULE_TEMPLATES } from './constants'
import type { AffinityRule, KeySource } from './types' import type { AffinityRule, KeySource } from './types'
...@@ -264,13 +264,11 @@ export function RuleEditorDialog(props: Props) { ...@@ -264,13 +264,11 @@ export function RuleEditorDialog(props: Props) {
</div> </div>
</div> </div>
<div className='flex items-center gap-2'> <SettingsSwitchField
<Switch checked={form.watch('skip_retry_on_failure')}
checked={form.watch('skip_retry_on_failure')} onCheckedChange={(v) => form.setValue('skip_retry_on_failure', v)}
onCheckedChange={(v) => form.setValue('skip_retry_on_failure', v)} label={t('Skip retry on failure')}
/> />
<Label>{t('Skip retry on failure')}</Label>
</div>
<Separator /> <Separator />
...@@ -415,34 +413,29 @@ export function RuleEditorDialog(props: Props) { ...@@ -415,34 +413,29 @@ export function RuleEditorDialog(props: Props) {
/> />
</div> </div>
<div className='grid grid-cols-3 gap-3'> <div className='grid gap-3 sm:grid-cols-3'>
<div className='flex items-center gap-2'> <SettingsSwitchField
<Switch checked={form.watch('include_using_group')}
checked={form.watch('include_using_group')} onCheckedChange={(v) =>
onCheckedChange={(v) => form.setValue('include_using_group', v)
form.setValue('include_using_group', v) }
} label={t('Include Group')}
/> className='border-b-0 py-0'
<Label className='text-xs'>{t('Include Group')}</Label> />
</div> <SettingsSwitchField
<div className='flex items-center gap-2'> checked={form.watch('include_model_name')}
<Switch onCheckedChange={(v) =>
checked={form.watch('include_model_name')} form.setValue('include_model_name', v)
onCheckedChange={(v) => }
form.setValue('include_model_name', v) label={t('Include Model')}
} className='border-b-0 py-0'
/> />
<Label className='text-xs'>{t('Include Model')}</Label> <SettingsSwitchField
</div> checked={form.watch('include_rule_name')}
<div className='flex items-center gap-2'> onCheckedChange={(v) => form.setValue('include_rule_name', v)}
<Switch label={t('Include Rule Name')}
checked={form.watch('include_rule_name')} className='border-b-0 py-0'
onCheckedChange={(v) => />
form.setValue('include_rule_name', v)
}
/>
<Label className='text-xs'>{t('Include Rule Name')}</Label>
</div>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
......
...@@ -21,7 +21,6 @@ import { useForm, type Resolver } from 'react-hook-form' ...@@ -21,7 +21,6 @@ import { useForm, type Resolver } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -33,6 +32,12 @@ import { ...@@ -33,6 +32,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -105,31 +110,28 @@ export function CheckinSettingsSection({ ...@@ -105,31 +110,28 @@ export function CheckinSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Check-in Settings')}>
title={t('Check-in Settings')}
description={t('Configure daily check-in rewards for users')}
>
<Form {...form}> <Form {...form}>
<form <SettingsForm onSubmit={form.handleSubmit(onSubmit)} autoComplete='off'>
onSubmit={form.handleSubmit(onSubmit)} <SettingsPageFormActions
autoComplete='off' onSave={form.handleSubmit(onSubmit)}
className='space-y-6' isSaving={updateOption.isPending || isSubmitting}
> isSaveDisabled={!isDirty}
saveLabel='Save check-in settings'
/>
<FormField <FormField
control={form.control} control={form.control}
name='enabled' name='enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable check-in feature')}</FormLabel>
{t('Enable check-in feature')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Allow users to check in daily for random quota rewards' 'Allow users to check in daily for random quota rewards'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
...@@ -137,7 +139,7 @@ export function CheckinSettingsSection({ ...@@ -137,7 +139,7 @@ export function CheckinSettingsSection({
disabled={updateOption.isPending || isSubmitting} disabled={updateOption.isPending || isSubmitting}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -188,16 +190,7 @@ export function CheckinSettingsSection({ ...@@ -188,16 +190,7 @@ export function CheckinSettingsSection({
/> />
</div> </div>
)} )}
</SettingsForm>
<Button
type='submit'
disabled={!isDirty || updateOption.isPending || isSubmitting}
>
{updateOption.isPending || isSubmitting
? t('Saving...')
: t('Save check-in settings')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -19,10 +19,8 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,10 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
import * as z from 'zod' import * as z from 'zod'
import type { Resolver } from 'react-hook-form' import type { Resolver } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { RotateCcw } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_CURRENCY_CONFIG } from '@/stores/system-config-store' import { DEFAULT_CURRENCY_CONFIG } from '@/stores/system-config-store'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -44,6 +42,12 @@ import { ...@@ -44,6 +42,12 @@ import {
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { FormDirtyIndicator } from '../components/form-dirty-indicator' import { FormDirtyIndicator } from '../components/form-dirty-indicator'
import { FormNavigationGuard } from '../components/form-navigation-guard' 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 { SettingsSection } from '../components/settings-section'
import { useSettingsForm } from '../hooks/use-settings-form' import { useSettingsForm } from '../hooks/use-settings-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -141,12 +145,15 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { ...@@ -141,12 +145,15 @@ export function PricingSection({ defaultValues }: PricingSectionProps) {
<> <>
<FormNavigationGuard when={isDirty} /> <FormNavigationGuard when={isDirty} />
<SettingsSection <SettingsSection title={t('Pricing & Display')}>
title={t('Pricing & Display')}
description={t('Configure pricing model and display options')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit} className='space-y-6'> <SettingsForm onSubmit={handleSubmit}>
<SettingsPageFormActions
onSave={handleSubmit}
onReset={handleReset}
isSaving={updateOption.isPending || isSubmitting}
isResetDisabled={!isDirty}
/>
<FormDirtyIndicator isDirty={isDirty} /> <FormDirtyIndicator isDirty={isDirty} />
{showQuotaPerUnit && ( {showQuotaPerUnit && (
<FormField <FormField
...@@ -320,11 +327,9 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { ...@@ -320,11 +327,9 @@ export function PricingSection({ defaultValues }: PricingSectionProps) {
control={form.control} control={form.control}
name='DisplayInCurrencyEnabled' name='DisplayInCurrencyEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Display in Currency')}</FormLabel>
{t('Display in Currency')}
</FormLabel>
<FormDescription> <FormDescription>
{displayType === 'TOKENS' {displayType === 'TOKENS'
? t( ? t(
...@@ -332,14 +337,14 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { ...@@ -332,14 +337,14 @@ export function PricingSection({ defaultValues }: PricingSectionProps) {
) )
: t('Show prices in currency instead of quota.')} : t('Show prices in currency instead of quota.')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
)} )}
...@@ -348,43 +353,23 @@ export function PricingSection({ defaultValues }: PricingSectionProps) { ...@@ -348,43 +353,23 @@ export function PricingSection({ defaultValues }: PricingSectionProps) {
control={form.control} control={form.control}
name='DisplayTokenStatEnabled' name='DisplayTokenStatEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Display Token Statistics')}</FormLabel>
{t('Display Token Statistics')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Show token usage statistics in the UI')} {t('Show token usage statistics in the UI')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</SettingsForm>
<div className='flex gap-2'>
<Button
type='submit'
disabled={updateOption.isPending || isSubmitting}
>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
<Button
type='button'
variant='outline'
onClick={handleReset}
disabled={!isDirty || updateOption.isPending || isSubmitting}
>
<RotateCcw className='mr-2 h-4 w-4' />
{t('Reset')}
</Button>
</div>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
</> </>
......
...@@ -20,7 +20,6 @@ import * as z from 'zod' ...@@ -20,7 +20,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -32,6 +31,12 @@ import { ...@@ -32,6 +31,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -73,12 +78,13 @@ export function SystemBehaviorSection({ ...@@ -73,12 +78,13 @@ export function SystemBehaviorSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('System Behavior')}>
title={t('System Behavior')}
description={t('Configure system-wide behavior and defaults')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='RetryTimes' name='RetryTimes'
...@@ -109,22 +115,20 @@ export function SystemBehaviorSection({ ...@@ -109,22 +115,20 @@ export function SystemBehaviorSection({
control={form.control} control={form.control}
name='DefaultCollapseSidebar' name='DefaultCollapseSidebar'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Default Collapse Sidebar')}</FormLabel>
{t('Default Collapse Sidebar')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Sidebar collapsed by default for new users')} {t('Sidebar collapsed by default for new users')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -132,22 +136,20 @@ export function SystemBehaviorSection({ ...@@ -132,22 +136,20 @@ export function SystemBehaviorSection({
control={form.control} control={form.control}
name='DemoSiteEnabled' name='DemoSiteEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Demo Site Mode')}</FormLabel>
{t('Demo Site Mode')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Enable demo mode with limited functionality')} {t('Enable demo mode with limited functionality')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -155,29 +157,23 @@ export function SystemBehaviorSection({ ...@@ -155,29 +157,23 @@ export function SystemBehaviorSection({
control={form.control} control={form.control}
name='SelfUseModeEnabled' name='SelfUseModeEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Self-Use Mode')}</FormLabel>
{t('Self-Use Mode')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Optimize system for self-hosted single-user usage')} {t('Optimize system for self-hosted single-user usage')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -17,14 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,14 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { Outlet } from '@tanstack/react-router' import { Outlet } from '@tanstack/react-router'
import { Main } from '@/components/layout'
export function SystemSettings() { export function SystemSettings() {
return ( return <Outlet />
<Main>
<div className='min-h-0 flex-1 px-4 pt-6 pb-4'>
<Outlet />
</div>
</Main>
)
} }
...@@ -20,7 +20,6 @@ import * as z from 'zod' ...@@ -20,7 +20,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -32,6 +31,12 @@ import { ...@@ -32,6 +31,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -138,16 +143,14 @@ export function EmailSettingsSection({ ...@@ -138,16 +143,14 @@ export function EmailSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('SMTP Email')}>
title={t('SMTP Email')}
description={t('Configure outgoing email server for notifications')}
>
<Form {...form}> <Form {...form}>
<form <SettingsForm onSubmit={form.handleSubmit(onSubmit)} autoComplete='off'>
onSubmit={form.handleSubmit(onSubmit)} <SettingsPageFormActions
className='space-y-6' onSave={form.handleSubmit(onSubmit)}
autoComplete='off' isSaving={updateOption.isPending}
> saveLabel='Save SMTP settings'
/>
<FormField <FormField
control={form.control} control={form.control}
name='SMTPServer' name='SMTPServer'
...@@ -198,22 +201,20 @@ export function EmailSettingsSection({ ...@@ -198,22 +201,20 @@ export function EmailSettingsSection({
control={form.control} control={form.control}
name='SMTPSSLEnabled' name='SMTPSSLEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable SSL/TLS')}</FormLabel>
{t('Enable SSL/TLS')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Use secure connection when sending emails')} {t('Use secure connection when sending emails')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -221,22 +222,20 @@ export function EmailSettingsSection({ ...@@ -221,22 +222,20 @@ export function EmailSettingsSection({
control={form.control} control={form.control}
name='SMTPForceAuthLogin' name='SMTPForceAuthLogin'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Force AUTH LOGIN')}</FormLabel>
{t('Force AUTH LOGIN')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Force SMTP authentication using AUTH LOGIN method')} {t('Force SMTP authentication using AUTH LOGIN method')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</div> </div>
...@@ -307,11 +306,7 @@ export function EmailSettingsSection({ ...@@ -307,11 +306,7 @@ export function EmailSettingsSection({
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save SMTP settings')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -37,6 +37,12 @@ import { ...@@ -37,6 +37,12 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { testDeploymentConnectionWithKey } from '@/features/models/api' import { testDeploymentConnectionWithKey } from '@/features/models/api'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -129,29 +135,26 @@ export function IoNetDeploymentSettingsSection({ ...@@ -129,29 +135,26 @@ export function IoNetDeploymentSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('io.net Deployments')}>
title={t('io.net Deployments')}
description={t('Configure io.net API key for model deployments')}
>
<Form {...form}> <Form {...form}>
<form <SettingsForm onSubmit={form.handleSubmit(onSubmit)} autoComplete='off'>
onSubmit={form.handleSubmit(onSubmit)} <SettingsPageFormActions
autoComplete='off' onSave={form.handleSubmit(onSubmit)}
className='space-y-6' isSaving={updateOption.isPending || isSubmitting}
> isSaveDisabled={!isDirty}
saveLabel='Save io.net settings'
/>
<FormField <FormField
control={form.control} control={form.control}
name='enabled' name='enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable io.net deployments')}</FormLabel>
{t('Enable io.net deployments')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Enable io.net model deployment service in console')} {t('Enable io.net model deployment service in console')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
...@@ -159,7 +162,7 @@ export function IoNetDeploymentSettingsSection({ ...@@ -159,7 +162,7 @@ export function IoNetDeploymentSettingsSection({
disabled={updateOption.isPending || isSubmitting} disabled={updateOption.isPending || isSubmitting}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -253,16 +256,7 @@ export function IoNetDeploymentSettingsSection({ ...@@ -253,16 +256,7 @@ export function IoNetDeploymentSettingsSection({
) : null} ) : null}
</> </>
) : null} ) : null}
</SettingsForm>
<Button
type='submit'
disabled={!isDirty || updateOption.isPending || isSubmitting}
>
{updateOption.isPending || isSubmitting
? t('Saving...')
: t('Save io.net settings')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -23,7 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod' ...@@ -23,7 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { parseHttpStatusCodeRules } from '@/lib/http-status-code-rules' import { parseHttpStatusCodeRules } from '@/lib/http-status-code-rules'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -36,6 +35,12 @@ import { ...@@ -36,6 +35,12 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -243,35 +248,33 @@ export function MonitoringSettingsSection({ ...@@ -243,35 +248,33 @@ export function MonitoringSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Monitoring & Alerts')}>
title={t('Monitoring & Alerts')}
description={t(
'Automatically test channels and notify users when limits are hit'
)}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save monitoring rules'
/>
<div className='grid gap-6 md:grid-cols-2'> <div className='grid gap-6 md:grid-cols-2'>
<FormField <FormField
control={form.control} control={form.control}
name='monitor_setting.auto_test_channel_enabled' name='monitor_setting.auto_test_channel_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Scheduled channel tests')}</FormLabel>
{t('Scheduled channel tests')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Automatically probe all channels in the background')} {t('Automatically probe all channels in the background')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -364,22 +367,20 @@ export function MonitoringSettingsSection({ ...@@ -364,22 +367,20 @@ export function MonitoringSettingsSection({
control={form.control} control={form.control}
name='AutomaticDisableChannelEnabled' name='AutomaticDisableChannelEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Disable on failure')}</FormLabel>
{t('Disable on failure')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Automatically disable channels when tests fail')} {t('Automatically disable channels when tests fail')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -387,22 +388,20 @@ export function MonitoringSettingsSection({ ...@@ -387,22 +388,20 @@ export function MonitoringSettingsSection({
control={form.control} control={form.control}
name='AutomaticEnableChannelEnabled' name='AutomaticEnableChannelEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Re-enable on success')}</FormLabel>
{t('Re-enable on success')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Bring channels back online after successful checks')} {t('Bring channels back online after successful checks')}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</div> </div>
...@@ -492,13 +491,7 @@ export function MonitoringSettingsSection({ ...@@ -492,13 +491,7 @@ export function MonitoringSettingsSection({
)} )}
/> />
</div> </div>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending
? t('Saving...')
: t('Save monitoring rules')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -41,6 +41,8 @@ import { ...@@ -41,6 +41,8 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { SettingsForm } from '../components/settings-form-layout'
import { SettingsPageActionsPortal } from '../components/settings-page-context'
import { removeTrailingSlash } from './utils' import { removeTrailingSlash } from './utils'
import { import {
type CatalogStore, type CatalogStore,
...@@ -451,6 +453,16 @@ export function WaffoPancakeSettingsSection(props: Props) { ...@@ -451,6 +453,16 @@ export function WaffoPancakeSettingsSection(props: Props) {
return ( return (
<div className='space-y-4 pt-4'> <div className='space-y-4 pt-4'>
<SettingsPageActionsPortal>
<Button
type='button'
size='sm'
onClick={handleSave}
disabled={saving || !chosenStoreID || !chosenProductID}
>
{saving ? t('Saving...') : t('Save Waffo Pancake settings')}
</Button>
</SettingsPageActionsPortal>
<div> <div>
<h3 className='text-lg font-medium'>{t('Waffo Pancake MoR')}</h3> <h3 className='text-lg font-medium'>{t('Waffo Pancake MoR')}</h3>
<p className='text-muted-foreground text-sm'> <p className='text-muted-foreground text-sm'>
...@@ -460,9 +472,9 @@ export function WaffoPancakeSettingsSection(props: Props) { ...@@ -460,9 +472,9 @@ export function WaffoPancakeSettingsSection(props: Props) {
</p> </p>
</div> </div>
<Form {...form}> <Form {...form}>
<form <SettingsForm
onSubmit={(e) => e.preventDefault()} onSubmit={(e) => e.preventDefault()}
className='space-y-4' className='gap-y-4'
data-no-autosubmit='true' data-no-autosubmit='true'
> >
{/* Blue box — webhook configuration only. */} {/* Blue box — webhook configuration only. */}
...@@ -685,13 +697,6 @@ export function WaffoPancakeSettingsSection(props: Props) { ...@@ -685,13 +697,6 @@ export function WaffoPancakeSettingsSection(props: Props) {
) : null} ) : null}
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Button
type='button'
onClick={handleSave}
disabled={saving || !chosenStoreID || !chosenProductID}
>
{saving ? t('Saving...') : t('Save Waffo Pancake settings')}
</Button>
{storeID || productID ? ( {storeID || productID ? (
<div className='text-muted-foreground flex flex-wrap gap-x-3 gap-y-1 text-xs'> <div className='text-muted-foreground flex flex-wrap gap-x-3 gap-y-1 text-xs'>
{storeID ? ( {storeID ? (
...@@ -714,7 +719,7 @@ export function WaffoPancakeSettingsSection(props: Props) { ...@@ -714,7 +719,7 @@ export function WaffoPancakeSettingsSection(props: Props) {
) : null} ) : null}
</div> </div>
</div> </div>
</form> </SettingsForm>
</Form> </Form>
</div> </div>
) )
......
...@@ -33,7 +33,6 @@ import { ...@@ -33,7 +33,6 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { import {
Table, Table,
TableBody, TableBody,
...@@ -43,6 +42,8 @@ import { ...@@ -43,6 +42,8 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsPageActionsPortal } from '../components/settings-page-context'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
export interface WaffoSettingsValues { export interface WaffoSettingsValues {
...@@ -212,6 +213,16 @@ export function WaffoSettingsSection(props: Props) { ...@@ -212,6 +213,16 @@ export function WaffoSettingsSection(props: Props) {
return ( return (
<> <>
<div className='space-y-4 pt-4'> <div className='space-y-4 pt-4'>
<SettingsPageActionsPortal>
<Button
type='button'
size='sm'
onClick={handleSave}
disabled={loading}
>
{loading ? t('Saving...') : t('Save Waffo settings')}
</Button>
</SettingsPageActionsPortal>
<div> <div>
<h3 className='text-lg font-medium'> <h3 className='text-lg font-medium'>
{t('Waffo Aggregator Gateway')} {t('Waffo Aggregator Gateway')}
...@@ -230,21 +241,19 @@ export function WaffoSettingsSection(props: Props) { ...@@ -230,21 +241,19 @@ export function WaffoSettingsSection(props: Props) {
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className='grid grid-cols-2 gap-4'> <div className='grid gap-4 sm:grid-cols-2'>
<div className='flex items-center gap-2'> <SettingsSwitchField
<Switch checked={form.watch('WaffoEnabled')}
checked={form.watch('WaffoEnabled')} onCheckedChange={(v) => form.setValue('WaffoEnabled', v)}
onCheckedChange={(v) => form.setValue('WaffoEnabled', v)} label={t('Enable Waffo')}
/> className='border-b-0 py-0'
<Label>{t('Enable Waffo')}</Label> />
</div> <SettingsSwitchField
<div className='flex items-center gap-2'> checked={form.watch('WaffoSandbox')}
<Switch onCheckedChange={(v) => form.setValue('WaffoSandbox', v)}
checked={form.watch('WaffoSandbox')} label={t('Sandbox mode')}
onCheckedChange={(v) => form.setValue('WaffoSandbox', v)} className='border-b-0 py-0'
/> />
<Label>{t('Sandbox mode')}</Label>
</div>
</div> </div>
<div className='grid grid-cols-2 gap-4'> <div className='grid grid-cols-2 gap-4'>
...@@ -416,10 +425,6 @@ export function WaffoSettingsSection(props: Props) { ...@@ -416,10 +425,6 @@ export function WaffoSettingsSection(props: Props) {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
<Button onClick={handleSave} disabled={loading}>
{loading ? t('Saving...') : t('Save Changes')}
</Button>
</div> </div>
<Dialog open={methodDialogOpen} onOpenChange={setMethodDialogOpen}> <Dialog open={methodDialogOpen} onOpenChange={setMethodDialogOpen}>
......
...@@ -20,7 +20,6 @@ import * as z from 'zod' ...@@ -20,7 +20,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -32,6 +31,12 @@ import { ...@@ -32,6 +31,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -100,18 +105,14 @@ export function WorkerSettingsSection({ ...@@ -100,18 +105,14 @@ export function WorkerSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Worker Proxy')}>
title={t('Worker Proxy')}
description={t(
'Configure upstream worker or proxy service for outbound requests'
)}
>
<Form {...form}> <Form {...form}>
<form <SettingsForm onSubmit={form.handleSubmit(onSubmit)} autoComplete='off'>
onSubmit={form.handleSubmit(onSubmit)} <SettingsPageFormActions
autoComplete='off' onSave={form.handleSubmit(onSubmit)}
className='space-y-6' isSaving={updateOption.isPending}
> saveLabel='Save Worker settings'
/>
<FormField <FormField
control={form.control} control={form.control}
name='WorkerUrl' name='WorkerUrl'
...@@ -167,33 +168,25 @@ export function WorkerSettingsSection({ ...@@ -167,33 +168,25 @@ export function WorkerSettingsSection({
control={form.control} control={form.control}
name='WorkerAllowHttpImageRequestEnabled' name='WorkerAllowHttpImageRequestEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Allow HTTP image requests')}</FormLabel>
{t('Allow HTTP image requests')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Enable when proxying workers that fetch images over HTTP.' 'Enable when proxying workers that fetch images over HTTP.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending
? t('Saving...')
: t('Save Worker settings')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -21,17 +21,23 @@ import * as z from 'zod' ...@@ -21,17 +21,23 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import {
SettingsControlChildren,
SettingsForm,
SettingsSwitchContent,
SettingsControlGroup,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
import { import {
...@@ -201,12 +207,16 @@ export function HeaderNavigationSection({ ...@@ -201,12 +207,16 @@ export function HeaderNavigationSection({
] ]
return ( return (
<SettingsSection <SettingsSection title={t('Header navigation')}>
title={t('Header navigation')}
description={t('Enable or disable top navigation modules globally.')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
onReset={resetToDefault}
isSaving={updateOption.isPending}
resetLabel='Reset to default'
saveLabel='Save navigation'
/>
<div className='grid gap-4 md:grid-cols-2'> <div className='grid gap-4 md:grid-cols-2'>
{simpleModules.map((module) => ( {simpleModules.map((module) => (
<FormField <FormField
...@@ -214,13 +224,11 @@ export function HeaderNavigationSection({ ...@@ -214,13 +224,11 @@ export function HeaderNavigationSection({
control={form.control} control={form.control}
name={module.key} name={module.key}
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-start justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5 pe-4'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{module.title}</FormLabel>
{module.title}
</FormLabel>
<FormDescription>{module.description}</FormDescription> <FormDescription>{module.description}</FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
...@@ -228,7 +236,7 @@ export function HeaderNavigationSection({ ...@@ -228,7 +236,7 @@ export function HeaderNavigationSection({
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </SettingsSwitchItem>
)} )}
/> />
))} ))}
...@@ -236,18 +244,16 @@ export function HeaderNavigationSection({ ...@@ -236,18 +244,16 @@ export function HeaderNavigationSection({
<div className='grid gap-4 lg:grid-cols-2'> <div className='grid gap-4 lg:grid-cols-2'>
{accessModules.map((module) => ( {accessModules.map((module) => (
<div key={module.enabledKey} className='rounded-lg border p-4'> <SettingsControlGroup key={module.enabledKey}>
<FormField <FormField
control={form.control} control={form.control}
name={module.enabledKey} name={module.enabledKey}
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-start justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5 pe-4'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{module.title}</FormLabel>
{module.title}
</FormLabel>
<FormDescription>{module.description}</FormDescription> <FormDescription>{module.description}</FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
...@@ -255,7 +261,7 @@ export function HeaderNavigationSection({ ...@@ -255,7 +261,7 @@ export function HeaderNavigationSection({
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -263,39 +269,30 @@ export function HeaderNavigationSection({ ...@@ -263,39 +269,30 @@ export function HeaderNavigationSection({
control={form.control} control={form.control}
name={module.requireAuthKey} name={module.requireAuthKey}
render={({ field }) => ( render={({ field }) => (
<FormItem className='mt-4 flex flex-row items-start justify-between rounded-lg border border-dashed p-4'> <SettingsControlChildren>
<div className='space-y-0.5 pe-4'> <SettingsSwitchItem className='border-b-0 py-2'>
<FormLabel className='text-base'> <SettingsSwitchContent>
{module.requireAuthTitle} <FormLabel>{module.requireAuthTitle}</FormLabel>
</FormLabel> <FormDescription>
<FormDescription> {module.requireAuthDescription}
{module.requireAuthDescription} </FormDescription>
</FormDescription> </SettingsSwitchContent>
</div> <FormControl>
<FormControl> <Switch
<Switch checked={field.value}
checked={field.value} onCheckedChange={field.onChange}
onCheckedChange={field.onChange} disabled={!form.watch(module.requireAuthDependsOn)}
disabled={!form.watch(module.requireAuthDependsOn)} />
/> </FormControl>
</FormControl> <FormMessage />
<FormMessage /> </SettingsSwitchItem>
</FormItem> </SettingsControlChildren>
)} )}
/> />
</div> </SettingsControlGroup>
))} ))}
</div> </div>
</SettingsForm>
<div className='flex flex-wrap gap-3'>
<Button type='button' variant='outline' onClick={resetToDefault}>
{t('Reset to default')}
</Button>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save navigation')}
</Button>
</div>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -39,13 +39,19 @@ import { ...@@ -39,13 +39,19 @@ import {
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { DateTimePicker } from '@/components/datetime-picker' import { DateTimePicker } from '@/components/datetime-picker'
import { deleteLogsBefore } from '../api' import { deleteLogsBefore } from '../api'
import {
SettingsControlGroup,
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -161,27 +167,27 @@ export function LogSettingsSection({ ...@@ -161,27 +167,27 @@ export function LogSettingsSection({
} }
return ( return (
<SettingsSection <SettingsSection title={t('Log Maintenance')}>
title={t('Log Maintenance')}
description={t('Control log retention and clean historical data.')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save log settings'
/>
<FormField <FormField
control={form.control} control={form.control}
name='LogConsumeEnabled' name='LogConsumeEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-start justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5 pe-4'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Record quota usage')}</FormLabel>
{t('Record quota usage')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Track per-request consumption to power usage analytics. Keeping this on increases database writes.' 'Track per-request consumption to power usage analytics. Keeping this on increases database writes.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
...@@ -189,11 +195,11 @@ export function LogSettingsSection({ ...@@ -189,11 +195,11 @@ export function LogSettingsSection({
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </SettingsSwitchItem>
)} )}
/> />
<div className='space-y-4 rounded-lg border p-4'> <SettingsControlGroup className='space-y-3'>
<div> <div>
<h4 className='text-sm font-medium'>{t('Clean history logs')}</h4> <h4 className='text-sm font-medium'>{t('Clean history logs')}</h4>
<p className='text-muted-foreground text-sm'> <p className='text-muted-foreground text-sm'>
...@@ -223,12 +229,8 @@ export function LogSettingsSection({ ...@@ -223,12 +229,8 @@ export function LogSettingsSection({
{isCleaning ? t('Cleaning...') : t('Clean logs')} {isCleaning ? t('Cleaning...') : t('Clean logs')}
</Button> </Button>
</div> </div>
</div> </SettingsControlGroup>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save log settings')}
</Button>
</form>
</Form> </Form>
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<AlertDialogContent> <AlertDialogContent>
......
...@@ -21,7 +21,6 @@ import * as z from 'zod' ...@@ -21,7 +21,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -31,6 +30,8 @@ import { ...@@ -31,6 +30,8 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Textarea } from '@/components/ui/textarea' 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 { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -70,14 +71,14 @@ export function NoticeSection({ defaultValue }: NoticeSectionProps) { ...@@ -70,14 +71,14 @@ export function NoticeSection({ defaultValue }: NoticeSectionProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('System Notice')}>
title={t('System Notice')}
description={t(
'Broadcast a global banner to users. Markdown is supported.'
)}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
saveLabel='Save notice'
/>
<FormField <FormField
control={form.control} control={form.control}
name='Notice' name='Notice'
...@@ -97,11 +98,7 @@ export function NoticeSection({ defaultValue }: NoticeSectionProps) { ...@@ -97,11 +98,7 @@ export function NoticeSection({ defaultValue }: NoticeSectionProps) {
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save notice')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -59,6 +59,12 @@ import { ...@@ -59,6 +59,12 @@ import {
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -294,14 +300,13 @@ export function PerformanceSection(props: Props) { ...@@ -294,14 +300,13 @@ export function PerformanceSection(props: Props) {
: 0 : 0
return ( return (
<SettingsSection <SettingsSection title={t('Performance Settings')}>
title={t('Performance Settings')}
description={t(
'Disk cache, system performance monitoring, and operation statistics'
)}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
{/* Disk Cache Settings */} {/* Disk Cache Settings */}
<div> <div>
<h4 className='font-medium'>{t('Disk Cache Settings')}</h4> <h4 className='font-medium'>{t('Disk Cache Settings')}</h4>
...@@ -317,15 +322,17 @@ export function PerformanceSection(props: Props) { ...@@ -317,15 +322,17 @@ export function PerformanceSection(props: Props) {
control={form.control} control={form.control}
name='performance_setting.disk_cache_enabled' name='performance_setting.disk_cache_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center gap-2'> <SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable Disk Cache')}</FormLabel>
</SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel>{t('Enable Disk Cache')}</FormLabel> </SettingsSwitchItem>
</FormItem>
)} )}
/> />
<FormField <FormField
...@@ -415,15 +422,17 @@ export function PerformanceSection(props: Props) { ...@@ -415,15 +422,17 @@ export function PerformanceSection(props: Props) {
control={form.control} control={form.control}
name='performance_setting.monitor_enabled' name='performance_setting.monitor_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center gap-2'> <SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable Performance Monitoring')}</FormLabel>
</SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel>{t('Enable Performance Monitoring')}</FormLabel> </SettingsSwitchItem>
</FormItem>
)} )}
/> />
<FormField <FormField
...@@ -492,15 +501,19 @@ export function PerformanceSection(props: Props) { ...@@ -492,15 +501,19 @@ export function PerformanceSection(props: Props) {
control={form.control} control={form.control}
name='perf_metrics_setting.enabled' name='perf_metrics_setting.enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center gap-2'> <SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>
{t('Enable model performance metrics')}
</FormLabel>
</SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel>{t('Enable model performance metrics')}</FormLabel> </SettingsSwitchItem>
</FormItem>
)} )}
/> />
<FormField <FormField
...@@ -573,11 +586,7 @@ export function PerformanceSection(props: Props) { ...@@ -573,11 +586,7 @@ export function PerformanceSection(props: Props) {
)} )}
/> />
</div> </div>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
<Separator /> <Separator />
......
...@@ -19,16 +19,22 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,16 +19,22 @@ For commercial licensing, please contact support@quantumnous.com
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem,
FormLabel, FormLabel,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import {
SettingsControlChildren,
SettingsForm,
SettingsSwitchContent,
SettingsControlGroup,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
import { import {
...@@ -175,14 +181,16 @@ export function SidebarModulesSection({ ...@@ -175,14 +181,16 @@ export function SidebarModulesSection({
const sections = Object.entries(config) const sections = Object.entries(config)
return ( return (
<SettingsSection <SettingsSection title={t('Sidebar modules')}>
title={t('Sidebar modules')}
description={t(
'Control which sidebar areas and modules are available to all users.'
)}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
onReset={resetToDefault}
isSaving={updateOption.isPending}
resetLabel='Reset to default'
saveLabel='Save sidebar modules'
/>
{sections.map(([sectionKey, sectionConfig]) => { {sections.map(([sectionKey, sectionConfig]) => {
const sectionInfo = sectionMeta[sectionKey] ?? { const sectionInfo = sectionMeta[sectionKey] ?? {
title: toTitleCase(sectionKey), title: toTitleCase(sectionKey),
...@@ -193,32 +201,30 @@ export function SidebarModulesSection({ ...@@ -193,32 +201,30 @@ export function SidebarModulesSection({
) )
return ( return (
<div key={sectionKey} className='rounded-lg border p-4'> <SettingsControlGroup key={sectionKey}>
<FormField <FormField
control={form.control} control={form.control}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
name={`${sectionKey}.enabled` as any} name={`${sectionKey}.enabled` as any}
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-start justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5 pe-4'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{sectionInfo.title}</FormLabel>
{sectionInfo.title}
</FormLabel>
<FormDescription> <FormDescription>
{sectionInfo.description} {sectionInfo.description}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={Boolean(field.value)} checked={Boolean(field.value)}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
<div className='mt-4 grid gap-4 md:grid-cols-2'> <SettingsControlChildren className='grid gap-3 md:grid-cols-2'>
{modules.map(([moduleKey]) => { {modules.map(([moduleKey]) => {
const moduleInfo = moduleMeta[sectionKey]?.[moduleKey] ?? { const moduleInfo = moduleMeta[sectionKey]?.[moduleKey] ?? {
title: toTitleCase(moduleKey), title: toTitleCase(moduleKey),
...@@ -231,15 +237,13 @@ export function SidebarModulesSection({ ...@@ -231,15 +237,13 @@ export function SidebarModulesSection({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
name={`${sectionKey}.${moduleKey}` as any} name={`${sectionKey}.${moduleKey}` as any}
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-start justify-between rounded-lg border p-4'> <SettingsSwitchItem className='border-b-0 py-2'>
<div className='space-y-0.5 pe-4'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{moduleInfo.title}</FormLabel>
{moduleInfo.title}
</FormLabel>
<FormDescription> <FormDescription>
{moduleInfo.description} {moduleInfo.description}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={Boolean(field.value)} checked={Boolean(field.value)}
...@@ -250,27 +254,16 @@ export function SidebarModulesSection({ ...@@ -250,27 +254,16 @@ export function SidebarModulesSection({
} }
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
) )
})} })}
</div> </SettingsControlChildren>
</div> </SettingsControlGroup>
) )
})} })}
</SettingsForm>
<div className='flex flex-wrap gap-3'>
<Button type='button' variant='outline' onClick={resetToDefault}>
{t('Reset to default')}
</Button>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending
? t('Saving...')
: t('Save sidebar modules')}
</Button>
</div>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -110,10 +110,7 @@ export function UpdateCheckerSection({ ...@@ -110,10 +110,7 @@ export function UpdateCheckerSection({
return ( return (
<> <>
<SettingsSection <SettingsSection title={t('System maintenance')}>
title={t('System maintenance')}
description={t('Review current version and fetch release notes.')}
>
<div className='space-y-6'> <div className='space-y-6'>
<div className='grid gap-4 md:grid-cols-2'> <div className='grid gap-4 md:grid-cols-2'>
<div className='rounded-lg border p-4'> <div className='rounded-lg border p-4'>
......
...@@ -22,7 +22,6 @@ import { useForm } from 'react-hook-form' ...@@ -22,7 +22,6 @@ import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -35,6 +34,13 @@ import { ...@@ -35,6 +34,13 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsControlGroup,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
import { import {
...@@ -173,15 +179,14 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) { ...@@ -173,15 +179,14 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('Claude')}>
title={t('Claude')}
description={t(
'Override Anthropic headers, defaults, and thinking adapter behavior'
)}
>
<Form {...form}> <Form {...form}>
{/* eslint-disable-next-line react-hooks/refs */} {/* eslint-disable-next-line react-hooks/refs */}
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='claude.model_headers_settings' name='claude.model_headers_settings'
...@@ -219,29 +224,27 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) { ...@@ -219,29 +224,27 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) {
)} )}
/> />
<div className='space-y-4 rounded-lg border p-4'> <SettingsControlGroup>
<FormField <FormField
control={form.control} control={form.control}
name='claude.thinking_adapter_enabled' name='claude.thinking_adapter_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Thinking Adapter')}</FormLabel>
{t('Thinking Adapter')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Translate `-thinking` suffixes into Anthropic native thinking models while keeping pricing predictable.' 'Translate `-thinking` suffixes into Anthropic native thinking models while keeping pricing predictable.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -267,12 +270,8 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) { ...@@ -267,12 +270,8 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) {
</FormItem> </FormItem>
)} )}
/> />
</div> </SettingsControlGroup>
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -22,7 +22,6 @@ import { useForm } from 'react-hook-form' ...@@ -22,7 +22,6 @@ import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -35,6 +34,13 @@ import { ...@@ -35,6 +34,13 @@ import {
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsControlGroup,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
import { import {
...@@ -226,14 +232,13 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) { ...@@ -226,14 +232,13 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
) )
return ( return (
<SettingsSection <SettingsSection title={t('Gemini')}>
title={t('Gemini')}
description={t(
'Configure Gemini safety behavior, version overrides, and thinking adapter'
)}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='gemini.safety_settings' name='gemini.safety_settings'
...@@ -295,16 +300,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) { ...@@ -295,16 +300,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
)} )}
/> />
<div className='space-y-4 rounded-lg border p-4'> <SettingsControlGroup>
<FormField <FormField
control={form.control} control={form.control}
name='gemini.thinking_adapter_enabled' name='gemini.thinking_adapter_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Thinking Adapter')}</FormLabel>
{t('Thinking Adapter')}
</FormLabel>
<FormDescription> <FormDescription>
{t('Supports `-thinking`, `-thinking-')} {t('Supports `-thinking`, `-thinking-')}
{'{{budget}}'} {'{{budget}}'}
...@@ -312,14 +315,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) { ...@@ -312,14 +315,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
'`, and `-nothinking` suffixes while routing to the correct Gemini variant.' '`, and `-nothinking` suffixes while routing to the correct Gemini variant.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -353,15 +356,15 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) { ...@@ -353,15 +356,15 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
)} )}
</p> </p>
)} )}
</div> </SettingsControlGroup>
<FormField <FormField
control={form.control} control={form.control}
name='gemini.function_call_thought_signature_enabled' name='gemini.function_call_thought_signature_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>
{t('Enable FunctionCall thoughtSignature Fill')} {t('Enable FunctionCall thoughtSignature Fill')}
</FormLabel> </FormLabel>
<FormDescription> <FormDescription>
...@@ -369,14 +372,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) { ...@@ -369,14 +372,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
'Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format' 'Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -384,31 +387,25 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) { ...@@ -384,31 +387,25 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
control={form.control} control={form.control}
name='gemini.remove_function_response_id_enabled' name='gemini.remove_function_response_id_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Remove functionResponse.id field')}</FormLabel>
{t('Remove functionResponse.id field')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.' 'Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -38,6 +38,12 @@ import { Separator } from '@/components/ui/separator' ...@@ -38,6 +38,12 @@ import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -186,36 +192,33 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) { ...@@ -186,36 +192,33 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) {
} }
return ( return (
<SettingsSection <SettingsSection title={t('Global Model Configuration')}>
title={t('Global Model Configuration')}
description={t(
'Control passthrough behavior and connection keep-alive settings'
)}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='global.pass_through_request_enabled' name='global.pass_through_request_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Enable Request Passthrough')}</FormLabel>
{t('Enable Request Passthrough')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Forward requests directly to upstream providers without any post-processing.' 'Forward requests directly to upstream providers without any post-processing.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -349,24 +352,22 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) { ...@@ -349,24 +352,22 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) {
control={form.control} control={form.control}
name='general_setting.ping_interval_enabled' name='general_setting.ping_interval_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Keep-alive Ping')}</FormLabel>
{t('Keep-alive Ping')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Periodically send ping frames to keep streaming connections active.' 'Periodically send ping frames to keep streaming connections active.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
...@@ -402,11 +403,7 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) { ...@@ -402,11 +403,7 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) {
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -20,7 +20,6 @@ import * as z from 'zod' ...@@ -20,7 +20,6 @@ import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { import {
Form, Form,
FormControl, FormControl,
...@@ -31,6 +30,12 @@ import { ...@@ -31,6 +30,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' 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 { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -79,24 +84,19 @@ export function GrokSettingsCard(props: Props) { ...@@ -79,24 +84,19 @@ export function GrokSettingsCard(props: Props) {
const enabled = form.watch('grok.violation_deduction_enabled') const enabled = form.watch('grok.violation_deduction_enabled')
return ( return (
<SettingsSection <SettingsSection title={t('Grok Settings')}>
title={t('Grok Settings')}
description={t('Configure xAI Grok model specific settings')}
>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField <FormField
control={form.control} control={form.control}
name='grok.violation_deduction_enabled' name='grok.violation_deduction_enabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex items-center gap-2'> <SettingsSwitchItem>
<FormControl> <SettingsSwitchContent>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div>
<FormLabel>{t('Enable violation deduction')}</FormLabel> <FormLabel>{t('Enable violation deduction')}</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
...@@ -111,8 +111,14 @@ export function GrokSettingsCard(props: Props) { ...@@ -111,8 +111,14 @@ export function GrokSettingsCard(props: Props) {
{t('Official documentation')} {t('Official documentation')}
</a> </a>
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
</FormItem> <FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)} )}
/> />
...@@ -139,11 +145,7 @@ export function GrokSettingsCard(props: Props) { ...@@ -139,11 +145,7 @@ export function GrokSettingsCard(props: Props) {
</FormItem> </FormItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
</Button>
</form>
</Form> </Form>
</SettingsSection> </SettingsSection>
) )
......
...@@ -45,6 +45,12 @@ import { ...@@ -45,6 +45,12 @@ import {
} from '@/components/ui/sheet' } from '@/components/ui/sheet'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageActionsPortal } from '../components/settings-page-context'
import { GroupRatioVisualEditor } from './group-ratio-visual-editor' import { GroupRatioVisualEditor } from './group-ratio-visual-editor'
import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor' import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor'
...@@ -112,6 +118,16 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -112,6 +118,16 @@ export const GroupRatioForm = memo(function GroupRatioForm({
<GroupPricingGuide open={guideOpen} onOpenChange={setGuideOpen} /> <GroupPricingGuide open={guideOpen} onOpenChange={setGuideOpen} />
<Form {...form}> <Form {...form}>
<SettingsPageActionsPortal>
<Button
type='button'
size='sm'
onClick={form.handleSubmit(onSave)}
disabled={isSaving}
>
{isSaving ? t('Saving...') : t('Save group ratios')}
</Button>
</SettingsPageActionsPortal>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
<div className='space-y-6'> <div className='space-y-6'>
<GroupRatioVisualEditor <GroupRatioVisualEditor
...@@ -136,33 +152,27 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -136,33 +152,27 @@ export const GroupRatioForm = memo(function GroupRatioForm({
control={form.control} control={form.control}
name='DefaultUseAutoGroup' name='DefaultUseAutoGroup'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Default to auto groups')}</FormLabel>
{t('Default to auto groups')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'When enabled, newly created tokens start in the first auto group.' 'When enabled, newly created tokens start in the first auto group.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
<Button onClick={form.handleSubmit(onSave)} disabled={isSaving}>
{isSaving ? t('Saving...') : t('Save group ratios')}
</Button>
</div> </div>
) : ( ) : (
<form onSubmit={form.handleSubmit(onSave)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSave)}>
<FormField <FormField
control={form.control} control={form.control}
name='GroupRatio' name='GroupRatio'
...@@ -284,31 +294,25 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -284,31 +294,25 @@ export const GroupRatioForm = memo(function GroupRatioForm({
control={form.control} control={form.control}
name='DefaultUseAutoGroup' name='DefaultUseAutoGroup'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Default to auto groups')}</FormLabel>
{t('Default to auto groups')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'When enabled, newly created tokens start in the first auto group.' 'When enabled, newly created tokens start in the first auto group.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</SettingsForm>
<Button type='submit' disabled={isSaving}>
{isSaving ? t('Saving...') : t('Save group ratios')}
</Button>
</form>
)} )}
</Form> </Form>
</div> </div>
......
...@@ -21,6 +21,7 @@ import type { ModelSettings } from '../types' ...@@ -21,6 +21,7 @@ import type { ModelSettings } from '../types'
import { import {
MODELS_DEFAULT_SECTION, MODELS_DEFAULT_SECTION,
getModelsSectionContent, getModelsSectionContent,
getModelsSectionMeta,
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultModelSettings: ModelSettings = { const defaultModelSettings: ModelSettings = {
...@@ -77,6 +78,7 @@ export function ModelSettings() { ...@@ -77,6 +78,7 @@ export function ModelSettings() {
defaultSettings={defaultModelSettings} defaultSettings={defaultModelSettings}
defaultSection={MODELS_DEFAULT_SECTION} defaultSection={MODELS_DEFAULT_SECTION}
getSectionContent={getModelsSectionContent} getSectionContent={getModelsSectionContent}
getSectionMeta={getModelsSectionMeta}
/> />
) )
} }
...@@ -33,11 +33,9 @@ import { ...@@ -33,11 +33,9 @@ import {
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import { import {
Field, Field,
FieldContent,
FieldDescription, FieldDescription,
FieldGroup, FieldGroup,
FieldLabel, FieldLabel,
FieldTitle,
} from '@/components/ui/field' } from '@/components/ui/field'
import { import {
Form, Form,
...@@ -62,9 +60,12 @@ import { ...@@ -62,9 +60,12 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from '@/components/ui/sheet' } from '@/components/ui/sheet'
import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { combineBillingExpr } from '@/features/pricing/lib/billing-expr' import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
import {
SettingsControlGroup,
SettingsSwitchField,
} from '../components/settings-form-layout'
import { formatPricingNumber } from './pricing-format' import { formatPricingNumber } from './pricing-format'
import { TieredPricingEditor } from './tiered-pricing-editor' import { TieredPricingEditor } from './tiered-pricing-editor'
...@@ -1005,36 +1006,29 @@ function PriceLane(props: { ...@@ -1005,36 +1006,29 @@ function PriceLane(props: {
const effectiveDisabled = props.disabled || !props.enabled const effectiveDisabled = props.disabled || !props.enabled
return ( return (
<Field <SettingsControlGroup
className={cn( className={cn('space-y-3', effectiveDisabled && 'opacity-75')}
'rounded-lg border p-3',
effectiveDisabled && 'bg-muted/35'
)}
data-disabled={effectiveDisabled || undefined} data-disabled={effectiveDisabled || undefined}
> >
<div className='flex items-start justify-between gap-3'> <SettingsSwitchField
<FieldContent> checked={props.enabled}
<FieldTitle>{props.title}</FieldTitle> disabled={props.disabled}
<FieldDescription>{props.description}</FieldDescription> onCheckedChange={props.onEnabledChange}
</FieldContent> label={props.title}
<Switch description={props.description}
checked={props.enabled} aria-label={props.title}
disabled={props.disabled} />
onCheckedChange={props.onEnabledChange}
aria-label={props.title}
/>
</div>
<PriceInput <PriceInput
value={props.value} value={props.value}
placeholder={props.placeholder} placeholder={props.placeholder}
disabled={effectiveDisabled} disabled={effectiveDisabled}
onChange={props.onChange} onChange={props.onChange}
/> />
<FieldDescription> <p className='text-muted-foreground text-xs'>
{props.enabled {props.enabled
? t('USD price per 1M tokens.') ? t('USD price per 1M tokens.')
: t('Disabled lanes are omitted on save.')} : t('Disabled lanes are omitted on save.')}
</FieldDescription> </p>
</Field> </SettingsControlGroup>
) )
} }
...@@ -32,6 +32,12 @@ import { ...@@ -32,6 +32,12 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
} from '../components/settings-form-layout'
import { SettingsPageActionsPortal } from '../components/settings-page-context'
import { ModelRatioVisualEditor } from './model-ratio-visual-editor' import { ModelRatioVisualEditor } from './model-ratio-visual-editor'
type ModelFormValues = { type ModelFormValues = {
...@@ -99,6 +105,25 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -99,6 +105,25 @@ export const ModelRatioForm = memo(function ModelRatioForm({
</div> </div>
<Form {...form}> <Form {...form}>
<SettingsPageActionsPortal>
<Button
type='button'
variant='destructive'
size='sm'
onClick={onReset}
disabled={isResetting}
>
{t('Reset prices')}
</Button>
<Button
type='button'
size='sm'
onClick={form.handleSubmit(onSave)}
disabled={isSaving}
>
{isSaving ? t('Saving...') : t('Save model prices')}
</Button>
</SettingsPageActionsPortal>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
<div className='space-y-6'> <div className='space-y-6'>
<ModelRatioVisualEditor <ModelRatioVisualEditor
...@@ -127,43 +152,27 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -127,43 +152,27 @@ export const ModelRatioForm = memo(function ModelRatioForm({
control={form.control} control={form.control}
name='ExposeRatioEnabled' name='ExposeRatioEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Expose ratio API')}</FormLabel>
{t('Expose ratio API')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Allow clients to query configured ratios via `/api/ratio`.' 'Allow clients to query configured ratios via `/api/ratio`.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
<div className='flex flex-wrap gap-4'>
<Button onClick={form.handleSubmit(onSave)} disabled={isSaving}>
{isSaving ? t('Saving...') : t('Save model prices')}
</Button>
<Button
type='button'
variant='destructive'
onClick={onReset}
disabled={isResetting}
>
{t('Reset prices')}
</Button>
</div>
</div> </div>
) : ( ) : (
<form onSubmit={form.handleSubmit(onSave)} className='space-y-6'> <SettingsForm onSubmit={form.handleSubmit(onSave)}>
<FormField <FormField
control={form.control} control={form.control}
name='ModelPrice' name='ModelPrice'
...@@ -318,41 +327,25 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -318,41 +327,25 @@ export const ModelRatioForm = memo(function ModelRatioForm({
control={form.control} control={form.control}
name='ExposeRatioEnabled' name='ExposeRatioEnabled'
render={({ field }) => ( render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'> <SettingsSwitchItem>
<div className='space-y-0.5'> <SettingsSwitchContent>
<FormLabel className='text-base'> <FormLabel>{t('Expose ratio API')}</FormLabel>
{t('Expose ratio API')}
</FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Allow clients to query configured ratios via `/api/ratio`.' 'Allow clients to query configured ratios via `/api/ratio`.'
)} )}
</FormDescription> </FormDescription>
</div> </SettingsSwitchContent>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </SettingsSwitchItem>
)} )}
/> />
</SettingsForm>
<div className='flex flex-wrap gap-4'>
<Button type='submit' disabled={isSaving}>
{isSaving ? t('Saving...') : t('Save model prices')}
</Button>
<Button
type='button'
variant='destructive'
onClick={onReset}
disabled={isResetting}
>
{t('Reset prices')}
</Button>
</div>
</form>
)} )}
</Form> </Form>
</div> </div>
......
...@@ -204,7 +204,6 @@ type RatioSettingsCardProps = { ...@@ -204,7 +204,6 @@ type RatioSettingsCardProps = {
groupDefaults: GroupFormValues groupDefaults: GroupFormValues
toolPricesDefault: string toolPricesDefault: string
titleKey?: string titleKey?: string
descriptionKey?: string
visibleTabs?: RatioTabId[] visibleTabs?: RatioTabId[]
} }
...@@ -213,7 +212,6 @@ export function RatioSettingsCard({ ...@@ -213,7 +212,6 @@ export function RatioSettingsCard({
groupDefaults, groupDefaults,
toolPricesDefault, toolPricesDefault,
titleKey = 'Pricing Ratios', titleKey = 'Pricing Ratios',
descriptionKey = 'Configure model, caching, and group ratios used for billing',
visibleTabs = ['models', 'groups', 'tool-prices', 'upstream-sync'], visibleTabs = ['models', 'groups', 'tool-prices', 'upstream-sync'],
}: RatioSettingsCardProps) { }: RatioSettingsCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -497,7 +495,7 @@ export function RatioSettingsCard({ ...@@ -497,7 +495,7 @@ export function RatioSettingsCard({
} }
return ( return (
<SettingsSection title={t(titleKey)} description={t(descriptionKey)}> <SettingsSection title={t(titleKey)}>
{visibleTabs.length === 1 ? ( {visibleTabs.length === 1 ? (
renderTabContent(defaultTab) renderTabContent(defaultTab)
) : ( ) : (
......
...@@ -39,7 +39,6 @@ const MODELS_SECTIONS = [ ...@@ -39,7 +39,6 @@ const MODELS_SECTIONS = [
{ {
id: 'global', id: 'global',
titleKey: 'Global Model Configuration', titleKey: 'Global Model Configuration',
descriptionKey: 'Configure global model settings',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<GlobalSettingsCard <GlobalSettingsCard
defaultValues={{ defaultValues={{
...@@ -68,7 +67,6 @@ const MODELS_SECTIONS = [ ...@@ -68,7 +67,6 @@ const MODELS_SECTIONS = [
{ {
id: 'gemini', id: 'gemini',
titleKey: 'Gemini', titleKey: 'Gemini',
descriptionKey: 'Configure Gemini model settings',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<GeminiSettingsCard <GeminiSettingsCard
defaultValues={{ defaultValues={{
...@@ -93,7 +91,6 @@ const MODELS_SECTIONS = [ ...@@ -93,7 +91,6 @@ const MODELS_SECTIONS = [
{ {
id: 'claude', id: 'claude',
titleKey: 'Claude', titleKey: 'Claude',
descriptionKey: 'Configure Claude model settings',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<ClaudeSettingsCard <ClaudeSettingsCard
defaultValues={{ defaultValues={{
...@@ -112,7 +109,6 @@ const MODELS_SECTIONS = [ ...@@ -112,7 +109,6 @@ const MODELS_SECTIONS = [
{ {
id: 'grok', id: 'grok',
titleKey: 'Grok', titleKey: 'Grok',
descriptionKey: 'Configure xAI Grok model settings',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<GrokSettingsCard <GrokSettingsCard
defaultValues={{ defaultValues={{
...@@ -127,7 +123,6 @@ const MODELS_SECTIONS = [ ...@@ -127,7 +123,6 @@ const MODELS_SECTIONS = [
{ {
id: 'channel-affinity', id: 'channel-affinity',
titleKey: 'Channel Affinity', titleKey: 'Channel Affinity',
descriptionKey: 'Configure channel affinity (sticky routing) rules',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<ChannelAffinitySection <ChannelAffinitySection
defaultValues={{ defaultValues={{
...@@ -148,7 +143,6 @@ const MODELS_SECTIONS = [ ...@@ -148,7 +143,6 @@ const MODELS_SECTIONS = [
{ {
id: 'model-deployment', id: 'model-deployment',
titleKey: 'Model Deployment', titleKey: 'Model Deployment',
descriptionKey: 'Configure model deployment provider settings',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
<IoNetDeploymentSettingsSection <IoNetDeploymentSettingsSection
defaultValues={{ defaultValues={{
...@@ -173,3 +167,4 @@ export const MODELS_SECTION_IDS = modelsRegistry.sectionIds ...@@ -173,3 +167,4 @@ export const MODELS_SECTION_IDS = modelsRegistry.sectionIds
export const MODELS_DEFAULT_SECTION = modelsRegistry.defaultSection export const MODELS_DEFAULT_SECTION = modelsRegistry.defaultSection
export const getModelsSectionNavItems = modelsRegistry.getSectionNavItems export const getModelsSectionNavItems = modelsRegistry.getSectionNavItems
export const getModelsSectionContent = modelsRegistry.getSectionContent export const getModelsSectionContent = modelsRegistry.getSectionContent
export const getModelsSectionMeta = modelsRegistry.getSectionMeta
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment