From 9a154c7a846388eb649454d4535877c1d867eece Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Thu, 18 Jun 2026 23:58:04 +0800 Subject: [PATCH 1/6] feat(config): add simple/full editor modes and task-oriented sections Default to a compact simple mode that surfaces only high-frequency fields (port, API keys, proxy, debug, log-to-file, quota fallbacks). Full mode reorganizes the visual editor into 7 task-oriented sections (connectivity, network, logging, quota, streaming, advanced, payload), promoting network to top level and collapsing TLS/remote, the advanced group, and each payload rule group by default. Mode choice persists in localStorage. Presentation-only: field semantics, onChange, and YAML sync are unchanged. --- .../config/VisualConfigEditor.module.scss | 114 ++ src/components/config/VisualConfigEditor.tsx | 1795 +++++++++-------- src/i18n/locales/en.json | 21 + src/i18n/locales/ru.json | 21 + src/i18n/locales/zh-CN.json | 21 + src/i18n/locales/zh-TW.json | 21 + 6 files changed, 1170 insertions(+), 823 deletions(-) diff --git a/src/components/config/VisualConfigEditor.module.scss b/src/components/config/VisualConfigEditor.module.scss index a9671297..2c998e16 100644 --- a/src/components/config/VisualConfigEditor.module.scss +++ b/src/components/config/VisualConfigEditor.module.scss @@ -140,6 +140,9 @@ .overviewHeader { display: flex; align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; min-width: 0; @include mobile { @@ -172,6 +175,117 @@ background: var(--warning-bg); } +.modeSwitch { + display: inline-flex; + gap: 4px; + padding: 3px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-secondary); +} + +.modeButton { + @include button-reset; + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 30px; + padding: 0 12px; + border-radius: 6px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 700; + line-height: 1.2; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover { + color: var(--text-primary); + } +} + +.modeButtonActive { + background: var(--bg-primary); + color: var(--text-primary); + box-shadow: 0 1px 2px color-mix(in srgb, var(--text-primary) 10%, transparent); +} + +.simpleView { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 760px; + min-width: 0; +} + +.simpleForm { + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; +} + +.simpleBanner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + padding: 12px 14px; + border: 1px solid var(--warning-border); + border-radius: 8px; + background: var(--warning-bg); + color: var(--warning-text); + font-size: 13px; + font-weight: 600; + line-height: 1.5; +} + +.simpleBannerAction { + @include button-reset; + flex: 0 0 auto; + padding: 6px 12px; + border: 1px solid var(--warning-border); + border-radius: 6px; + color: var(--warning-text); + font-size: 12px; + font-weight: 700; + transition: background-color 0.15s ease; + + &:hover { + background: color-mix(in srgb, var(--warning-text) 10%, transparent); + } +} + +.simpleMore { + @include button-reset; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + min-height: 46px; + padding: 12px 16px; + border: 1px dashed var(--border-color); + border-radius: 8px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 700; + text-align: center; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + color 0.15s ease; + + &:hover { + border-color: var(--text-primary); + color: var(--text-primary); + background: color-mix(in srgb, var(--text-primary) 4%, transparent); + } +} + .workspace { display: flex; flex-direction: column; diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index 9c8b694a..ff1070d9 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -10,15 +10,18 @@ import { } 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, - IconDiamond, IconKey, + IconNetwork, IconSatellite, - IconSettings, + IconScrollText, + IconShield, + IconSlidersHorizontal, IconTimer, type IconProps, } from '@/components/ui/icons'; @@ -41,7 +44,18 @@ import { } from './VisualConfigEditorBlocks'; import styles from './VisualConfigEditor.module.scss'; -type VisualSectionId = 'server' | 'auth' | 'system' | 'quota' | 'streaming' | 'payload'; +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; @@ -178,13 +192,22 @@ export function VisualConfigEditor({ const nonstreamKeepaliveInputId = useId(); const nonstreamKeepaliveHintId = `${nonstreamKeepaliveInputId}-hint`; const nonstreamKeepaliveErrorId = `${nonstreamKeepaliveInputId}-error`; - const [activeSectionId, setActiveSectionId] = useState('server'); + const [mode, setMode] = useState(() => { + const saved = localStorage.getItem(EDITOR_MODE_STORAGE_KEY); + return saved === 'full' ? 'full' : 'simple'; + }); + const [activeSectionId, setActiveSectionId] = useState('connectivity'); const sectionRefs = useRef>>({}); const mobileNavScrollerRef = useRef(null); const mobileNavButtonRefs = useRef>>( {} ); + 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 = @@ -270,31 +293,32 @@ export function VisualConfigEditor({ const sections = useMemo( () => [ { - id: 'server', - title: t('config_management.visual.sections.server.title'), - icon: IconSettings, + id: 'connectivity', + title: t('config_management.visual.sections.connectivity.title'), + icon: IconKey, errorCount: countErrors(['port']), }, { - id: 'auth', - title: t('config_management.visual.sections.auth.title'), - icon: IconKey, - errorCount: 0, - }, - { - id: 'system', - title: t('config_management.visual.sections.system.title'), - icon: IconDiamond, + id: 'network', + title: t('config_management.visual.sections.network.title'), + icon: IconNetwork, errorCount: countErrors([ - 'errorLogsMaxFiles', - 'logsMaxTotalSizeMb', - 'redisUsageQueueRetentionSeconds', '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'), @@ -311,6 +335,12 @@ export function VisualConfigEditor({ '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'), @@ -323,9 +353,15 @@ export function VisualConfigEditor({ 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; @@ -350,10 +386,10 @@ export function VisualConfigEditor({ } return () => observer.disconnect(); - }, [isCurrentLayer, sections]); + }, [isCurrentLayer, mode, sections]); useEffect(() => { - if (!isCurrentLayer || !isMobile) return; + if (mode !== 'full' || !isCurrentLayer || !isMobile) return; const scroller = mobileNavScrollerRef.current; const button = mobileNavButtonRefs.current[activeSectionId]; if (!scroller || !button) return; @@ -371,7 +407,7 @@ export function VisualConfigEditor({ left: targetLeft, behavior: 'smooth', }); - }, [activeSectionId, isCurrentLayer, isMobile]); + }, [activeSectionId, isCurrentLayer, isMobile, mode]); const handleSectionJump = useCallback((sectionId: VisualSectionId) => { setActiveSectionId(sectionId); @@ -382,6 +418,80 @@ export function VisualConfigEditor({ }); }, []); + // Shared high-frequency field blocks — reused verbatim by both the simple view and full mode + // so the two layouts never drift apart. + const portField = ( + onChange({ port: e.target.value })} + disabled={disabled} + error={portError} + /> + ); + + const proxyUrlField = ( + onChange({ proxyUrl: e.target.value })} + disabled={disabled} + /> + ); + + const apiKeysField = ( +
+ +
+ ); + + const debugToggle = ( + onChange({ debug })} + /> + ); + + const loggingToFileToggle = ( + onChange({ loggingToFile })} + /> + ); + + const quotaSwitchProjectToggle = ( + onChange({ quotaSwitchProject })} + /> + ); + + const quotaSwitchPreviewModelToggle = ( + onChange({ quotaSwitchPreviewModel })} + /> + ); + const navContent = (
{sections.map((section, index) => { @@ -422,11 +532,35 @@ export function VisualConfigEditor({
+
+ + +
- - {t('config_management.visual.quick_jump', { defaultValue: '快速跳转' })} - - {activeSection?.title} + {mode === 'full' && activeSection ? ( + {activeSection.title} + ) : null} {hasValidationIssues ? ( {t('config_management.visual.validation.validation_blocked')} @@ -436,836 +570,851 @@ export function VisualConfigEditor({
-
- {isMobile ? ( -
-
- {sections.map((section, index) => ( - - ))} + {mode === 'simple' ? ( +
+ {hasHiddenValidationIssues ? ( +
+ {t('config_management.visual.mode.validation_banner')} +
+ ) : null} + +
+ {portField} + {apiKeysField} + {proxyUrlField} + {debugToggle} + {loggingToFileToggle} + {quotaSwitchProjectToggle} + {quotaSwitchPreviewModelToggle}
- ) : null} - - -
- { - sectionRefs.current.server = node; - }} - indexLabel="01" - icon={} - title={t('config_management.visual.sections.server.title')} - description={t('config_management.visual.sections.server.description')} + +
+ ) : ( +
+ {isMobile ? ( +
+
+ {sections.map((section, index) => ( + + ))} +
+
+ ) : null} + + + +
+ { + sectionRefs.current.connectivity = node; + }} + indexLabel="01" + icon={} + title={t('config_management.visual.sections.connectivity.title')} + description={t('config_management.visual.sections.connectivity.description')} + > + + + onChange({ host: e.target.value })} + disabled={disabled} + /> + {portField} + + onChange({ host: e.target.value })} + 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')} /> - onChange({ port: e.target.value })} + + {apiKeysField} + + + + onChange({ tlsEnable })} + /> + + {values.tlsEnable ? ( + <> + + + onChange({ tlsCert: e.target.value })} + disabled={disabled} + /> + onChange({ tlsKey: e.target.value })} + disabled={disabled} + /> + + + ) : null} + + + + + + + onChange({ rmAllowRemote })} + /> + onChange({ rmDisableControlPanel })} + /> + + onChange({ rmDisableAutoUpdatePanel }) + } + /> + + + onChange({ rmSecretKey: e.target.value })} + disabled={disabled} + /> + onChange({ rmPanelRepo: e.target.value })} + disabled={disabled} + /> + + + + + + + { + sectionRefs.current.network = node; + }} + indexLabel="02" + icon={} + title={t('config_management.visual.sections.network.title')} + description={t('config_management.visual.sections.network.description')} + > + + + {proxyUrlField} + onChange({ requestRetry: e.target.value })} + disabled={disabled} + error={requestRetryError} + /> + onChange({ maxRetryCredentials: e.target.value })} + disabled={disabled} + hint={t('config_management.visual.sections.network.max_retry_credentials_hint')} + error={maxRetryCredentialsError} + /> + onChange({ maxRetryInterval: e.target.value })} + disabled={disabled} + error={maxRetryIntervalError} + /> + onChange({ authAutoRefreshWorkers: e.target.value })} + disabled={disabled} + hint={t( + 'config_management.visual.sections.network.auth_auto_refresh_workers_hint' + )} + error={authAutoRefreshWorkersError} + /> + + + onChange({ + disableImageGeneration: + nextValue as VisualConfigValues['disableImageGeneration'], + }) + } + /> + + onChange({ gptImage2BaseModel: e.target.value })} + disabled={disabled} + hint={t('config_management.visual.sections.network.gpt_image_2_base_model_hint')} + /> + onChange({ routingSessionAffinityTTL: e.target.value })} + disabled={disabled} + /> + + + + onChange({ forceModelPrefix })} + /> + onChange({ passthroughHeaders })} + /> + onChange({ disableCooling })} + /> + onChange({ routingSessionAffinity })} + /> + onChange({ wsAuth })} + /> + onChange({ enableGeminiCliEndpoint })} + /> + + + + + { + sectionRefs.current.logging = node; + }} + indexLabel="03" + icon={} + title={t('config_management.visual.sections.logging.title')} + description={t('config_management.visual.sections.logging.description')} + > + + + {debugToggle} + onChange({ commercialMode })} + /> + {loggingToFileToggle} + + + + onChange({ logsMaxTotalSizeMb: e.target.value })} + disabled={disabled} + error={logsMaxSizeError} + /> + onChange({ errorLogsMaxFiles: e.target.value })} + disabled={disabled} + error={errorLogsMaxFilesError} + /> + onChange({ redisUsageQueueRetentionSeconds: e.target.value })} + disabled={disabled} + hint={t('config_management.visual.sections.system.redis_usage_retention_hint')} + error={redisUsageQueueRetentionError} + /> + + + + onChange({ usageStatisticsEnabled })} + /> + + + + + { + sectionRefs.current.quota = node; + }} + indexLabel="04" + icon={} + title={t('config_management.visual.sections.quota.title')} + description={t('config_management.visual.sections.quota.description')} + > + + {quotaSwitchProjectToggle} + {quotaSwitchPreviewModelToggle} + onChange({ quotaAntigravityCredits })} /> + - { + sectionRefs.current.streaming = node; + }} + indexLabel="05" + icon={} + title={t('config_management.visual.sections.streaming.title')} + description={t('config_management.visual.sections.streaming.description')} + > + + + +
+ + onChange({ + streaming: { + ...values.streaming, + keepaliveSeconds: e.target.value, + }, + }) + } + disabled={disabled} + /> + {isKeepaliveDisabled ? ( + + {t('config_management.visual.sections.streaming.disabled')} + + ) : null} +
+
+ + + onChange({ + streaming: { + ...values.streaming, + bootstrapRetries: e.target.value, + }, + }) + } + disabled={disabled} + hint={t('config_management.visual.sections.streaming.bootstrap_hint')} + error={bootstrapRetriesError} + /> +
+ + + +
+ + onChange({ + streaming: { + ...values.streaming, + nonstreamKeepaliveInterval: e.target.value, + }, + }) + } + disabled={disabled} + /> + {isNonstreamKeepaliveDisabled ? ( + + {t('config_management.visual.sections.streaming.disabled')} + + ) : null} +
+
+
+
+ + + { + sectionRefs.current.advanced = node; + }} + indexLabel="06" + icon={} + title={t('config_management.visual.sections.advanced.title')} + description={t('config_management.visual.sections.advanced.description')} + > + - onChange({ tlsEnable })} - /> + + onChange({ pluginsEnabled })} + /> + - {values.tlsEnable ? ( - <> - + +
+ + +
+ {t('config_management.visual.sections.system.plugin_store_sources_hint')} +
+
+
+ + + + + + onChange({ antigravitySignatureCacheEnabled }) + } + /> + + onChange({ antigravitySignatureBypassStrict }) + } + /> + + + + + + +
+

+ {t('config_management.visual.sections.headers.claude_title')} +

+
onChange({ tlsCert: e.target.value })} + 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} /> onChange({ tlsKey: e.target.value })} + 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} + /> + onChange({ claudeHeaderRuntimeVersion: e.target.value })} + disabled={disabled} + /> + onChange({ claudeHeaderOs: e.target.value })} + disabled={disabled} + /> + onChange({ claudeHeaderArch: e.target.value })} + disabled={disabled} + /> + onChange({ claudeHeaderTimeout: e.target.value })} disabled={disabled} /> - - ) : null} + + + onChange({ claudeHeaderStabilizeDeviceProfile }) + } + /> + + +
+

