Files
Cli-Proxy-API-Management-Ce…/src/components/config/VisualConfigEditor.tsx
T
LTbinglingfeng b5344a7586 refactor(config): flatten advanced section and card-style simple-mode fields
- Drop the inner Collapsible in the advanced section; surface Plugins and Antigravity Signature as labeled sub-groups instead of a nested disclosure
- Wrap bare simple-mode inputs in .simpleField cards for visual consistency with toggle rows and API-key blocks
2026-06-19 01:03:03 +08:00

1433 lines
56 KiB
TypeScript

import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
type ComponentType,
type ReactNode,
} from 'react';
import { useTranslation } from 'react-i18next';
import { usePageTransitionLayer } from '@/components/common/PageTransitionLayer';
import { Collapsible } from '@/components/ui/Collapsible';
import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import {
IconCode,
IconKey,
IconNetwork,
IconSatellite,
IconScrollText,
IconShield,
IconSlidersHorizontal,
IconTimer,
type IconProps,
} from '@/components/ui/icons';
import { ConfigSection } from '@/components/config/ConfigSection';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import type {
PayloadFilterRule,
PayloadParamValidationErrorCode,
PayloadRule,
VisualConfigFieldPath,
VisualConfigValidationErrorCode,
VisualConfigValidationErrors,
VisualConfigValues,
} from '@/types/visualConfig';
import {
ApiKeysCardEditor,
PayloadFilterRulesEditor,
PayloadRulesEditor,
StringListEditor,
} from './VisualConfigEditorBlocks';
import styles from './VisualConfigEditor.module.scss';
type VisualSectionId =
| 'connectivity'
| 'network'
| 'logging'
| 'quota'
| 'streaming'
| 'advanced'
| 'payload';
type EditorMode = 'simple' | 'full';
const EDITOR_MODE_STORAGE_KEY = 'config-management:editor-mode';
type VisualSection = {
id: VisualSectionId;
title: string;
icon: ComponentType<IconProps>;
errorCount: number;
};
interface VisualConfigEditorProps {
values: VisualConfigValues;
validationErrors?: VisualConfigValidationErrors;
hasPayloadValidationErrors?: boolean;
disabled?: boolean;
onChange: (values: Partial<VisualConfigValues>) => void;
}
function getValidationMessage(
t: ReturnType<typeof useTranslation>['t'],
errorCode?: VisualConfigValidationErrorCode | PayloadParamValidationErrorCode
) {
if (!errorCode) return undefined;
return t(`config_management.visual.validation.${errorCode}`);
}
type ToggleRowProps = {
title: string;
description?: string;
checked: boolean;
disabled?: boolean;
onChange: (value: boolean) => void;
};
function ToggleRow({ title, description, checked, disabled, onChange }: ToggleRowProps) {
return (
<div className={styles.toggleRow}>
<div className={styles.toggleCopy}>
<div className={styles.toggleTitle}>{title}</div>
{description ? <div className={styles.toggleDescription}>{description}</div> : null}
</div>
<ToggleSwitch checked={checked} onChange={onChange} disabled={disabled} ariaLabel={title} />
</div>
);
}
function SectionGrid({ children }: { children: ReactNode }) {
return <div className={styles.sectionGrid}>{children}</div>;
}
function SectionStack({ children }: { children: ReactNode }) {
return <div className={styles.sectionStack}>{children}</div>;
}
function Divider() {
return <div className={styles.divider} />;
}
function SectionSubsection({
title,
description,
children,
}: {
title: string;
description?: string;
children: ReactNode;
}) {
return (
<div className={styles.subsection}>
<div className={styles.subsectionHeader}>
<h3 className={styles.subsectionTitle}>{title}</h3>
{description ? <p className={styles.subsectionDescription}>{description}</p> : null}
</div>
{children}
</div>
);
}
function FieldShell({
label,
labelId,
htmlFor,
hint,
hintId,
error,
errorId,
children,
}: {
label: string;
labelId?: string;
htmlFor?: string;
hint?: string;
hintId?: string;
error?: string;
errorId?: string;
children: ReactNode;
}) {
return (
<div className={styles.fieldShell}>
<label id={labelId} htmlFor={htmlFor} className={styles.fieldLabel}>
{label}
</label>
{children}
{error ? (
<div id={errorId} className="error-box">
{error}
</div>
) : null}
{hint ? (
<div id={hintId} className={styles.fieldHint}>
{hint}
</div>
) : null}
</div>
);
}
export function VisualConfigEditor({
values,
validationErrors,
hasPayloadValidationErrors = false,
disabled = false,
onChange,
}: VisualConfigEditorProps) {
const { t } = useTranslation();
const pageTransitionLayer = usePageTransitionLayer();
const isCurrentLayer = pageTransitionLayer ? pageTransitionLayer.isCurrentLayer : true;
const isMobile = useMediaQuery('(max-width: 768px)');
const routingStrategyLabelId = useId();
const routingStrategyHintId = `${routingStrategyLabelId}-hint`;
const disableImageGenerationLabelId = useId();
const disableImageGenerationHintId = `${disableImageGenerationLabelId}-hint`;
const keepaliveInputId = useId();
const keepaliveHintId = `${keepaliveInputId}-hint`;
const keepaliveErrorId = `${keepaliveInputId}-error`;
const nonstreamKeepaliveInputId = useId();
const nonstreamKeepaliveHintId = `${nonstreamKeepaliveInputId}-hint`;
const nonstreamKeepaliveErrorId = `${nonstreamKeepaliveInputId}-error`;
const [mode, setMode] = useState<EditorMode>(() => {
const saved = localStorage.getItem(EDITOR_MODE_STORAGE_KEY);
return saved === 'full' ? 'full' : 'simple';
});
const [activeSectionId, setActiveSectionId] = useState<VisualSectionId>('connectivity');
const sectionRefs = useRef<Partial<Record<VisualSectionId, HTMLElement | null>>>({});
const mobileNavScrollerRef = useRef<HTMLDivElement | null>(null);
const mobileNavButtonRefs = useRef<Partial<Record<VisualSectionId, HTMLButtonElement | null>>>(
{}
);
const handleModeChange = useCallback((next: EditorMode) => {
setMode(next);
localStorage.setItem(EDITOR_MODE_STORAGE_KEY, next);
}, []);
const isKeepaliveDisabled =
values.streaming.keepaliveSeconds === '' || values.streaming.keepaliveSeconds === '0';
const isNonstreamKeepaliveDisabled =
values.streaming.nonstreamKeepaliveInterval === '' ||
values.streaming.nonstreamKeepaliveInterval === '0';
const portError = getValidationMessage(t, validationErrors?.port);
const logsMaxSizeError = getValidationMessage(t, validationErrors?.logsMaxTotalSizeMb);
const errorLogsMaxFilesError = getValidationMessage(t, validationErrors?.errorLogsMaxFiles);
const redisUsageQueueRetentionError = getValidationMessage(
t,
validationErrors?.redisUsageQueueRetentionSeconds
);
const requestRetryError = getValidationMessage(t, validationErrors?.requestRetry);
const maxRetryCredentialsError = getValidationMessage(t, validationErrors?.maxRetryCredentials);
const maxRetryIntervalError = getValidationMessage(t, validationErrors?.maxRetryInterval);
const authAutoRefreshWorkersError = getValidationMessage(
t,
validationErrors?.authAutoRefreshWorkers
);
const keepaliveError = getValidationMessage(t, validationErrors?.['streaming.keepaliveSeconds']);
const bootstrapRetriesError = getValidationMessage(
t,
validationErrors?.['streaming.bootstrapRetries']
);
const nonstreamKeepaliveError = getValidationMessage(
t,
validationErrors?.['streaming.nonstreamKeepaliveInterval']
);
const handleApiKeysTextChange = useCallback(
(apiKeysText: string) => onChange({ apiKeysText }),
[onChange]
);
const handlePluginStoreSourcesChange = useCallback(
(pluginStoreSources: string[]) => onChange({ pluginStoreSources }),
[onChange]
);
const handlePayloadDefaultRulesChange = useCallback(
(payloadDefaultRules: PayloadRule[]) => onChange({ payloadDefaultRules }),
[onChange]
);
const handlePayloadDefaultRawRulesChange = useCallback(
(payloadDefaultRawRules: PayloadRule[]) => onChange({ payloadDefaultRawRules }),
[onChange]
);
const handlePayloadOverrideRulesChange = useCallback(
(payloadOverrideRules: PayloadRule[]) => onChange({ payloadOverrideRules }),
[onChange]
);
const handlePayloadOverrideRawRulesChange = useCallback(
(payloadOverrideRawRules: PayloadRule[]) => onChange({ payloadOverrideRawRules }),
[onChange]
);
const handlePayloadFilterRulesChange = useCallback(
(payloadFilterRules: PayloadFilterRule[]) => onChange({ payloadFilterRules }),
[onChange]
);
const disableImageGenerationOptions = useMemo(
() => [
{
value: 'false',
label: t('config_management.visual.sections.network.disable_image_generation_false'),
},
{
value: 'true',
label: t('config_management.visual.sections.network.disable_image_generation_true'),
},
{
value: 'chat',
label: t('config_management.visual.sections.network.disable_image_generation_chat'),
},
],
[t]
);
const countErrors = useCallback(
(fields: VisualConfigFieldPath[]) =>
fields.reduce((total, field) => total + (validationErrors?.[field] ? 1 : 0), 0),
[validationErrors]
);
const sections = useMemo<VisualSection[]>(
() => [
{
id: 'connectivity',
title: t('config_management.visual.sections.connectivity.title'),
icon: IconKey,
errorCount: countErrors(['port']),
},
{
id: 'network',
title: t('config_management.visual.sections.network.title'),
icon: IconNetwork,
errorCount: countErrors([
'requestRetry',
'maxRetryCredentials',
'maxRetryInterval',
'authAutoRefreshWorkers',
]),
},
{
id: 'logging',
title: t('config_management.visual.sections.logging.title'),
icon: IconScrollText,
errorCount: countErrors([
'errorLogsMaxFiles',
'logsMaxTotalSizeMb',
'redisUsageQueueRetentionSeconds',
]),
},
{
id: 'quota',
title: t('config_management.visual.sections.quota.title'),
icon: IconTimer,
errorCount: 0,
},
{
id: 'streaming',
title: t('config_management.visual.sections.streaming.title'),
icon: IconSatellite,
errorCount: countErrors([
'streaming.keepaliveSeconds',
'streaming.bootstrapRetries',
'streaming.nonstreamKeepaliveInterval',
]),
},
{
id: 'advanced',
title: t('config_management.visual.sections.advanced.title'),
icon: IconShield,
errorCount: 0,
},
{
id: 'payload',
title: t('config_management.visual.sections.payload.title'),
icon: IconCode,
errorCount: hasPayloadValidationErrors ? 1 : 0,
},
],
[countErrors, hasPayloadValidationErrors, t]
);
const hasValidationIssues =
sections.some((section) => section.errorCount > 0) || hasPayloadValidationErrors;
// Validation errors that live in fields not surfaced by simple mode (everything except `port`).
const hasHiddenValidationIssues =
(Object.keys(validationErrors ?? {}) as VisualConfigFieldPath[]).some(
(field) => field !== 'port' && Boolean(validationErrors?.[field])
) || hasPayloadValidationErrors;
const activeSection = sections.find((section) => section.id === activeSectionId) ?? sections[0];
useEffect(() => {
if (mode !== 'full') return undefined;
if (!isCurrentLayer) return undefined;
if (typeof IntersectionObserver === 'undefined') return undefined;
const observer = new IntersectionObserver(
(entries) => {
const visibleEntries = entries
.filter((entry) => entry.isIntersecting)
.sort((left, right) => right.intersectionRatio - left.intersectionRatio);
if (visibleEntries.length === 0) return;
setActiveSectionId(visibleEntries[0].target.id as VisualSectionId);
},
{
rootMargin: '-18% 0px -58% 0px',
threshold: [0.12, 0.3, 0.55],
}
);
for (const section of sections) {
const element = sectionRefs.current[section.id];
if (element) observer.observe(element);
}
return () => observer.disconnect();
}, [isCurrentLayer, mode, sections]);
useEffect(() => {
if (mode !== 'full' || !isCurrentLayer || !isMobile) return;
const scroller = mobileNavScrollerRef.current;
const button = mobileNavButtonRefs.current[activeSectionId];
if (!scroller || !button) return;
const scrollerRect = scroller.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const centeredLeft =
scroller.scrollLeft +
(buttonRect.left - scrollerRect.left) -
(scroller.clientWidth - buttonRect.width) / 2;
const maxScrollLeft = Math.max(scroller.scrollWidth - scroller.clientWidth, 0);
const targetLeft = Math.min(Math.max(centeredLeft, 0), maxScrollLeft);
scroller.scrollTo({
left: targetLeft,
behavior: 'smooth',
});
}, [activeSectionId, isCurrentLayer, isMobile, mode]);
const handleSectionJump = useCallback((sectionId: VisualSectionId) => {
setActiveSectionId(sectionId);
sectionRefs.current[sectionId]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start',
});
}, []);
// Shared high-frequency field blocks — reused verbatim by both the simple view and full mode
// so the two layouts never drift apart.
const portField = (
<Input
label={t('config_management.visual.sections.server.port')}
type="number"
placeholder="8317"
value={values.port}
onChange={(e) => onChange({ port: e.target.value })}
disabled={disabled}
error={portError}
/>
);
const proxyUrlField = (
<Input
label={t('config_management.visual.sections.network.proxy_url')}
placeholder="socks5://user:pass@127.0.0.1:1080/"
value={values.proxyUrl}
onChange={(e) => onChange({ proxyUrl: e.target.value })}
disabled={disabled}
/>
);
const apiKeysField = (
<div className={styles.subsection}>
<ApiKeysCardEditor
value={values.apiKeysText}
disabled={disabled}
onChange={handleApiKeysTextChange}
/>
</div>
);
const debugToggle = (
<ToggleRow
title={t('config_management.visual.sections.system.debug')}
description={t('config_management.visual.sections.system.debug_desc')}
checked={values.debug}
disabled={disabled}
onChange={(debug) => onChange({ debug })}
/>
);
const loggingToFileToggle = (
<ToggleRow
title={t('config_management.visual.sections.system.logging_to_file')}
description={t('config_management.visual.sections.system.logging_to_file_desc')}
checked={values.loggingToFile}
disabled={disabled}
onChange={(loggingToFile) => onChange({ loggingToFile })}
/>
);
const quotaSwitchProjectToggle = (
<ToggleRow
title={t('config_management.visual.sections.quota.switch_project')}
description={t('config_management.visual.sections.quota.switch_project_desc')}
checked={values.quotaSwitchProject}
disabled={disabled}
onChange={(quotaSwitchProject) => onChange({ quotaSwitchProject })}
/>
);
const quotaSwitchPreviewModelToggle = (
<ToggleRow
title={t('config_management.visual.sections.quota.switch_preview_model')}
description={t('config_management.visual.sections.quota.switch_preview_model_desc')}
checked={values.quotaSwitchPreviewModel}
disabled={disabled}
onChange={(quotaSwitchPreviewModel) => onChange({ quotaSwitchPreviewModel })}
/>
);
const navContent = (
<div className={styles.navList}>
{sections.map((section, index) => {
const Icon = section.icon;
return (
<button
key={section.id}
type="button"
className={`${styles.navButton} ${
activeSectionId === section.id ? styles.navButtonActive : ''
}`}
onClick={() => handleSectionJump(section.id)}
>
<span className={styles.navIndex}>{String(index + 1).padStart(2, '0')}</span>
<span className={styles.navMain}>
<span className={styles.navHeadingRow}>
<span className={styles.navLabelWrap}>
<span className={styles.navIcon}>
<Icon size={14} />
</span>
<span className={styles.navLabel}>{section.title}</span>
</span>
{section.errorCount > 0 ? (
<span className={styles.navBadge} aria-hidden="true">
{section.errorCount}
</span>
) : null}
</span>
</span>
</button>
);
})}
</div>
);
return (
<div className={styles.visualEditor}>
<div className={styles.overview}>
<div className={styles.overviewHeader}>
<div
className={styles.modeSwitch}
role="group"
aria-label={t('config_management.visual.mode.label')}
>
<button
type="button"
className={`${styles.modeButton} ${mode === 'simple' ? styles.modeButtonActive : ''}`}
onClick={() => handleModeChange('simple')}
aria-pressed={mode === 'simple'}
>
<IconSlidersHorizontal size={14} />
{t('config_management.visual.mode.simple')}
</button>
<button
type="button"
className={`${styles.modeButton} ${mode === 'full' ? styles.modeButtonActive : ''}`}
onClick={() => handleModeChange('full')}
aria-pressed={mode === 'full'}
>
{t('config_management.visual.mode.full')}
</button>
</div>
<div className={styles.overviewMeta}>
{mode === 'full' && activeSection ? (
<span className={styles.overviewPill}>{activeSection.title}</span>
) : null}
{hasValidationIssues ? (
<span className={`${styles.overviewPill} ${styles.overviewPillWarning}`}>
{t('config_management.visual.validation.validation_blocked')}
</span>
) : null}
</div>
</div>
</div>
{mode === 'simple' ? (
<div className={styles.simpleView}>
{hasHiddenValidationIssues ? (
<div className={styles.simpleBanner} role="alert">
<span>{t('config_management.visual.mode.validation_banner')}</span>
<button
type="button"
className={styles.simpleBannerAction}
onClick={() => handleModeChange('full')}
>
{t('config_management.visual.mode.switch_to_full')}
</button>
</div>
) : null}
<div className={styles.simpleForm}>
<div className={styles.simpleField}>{portField}</div>
{apiKeysField}
<div className={styles.simpleField}>{proxyUrlField}</div>
{debugToggle}
{loggingToFileToggle}
{quotaSwitchProjectToggle}
{quotaSwitchPreviewModelToggle}
</div>
<button
type="button"
className={styles.simpleMore}
onClick={() => handleModeChange('full')}
>
{t('config_management.visual.mode.more_settings', { total: sections.length })}
</button>
</div>
) : (
<div className={styles.workspace}>
{isMobile ? (
<div className={styles.mobileSectionNav}>
<div
ref={mobileNavScrollerRef}
className={styles.mobileSectionNavScroller}
aria-label={t('config_management.visual.quick_jump', { defaultValue: '快速跳转' })}
>
{sections.map((section, index) => (
<button
key={section.id}
ref={(node) => {
mobileNavButtonRefs.current[section.id] = node;
}}
type="button"
className={`${styles.mobileSectionNavButton} ${
activeSectionId === section.id ? styles.mobileSectionNavButtonActive : ''
}`}
onClick={() => handleSectionJump(section.id)}
>
<span className={styles.mobileSectionNavIndex}>
{String(index + 1).padStart(2, '0')}
</span>
<span className={styles.mobileSectionNavLabel}>{section.title}</span>
{section.errorCount > 0 ? (
<span className={styles.mobileSectionNavBadge} aria-hidden="true">
{section.errorCount}
</span>
) : null}
</button>
))}
</div>
</div>
) : null}
<aside className={styles.sidebar}>
<div className={styles.sidebarRail}>{navContent}</div>
</aside>
<div className={styles.sections}>
<ConfigSection
id="connectivity"
ref={(node) => {
sectionRefs.current.connectivity = node;
}}
indexLabel="01"
icon={<IconKey size={16} />}
title={t('config_management.visual.sections.connectivity.title')}
description={t('config_management.visual.sections.connectivity.description')}
>
<SectionStack>
<SectionGrid>
<Input
label={t('config_management.visual.sections.server.host')}
placeholder="0.0.0.0"
value={values.host}
onChange={(e) => onChange({ host: e.target.value })}
disabled={disabled}
/>
{portField}
</SectionGrid>
<Input
label={t('config_management.visual.sections.auth.auth_dir')}
placeholder="~/.cli-proxy-api"
value={values.authDir}
onChange={(e) => onChange({ authDir: e.target.value })}
disabled={disabled}
hint={t('config_management.visual.sections.auth.auth_dir_hint')}
/>
{apiKeysField}
<Collapsible
label={t('config_management.visual.sections.tls.title')}
hint={t('config_management.visual.sections.tls.description')}
defaultOpen={false}
>
<SectionStack>
<ToggleRow
title={t('config_management.visual.sections.tls.enable')}
description={t('config_management.visual.sections.tls.enable_desc')}
checked={values.tlsEnable}
disabled={disabled}
onChange={(tlsEnable) => onChange({ tlsEnable })}
/>
{values.tlsEnable ? (
<>
<Divider />
<SectionGrid>
<Input
label={t('config_management.visual.sections.tls.cert')}
placeholder="/path/to/cert.pem"
value={values.tlsCert}
onChange={(e) => onChange({ tlsCert: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.tls.key')}
placeholder="/path/to/key.pem"
value={values.tlsKey}
onChange={(e) => onChange({ tlsKey: e.target.value })}
disabled={disabled}
/>
</SectionGrid>
</>
) : null}
</SectionStack>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.remote.title')}
hint={t('config_management.visual.sections.remote.description')}
defaultOpen={false}
>
<SectionStack>
<SectionGrid>
<ToggleRow
title={t('config_management.visual.sections.remote.allow_remote')}
description={t(
'config_management.visual.sections.remote.allow_remote_desc'
)}
checked={values.rmAllowRemote}
disabled={disabled}
onChange={(rmAllowRemote) => onChange({ rmAllowRemote })}
/>
<ToggleRow
title={t('config_management.visual.sections.remote.disable_panel')}
description={t(
'config_management.visual.sections.remote.disable_panel_desc'
)}
checked={values.rmDisableControlPanel}
disabled={disabled}
onChange={(rmDisableControlPanel) => onChange({ rmDisableControlPanel })}
/>
<ToggleRow
title={t(
'config_management.visual.sections.remote.disable_auto_update_panel'
)}
description={t(
'config_management.visual.sections.remote.disable_auto_update_panel_desc'
)}
checked={values.rmDisableAutoUpdatePanel}
disabled={disabled}
onChange={(rmDisableAutoUpdatePanel) =>
onChange({ rmDisableAutoUpdatePanel })
}
/>
</SectionGrid>
<SectionGrid>
<Input
label={t('config_management.visual.sections.remote.secret_key')}
type="password"
placeholder={t(
'config_management.visual.sections.remote.secret_key_placeholder'
)}
value={values.rmSecretKey}
onChange={(e) => onChange({ rmSecretKey: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.remote.panel_repo')}
placeholder="https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
value={values.rmPanelRepo}
onChange={(e) => onChange({ rmPanelRepo: e.target.value })}
disabled={disabled}
/>
</SectionGrid>
</SectionStack>
</Collapsible>
</SectionStack>
</ConfigSection>
<ConfigSection
id="network"
ref={(node) => {
sectionRefs.current.network = node;
}}
indexLabel="02"
icon={<IconNetwork size={16} />}
title={t('config_management.visual.sections.network.title')}
description={t('config_management.visual.sections.network.description')}
>
<SectionStack>
<SectionGrid>
{proxyUrlField}
<Input
label={t('config_management.visual.sections.network.request_retry')}
type="number"
placeholder="3"
value={values.requestRetry}
onChange={(e) => onChange({ requestRetry: e.target.value })}
disabled={disabled}
error={requestRetryError}
/>
<Input
label={t('config_management.visual.sections.network.max_retry_credentials')}
type="number"
placeholder="0"
value={values.maxRetryCredentials}
onChange={(e) => onChange({ maxRetryCredentials: e.target.value })}
disabled={disabled}
hint={t('config_management.visual.sections.network.max_retry_credentials_hint')}
error={maxRetryCredentialsError}
/>
<Input
label={t('config_management.visual.sections.network.max_retry_interval')}
type="number"
placeholder="30"
value={values.maxRetryInterval}
onChange={(e) => onChange({ maxRetryInterval: e.target.value })}
disabled={disabled}
error={maxRetryIntervalError}
/>
<Input
label={t('config_management.visual.sections.network.auth_auto_refresh_workers')}
type="number"
placeholder="16"
value={values.authAutoRefreshWorkers}
onChange={(e) => onChange({ authAutoRefreshWorkers: e.target.value })}
disabled={disabled}
hint={t(
'config_management.visual.sections.network.auth_auto_refresh_workers_hint'
)}
error={authAutoRefreshWorkersError}
/>
<FieldShell
label={t('config_management.visual.sections.network.routing_strategy')}
labelId={routingStrategyLabelId}
hint={t('config_management.visual.sections.network.routing_strategy_hint')}
hintId={routingStrategyHintId}
>
<Select
value={values.routingStrategy}
options={[
{
value: 'round-robin',
label: t(
'config_management.visual.sections.network.strategy_round_robin'
),
},
{
value: 'fill-first',
label: t('config_management.visual.sections.network.strategy_fill_first'),
},
]}
id={`${routingStrategyLabelId}-select`}
disabled={disabled}
ariaLabelledBy={routingStrategyLabelId}
ariaDescribedBy={routingStrategyHintId}
onChange={(nextValue) =>
onChange({
routingStrategy: nextValue as VisualConfigValues['routingStrategy'],
})
}
/>
</FieldShell>
<FieldShell
label={t('config_management.visual.sections.network.disable_image_generation')}
labelId={disableImageGenerationLabelId}
hint={t(
'config_management.visual.sections.network.disable_image_generation_hint'
)}
hintId={disableImageGenerationHintId}
>
<Select
value={values.disableImageGeneration}
options={disableImageGenerationOptions}
id={`${disableImageGenerationLabelId}-select`}
disabled={disabled}
ariaLabelledBy={disableImageGenerationLabelId}
ariaDescribedBy={disableImageGenerationHintId}
onChange={(nextValue) =>
onChange({
disableImageGeneration:
nextValue as VisualConfigValues['disableImageGeneration'],
})
}
/>
</FieldShell>
<Input
label={t('config_management.visual.sections.network.gpt_image_2_base_model')}
placeholder="gpt-5.4-mini"
value={values.gptImage2BaseModel}
onChange={(e) => onChange({ gptImage2BaseModel: e.target.value })}
disabled={disabled}
hint={t(
'config_management.visual.sections.network.gpt_image_2_base_model_hint'
)}
/>
<Input
label={t('config_management.visual.sections.network.session_affinity_ttl')}
placeholder="1h"
value={values.routingSessionAffinityTTL}
onChange={(e) => onChange({ routingSessionAffinityTTL: e.target.value })}
disabled={disabled}
/>
</SectionGrid>
<SectionGrid>
<ToggleRow
title={t('config_management.visual.sections.network.force_model_prefix')}
description={t(
'config_management.visual.sections.network.force_model_prefix_desc'
)}
checked={values.forceModelPrefix}
disabled={disabled}
onChange={(forceModelPrefix) => onChange({ forceModelPrefix })}
/>
<ToggleRow
title={t('config_management.visual.sections.network.passthrough_headers')}
description={t(
'config_management.visual.sections.network.passthrough_headers_desc'
)}
checked={values.passthroughHeaders}
disabled={disabled}
onChange={(passthroughHeaders) => onChange({ passthroughHeaders })}
/>
<ToggleRow
title={t('config_management.visual.sections.network.disable_cooling')}
description={t(
'config_management.visual.sections.network.disable_cooling_desc'
)}
checked={values.disableCooling}
disabled={disabled}
onChange={(disableCooling) => onChange({ disableCooling })}
/>
<ToggleRow
title={t('config_management.visual.sections.network.session_affinity')}
checked={values.routingSessionAffinity}
disabled={disabled}
onChange={(routingSessionAffinity) => onChange({ routingSessionAffinity })}
/>
<ToggleRow
title={t('config_management.visual.sections.network.ws_auth')}
description={t('config_management.visual.sections.network.ws_auth_desc')}
checked={values.wsAuth}
disabled={disabled}
onChange={(wsAuth) => onChange({ wsAuth })}
/>
<ToggleRow
title={t(
'config_management.visual.sections.network.enable_gemini_cli_endpoint'
)}
description={t(
'config_management.visual.sections.network.enable_gemini_cli_endpoint_desc'
)}
checked={values.enableGeminiCliEndpoint}
disabled={disabled}
onChange={(enableGeminiCliEndpoint) => onChange({ enableGeminiCliEndpoint })}
/>
</SectionGrid>
</SectionStack>
</ConfigSection>
<ConfigSection
id="logging"
ref={(node) => {
sectionRefs.current.logging = node;
}}
indexLabel="03"
icon={<IconScrollText size={16} />}
title={t('config_management.visual.sections.logging.title')}
description={t('config_management.visual.sections.logging.description')}
>
<SectionStack>
<SectionGrid>
{debugToggle}
<ToggleRow
title={t('config_management.visual.sections.system.commercial_mode')}
description={t('config_management.visual.sections.system.commercial_mode_desc')}
checked={values.commercialMode}
disabled={disabled}
onChange={(commercialMode) => onChange({ commercialMode })}
/>
{loggingToFileToggle}
</SectionGrid>
<SectionGrid>
<Input
label={t('config_management.visual.sections.system.logs_max_size')}
type="number"
placeholder="0"
value={values.logsMaxTotalSizeMb}
onChange={(e) => onChange({ logsMaxTotalSizeMb: e.target.value })}
disabled={disabled}
error={logsMaxSizeError}
/>
<Input
label={t('config_management.visual.sections.system.error_logs_max_files')}
type="number"
placeholder="10"
value={values.errorLogsMaxFiles}
onChange={(e) => onChange({ errorLogsMaxFiles: e.target.value })}
disabled={disabled}
error={errorLogsMaxFilesError}
/>
<Input
label={t('config_management.visual.sections.system.redis_usage_retention')}
type="number"
placeholder="60"
value={values.redisUsageQueueRetentionSeconds}
onChange={(e) => onChange({ redisUsageQueueRetentionSeconds: e.target.value })}
disabled={disabled}
hint={t('config_management.visual.sections.system.redis_usage_retention_hint')}
error={redisUsageQueueRetentionError}
/>
</SectionGrid>
<SectionGrid>
<ToggleRow
title={t('config_management.visual.sections.system.usage_statistics_enabled')}
description={t(
'config_management.visual.sections.system.usage_statistics_enabled_desc'
)}
checked={values.usageStatisticsEnabled}
disabled={disabled}
onChange={(usageStatisticsEnabled) => onChange({ usageStatisticsEnabled })}
/>
</SectionGrid>
</SectionStack>
</ConfigSection>
<ConfigSection
id="quota"
ref={(node) => {
sectionRefs.current.quota = node;
}}
indexLabel="04"
icon={<IconTimer size={16} />}
title={t('config_management.visual.sections.quota.title')}
description={t('config_management.visual.sections.quota.description')}
>
<SectionGrid>
{quotaSwitchProjectToggle}
{quotaSwitchPreviewModelToggle}
<ToggleRow
title={t('config_management.visual.sections.quota.antigravity_credits')}
checked={values.quotaAntigravityCredits}
disabled={disabled}
onChange={(quotaAntigravityCredits) => onChange({ quotaAntigravityCredits })}
/>
</SectionGrid>
</ConfigSection>
<ConfigSection
id="streaming"
ref={(node) => {
sectionRefs.current.streaming = node;
}}
indexLabel="05"
icon={<IconSatellite size={16} />}
title={t('config_management.visual.sections.streaming.title')}
description={t('config_management.visual.sections.streaming.description')}
>
<SectionStack>
<SectionGrid>
<FieldShell
label={t('config_management.visual.sections.streaming.keepalive_seconds')}
htmlFor={keepaliveInputId}
hint={t('config_management.visual.sections.streaming.keepalive_hint')}
hintId={keepaliveHintId}
error={keepaliveError}
errorId={keepaliveErrorId}
>
<div className={styles.fieldControl}>
<input
id={keepaliveInputId}
className="input"
type="number"
placeholder="0"
value={values.streaming.keepaliveSeconds}
onChange={(e) =>
onChange({
streaming: {
...values.streaming,
keepaliveSeconds: e.target.value,
},
})
}
disabled={disabled}
/>
{isKeepaliveDisabled ? (
<span className={styles.inlinePill}>
{t('config_management.visual.sections.streaming.disabled')}
</span>
) : null}
</div>
</FieldShell>
<Input
label={t('config_management.visual.sections.streaming.bootstrap_retries')}
type="number"
placeholder="1"
value={values.streaming.bootstrapRetries}
onChange={(e) =>
onChange({
streaming: {
...values.streaming,
bootstrapRetries: e.target.value,
},
})
}
disabled={disabled}
hint={t('config_management.visual.sections.streaming.bootstrap_hint')}
error={bootstrapRetriesError}
/>
</SectionGrid>
<SectionGrid>
<FieldShell
label={t('config_management.visual.sections.streaming.nonstream_keepalive')}
htmlFor={nonstreamKeepaliveInputId}
hint={t('config_management.visual.sections.streaming.nonstream_keepalive_hint')}
hintId={nonstreamKeepaliveHintId}
error={nonstreamKeepaliveError}
errorId={nonstreamKeepaliveErrorId}
>
<div className={styles.fieldControl}>
<input
id={nonstreamKeepaliveInputId}
className="input"
type="number"
placeholder="0"
value={values.streaming.nonstreamKeepaliveInterval}
onChange={(e) =>
onChange({
streaming: {
...values.streaming,
nonstreamKeepaliveInterval: e.target.value,
},
})
}
disabled={disabled}
/>
{isNonstreamKeepaliveDisabled ? (
<span className={styles.inlinePill}>
{t('config_management.visual.sections.streaming.disabled')}
</span>
) : null}
</div>
</FieldShell>
</SectionGrid>
</SectionStack>
</ConfigSection>
<ConfigSection
id="advanced"
ref={(node) => {
sectionRefs.current.advanced = node;
}}
indexLabel="06"
icon={<IconShield size={16} />}
title={t('config_management.visual.sections.advanced.title')}
description={t('config_management.visual.sections.advanced.description')}
>
<SectionStack>
<Collapsible
label={t('config_management.visual.sections.advanced.plugins_title')}
defaultOpen={false}
>
<SectionStack>
<SectionGrid>
<ToggleRow
title={t('config_management.visual.sections.system.plugins_enabled')}
description={t(
'config_management.visual.sections.system.plugins_enabled_desc'
)}
checked={values.pluginsEnabled}
disabled={disabled}
onChange={(pluginsEnabled) => onChange({ pluginsEnabled })}
/>
</SectionGrid>
<SectionSubsection
title={t('config_management.visual.sections.system.plugin_store_sources')}
description={t(
'config_management.visual.sections.system.plugin_store_sources_desc'
)}
>
<div className={styles.fieldShell}>
<label className={styles.fieldLabel}>
{t('config_management.visual.sections.system.plugin_store_sources_label')}
</label>
<StringListEditor
value={values.pluginStoreSources}
disabled={disabled}
placeholder={t(
'config_management.visual.sections.system.plugin_store_sources_placeholder'
)}
inputAriaLabel={t(
'config_management.visual.sections.system.plugin_store_sources_label'
)}
onChange={handlePluginStoreSourcesChange}
/>
<div className={styles.fieldHint}>
{t('config_management.visual.sections.system.plugin_store_sources_hint')}
</div>
</div>
</SectionSubsection>
</SectionStack>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.advanced.signature_title')}
defaultOpen={false}
>
<SectionGrid>
<ToggleRow
title={t(
'config_management.visual.sections.system.antigravity_signature_cache'
)}
description={t(
'config_management.visual.sections.system.antigravity_signature_cache_desc'
)}
checked={values.antigravitySignatureCacheEnabled}
disabled={disabled}
onChange={(antigravitySignatureCacheEnabled) =>
onChange({ antigravitySignatureCacheEnabled })
}
/>
<ToggleRow
title={t(
'config_management.visual.sections.system.antigravity_signature_strict'
)}
description={t(
'config_management.visual.sections.system.antigravity_signature_strict_desc'
)}
checked={values.antigravitySignatureBypassStrict}
disabled={disabled}
onChange={(antigravitySignatureBypassStrict) =>
onChange({ antigravitySignatureBypassStrict })
}
/>
</SectionGrid>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.headers.title')}
hint={t('config_management.visual.sections.headers.description')}
defaultOpen={false}
>
<SectionStack>
<div className={styles.subsectionHeader}>
<h3 className={styles.subsectionTitle}>
{t('config_management.visual.sections.headers.claude_title')}
</h3>
</div>
<SectionGrid>
<Input
label={t('config_management.visual.sections.headers.user_agent')}
placeholder="claude-cli/2.1.44 (external, sdk-cli)"
value={values.claudeHeaderUserAgent}
onChange={(e) => onChange({ claudeHeaderUserAgent: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.headers.package_version')}
placeholder="0.74.0"
value={values.claudeHeaderPackageVersion}
onChange={(e) => onChange({ claudeHeaderPackageVersion: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.headers.runtime_version')}
placeholder="v24.3.0"
value={values.claudeHeaderRuntimeVersion}
onChange={(e) => onChange({ claudeHeaderRuntimeVersion: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.headers.os')}
placeholder="MacOS"
value={values.claudeHeaderOs}
onChange={(e) => onChange({ claudeHeaderOs: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.headers.arch')}
placeholder="arm64"
value={values.claudeHeaderArch}
onChange={(e) => onChange({ claudeHeaderArch: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.headers.timeout')}
placeholder="600"
value={values.claudeHeaderTimeout}
onChange={(e) => onChange({ claudeHeaderTimeout: e.target.value })}
disabled={disabled}
/>
</SectionGrid>
<SectionGrid>
<ToggleRow
title={t('config_management.visual.sections.headers.stabilize_device')}
description={t(
'config_management.visual.sections.headers.stabilize_device_desc'
)}
checked={values.claudeHeaderStabilizeDeviceProfile}
disabled={disabled}
onChange={(claudeHeaderStabilizeDeviceProfile) =>
onChange({ claudeHeaderStabilizeDeviceProfile })
}
/>
</SectionGrid>
<Divider />
<div className={styles.subsectionHeader}>
<h3 className={styles.subsectionTitle}>
{t('config_management.visual.sections.headers.codex_title')}
</h3>
</div>
<SectionGrid>
<Input
label={t('config_management.visual.sections.headers.user_agent')}
placeholder="codex_cli_rs/0.114.0 (Mac OS 14.2.0; x86_64) vscode/1.111.0"
value={values.codexHeaderUserAgent}
onChange={(e) => onChange({ codexHeaderUserAgent: e.target.value })}
disabled={disabled}
/>
<Input
label={t('config_management.visual.sections.headers.beta_features')}
placeholder="multi_agent"
value={values.codexHeaderBetaFeatures}
onChange={(e) => onChange({ codexHeaderBetaFeatures: e.target.value })}
disabled={disabled}
/>
</SectionGrid>
<SectionGrid>
<ToggleRow
title={t(
'config_management.visual.sections.headers.codex_identity_confuse'
)}
description={t(
'config_management.visual.sections.headers.codex_identity_confuse_desc'
)}
checked={values.codexIdentityConfuse}
disabled={disabled}
onChange={(codexIdentityConfuse) => onChange({ codexIdentityConfuse })}
/>
</SectionGrid>
</SectionStack>
</Collapsible>
</SectionStack>
</ConfigSection>
<ConfigSection
id="payload"
ref={(node) => {
sectionRefs.current.payload = node;
}}
indexLabel="07"
icon={<IconCode size={16} />}
title={t('config_management.visual.sections.payload.title')}
description={t('config_management.visual.sections.payload.description')}
>
<SectionStack>
<Collapsible
label={t('config_management.visual.sections.payload.default_rules')}
hint={t('config_management.visual.sections.payload.default_rules_desc')}
defaultOpen={hasPayloadValidationErrors}
>
<PayloadRulesEditor
value={values.payloadDefaultRules}
disabled={disabled}
onChange={handlePayloadDefaultRulesChange}
/>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.payload.default_raw_rules')}
hint={t('config_management.visual.sections.payload.default_raw_rules_desc')}
defaultOpen={hasPayloadValidationErrors}
>
<PayloadRulesEditor
value={values.payloadDefaultRawRules}
disabled={disabled}
rawJsonValues
onChange={handlePayloadDefaultRawRulesChange}
/>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.payload.override_rules')}
hint={t('config_management.visual.sections.payload.override_rules_desc')}
defaultOpen={hasPayloadValidationErrors}
>
<PayloadRulesEditor
value={values.payloadOverrideRules}
disabled={disabled}
protocolFirst
onChange={handlePayloadOverrideRulesChange}
/>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.payload.override_raw_rules')}
hint={t('config_management.visual.sections.payload.override_raw_rules_desc')}
defaultOpen={hasPayloadValidationErrors}
>
<PayloadRulesEditor
value={values.payloadOverrideRawRules}
disabled={disabled}
protocolFirst
rawJsonValues
onChange={handlePayloadOverrideRawRulesChange}
/>
</Collapsible>
<Collapsible
label={t('config_management.visual.sections.payload.filter_rules')}
hint={t('config_management.visual.sections.payload.filter_rules_desc')}
defaultOpen={hasPayloadValidationErrors}
>
<PayloadFilterRulesEditor
value={values.payloadFilterRules}
disabled={disabled}
onChange={handlePayloadFilterRulesChange}
/>
</Collapsible>
</SectionStack>
</ConfigSection>
</div>
</div>
)}
</div>
);
}