| import { RotateCcw, Sparkles } from "lucide-react"; |
| import { type ReactNode, useRef, useState } from "react"; |
| import { Controller } from "react-hook-form"; |
| import { useTranslation } from "react-i18next"; |
|
|
| import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; |
| import { Button } from "@/components/ui/button"; |
| import { Badge } from "@/components/ui/badge"; |
| import { Input } from "@/components/ui/input"; |
| import { Label } from "@/components/ui/label"; |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; |
| import { Spinner } from "@/components/ui/spinner"; |
| import { Switch } from "@/components/ui/switch"; |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
| import { Textarea } from "@/components/ui/textarea"; |
| import { EgressNodes } from "@/features/settings/egress-nodes"; |
| import { VersionUpdateSection } from "@/features/system/version-update"; |
| import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; |
| import { isByteSizeUnit, isDurationUnit, MAX_ROUTING_ATTEMPTS, type ByteSizeValue, type DurationValue, UNLIMITED_ROUTING_ATTEMPTS } from "@/features/settings/settings-model"; |
| import { useSettings } from "@/features/settings/use-settings"; |
| import { ErrorState } from "@/shared/components/data-state"; |
| import { cn } from "@/shared/lib/cn"; |
|
|
| export function SettingsPage() { |
| const { t } = useTranslation(); |
| const { form, settingsQuery, updateMutation, reset } = useSettings(); |
| const [autoCleanConfirm, setAutoCleanConfirm] = useState<"enabled" | "includeDisabled" | null>(null); |
| const [unlimitedAttemptsConfirm, setUnlimitedAttemptsConfirm] = useState(false); |
| const limitedRoutingAttemptsRef = useRef(3); |
| const autoCleanEnabled = form.watch("accounts.autoCleanReauthEnabled") === true; |
| const buildForbiddenReauthEnabled = form.watch("accounts.markBuildForbiddenReauth") === true; |
| const segmentedSelectorEnabled = form.watch("routing.segmentedSelector.enabled") === true; |
|
|
| if (settingsQuery.isError) { |
| return <ErrorState message={settingsQuery.error.message} onRetry={() => void settingsQuery.refetch()} />; |
| } |
|
|
| const snapshot = settingsQuery.data; |
| const loading = settingsQuery.isPending; |
| const statsigMode = form.watch("providerWeb.statsigMode"); |
| const draftClearanceMode = form.watch("providerWeb.clearanceMode"); |
| const activeClearanceMode = snapshot?.config.providerWeb.clearanceMode ?? draftClearanceMode; |
| const statsigManualConfigured = form.watch("providerWeb.statsigManualConfigured"); |
| const buildClientVersion = form.watch("providerBuild.clientVersion"); |
| const buildUserAgent = form.watch("providerBuild.userAgent"); |
| const recommendedBuild = snapshot?.recommendedProviderBuild; |
| const recommendedBuildApplied = recommendedBuild != null |
| && buildClientVersion === recommendedBuild.clientVersion |
| && buildUserAgent === recommendedBuild.userAgent; |
| const syncRecommendedBuild = () => { |
| if (!recommendedBuild) return; |
| form.setValue("providerBuild.clientVersion", recommendedBuild.clientVersion, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); |
| form.setValue("providerBuild.userAgent", recommendedBuild.userAgent, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); |
| }; |
|
|
| return ( |
| <form className="w-full space-y-5" onSubmit={form.handleSubmit((values) => updateMutation.mutate(values))}> |
| <header className="relative sticky top-12 z-30 -mx-2 flex min-h-12 items-center justify-between gap-3 bg-background px-2 py-2 lg:top-20 lg:z-40 lg:before:pointer-events-none lg:before:absolute lg:before:inset-x-0 lg:before:-top-[100vh] lg:before:h-[100vh] lg:before:bg-background lg:before:content-['']"> |
| <div className="min-w-0"> |
| <h1 className="text-xl font-medium">{t("settings.title")}</h1> |
| <p className="sr-only">{t("settings.description")}</p> |
| </div> |
| <div className="flex shrink-0 flex-wrap items-center gap-2"> |
| <Tooltip> |
| <TooltipTrigger asChild> |
| <Button type="button" variant="ghost" size="icon" className="size-8" aria-label={t("common.reset")} disabled={loading || updateMutation.isPending || !form.formState.isDirty} onClick={reset}> |
| <RotateCcw /> |
| </Button> |
| </TooltipTrigger> |
| <TooltipContent>{t("common.reset")}</TooltipContent> |
| </Tooltip> |
| <Button type="submit" size="sm" disabled={loading || updateMutation.isPending || !form.formState.isDirty}> |
| {updateMutation.isPending ? <Spinner /> : null}{t("common.save")} |
| </Button> |
| </div> |
| </header> |
| |
| {loading ? <div className="flex min-h-64 items-center justify-center"><Spinner /></div> : null} |
| {snapshot ? ( |
| <Tabs defaultValue="build" className="flex min-w-0 flex-col gap-7 lg:flex-row lg:items-start"> |
| <TabsList className="flex h-auto w-full max-w-full shrink-0 justify-start gap-1 overflow-x-auto overscroll-x-contain rounded-none bg-transparent p-0 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden [&>span]:rounded-md [&>span]:bg-muted/70 [&>span]:shadow-none lg:sticky lg:top-[148px] lg:w-56 lg:flex-col lg:items-stretch lg:overflow-visible"> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="build">{t("models.providerGrokBuild")}</TabsTrigger> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="web">{t("settings.web.title")}</TabsTrigger> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="console">{t("console.name")}</TabsTrigger> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="delivery">{t("settings.groups.delivery")}</TabsTrigger> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="policies">{t("settings.groups.policies")}</TabsTrigger> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="accounts">{t("settings.accounts.title")}</TabsTrigger> |
| <TabsTrigger className="h-9 w-auto shrink-0 justify-start rounded-md px-3 text-xs data-[state=active]:font-medium lg:w-full" value="about">{t("updates.title")}</TabsTrigger> |
| </TabsList> |
| |
| <div className="min-w-0 flex-1"> |
| <SettingsPane value="build"> |
| <SettingsSection |
| title={t("models.providerGrokBuild")} |
| action={recommendedBuild && !recommendedBuildApplied ? ( |
| <Tooltip> |
| <TooltipTrigger asChild> |
| <Button type="button" variant="secondary" size="sm" disabled={loading || updateMutation.isPending} onClick={syncRecommendedBuild}> |
| <Sparkles />{t("settings.provider.syncRecommendedVersion")} |
| </Button> |
| </TooltipTrigger> |
| <TooltipContent>{t("settings.provider.syncRecommendedVersionDescription")}</TooltipContent> |
| </Tooltip> |
| ) : undefined} |
| > |
| <div className="space-y-0"> |
| <SettingsField controlId="provider-base-url" className="sm:col-span-2" label={t("settings.provider.baseURL")} description={t("settings.provider.baseURLHelp")} error={form.formState.errors.providerBuild?.baseURL?.message}><Input id="provider-base-url" {...form.register("providerBuild.baseURL")} /></SettingsField> |
| <SettingsField controlId="provider-fallback-base-url" className="sm:col-span-2" label={t("settings.provider.fallbackBaseURL")} description={t("settings.provider.fallbackBaseURLHelp")} error={form.formState.errors.providerBuild?.fallbackBaseURL?.message}><Input id="provider-fallback-base-url" {...form.register("providerBuild.fallbackBaseURL")} /></SettingsField> |
| <SettingsField controlId="provider-client-version" label={t("settings.provider.clientVersion")} description={t("settings.provider.clientVersionHelp")} badge={recommendedBuild ? t("settings.provider.recommendedVersion", { version: recommendedBuild.clientVersion }) : undefined} error={form.formState.errors.providerBuild?.clientVersion?.message}><Input id="provider-client-version" {...form.register("providerBuild.clientVersion")} /></SettingsField> |
| <SettingsField controlId="provider-client-identifier" label={t("settings.provider.clientIdentifier")} description={t("settings.provider.clientIdentifierHelp")} error={form.formState.errors.providerBuild?.clientIdentifier?.message}><Input id="provider-client-identifier" {...form.register("providerBuild.clientIdentifier")} /></SettingsField> |
| <SettingsField controlId="provider-token-auth" label={t("settings.provider.tokenAuth")} description={t("settings.provider.tokenAuthHelp")} error={form.formState.errors.providerBuild?.tokenAuth?.message}><Input id="provider-token-auth" autoComplete="off" {...form.register("providerBuild.tokenAuth")} /></SettingsField> |
| <SettingsField controlId="provider-user-agent" label={t("settings.provider.userAgent")} description={t("settings.provider.userAgentHelp")} error={form.formState.errors.providerBuild?.userAgent?.message}><Input id="provider-user-agent" {...form.register("providerBuild.userAgent")} /></SettingsField> |
| <SettingsField controlId="provider-response-header-timeout" label={t("settingsBuildTransport.responseHeaderTimeout")} description={t("settingsBuildTransport.responseHeaderTimeoutHelp")} error={form.formState.errors.providerBuild?.responseHeaderTimeout?.message}><Controller control={form.control} name="providerBuild.responseHeaderTimeout" render={({ field }) => <DurationInput id="provider-response-header-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| </div> |
| </SettingsSection> |
| </SettingsPane> |
| |
| <SettingsPane value="web"> |
| <SettingsSection title={t("settings.web.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="web-base-url" className="sm:col-span-2" label={t("settings.web.baseURL")} description={t("settings.web.baseURLHelp")} error={form.formState.errors.providerWeb?.baseURL?.message}><Input id="web-base-url" {...form.register("providerWeb.baseURL")} /></SettingsField> |
| <SettingsField controlId="web-statsig-mode" className="sm:col-span-2" label={t("settings.web.statsigMode")} description={t("settings.web.statsigModeHelp")} error={form.formState.errors.providerWeb?.statsigMode?.message}> |
| <Controller control={form.control} name="providerWeb.statsigMode" render={({ field }) => ( |
| <Tabs value={field.value} onValueChange={field.onChange}> |
| <TabsList id="web-statsig-mode" className="grid w-full grid-cols-2 bg-muted/55"> |
| <TabsTrigger value="manual" className="font-normal">{t("settings.web.statsigManual")}</TabsTrigger> |
| <TabsTrigger value="url" className="font-normal">{t("settings.web.statsigURL")}</TabsTrigger> |
| </TabsList> |
| </Tabs> |
| )} /> |
| </SettingsField> |
| {statsigMode === "manual" ? ( |
| <SettingsField controlId="web-statsig-manual" className="sm:col-span-2" label={t("settings.web.statsigValue")} description={t("settings.web.statsigValueHelp")} badge={statsigManualConfigured ? t("settings.web.statsigConfigured") : undefined} error={form.formState.errors.providerWeb?.statsigManualValue?.message}> |
| <Input id="web-statsig-manual" type="password" autoComplete="off" placeholder={statsigManualConfigured ? t("settings.web.statsigKeepConfigured") : t("settings.web.statsigValuePlaceholder")} {...form.register("providerWeb.statsigManualValue")} /> |
| </SettingsField> |
| ) : ( |
| <SettingsField controlId="web-statsig-url" className="sm:col-span-2" label={t("settings.web.statsigSignerURL")} description={t("settings.web.statsigSignerURLHelp")} error={form.formState.errors.providerWeb?.statsigSignerURL?.message}> |
| <Input id="web-statsig-url" type="url" placeholder="http://grok-signer-go:8788/sign" {...form.register("providerWeb.statsigSignerURL")} /> |
| </SettingsField> |
| )} |
| <SettingsField controlId="web-quota-timeout" label={t("settings.web.quotaTimeout")} description={t("settings.web.quotaTimeoutHelp")} error={form.formState.errors.providerWeb?.quotaTimeout?.message}><Controller control={form.control} name="providerWeb.quotaTimeout" render={({ field }) => <DurationInput id="web-quota-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="web-chat-timeout" label={t("settings.web.chatTimeout")} description={t("settings.web.chatTimeoutHelp")} error={form.formState.errors.providerWeb?.chatTimeout?.message}><Controller control={form.control} name="providerWeb.chatTimeout" render={({ field }) => <DurationInput id="web-chat-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="web-image-timeout" label={t("settings.web.imageTimeout")} description={t("settings.web.imageTimeoutHelp")} error={form.formState.errors.providerWeb?.imageTimeout?.message}><Controller control={form.control} name="providerWeb.imageTimeout" render={({ field }) => <DurationInput id="web-image-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="web-video-timeout" label={t("settings.web.videoTimeout")} description={t("settings.web.videoTimeoutHelp")} error={form.formState.errors.providerWeb?.videoTimeout?.message}><Controller control={form.control} name="providerWeb.videoTimeout" render={({ field }) => <DurationInput id="web-video-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="web-media-concurrency" label={t("settings.web.mediaConcurrency")} description={t("settings.web.mediaConcurrencyHelp")} badge={t("settings.restartRequired")} error={form.formState.errors.providerWeb?.mediaConcurrency?.message}><Input id="web-media-concurrency" type="number" min={1} max={64} {...form.register("providerWeb.mediaConcurrency", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="web-recovery-base" label={t("settings.web.recoveryBackoffBase")} description={t("settings.web.recoveryBackoffBaseHelp")} error={form.formState.errors.providerWeb?.recoveryBackoffBase?.message}><Controller control={form.control} name="providerWeb.recoveryBackoffBase" render={({ field }) => <DurationInput id="web-recovery-base" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="web-recovery-max" label={t("settings.web.recoveryBackoffMax")} description={t("settings.web.recoveryBackoffMaxHelp")} error={form.formState.errors.providerWeb?.recoveryBackoffMax?.message}><Controller control={form.control} name="providerWeb.recoveryBackoffMax" render={({ field }) => <DurationInput id="web-recovery-max" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="web-nsfw" label={t("settings.web.allowNSFW")} description={t("settings.web.allowNSFWHelp")}><Controller control={form.control} name="providerWeb.allowNSFW" render={({ field }) => <div className="flex h-8 items-center"><Switch id="web-nsfw" checked={field.value} onCheckedChange={field.onChange} /></div>} /></SettingsField> |
| </div> |
| </SettingsSection> |
| </SettingsPane> |
| |
| <SettingsPane value="console"> |
| <SettingsSection title={t("console.name")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="console-base-url" className="sm:col-span-2" label={t("console.baseURL")} description={t("settings.console.baseURLHelp")} error={form.formState.errors.providerConsole?.baseURL?.message}><Input id="console-base-url" type="url" {...form.register("providerConsole.baseURL")} /></SettingsField> |
| <SettingsField controlId="console-chat-timeout" label={t("console.chatTimeout")} description={t("settings.console.chatTimeoutHelp")} error={form.formState.errors.providerConsole?.chatTimeout?.message}><Controller control={form.control} name="providerConsole.chatTimeout" render={({ field }) => <DurationInput id="console-chat-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| </div> |
| </SettingsSection> |
| </SettingsPane> |
| |
| <SettingsPane value="delivery"> |
| <SettingsSection title={t("settings.media.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="media-max-image-size" label={t("settings.media.maxImageSize")} description={t("settings.media.maxImageSizeHelp")} error={form.formState.errors.media?.maxImageSize?.message}> |
| <Controller control={form.control} name="media.maxImageSize" render={({ field }) => <ByteSizeInput id="media-max-image-size" value={field.value} onChange={field.onChange} />} /> |
| </SettingsField> |
| <SettingsField controlId="media-max-total-size" label={t("settings.media.maxTotalSize")} description={t("settings.media.maxTotalSizeHelp")} error={form.formState.errors.media?.maxTotalSize?.message}> |
| <Controller control={form.control} name="media.maxTotalSize" render={({ field }) => <ByteSizeInput id="media-max-total-size" value={field.value} onChange={field.onChange} />} /> |
| </SettingsField> |
| <SettingsField controlId="media-cleanup-threshold" label={t("settings.media.cleanupThresholdPercent")} description={t("settings.media.cleanupThresholdPercentHelp")} error={form.formState.errors.media?.cleanupThresholdPercent?.message}> |
| <div className="flex min-w-0"> |
| <Input id="media-cleanup-threshold" type="number" min={50} max={95} className="min-w-0 rounded-r-none" {...form.register("media.cleanupThresholdPercent", { valueAsNumber: true })} /> |
| <div className="flex h-8 w-24 shrink-0 items-center justify-start rounded-r-md bg-secondary/55 px-3 text-xs text-foreground">%</div> |
| </div> |
| </SettingsField> |
| <SettingsField controlId="media-cleanup-interval" label={t("settings.media.cleanupInterval")} description={t("settings.media.cleanupIntervalHelp")} error={form.formState.errors.media?.cleanupInterval?.message}> |
| <Controller control={form.control} name="media.cleanupInterval" render={({ field }) => <DurationInput id="media-cleanup-interval" value={field.value} onChange={field.onChange} />} /> |
| </SettingsField> |
| <SettingsField controlId="frontend-public-api-base-url" label={t("settings.media.publicApiBaseURL")} description={t("settings.media.publicApiBaseURLHelp")} error={form.formState.errors.frontend?.publicApiBaseURL?.message} className="sm:col-span-2"> |
| <Input id="frontend-public-api-base-url" placeholder="https://api.example.com" {...form.register("frontend.publicApiBaseURL")} /> |
| </SettingsField> |
| </div> |
| </SettingsSection> |
| |
| <SettingsSection title={t("settings.egress.clearance")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="egress-clearance-mode" className="sm:col-span-2" label={t("settings.web.clearanceMode")} description={t("settings.web.clearanceModeHelp")} error={form.formState.errors.providerWeb?.clearanceMode?.message}> |
| <Controller control={form.control} name="providerWeb.clearanceMode" render={({ field }) => ( |
| <Tabs value={field.value} onValueChange={field.onChange}> |
| <TabsList id="egress-clearance-mode" className="grid w-full grid-cols-2 bg-muted/55"> |
| <TabsTrigger value="manual" className="font-normal">{t("settings.web.clearanceManual")}</TabsTrigger> |
| <TabsTrigger value="flaresolverr" className="font-normal">{t("settings.web.clearanceFlareSolverr")}</TabsTrigger> |
| </TabsList> |
| </Tabs> |
| )} /> |
| </SettingsField> |
| {draftClearanceMode === "flaresolverr" ? <> |
| <SettingsField controlId="egress-flaresolverr-url" className="sm:col-span-2" label={t("settings.web.flareSolverrURL")} description={t("settings.web.flareSolverrURLHelp")} error={form.formState.errors.providerWeb?.flareSolverrURL?.message}><Input id="egress-flaresolverr-url" type="url" placeholder="http://flaresolverr:8191" {...form.register("providerWeb.flareSolverrURL")} /></SettingsField> |
| <SettingsField controlId="egress-clearance-timeout" label={t("settings.web.clearanceTimeout")} description={t("settings.web.clearanceTimeoutHelp")} error={form.formState.errors.providerWeb?.clearanceTimeout?.message}><Controller control={form.control} name="providerWeb.clearanceTimeout" render={({ field }) => <DurationInput id="egress-clearance-timeout" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="egress-clearance-refresh" label={t("settings.web.clearanceRefresh")} description={t("settings.web.clearanceRefreshHelp")} error={form.formState.errors.providerWeb?.clearanceRefresh?.message}><Controller control={form.control} name="providerWeb.clearanceRefresh" render={({ field }) => <DurationInput id="egress-clearance-refresh" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| </> : null} |
| </div> |
| </SettingsSection> |
| |
| <EgressNodes title={t("settings.egress.title")} clearanceMode={activeClearanceMode} /> |
| </SettingsPane> |
| |
| <SettingsPane value="policies"> |
| <SettingsSection title={t("settings.server.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="server-max-concurrent-requests" label={t("settings.server.maxConcurrentRequests")} description={t("settings.server.maxConcurrentRequestsHelp")} error={form.formState.errors.server?.maxConcurrentRequests?.message}> |
| <Input id="server-max-concurrent-requests" type="number" min={1} max={100_000} {...form.register("server.maxConcurrentRequests", { valueAsNumber: true })} /> |
| </SettingsField> |
| </div> |
| </SettingsSection> |
| |
| <SettingsSection title={t("settings.batch.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="batch-import-concurrency" label={t("settings.batch.importConcurrency")} description={t("settings.batch.importConcurrencyHelp")} error={form.formState.errors.batch?.importConcurrency?.message}><Input id="batch-import-concurrency" type="number" min={1} max={50} {...form.register("batch.importConcurrency", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="batch-conversion-concurrency" label={t("settings.batch.conversionConcurrency")} description={t("settings.batch.conversionConcurrencyHelp")} error={form.formState.errors.batch?.conversionConcurrency?.message}><Input id="batch-conversion-concurrency" type="number" min={1} max={50} {...form.register("batch.conversionConcurrency", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="batch-sync-concurrency" label={t("settings.batch.syncConcurrency")} description={t("settings.batch.syncConcurrencyHelp")} error={form.formState.errors.batch?.syncConcurrency?.message}><Input id="batch-sync-concurrency" type="number" min={1} max={50} {...form.register("batch.syncConcurrency", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="batch-refresh-concurrency" label={t("settings.batch.refreshConcurrency")} description={t("settings.batch.refreshConcurrencyHelp")} error={form.formState.errors.batch?.refreshConcurrency?.message}><Input id="batch-refresh-concurrency" type="number" min={1} max={50} {...form.register("batch.refreshConcurrency", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="batch-random-delay" label={t("settings.batch.randomDelay")} description={t("settings.batch.randomDelayHelp")} error={form.formState.errors.batch?.randomDelay?.message}><Input id="batch-random-delay" type="number" min={0} max={5_000} step={10} {...form.register("batch.randomDelay", { valueAsNumber: true })} /></SettingsField> |
| </div> |
| </SettingsSection> |
| |
| <SettingsSection title={t("settings.routing.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="routing-sticky-ttl" label={t("settings.routing.stickyTTL")} description={t("settings.routing.stickyTTLHelp")} error={form.formState.errors.routing?.stickyTTL?.message}><Controller control={form.control} name="routing.stickyTTL" render={({ field }) => <DurationInput id="routing-sticky-ttl" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="routing-cooldown-base" label={t("settings.routing.cooldownBase")} description={t("settings.routing.cooldownBaseHelp")} error={form.formState.errors.routing?.cooldownBase?.message}><Controller control={form.control} name="routing.cooldownBase" render={({ field }) => <DurationInput id="routing-cooldown-base" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="routing-cooldown-max" label={t("settings.routing.cooldownMax")} description={t("settings.routing.cooldownMaxHelp")} error={form.formState.errors.routing?.cooldownMax?.message}><Controller control={form.control} name="routing.cooldownMax" render={({ field }) => <DurationInput id="routing-cooldown-max" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="routing-capacity-wait" label={t("settings.routing.capacityWait", { defaultValue: "Saturated account wait" })} description={t("settings.routing.capacityWaitHelp")} error={form.formState.errors.routing?.capacityWait?.message}><Controller control={form.control} name="routing.capacityWait" render={({ field }) => <DurationInput id="routing-capacity-wait" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="routing-max-attempts" label={t("settings.routing.maxAttempts")} description={t("settingsRoutingAttempts.help")} error={form.formState.errors.routing?.maxAttempts?.message}> |
| <Controller control={form.control} name="routing.maxAttempts" render={({ field }) => { |
| const unlimited = field.value === UNLIMITED_ROUTING_ATTEMPTS; |
| return ( |
| <div className="flex h-9 items-center gap-3"> |
| <Input |
| id="routing-max-attempts" |
| ref={field.ref} |
| name={field.name} |
| type="number" |
| min={1} |
| max={MAX_ROUTING_ATTEMPTS} |
| disabled={unlimited} |
| value={unlimited || !Number.isFinite(field.value) ? "" : field.value} |
| placeholder={t("settingsRoutingAttempts.unlimited")} |
| onBlur={field.onBlur} |
| onChange={(event) => field.onChange(event.currentTarget.valueAsNumber)} |
| /> |
| <div className="flex shrink-0 items-center gap-2"> |
| <span className="text-xs text-muted-foreground">{t("settingsRoutingAttempts.unlimited")}</span> |
| <Switch |
| id="routing-max-attempts-unlimited" |
| aria-label={t("settingsRoutingAttempts.unlimited")} |
| checked={unlimited} |
| onCheckedChange={(checked) => { |
| if (checked) { |
| if (field.value > 0) limitedRoutingAttemptsRef.current = field.value; |
| setUnlimitedAttemptsConfirm(true); |
| return; |
| } |
| field.onChange(limitedRoutingAttemptsRef.current); |
| }} |
| /> |
| </div> |
| </div> |
| ); |
| }} /> |
| </SettingsField> |
| <SettingsField controlId="routing-prefer-free-build" label={t("settings.routing.preferFreeBuild")} description={t("settings.routing.preferFreeBuildHelp")}><Controller control={form.control} name="routing.preferFreeBuild" render={({ field }) => <div className="flex h-9 items-center"><Switch id="routing-prefer-free-build" checked={field.value} onCheckedChange={field.onChange} /></div>} /></SettingsField> |
| <SettingsField controlId="routing-segmented-selector-enabled" label={t("settingsRoutingSegmented.enabled")} description={t("settingsRoutingSegmented.enabledHelp")}><Controller control={form.control} name="routing.segmentedSelector.enabled" render={({ field }) => <div className="flex h-9 items-center"><Switch id="routing-segmented-selector-enabled" checked={field.value} onCheckedChange={field.onChange} /></div>} /></SettingsField> |
| <SettingsField controlId="routing-segmented-min-candidates" label={t("settingsRoutingSegmented.minCandidates")} description={t("settingsRoutingSegmented.minCandidatesHelp")} error={form.formState.errors.routing?.segmentedSelector?.minCandidates?.message}><Input id="routing-segmented-min-candidates" type="number" min={100} max={1_000_000} disabled={!segmentedSelectorEnabled} {...form.register("routing.segmentedSelector.minCandidates", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="routing-segmented-window-size" label={t("settingsRoutingSegmented.windowSize")} description={t("settingsRoutingSegmented.windowSizeHelp")} error={form.formState.errors.routing?.segmentedSelector?.windowSize?.message}><Input id="routing-segmented-window-size" type="number" min={8} max={256} disabled={!segmentedSelectorEnabled} {...form.register("routing.segmentedSelector.windowSize", { valueAsNumber: true })} /></SettingsField> |
| </div> |
| <AlertDialog open={unlimitedAttemptsConfirm} onOpenChange={setUnlimitedAttemptsConfirm}> |
| <AlertDialogContent> |
| <AlertDialogHeader> |
| <AlertDialogTitle>{t("settingsRoutingAttempts.unlimitedTitle")}</AlertDialogTitle> |
| <AlertDialogDescription>{t("settingsRoutingAttempts.unlimitedDescription")}</AlertDialogDescription> |
| </AlertDialogHeader> |
| <AlertDialogFooter> |
| <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel> |
| <AlertDialogAction |
| className="bg-destructive text-white hover:bg-destructive/90" |
| onClick={() => { |
| form.setValue("routing.maxAttempts", UNLIMITED_ROUTING_ATTEMPTS, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); |
| setUnlimitedAttemptsConfirm(false); |
| }} |
| > |
| {t("settingsRoutingAttempts.unlimitedConfirm")} |
| </AlertDialogAction> |
| </AlertDialogFooter> |
| </AlertDialogContent> |
| </AlertDialog> |
| </SettingsSection> |
| |
| <SettingsSection title={t("settings.audit.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="audit-buffer-size" label={t("settings.audit.bufferSize")} description={t("settings.audit.bufferSizeHelp")} badge={t("settings.restartRequired")} error={form.formState.errors.audit?.bufferSize?.message}><Input id="audit-buffer-size" type="number" min={1} max={262_144} {...form.register("audit.bufferSize", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="audit-batch-size" label={t("settings.audit.batchSize")} description={t("settings.audit.batchSizeHelp")} error={form.formState.errors.audit?.batchSize?.message}><Input id="audit-batch-size" type="number" min={1} max={4_096} {...form.register("audit.batchSize", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="audit-flush-interval" label={t("settings.audit.flushInterval")} description={t("settings.audit.flushIntervalHelp")} error={form.formState.errors.audit?.flushInterval?.message}><Controller control={form.control} name="audit.flushInterval" render={({ field }) => <DurationInput id="audit-flush-interval" value={field.value} onChange={field.onChange} />} /></SettingsField> |
| <SettingsField controlId="audit-commit-delay" label={t("settings.audit.commitDelay")} description={t("settings.audit.commitDelayHelp")} error={form.formState.errors.audit?.commitDelayMS?.message}><Input id="audit-commit-delay" type="number" min={1} max={50} {...form.register("audit.commitDelayMS", { valueAsNumber: true })} /></SettingsField> |
| </div> |
| </SettingsSection> |
| |
| <SettingsSection title={t("settings.clientKeys.title")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="client-key-default-rpm" label={t("settings.clientKeys.rpmLimit")} description={t("settings.clientKeys.rpmLimitHelp")} error={form.formState.errors.clientKeyDefaults?.rpmLimit?.message}><Input id="client-key-default-rpm" type="number" min={1} max={100_000} {...form.register("clientKeyDefaults.rpmLimit", { valueAsNumber: true })} /></SettingsField> |
| <SettingsField controlId="client-key-default-concurrency" label={t("settings.clientKeys.maxConcurrent")} description={t("settings.clientKeys.maxConcurrentHelp")} error={form.formState.errors.clientKeyDefaults?.maxConcurrent?.message}><Input id="client-key-default-concurrency" type="number" min={1} max={1_024} {...form.register("clientKeyDefaults.maxConcurrent", { valueAsNumber: true })} /></SettingsField> |
| </div> |
| </SettingsSection> |
| </SettingsPane> |
| |
| <SettingsPane value="accounts"> |
| <SettingsSection title={t("settings.accounts.invalidationTitle")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="accounts-mark-build-forbidden-reauth" label={t("settingsBuildForbidden.markInvalid")} description={t("settingsBuildForbidden.markInvalidHelp")}> |
| <Controller control={form.control} name="accounts.markBuildForbiddenReauth" render={({ field }) => ( |
| <div className="flex h-9 items-center"> |
| <Switch id="accounts-mark-build-forbidden-reauth" checked={Boolean(field.value)} onCheckedChange={field.onChange} /> |
| </div> |
| )} /> |
| </SettingsField> |
| <SettingsField |
| controlId="accounts-build-forbidden-reauth-codes" |
| className="sm:col-span-2" |
| label={t("settingsBuildForbidden.codes")} |
| description={t("settingsBuildForbidden.codesHelp")} |
| error={form.formState.errors.accounts?.buildForbiddenReauthCodes ? t("settingsBuildForbidden.codesInvalid") : undefined} |
| > |
| <Textarea |
| id="accounts-build-forbidden-reauth-codes" |
| className="min-h-24 font-mono" |
| disabled={!buildForbiddenReauthEnabled} |
| placeholder={t("settingsBuildForbidden.codesPlaceholder")} |
| {...form.register("accounts.buildForbiddenReauthCodes")} |
| /> |
| </SettingsField> |
| </div> |
| </SettingsSection> |
| |
| <SettingsSection title={t("settings.accounts.cleanupTitle")}> |
| <div className="space-y-0"> |
| <SettingsField controlId="accounts-auto-clean-reauth-enabled" label={t("settings.accounts.autoCleanReauthEnabled")} description={t("settings.accounts.autoCleanReauthEnabledHelp")}> |
| <Controller control={form.control} name="accounts.autoCleanReauthEnabled" render={({ field }) => ( |
| <div className="flex h-9 items-center"> |
| <Switch |
| id="accounts-auto-clean-reauth-enabled" |
| checked={Boolean(field.value)} |
| onCheckedChange={(checked) => { |
| if (checked) { |
| setAutoCleanConfirm("enabled"); |
| return; |
| } |
| field.onChange(false); |
| form.setValue("accounts.autoCleanIncludeDisabled", false, { shouldDirty: true, shouldTouch: true }); |
| }} |
| /> |
| </div> |
| )} /> |
| </SettingsField> |
| <SettingsField controlId="accounts-auto-clean-reauth-interval" label={t("settings.accounts.autoCleanReauthInterval")} description={t("settings.accounts.autoCleanReauthIntervalHelp")} error={form.formState.errors.accounts?.autoCleanReauthInterval?.message}> |
| <Controller control={form.control} name="accounts.autoCleanReauthInterval" render={({ field }) => ( |
| <DurationInput id="accounts-auto-clean-reauth-interval" value={field.value} onChange={field.onChange} disabled={!autoCleanEnabled} /> |
| )} /> |
| </SettingsField> |
| <SettingsField controlId="accounts-auto-clean-reauth-min-age" label={t("settings.accounts.autoCleanReauthMinAge")} description={t("settings.accounts.autoCleanReauthMinAgeHelp")} error={form.formState.errors.accounts?.autoCleanReauthMinAge?.message}> |
| <Controller control={form.control} name="accounts.autoCleanReauthMinAge" render={({ field }) => ( |
| <DurationInput id="accounts-auto-clean-reauth-min-age" value={field.value} onChange={field.onChange} disabled={!autoCleanEnabled} /> |
| )} /> |
| </SettingsField> |
| <SettingsField controlId="accounts-auto-clean-include-disabled" label={t("settings.accounts.autoCleanIncludeDisabled")} description={t("settings.accounts.autoCleanIncludeDisabledHelp")}> |
| <Controller control={form.control} name="accounts.autoCleanIncludeDisabled" render={({ field }) => ( |
| <div className="flex h-9 items-center"> |
| <Switch |
| id="accounts-auto-clean-include-disabled" |
| checked={Boolean(field.value)} |
| disabled={!autoCleanEnabled} |
| onCheckedChange={(checked) => { |
| if (checked) { |
| setAutoCleanConfirm("includeDisabled"); |
| return; |
| } |
| field.onChange(false); |
| }} |
| /> |
| </div> |
| )} /> |
| </SettingsField> |
| </div> |
| <AlertDialog open={autoCleanConfirm !== null} onOpenChange={(open) => { if (!open) setAutoCleanConfirm(null); }}> |
| <AlertDialogContent> |
| <AlertDialogHeader> |
| <AlertDialogTitle> |
| {autoCleanConfirm === "includeDisabled" |
| ? t("settings.accounts.autoCleanIncludeDisabledTitle") |
| : t("settings.accounts.autoCleanEnableTitle")} |
| </AlertDialogTitle> |
| <AlertDialogDescription> |
| {autoCleanConfirm === "includeDisabled" |
| ? t("settings.accounts.autoCleanIncludeDisabledDescription") |
| : t("settings.accounts.autoCleanEnableDescription")} |
| </AlertDialogDescription> |
| </AlertDialogHeader> |
| <AlertDialogFooter> |
| <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel> |
| <AlertDialogAction |
| className="bg-destructive text-white hover:bg-destructive/90" |
| onClick={() => { |
| if (autoCleanConfirm === "includeDisabled") { |
| form.setValue("accounts.autoCleanIncludeDisabled", true, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); |
| } else { |
| form.setValue("accounts.autoCleanReauthEnabled", true, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); |
| } |
| setAutoCleanConfirm(null); |
| }} |
| > |
| {t("settings.accounts.autoCleanConfirm")} |
| </AlertDialogAction> |
| </AlertDialogFooter> |
| </AlertDialogContent> |
| </AlertDialog> |
| </SettingsSection> |
| </SettingsPane> |
| |
| <SettingsPane value="about"> |
| <VersionUpdateSection /> |
| </SettingsPane> |
| </div> |
| </Tabs> |
| ) : null} |
| </form> |
| ); |
| } |
|
|
| function ByteSizeInput({ id, value, onChange }: { id: string; value?: ByteSizeValue; onChange: (value: ByteSizeValue) => void }) { |
| const { t } = useTranslation(); |
| const unit = value?.unit ?? "MiB"; |
| return ( |
| <div className="flex min-w-0"> |
| <Input |
| id={id} |
| type="number" |
| min="0.001" |
| step="any" |
| className="min-w-0 rounded-r-none" |
| value={Number.isFinite(value?.value) ? value?.value : ""} |
| onChange={(event) => onChange({ value: event.target.value === "" ? Number.NaN : Number(event.target.value), unit })} |
| /> |
| <Select value={unit} onValueChange={(nextUnit) => { if (isByteSizeUnit(nextUnit)) onChange({ value: value?.value ?? 1, unit: nextUnit }); }}> |
| <SelectTrigger className="w-24 shrink-0 rounded-l-none bg-secondary/55" aria-label={t("settings.media.sizeUnit")}> |
| <SelectValue /> |
| </SelectTrigger> |
| <SelectContent> |
| <SelectItem value="MiB">MiB</SelectItem> |
| <SelectItem value="GiB">GiB</SelectItem> |
| </SelectContent> |
| </Select> |
| </div> |
| ); |
| } |
|
|
| function DurationInput({ id, value, onChange, disabled }: { id: string; value?: DurationValue; onChange: (value: DurationValue) => void; disabled?: boolean }) { |
| const { t } = useTranslation(); |
| const unit = value?.unit ?? "s"; |
| return ( |
| <div className="flex min-w-0"> |
| <Input |
| id={id} |
| type="number" |
| min="0.001" |
| step="any" |
| disabled={disabled} |
| className="min-w-0 rounded-r-none" |
| value={Number.isFinite(value?.value) ? value?.value : ""} |
| onChange={(event) => onChange({ value: event.target.value === "" ? Number.NaN : Number(event.target.value), unit })} |
| /> |
| <Select value={unit} disabled={disabled} onValueChange={(nextUnit) => { if (isDurationUnit(nextUnit)) onChange({ value: value?.value ?? 1, unit: nextUnit }); }}> |
| <SelectTrigger className="w-24 shrink-0 rounded-l-none bg-secondary/55" aria-label={t("settings.durationUnit")}> |
| <SelectValue /> |
| </SelectTrigger> |
| <SelectContent> |
| <SelectItem value="s">{t("settings.units.seconds")}</SelectItem> |
| <SelectItem value="m">{t("settings.units.minutes")}</SelectItem> |
| <SelectItem value="h">{t("settings.units.hours")}</SelectItem> |
| <SelectItem value="d">{t("settings.units.days")}</SelectItem> |
| </SelectContent> |
| </Select> |
| </div> |
| ); |
| } |
|
|
| function SettingsPane({ value, children }: { value: string; children: ReactNode }) { |
| return ( |
| <TabsContent value={value} forceMount className="m-0 space-y-8 data-[state=inactive]:hidden"> |
| {children} |
| </TabsContent> |
| ); |
| } |
|
|
| function SettingsSection({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) { |
| return ( |
| <section className="space-y-3"> |
| <div className="flex min-h-8 items-center justify-between gap-3 px-1"> |
| <h2 className="text-sm font-medium tracking-tight">{title}</h2> |
| {action} |
| </div> |
| <div className="min-w-0 w-full">{children}</div> |
| </section> |
| ); |
| } |
|
|
| function SettingsField({ controlId, label, badge, description, error, className, children }: { controlId: string; label: string; badge?: string; description?: string; error?: string; className?: string; children: ReactNode }) { |
| const { t } = useTranslation(); |
| return ( |
| <div className={cn("min-w-0 py-4", className)}> |
| <div className="grid min-w-0 gap-2.5 sm:grid-cols-[minmax(0,2fr)_minmax(0,1fr)] sm:items-center sm:gap-8"> |
| <div className="min-w-0"> |
| <div className="flex min-h-5 items-center gap-2"> |
| <Label htmlFor={controlId} className="text-xs font-medium">{label}</Label> |
| {badge ? <Badge variant="secondary" className="shrink-0 text-[10px]">{badge}</Badge> : null} |
| </div> |
| {description ? <p className="mt-1 max-w-xl text-xs leading-5 text-muted-foreground">{description}</p> : null} |
| {error ? <p className="mt-1 text-xs text-destructive">{t("settings.invalidValue")}</p> : null} |
| </div> |
| <div className="min-w-0">{children}</div> |
| </div> |
| </div> |
| ); |
| } |
|
|