+ {t('config_management.visual.sections.headers.codex_title')} +

+
+ + onChange({ codexHeaderUserAgent: e.target.value })} + disabled={disabled} + /> + onChange({ codexHeaderBetaFeatures: e.target.value })} + disabled={disabled} + /> + + + onChange({ codexIdentityConfuse })} + /> + +
+
-
+ + - - - - onChange({ rmAllowRemote })} - /> - onChange({ rmDisableControlPanel })} - /> - - onChange({ rmDisableAutoUpdatePanel }) - } - /> - - - onChange({ rmSecretKey: e.target.value })} - disabled={disabled} - /> - onChange({ rmPanelRepo: e.target.value })} - disabled={disabled} - /> - - - - - - - { - sectionRefs.current.auth = node; - }} - indexLabel="02" - icon={} - title={t('config_management.visual.sections.auth.title')} - description={t('config_management.visual.sections.auth.description')} - > - - onChange({ authDir: e.target.value })} - disabled={disabled} - hint={t('config_management.visual.sections.auth.auth_dir_hint')} - /> -
- -
-
-
- - { - sectionRefs.current.system = node; - }} - indexLabel="03" - icon={} - title={t('config_management.visual.sections.system.title')} - description={t('config_management.visual.sections.system.description')} - > - - - onChange({ debug })} - /> - onChange({ commercialMode })} - /> - onChange({ loggingToFile })} - /> - onChange({ pluginsEnabled })} - /> - - - -
- - { + sectionRefs.current.payload = node; + }} + indexLabel="07" + icon={} + title={t('config_management.visual.sections.payload.title')} + description={t('config_management.visual.sections.payload.description')} + > + + + -
- {t('config_management.visual.sections.system.plugin_store_sources_hint')} -
-
-
+ - - onChange({ logsMaxTotalSizeMb: e.target.value })} - disabled={disabled} - error={logsMaxSizeError} - /> - onChange({ errorLogsMaxFiles: e.target.value })} - disabled={disabled} - error={errorLogsMaxFilesError} - /> - onChange({ redisUsageQueueRetentionSeconds: e.target.value })} - disabled={disabled} - hint={t('config_management.visual.sections.system.redis_usage_retention_hint')} - error={redisUsageQueueRetentionError} - /> - - - onChange({ usageStatisticsEnabled })} - /> - - onChange({ antigravitySignatureCacheEnabled }) - } - /> - - onChange({ antigravitySignatureBypassStrict }) - } - /> - - - - -
-

- {t('config_management.visual.sections.headers.claude_title')} -

-
- - onChange({ claudeHeaderUserAgent: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderPackageVersion: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderRuntimeVersion: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderOs: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderArch: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderTimeout: e.target.value })} - disabled={disabled} - /> - - - - onChange({ claudeHeaderStabilizeDeviceProfile }) - } - /> - - -
-

- {t('config_management.visual.sections.headers.codex_title')} -

-
- - onChange({ codexHeaderUserAgent: e.target.value })} - disabled={disabled} - /> - onChange({ codexHeaderBetaFeatures: e.target.value })} - disabled={disabled} - /> - - - onChange({ codexIdentityConfuse })} - /> - -
-
- - - - - onChange({ proxyUrl: e.target.value })} - disabled={disabled} - /> - onChange({ requestRetry: e.target.value })} - disabled={disabled} - error={requestRetryError} - /> - onChange({ maxRetryCredentials: e.target.value })} - disabled={disabled} - hint={t( - 'config_management.visual.sections.network.max_retry_credentials_hint' - )} - error={maxRetryCredentialsError} - /> - onChange({ maxRetryInterval: e.target.value })} - disabled={disabled} - error={maxRetryIntervalError} - /> - onChange({ authAutoRefreshWorkers: e.target.value })} - disabled={disabled} - hint={t( - 'config_management.visual.sections.network.auth_auto_refresh_workers_hint' - )} - error={authAutoRefreshWorkersError} - /> - - - onChange({ - disableImageGeneration: - nextValue as VisualConfigValues['disableImageGeneration'], - }) - } - /> - - onChange({ gptImage2BaseModel: e.target.value })} - disabled={disabled} - hint={t( - 'config_management.visual.sections.network.gpt_image_2_base_model_hint' - )} - /> - onChange({ routingSessionAffinityTTL: e.target.value })} - disabled={disabled} - /> - - - - onChange({ forceModelPrefix })} - /> - onChange({ passthroughHeaders })} - /> - onChange({ disableCooling })} - /> - onChange({ routingSessionAffinity })} - /> - onChange({ wsAuth })} - /> - onChange({ enableGeminiCliEndpoint })} - /> - - - -
-
- - { - sectionRefs.current.quota = node; - }} - indexLabel="04" - icon={} - title={t('config_management.visual.sections.quota.title')} - description={t('config_management.visual.sections.quota.description')} - > - - onChange({ quotaSwitchProject })} - /> - onChange({ quotaSwitchPreviewModel })} - /> - onChange({ quotaAntigravityCredits })} - /> - - - - { - sectionRefs.current.streaming = node; - }} - indexLabel="05" - icon={} - title={t('config_management.visual.sections.streaming.title')} - description={t('config_management.visual.sections.streaming.description')} - > - - - -
- - onChange({ - streaming: { - ...values.streaming, - keepaliveSeconds: e.target.value, - }, - }) - } - disabled={disabled} - /> - {isKeepaliveDisabled ? ( - - {t('config_management.visual.sections.streaming.disabled')} - - ) : null} -
-
+ + - - onChange({ - streaming: { - ...values.streaming, - bootstrapRetries: e.target.value, - }, - }) - } - disabled={disabled} - hint={t('config_management.visual.sections.streaming.bootstrap_hint')} - error={bootstrapRetriesError} - /> -
- - - -
- - onChange({ - streaming: { - ...values.streaming, - nonstreamKeepaliveInterval: e.target.value, - }, - }) - } - disabled={disabled} - /> - {isNonstreamKeepaliveDisabled ? ( - - {t('config_management.visual.sections.streaming.disabled')} - - ) : null} -
-
-
-
-
+ + - { - sectionRefs.current.payload = node; - }} - indexLabel="06" - icon={} - title={t('config_management.visual.sections.payload.title')} - description={t('config_management.visual.sections.payload.description')} - > - - - - + + + - - - - - - - - - - - - - - - - - + + + + + +
-
+ )}
); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2b1c8697..13a9c54a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -909,7 +909,28 @@ "visual": { "notice": "Visual mode covers common fields. Review or edit unsupported config.yaml entries in source mode.", "quick_jump": "Quick Jump", + "mode": { + "simple": "Simple", + "full": "Full", + "label": "Editor mode", + "more_settings": "Switch to full mode to see all {{total}} sections", + "validation_banner": "Some advanced settings have validation errors and are blocking save. Switch to full mode to fix them.", + "switch_to_full": "Switch to full mode" + }, "sections": { + "connectivity": { + "title": "Access & Authentication", + "description": "Server address, port, auth directory, and API keys" + }, + "logging": { + "title": "Logging & Diagnostics", + "description": "Debug, log output, and usage statistics" + }, + "advanced": { + "title": "Advanced & Experimental", + "description": "Header spoofing, signature cache, plugin sources, and other rarely changed settings", + "disclosure": "Show advanced & experimental settings (hidden by default)" + }, "server": { "title": "Server Configuration", "description": "Basic server settings", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index ad3f1647..41adcd84 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -896,7 +896,28 @@ "visual": { "notice": "Визуальный режим охватывает основные поля. Остальные параметры config.yaml по-прежнему нужно проверять или редактировать в режиме исходника.", "quick_jump": "Быстрый переход", + "mode": { + "simple": "Простой", + "full": "Полный", + "label": "Режим редактора", + "more_settings": "Переключитесь в полный режим, чтобы увидеть все разделы ({{total}})", + "validation_banner": "В некоторых дополнительных параметрах есть ошибки проверки, сохранение заблокировано. Переключитесь в полный режим, чтобы исправить их.", + "switch_to_full": "Перейти в полный режим" + }, "sections": { + "connectivity": { + "title": "Доступ и аутентификация", + "description": "Адрес сервера, порт, каталог аутентификации и ключи API" + }, + "logging": { + "title": "Логи и диагностика", + "description": "Отладка, вывод логов и статистика использования" + }, + "advanced": { + "title": "Дополнительно и эксперименты", + "description": "Подмена заголовков, кэш подписей, источники плагинов и другие редко изменяемые настройки", + "disclosure": "Показать дополнительные и экспериментальные настройки (скрыты по умолчанию)" + }, "server": { "title": "Настройки сервера", "description": "Базовые параметры сервера", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 34d5032e..7a6f951e 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -909,7 +909,28 @@ "visual": { "notice": "可视化模式覆盖常用字段,未覆盖的配置仍需在源文件模式中查看或编辑。", "quick_jump": "快速跳转", + "mode": { + "simple": "简单", + "full": "完整", + "label": "编辑模式", + "more_settings": "切换到完整模式,查看全部 {{total}} 个配置分区", + "validation_banner": "部分高级配置项存在校验错误,已阻止保存。请切换到完整模式进行修复。", + "switch_to_full": "切换完整模式" + }, "sections": { + "connectivity": { + "title": "接入与认证", + "description": "服务地址、端口、认证目录与 API 密钥" + }, + "logging": { + "title": "日志与诊断", + "description": "调试、日志输出与使用统计" + }, + "advanced": { + "title": "高级与实验", + "description": "请求头伪装、签名缓存、插件源等不常修改的设置", + "disclosure": "展开高级与实验配置(默认隐藏)" + }, "server": { "title": "服务器配置", "description": "基础服务器设置", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 546d6de4..45be1a53 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -935,7 +935,28 @@ "visual": { "notice": "視覺化模式涵蓋常用欄位,未涵蓋的設定仍需在原始檔模式中查看或編輯。", "quick_jump": "快速跳轉", + "mode": { + "simple": "簡單", + "full": "完整", + "label": "編輯模式", + "more_settings": "切換到完整模式,檢視全部 {{total}} 個設定分區", + "validation_banner": "部分進階設定項存在驗證錯誤,已阻止儲存。請切換到完整模式進行修正。", + "switch_to_full": "切換完整模式" + }, "sections": { + "connectivity": { + "title": "接入與認證", + "description": "服務位址、連接埠、認證目錄與 API 金鑰" + }, + "logging": { + "title": "日誌與診斷", + "description": "除錯、日誌輸出與使用統計" + }, + "advanced": { + "title": "進階與實驗", + "description": "請求標頭偽裝、簽章快取、外掛來源等不常修改的設定", + "disclosure": "展開進階與實驗設定(預設隱藏)" + }, "server": { "title": "伺服器設定", "description": "基本伺服器設定", From b5344a7586c5f09b255dbc93d5af076f8a35d166 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 19 Jun 2026 01:03:03 +0800 Subject: [PATCH 2/6] 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 --- .../config/VisualConfigEditor.module.scss | 8 + src/components/config/VisualConfigEditor.tsx | 326 +++++++++--------- src/i18n/locales/en.json | 3 +- src/i18n/locales/ru.json | 3 +- src/i18n/locales/zh-CN.json | 3 +- src/i18n/locales/zh-TW.json | 3 +- 6 files changed, 185 insertions(+), 161 deletions(-) diff --git a/src/components/config/VisualConfigEditor.module.scss b/src/components/config/VisualConfigEditor.module.scss index 2c998e16..254ae277 100644 --- a/src/components/config/VisualConfigEditor.module.scss +++ b/src/components/config/VisualConfigEditor.module.scss @@ -227,6 +227,14 @@ min-width: 0; } +// Wrap bare inputs so they read as cards consistent with toggleRow / API key blocks. +.simpleField { + padding: 14px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: transparent; +} + .simpleBanner { display: flex; align-items: center; diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index ff1070d9..179a1d07 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -539,9 +539,7 @@ export function VisualConfigEditor({ > + )) + ) : ( +
+ {t('config_management.visual.search.no_results')} +
+ )} +
+ ) : null} +
{mode === 'simple' ? ( @@ -654,24 +824,28 @@ export function VisualConfigEditor({ > - onChange({ host: e.target.value })} - disabled={disabled} - /> + + onChange({ host: e.target.value })} + disabled={disabled} + /> + {portField} - onChange({ authDir: e.target.value })} - disabled={disabled} - hint={t('config_management.visual.sections.auth.auth_dir_hint')} - /> + + onChange({ authDir: e.target.value })} + disabled={disabled} + hint={t('config_management.visual.sections.auth.auth_dir_hint')} + /> + {apiKeysField} @@ -681,32 +855,38 @@ export function VisualConfigEditor({ defaultOpen={false} > - onChange({ tlsEnable })} - /> + + onChange({ tlsEnable })} + /> + {values.tlsEnable ? ( <> - onChange({ tlsCert: e.target.value })} - disabled={disabled} - /> - onChange({ tlsKey: e.target.value })} - disabled={disabled} - /> + + onChange({ tlsCert: e.target.value })} + disabled={disabled} + /> + + + onChange({ tlsKey: e.target.value })} + disabled={disabled} + /> + ) : null} @@ -720,56 +900,66 @@ export function VisualConfigEditor({ > - onChange({ rmAllowRemote })} - /> - onChange({ rmDisableControlPanel })} - /> - - onChange({ rmDisableAutoUpdatePanel }) - } - /> + + onChange({ rmAllowRemote })} + /> + + + onChange({ rmDisableControlPanel })} + /> + + + + onChange({ rmDisableAutoUpdatePanel }) + } + /> + - onChange({ rmSecretKey: e.target.value })} - disabled={disabled} - /> - onChange({ rmPanelRepo: e.target.value })} - disabled={disabled} - /> + + onChange({ rmSecretKey: e.target.value })} + disabled={disabled} + /> + + + onChange({ rmPanelRepo: e.target.value })} + disabled={disabled} + /> + @@ -789,171 +979,207 @@ export function VisualConfigEditor({ {proxyUrlField} - onChange({ requestRetry: e.target.value })} - disabled={disabled} - error={requestRetryError} - /> - onChange({ maxRetryCredentials: e.target.value })} - disabled={disabled} - hint={t('config_management.visual.sections.network.max_retry_credentials_hint')} - error={maxRetryCredentialsError} - /> - onChange({ maxRetryInterval: e.target.value })} - disabled={disabled} - error={maxRetryIntervalError} - /> - onChange({ authAutoRefreshWorkers: e.target.value })} - disabled={disabled} - hint={t( - 'config_management.visual.sections.network.auth_auto_refresh_workers_hint' - )} - error={authAutoRefreshWorkersError} - /> - - onChange({ requestRetry: e.target.value })} disabled={disabled} - ariaLabelledBy={routingStrategyLabelId} - ariaDescribedBy={routingStrategyHintId} - onChange={(nextValue) => - onChange({ - routingStrategy: nextValue as VisualConfigValues['routingStrategy'], - }) - } + error={requestRetryError} /> - - - onChange({ maxRetryCredentials: e.target.value })} disabled={disabled} - ariaLabelledBy={disableImageGenerationLabelId} - ariaDescribedBy={disableImageGenerationHintId} - onChange={(nextValue) => - onChange({ - disableImageGeneration: - nextValue as VisualConfigValues['disableImageGeneration'], - }) - } + hint={t( + 'config_management.visual.sections.network.max_retry_credentials_hint' + )} + error={maxRetryCredentialsError} /> - - onChange({ gptImage2BaseModel: e.target.value })} - disabled={disabled} - hint={t( - 'config_management.visual.sections.network.gpt_image_2_base_model_hint' - )} - /> - onChange({ routingSessionAffinityTTL: e.target.value })} - disabled={disabled} - /> + + + onChange({ maxRetryInterval: e.target.value })} + disabled={disabled} + error={maxRetryIntervalError} + /> + + + onChange({ authAutoRefreshWorkers: e.target.value })} + disabled={disabled} + hint={t( + 'config_management.visual.sections.network.auth_auto_refresh_workers_hint' + )} + error={authAutoRefreshWorkersError} + /> + + + + + onChange({ + disableImageGeneration: + nextValue as VisualConfigValues['disableImageGeneration'], + }) + } + /> + + + + onChange({ gptImage2BaseModel: e.target.value })} + disabled={disabled} + hint={t( + 'config_management.visual.sections.network.gpt_image_2_base_model_hint' + )} + /> + + + onChange({ routingSessionAffinityTTL: e.target.value })} + disabled={disabled} + /> + - onChange({ forceModelPrefix })} - /> - onChange({ passthroughHeaders })} - /> - onChange({ disableCooling })} - /> - onChange({ routingSessionAffinity })} - /> - onChange({ wsAuth })} - /> - onChange({ enableGeminiCliEndpoint })} - /> + + onChange({ forceModelPrefix })} + /> + + + onChange({ passthroughHeaders })} + /> + + + onChange({ disableCooling })} + /> + + + onChange({ routingSessionAffinity })} + /> + + + onChange({ wsAuth })} + /> + + + onChange({ enableGeminiCliEndpoint })} + /> + @@ -971,57 +1197,73 @@ export function VisualConfigEditor({ {debugToggle} - onChange({ commercialMode })} - /> + + onChange({ commercialMode })} + /> + {loggingToFileToggle} - onChange({ logsMaxTotalSizeMb: e.target.value })} - disabled={disabled} - error={logsMaxSizeError} - /> - onChange({ errorLogsMaxFiles: e.target.value })} - disabled={disabled} - error={errorLogsMaxFilesError} - /> - onChange({ redisUsageQueueRetentionSeconds: e.target.value })} - disabled={disabled} - hint={t('config_management.visual.sections.system.redis_usage_retention_hint')} - error={redisUsageQueueRetentionError} - /> + + onChange({ logsMaxTotalSizeMb: e.target.value })} + disabled={disabled} + error={logsMaxSizeError} + /> + + + onChange({ errorLogsMaxFiles: e.target.value })} + disabled={disabled} + error={errorLogsMaxFilesError} + /> + + + + onChange({ redisUsageQueueRetentionSeconds: e.target.value }) + } + disabled={disabled} + hint={t( + 'config_management.visual.sections.system.redis_usage_retention_hint' + )} + error={redisUsageQueueRetentionError} + /> + - onChange({ usageStatisticsEnabled })} - /> + + onChange({ usageStatisticsEnabled })} + /> + @@ -1039,12 +1281,14 @@ export function VisualConfigEditor({ {quotaSwitchProjectToggle} {quotaSwitchPreviewModelToggle} - onChange({ quotaAntigravityCredits })} - /> + + onChange({ quotaAntigravityCredits })} + /> + @@ -1060,91 +1304,99 @@ export function VisualConfigEditor({ > - -
- - onChange({ - streaming: { - ...values.streaming, - keepaliveSeconds: e.target.value, - }, - }) - } - disabled={disabled} - /> - {isKeepaliveDisabled ? ( - - {t('config_management.visual.sections.streaming.disabled')} - - ) : null} -
-
+ + +
+ + onChange({ + streaming: { + ...values.streaming, + keepaliveSeconds: e.target.value, + }, + }) + } + disabled={disabled} + /> + {isKeepaliveDisabled ? ( + + {t('config_management.visual.sections.streaming.disabled')} + + ) : null} +
+
+
- - onChange({ - streaming: { - ...values.streaming, - bootstrapRetries: e.target.value, - }, - }) - } - disabled={disabled} - hint={t('config_management.visual.sections.streaming.bootstrap_hint')} - error={bootstrapRetriesError} - /> + + + onChange({ + streaming: { + ...values.streaming, + bootstrapRetries: e.target.value, + }, + }) + } + disabled={disabled} + hint={t('config_management.visual.sections.streaming.bootstrap_hint')} + error={bootstrapRetriesError} + /> +
- -
- - onChange({ - streaming: { - ...values.streaming, - nonstreamKeepaliveInterval: e.target.value, - }, - }) - } - disabled={disabled} - /> - {isNonstreamKeepaliveDisabled ? ( - - {t('config_management.visual.sections.streaming.disabled')} - - ) : null} -
-
+ + +
+ + onChange({ + streaming: { + ...values.streaming, + nonstreamKeepaliveInterval: e.target.value, + }, + }) + } + disabled={disabled} + /> + {isNonstreamKeepaliveDisabled ? ( + + {t('config_management.visual.sections.streaming.disabled')} + + ) : null} +
+
+
@@ -1166,43 +1418,51 @@ export function VisualConfigEditor({ > - onChange({ pluginsEnabled })} - /> + + onChange({ pluginsEnabled })} + /> + - -
- - -
- {t('config_management.visual.sections.system.plugin_store_sources_hint')} + + +
+ + +
+ {t( + 'config_management.visual.sections.system.plugin_store_sources_hint' + )} +
-
- + + @@ -1211,32 +1471,36 @@ export function VisualConfigEditor({ defaultOpen={false} > - - onChange({ antigravitySignatureCacheEnabled }) - } - /> - - onChange({ antigravitySignatureBypassStrict }) - } - /> + + + onChange({ antigravitySignatureCacheEnabled }) + } + /> + + + + onChange({ antigravitySignatureBypassStrict }) + } + /> + @@ -1252,61 +1516,75 @@ export function VisualConfigEditor({
- onChange({ claudeHeaderUserAgent: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderPackageVersion: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderRuntimeVersion: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderOs: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderArch: e.target.value })} - disabled={disabled} - /> - onChange({ claudeHeaderTimeout: e.target.value })} - disabled={disabled} - /> + + onChange({ claudeHeaderUserAgent: e.target.value })} + disabled={disabled} + /> + + + onChange({ claudeHeaderPackageVersion: e.target.value })} + disabled={disabled} + /> + + + onChange({ claudeHeaderRuntimeVersion: e.target.value })} + disabled={disabled} + /> + + + onChange({ claudeHeaderOs: e.target.value })} + disabled={disabled} + /> + + + onChange({ claudeHeaderArch: e.target.value })} + disabled={disabled} + /> + + + onChange({ claudeHeaderTimeout: e.target.value })} + disabled={disabled} + /> + - - onChange({ claudeHeaderStabilizeDeviceProfile }) - } - /> + + + onChange({ claudeHeaderStabilizeDeviceProfile }) + } + /> +
@@ -1315,33 +1593,39 @@ export function VisualConfigEditor({
- onChange({ codexHeaderUserAgent: e.target.value })} - disabled={disabled} - /> - onChange({ codexHeaderBetaFeatures: e.target.value })} - disabled={disabled} - /> + + onChange({ codexHeaderUserAgent: e.target.value })} + disabled={disabled} + /> + + + onChange({ codexHeaderBetaFeatures: e.target.value })} + disabled={disabled} + /> + - onChange({ codexIdentityConfuse })} - /> + + onChange({ codexIdentityConfuse })} + /> +
@@ -1359,69 +1643,79 @@ export function VisualConfigEditor({ description={t('config_management.visual.sections.payload.description')} > - - - + + + + + - - - + + + + + - - - + + + + + - - - + + + + + - - - + + + + +
diff --git a/src/components/config/configSearchIndex.ts b/src/components/config/configSearchIndex.ts new file mode 100644 index 00000000..2536eb3c --- /dev/null +++ b/src/components/config/configSearchIndex.ts @@ -0,0 +1,499 @@ +// Search index for the visual config editor's global "jump to field" search. +// +// IMPORTANT: this index is maintained by hand and is NOT what drives field +// rendering — it only powers search. When you add, remove, or move a field in +// VisualConfigEditor.tsx, update the matching entry here (and wrap the field's +// JSX in using the same `fieldId`). + +export type VisualSectionId = + | 'connectivity' + | 'network' + | 'logging' + | 'quota' + | 'streaming' + | 'advanced' + | 'payload'; + +export interface ConfigFieldSearchEntry { + /** Stable anchor id; matches FieldAnchor's `fieldId` and the rendered DOM id. */ + fieldId: string; + sectionId: VisualSectionId; + /** i18n key resolved with t() at search time so matching follows the active language. */ + labelKey: string; + /** Optional secondary i18n key shown next to the label to disambiguate duplicates + * (e.g. Claude vs Codex "User-Agent"). Also searchable. */ + qualifierKey?: string; + /** Optional hint i18n key — searchable but not shown in results. */ + hintKey?: string; + /** Backend YAML key aliases, e.g. ['proxy-url']. Static strings (language-agnostic). */ + yamlKeys?: string[]; + /** Extra synonyms to match against (language-agnostic, lowercase). */ + keywords?: string[]; +} + +/** DOM id for a field anchor — kept in one place so the index and the anchors agree. */ +export const configFieldDomId = (fieldId: string) => `cfg-field-${fieldId}`; + +type Translate = (key: string) => string; + +// Compact helper: every label/hint key lives under config_management.visual. +const L = (key: string) => `config_management.visual.${key}`; + +export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [ + // ── connectivity ────────────────────────────────────────────────────────── + { + fieldId: 'host', + sectionId: 'connectivity', + labelKey: L('sections.server.host'), + yamlKeys: ['host'], + }, + { + fieldId: 'port', + sectionId: 'connectivity', + labelKey: L('sections.server.port'), + yamlKeys: ['port'], + }, + { + fieldId: 'authDir', + sectionId: 'connectivity', + labelKey: L('sections.auth.auth_dir'), + hintKey: L('sections.auth.auth_dir_hint'), + yamlKeys: ['auth-dir'], + }, + { + fieldId: 'apiKeys', + sectionId: 'connectivity', + labelKey: L('api_keys.label'), + yamlKeys: ['api-keys'], + keywords: ['api key', 'apikey', 'token'], + }, + { + fieldId: 'tlsEnable', + sectionId: 'connectivity', + labelKey: L('sections.tls.enable'), + hintKey: L('sections.tls.enable_desc'), + yamlKeys: ['tls'], + keywords: ['tls', 'ssl', 'https'], + }, + { + fieldId: 'tlsCert', + sectionId: 'connectivity', + labelKey: L('sections.tls.cert'), + yamlKeys: ['tls', 'cert'], + keywords: ['tls', 'ssl', 'certificate'], + }, + { + fieldId: 'tlsKey', + sectionId: 'connectivity', + labelKey: L('sections.tls.key'), + yamlKeys: ['tls', 'key'], + keywords: ['tls', 'ssl', 'private key'], + }, + { + fieldId: 'rmAllowRemote', + sectionId: 'connectivity', + labelKey: L('sections.remote.allow_remote'), + hintKey: L('sections.remote.allow_remote_desc'), + yamlKeys: ['remote-management', 'allow-remote'], + }, + { + fieldId: 'rmDisableControlPanel', + sectionId: 'connectivity', + labelKey: L('sections.remote.disable_panel'), + yamlKeys: ['remote-management', 'disable-control-panel'], + }, + { + fieldId: 'rmDisableAutoUpdatePanel', + sectionId: 'connectivity', + labelKey: L('sections.remote.disable_auto_update_panel'), + yamlKeys: ['remote-management', 'disable-auto-update-panel'], + }, + { + fieldId: 'rmSecretKey', + sectionId: 'connectivity', + labelKey: L('sections.remote.secret_key'), + yamlKeys: ['remote-management', 'secret-key'], + }, + { + fieldId: 'rmPanelRepo', + sectionId: 'connectivity', + labelKey: L('sections.remote.panel_repo'), + yamlKeys: ['remote-management', 'panel-github-repository'], + }, + // ── network ─────────────────────────────────────────────────────────────── + { + fieldId: 'proxyUrl', + sectionId: 'network', + labelKey: L('sections.network.proxy_url'), + yamlKeys: ['proxy-url'], + }, + { + fieldId: 'requestRetry', + sectionId: 'network', + labelKey: L('sections.network.request_retry'), + yamlKeys: ['request-retry'], + }, + { + fieldId: 'maxRetryCredentials', + sectionId: 'network', + labelKey: L('sections.network.max_retry_credentials'), + hintKey: L('sections.network.max_retry_credentials_hint'), + yamlKeys: ['max-retry-credentials'], + }, + { + fieldId: 'maxRetryInterval', + sectionId: 'network', + labelKey: L('sections.network.max_retry_interval'), + yamlKeys: ['max-retry-interval'], + }, + { + fieldId: 'authAutoRefreshWorkers', + sectionId: 'network', + labelKey: L('sections.network.auth_auto_refresh_workers'), + hintKey: L('sections.network.auth_auto_refresh_workers_hint'), + yamlKeys: ['auth-auto-refresh-workers'], + }, + { + fieldId: 'routingStrategy', + sectionId: 'network', + labelKey: L('sections.network.routing_strategy'), + hintKey: L('sections.network.routing_strategy_hint'), + yamlKeys: ['routing', 'strategy'], + keywords: ['round-robin', 'fill-first'], + }, + { + fieldId: 'disableImageGeneration', + sectionId: 'network', + labelKey: L('sections.network.disable_image_generation'), + hintKey: L('sections.network.disable_image_generation_hint'), + yamlKeys: ['disable-image-generation'], + }, + { + fieldId: 'gptImage2BaseModel', + sectionId: 'network', + labelKey: L('sections.network.gpt_image_2_base_model'), + hintKey: L('sections.network.gpt_image_2_base_model_hint'), + yamlKeys: ['gpt-image-2-base-model'], + }, + { + fieldId: 'routingSessionAffinityTTL', + sectionId: 'network', + labelKey: L('sections.network.session_affinity_ttl'), + yamlKeys: ['routing', 'session-affinity-ttl'], + }, + { + fieldId: 'forceModelPrefix', + sectionId: 'network', + labelKey: L('sections.network.force_model_prefix'), + hintKey: L('sections.network.force_model_prefix_desc'), + yamlKeys: ['force-model-prefix'], + }, + { + fieldId: 'passthroughHeaders', + sectionId: 'network', + labelKey: L('sections.network.passthrough_headers'), + hintKey: L('sections.network.passthrough_headers_desc'), + yamlKeys: ['passthrough-headers'], + }, + { + fieldId: 'disableCooling', + sectionId: 'network', + labelKey: L('sections.network.disable_cooling'), + hintKey: L('sections.network.disable_cooling_desc'), + yamlKeys: ['disable-cooling'], + }, + { + fieldId: 'routingSessionAffinity', + sectionId: 'network', + labelKey: L('sections.network.session_affinity'), + yamlKeys: ['routing', 'session-affinity'], + }, + { + fieldId: 'wsAuth', + sectionId: 'network', + labelKey: L('sections.network.ws_auth'), + hintKey: L('sections.network.ws_auth_desc'), + yamlKeys: ['ws-auth'], + keywords: ['websocket'], + }, + { + fieldId: 'enableGeminiCliEndpoint', + sectionId: 'network', + labelKey: L('sections.network.enable_gemini_cli_endpoint'), + hintKey: L('sections.network.enable_gemini_cli_endpoint_desc'), + yamlKeys: ['enable-gemini-cli-endpoint'], + }, + // ── logging ─────────────────────────────────────────────────────────────── + { + fieldId: 'debug', + sectionId: 'logging', + labelKey: L('sections.system.debug'), + hintKey: L('sections.system.debug_desc'), + yamlKeys: ['debug'], + }, + { + fieldId: 'commercialMode', + sectionId: 'logging', + labelKey: L('sections.system.commercial_mode'), + hintKey: L('sections.system.commercial_mode_desc'), + yamlKeys: ['commercial-mode'], + }, + { + fieldId: 'loggingToFile', + sectionId: 'logging', + labelKey: L('sections.system.logging_to_file'), + hintKey: L('sections.system.logging_to_file_desc'), + yamlKeys: ['logging-to-file'], + }, + { + fieldId: 'logsMaxTotalSizeMb', + sectionId: 'logging', + labelKey: L('sections.system.logs_max_size'), + yamlKeys: ['logs-max-total-size-mb'], + }, + { + fieldId: 'errorLogsMaxFiles', + sectionId: 'logging', + labelKey: L('sections.system.error_logs_max_files'), + yamlKeys: ['error-logs-max-files'], + }, + { + fieldId: 'redisUsageQueueRetentionSeconds', + sectionId: 'logging', + labelKey: L('sections.system.redis_usage_retention'), + hintKey: L('sections.system.redis_usage_retention_hint'), + yamlKeys: ['redis-usage-queue-retention-seconds'], + }, + { + fieldId: 'usageStatisticsEnabled', + sectionId: 'logging', + labelKey: L('sections.system.usage_statistics_enabled'), + hintKey: L('sections.system.usage_statistics_enabled_desc'), + yamlKeys: ['usage-statistics-enabled'], + }, + // ── quota ───────────────────────────────────────────────────────────────── + { + fieldId: 'quotaSwitchProject', + sectionId: 'quota', + labelKey: L('sections.quota.switch_project'), + hintKey: L('sections.quota.switch_project_desc'), + yamlKeys: ['quota-exceeded', 'switch-project'], + }, + { + fieldId: 'quotaSwitchPreviewModel', + sectionId: 'quota', + labelKey: L('sections.quota.switch_preview_model'), + hintKey: L('sections.quota.switch_preview_model_desc'), + yamlKeys: ['quota-exceeded', 'switch-preview-model'], + }, + { + fieldId: 'quotaAntigravityCredits', + sectionId: 'quota', + labelKey: L('sections.quota.antigravity_credits'), + yamlKeys: ['quota-exceeded', 'antigravity-credits'], + }, + // ── streaming ───────────────────────────────────────────────────────────── + { + fieldId: 'streamingKeepaliveSeconds', + sectionId: 'streaming', + labelKey: L('sections.streaming.keepalive_seconds'), + hintKey: L('sections.streaming.keepalive_hint'), + yamlKeys: ['streaming', 'keepalive-seconds'], + }, + { + fieldId: 'streamingBootstrapRetries', + sectionId: 'streaming', + labelKey: L('sections.streaming.bootstrap_retries'), + hintKey: L('sections.streaming.bootstrap_hint'), + yamlKeys: ['streaming', 'bootstrap-retries'], + }, + { + fieldId: 'streamingNonstreamKeepalive', + sectionId: 'streaming', + labelKey: L('sections.streaming.nonstream_keepalive'), + hintKey: L('sections.streaming.nonstream_keepalive_hint'), + yamlKeys: ['streaming', 'nonstream-keepalive-interval'], + }, + // ── advanced ────────────────────────────────────────────────────────────── + { + fieldId: 'pluginsEnabled', + sectionId: 'advanced', + labelKey: L('sections.system.plugins_enabled'), + hintKey: L('sections.system.plugins_enabled_desc'), + yamlKeys: ['plugins'], + }, + { + fieldId: 'pluginStoreSources', + sectionId: 'advanced', + labelKey: L('sections.system.plugin_store_sources'), + hintKey: L('sections.system.plugin_store_sources_hint'), + yamlKeys: ['plugins', 'store-sources'], + }, + { + fieldId: 'antigravitySignatureCacheEnabled', + sectionId: 'advanced', + labelKey: L('sections.system.antigravity_signature_cache'), + hintKey: L('sections.system.antigravity_signature_cache_desc'), + yamlKeys: ['antigravity-signature-cache-enabled'], + }, + { + fieldId: 'antigravitySignatureBypassStrict', + sectionId: 'advanced', + labelKey: L('sections.system.antigravity_signature_strict'), + hintKey: L('sections.system.antigravity_signature_strict_desc'), + yamlKeys: ['antigravity-signature-bypass-strict'], + }, + // Claude header defaults — qualifierKey disambiguates the shared "User-Agent" label. + { + fieldId: 'claudeHeaderUserAgent', + sectionId: 'advanced', + labelKey: L('sections.headers.user_agent'), + qualifierKey: L('sections.headers.claude_title'), + yamlKeys: ['claude-header-defaults', 'user-agent'], + keywords: ['claude'], + }, + { + fieldId: 'claudeHeaderPackageVersion', + sectionId: 'advanced', + labelKey: L('sections.headers.package_version'), + qualifierKey: L('sections.headers.claude_title'), + yamlKeys: ['claude-header-defaults', 'package-version'], + keywords: ['claude'], + }, + { + fieldId: 'claudeHeaderRuntimeVersion', + sectionId: 'advanced', + labelKey: L('sections.headers.runtime_version'), + qualifierKey: L('sections.headers.claude_title'), + yamlKeys: ['claude-header-defaults', 'runtime-version'], + keywords: ['claude'], + }, + { + fieldId: 'claudeHeaderOs', + sectionId: 'advanced', + labelKey: L('sections.headers.os'), + qualifierKey: L('sections.headers.claude_title'), + yamlKeys: ['claude-header-defaults', 'os'], + keywords: ['claude'], + }, + { + fieldId: 'claudeHeaderArch', + sectionId: 'advanced', + labelKey: L('sections.headers.arch'), + qualifierKey: L('sections.headers.claude_title'), + yamlKeys: ['claude-header-defaults', 'arch'], + keywords: ['claude'], + }, + { + fieldId: 'claudeHeaderTimeout', + sectionId: 'advanced', + labelKey: L('sections.headers.timeout'), + qualifierKey: L('sections.headers.claude_title'), + yamlKeys: ['claude-header-defaults', 'timeout'], + keywords: ['claude'], + }, + { + fieldId: 'claudeHeaderStabilizeDeviceProfile', + sectionId: 'advanced', + labelKey: L('sections.headers.stabilize_device'), + qualifierKey: L('sections.headers.claude_title'), + hintKey: L('sections.headers.stabilize_device_desc'), + yamlKeys: ['claude-header-defaults', 'stabilize-device-profile'], + keywords: ['claude'], + }, + // Codex header defaults. + { + fieldId: 'codexHeaderUserAgent', + sectionId: 'advanced', + labelKey: L('sections.headers.user_agent'), + qualifierKey: L('sections.headers.codex_title'), + yamlKeys: ['codex-header-defaults', 'user-agent'], + keywords: ['codex'], + }, + { + fieldId: 'codexHeaderBetaFeatures', + sectionId: 'advanced', + labelKey: L('sections.headers.beta_features'), + qualifierKey: L('sections.headers.codex_title'), + yamlKeys: ['codex-header-defaults', 'beta-features'], + keywords: ['codex'], + }, + { + fieldId: 'codexIdentityConfuse', + sectionId: 'advanced', + labelKey: L('sections.headers.codex_identity_confuse'), + qualifierKey: L('sections.headers.codex_title'), + hintKey: L('sections.headers.codex_identity_confuse_desc'), + yamlKeys: ['codex-header-defaults', 'identity-confuse'], + keywords: ['codex'], + }, + // ── payload (coarse: one entry per rule group) ────────────────────────────── + { + fieldId: 'payloadDefaultRules', + sectionId: 'payload', + labelKey: L('sections.payload.default_rules'), + hintKey: L('sections.payload.default_rules_desc'), + keywords: ['payload', 'rule'], + }, + { + fieldId: 'payloadDefaultRawRules', + sectionId: 'payload', + labelKey: L('sections.payload.default_raw_rules'), + hintKey: L('sections.payload.default_raw_rules_desc'), + keywords: ['payload', 'rule', 'json'], + }, + { + fieldId: 'payloadOverrideRules', + sectionId: 'payload', + labelKey: L('sections.payload.override_rules'), + hintKey: L('sections.payload.override_rules_desc'), + keywords: ['payload', 'rule'], + }, + { + fieldId: 'payloadOverrideRawRules', + sectionId: 'payload', + labelKey: L('sections.payload.override_raw_rules'), + hintKey: L('sections.payload.override_raw_rules_desc'), + keywords: ['payload', 'rule', 'json'], + }, + { + fieldId: 'payloadFilterRules', + sectionId: 'payload', + labelKey: L('sections.payload.filter_rules'), + hintKey: L('sections.payload.filter_rules_desc'), + keywords: ['payload', 'rule', 'filter'], + }, +]; + +const MAX_RESULTS = 8; + +/** + * Lowercase substring search over label + qualifier + hint + YAML keys + keywords. + * Returns the best ~8 matches, label/qualifier hits ranked above alias-only hits. + */ +export function searchConfigFields(query: string, t: Translate): ConfigFieldSearchEntry[] { + const q = query.trim().toLowerCase(); + if (!q) return []; + + const scored: { entry: ConfigFieldSearchEntry; score: number }[] = []; + + for (const entry of CONFIG_FIELD_SEARCH_INDEX) { + const label = t(entry.labelKey).toLowerCase(); + const qualifier = entry.qualifierKey ? t(entry.qualifierKey).toLowerCase() : ''; + const hint = entry.hintKey ? t(entry.hintKey).toLowerCase() : ''; + const yaml = (entry.yamlKeys ?? []).join(' ').toLowerCase(); + const keywords = (entry.keywords ?? []).join(' ').toLowerCase(); + + let score = Number.POSITIVE_INFINITY; + if (label.startsWith(q)) score = 0; + else if (label.includes(q)) score = 1; + else if (qualifier.includes(q) || keywords.includes(q)) score = 2; + else if (yaml.includes(q)) score = 3; + else if (hint.includes(q)) score = 4; + + if (Number.isFinite(score)) scored.push({ entry, score }); + } + + scored.sort((a, b) => a.score - b.score); + return scored.slice(0, MAX_RESULTS).map((item) => item.entry); +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index cdf2ae14..f9ede766 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -917,6 +917,10 @@ "validation_banner": "Some advanced settings have validation errors and are blocking save. Switch to full mode to fix them.", "switch_to_full": "Switch to full mode" }, + "search": { + "placeholder": "Search settings (label or YAML key)", + "no_results": "No matching settings" + }, "sections": { "connectivity": { "title": "Access & Authentication", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index d691bc3d..86cd97c8 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -904,6 +904,10 @@ "validation_banner": "В некоторых дополнительных параметрах есть ошибки проверки, сохранение заблокировано. Переключитесь в полный режим, чтобы исправить их.", "switch_to_full": "Перейти в полный режим" }, + "search": { + "placeholder": "Поиск настроек (по названию или ключу YAML)", + "no_results": "Нет подходящих настроек" + }, "sections": { "connectivity": { "title": "Доступ и аутентификация", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 9cafe6d1..0bc00c59 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -917,6 +917,10 @@ "validation_banner": "部分高级配置项存在校验错误,已阻止保存。请切换到完整模式进行修复。", "switch_to_full": "切换完整模式" }, + "search": { + "placeholder": "搜索配置项(标签或 YAML 键名)", + "no_results": "没有匹配的配置项" + }, "sections": { "connectivity": { "title": "接入与认证", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index aea09558..8606e90a 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -943,6 +943,10 @@ "validation_banner": "部分進階設定項存在驗證錯誤,已阻止儲存。請切換到完整模式進行修正。", "switch_to_full": "切換完整模式" }, + "search": { + "placeholder": "搜尋設定項(標籤或 YAML 鍵名)", + "no_results": "沒有符合的設定項" + }, "sections": { "connectivity": { "title": "接入與認證", From 96e41f59753dda3c15b9d1ff88185bceae74a281 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 19 Jun 2026 01:59:18 +0800 Subject: [PATCH 4/6] fix(config): jump correctly across horizontally snapped sections Field-search jumps used a single scrollIntoView that fought the full-mode sections' horizontal scroll-snap (scroll-snap-type: x mandatory + scroll-snap-align: start), so jumping from one section to a field in another landed on the wrong place. Switch the snap container to the target section instantly first, then scroll the field vertically with inline:'nearest' on the next frame so it can't re-trigger horizontal snapping. --- src/components/config/VisualConfigEditor.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index 92a8d02c..a33b81f2 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -253,8 +253,9 @@ export function VisualConfigEditor({ const el = document.getElementById(configFieldDomId(fieldId)); if (!el) { - // Field not rendered right now (e.g. TLS cert while TLS is disabled) — fall back to section. - sectionRefs.current[sectionId]?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + // Field not rendered right now (e.g. TLS cert while TLS is disabled) — fall back to + // bringing its section into view horizontally. + sectionRefs.current[sectionId]?.scrollIntoView({ block: 'nearest', inline: 'start' }); return; } @@ -270,8 +271,15 @@ export function VisualConfigEditor({ highlightedElRef.current?.classList.remove(styles.fieldHighlightActive); } + // Full-mode sections live in a horizontal scroll-snap container (`scroll-snap-type: x + // mandatory`). A single field-level scrollIntoView() tries to do the horizontal section + // switch AND the vertical field scroll at once, which the snap pulls back / lands wrong. + // So: (1) switch to the target section horizontally and instantly (no smooth → no snap + // fight), then (2) next frame, scroll the field vertically with inline:'nearest' so it + // can't re-trigger horizontal snapping. + sectionRefs.current[sectionId]?.scrollIntoView({ block: 'nearest', inline: 'start' }); requestAnimationFrame(() => { - el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); el.classList.add(styles.fieldHighlightActive); }); highlightedElRef.current = el; From a1d2e1167781258a7199c3b68f1c4a58c30a4712 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 19 Jun 2026 03:20:55 +0800 Subject: [PATCH 5/6] feat(search): enhance keyboard navigation and highlight for search results --- .../config/VisualConfigEditor.module.scss | 5 + src/components/config/VisualConfigEditor.tsx | 108 +++++++++++++++--- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/components/config/VisualConfigEditor.module.scss b/src/components/config/VisualConfigEditor.module.scss index efe72fcf..b0a7c45e 100644 --- a/src/components/config/VisualConfigEditor.module.scss +++ b/src/components/config/VisualConfigEditor.module.scss @@ -263,6 +263,11 @@ } } +// Keyboard/pointer "active" option — the aria-activedescendant target. +.searchResultItemActive { + background: color-mix(in srgb, var(--text-primary) 6%, transparent); +} + .searchResultLabel { display: inline-flex; align-items: baseline; diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index a33b81f2..ce2d1783 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -214,6 +214,10 @@ export function VisualConfigEditor({ // Dropdown visibility is tracked separately from the query text so a jump can close the // results while leaving the typed text in the box for further editing. const [searchOpen, setSearchOpen] = useState(false); + // Highlighted option for keyboard navigation of the results listbox (-1 = none). + const [activeResultIndex, setActiveResultIndex] = useState(0); + const searchListboxId = useId(); + const searchResultsRef = useRef(null); // A fresh object per jump; the effect handles it once (guarded by handledJumpRef) so it // never needs to clear state from inside the effect. const [jumpRequest, setJumpRequest] = useState<{ @@ -231,6 +235,14 @@ export function VisualConfigEditor({ }, []); const searchResults = useMemo(() => searchConfigFields(searchQuery, t), [searchQuery, t]); + // The results popup is visible only when the box is open AND there's a (trimmed) query. + const isResultsOpen = searchOpen && Boolean(searchQuery.trim()); + // Clamp the highlighted index to the current result set so a stale index (e.g. after the + // query narrows the list) never points past the end or at an option that no longer exists. + const effectiveActiveIndex = + searchResults.length > 0 + ? Math.min(Math.max(activeResultIndex, 0), searchResults.length - 1) + : -1; const handleResultJump = useCallback( (entry: ConfigFieldSearchEntry) => { @@ -310,6 +322,15 @@ export function VisualConfigEditor({ return () => document.removeEventListener('mousedown', handlePointerDown); }, [searchOpen]); + // Keep the highlighted option scrolled into view during keyboard navigation. + useEffect(() => { + if (!isResultsOpen || effectiveActiveIndex < 0) return; + const node = searchResultsRef.current?.querySelector( + `[data-result-index="${effectiveActiveIndex}"]` + ); + node?.scrollIntoView({ block: 'nearest' }); + }, [effectiveActiveIndex, isResultsOpen]); + const isKeepaliveDisabled = values.streaming.keepaliveSeconds === '' || values.streaming.keepaliveSeconds === '0'; const isNonstreamKeepaliveDisabled = @@ -522,6 +543,18 @@ export function VisualConfigEditor({ // Shared high-frequency field blocks — reused verbatim by both the simple view and full mode // so the two layouts never drift apart. + const hostField = ( + + onChange({ host: e.target.value })} + disabled={disabled} + /> + + ); + const portField = ( = 0 + ? `${searchListboxId}-opt-${effectiveActiveIndex}` + : undefined + } value={searchQuery} onChange={(e) => { setSearchQuery(e.target.value); setSearchOpen(true); + setActiveResultIndex(0); }} onFocus={() => setSearchOpen(true)} onKeyDown={(e) => { - // Ignore Enter/keys fired while an IME is composing (e.g. picking a - // Chinese candidate) — otherwise candidate selection triggers a jump. + // Ignore keys fired while an IME is composing (e.g. picking a Chinese + // candidate) — otherwise candidate selection triggers navigation/jump. if (e.nativeEvent.isComposing) return; if (e.key === 'Escape') { setSearchOpen(false); - } else if (e.key === 'Enter' && searchOpen && searchResults.length > 0) { + return; + } + if (e.key === 'ArrowDown') { e.preventDefault(); - handleResultJump(searchResults[0]); + if (!isResultsOpen) { + setSearchOpen(true); + return; + } + if (searchResults.length === 0) return; + setActiveResultIndex((effectiveActiveIndex + 1) % searchResults.length); + return; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (!isResultsOpen) { + setSearchOpen(true); + return; + } + if (searchResults.length === 0) return; + setActiveResultIndex( + effectiveActiveIndex <= 0 ? searchResults.length - 1 : effectiveActiveIndex - 1 + ); + return; + } + if (e.key === 'Enter' && isResultsOpen && searchResults.length > 0) { + e.preventDefault(); + handleResultJump(searchResults[effectiveActiveIndex] ?? searchResults[0]); } }} rightElement={ @@ -711,16 +778,28 @@ export function VisualConfigEditor({ } /> - {searchOpen && searchQuery.trim() ? ( -
+ {isResultsOpen ? ( +
{searchResults.length > 0 ? ( - searchResults.map((entry) => ( + searchResults.map((entry, index) => (