From f5926566806dd1a71b67e359e218600a0e6e08f3 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Tue, 16 Jun 2026 06:02:20 -0400 Subject: [PATCH] refactor(web): unify settings panel layouts with shared toolbar/body - Add PanelToolbar/PanelBody primitives so all four settings tabs share the same top-toolbar + scrollable-body rhythm under the unified header. - API panel: drop the heavy gray shadowed TabsList; move the create action into the toolbar next to the tabs, lighten per-tab hints. - Storage panel: reuse PanelToolbar for the generated-at/refresh bar. - Account panel: wrap content in PanelBody for consistent padding. - Models panel: keep the pinned LangBot Models (Space) card at the very top, above the add-custom-provider row (intentional pin), using PanelBody instead of a top toolbar. --- .../bot-form/RoutingRulesEditor.tsx | 1 - .../AccountSettingsPanel.tsx | 5 +- .../ApiIntegrationPanel.tsx | 390 +++++++++--------- .../dynamic-form/DynamicFormItemConfig.ts | 3 +- .../components/models-dialog/ModelsPanel.tsx | 22 +- .../settings-dialog/panel-layout.tsx | 45 ++ .../StorageAnalysisPanel.tsx | 5 +- .../overview-cards/SystemStatusCards.tsx | 1 - .../pipeline-form/PipelineFormComponent.tsx | 5 +- .../plugin-installed/PluginCardVO.ts | 4 +- web/src/app/infra/entities/common.ts | 2 +- web/src/app/infra/entities/form/dynamic.ts | 2 +- web/src/app/infra/entities/plugin/index.ts | 2 +- web/src/app/wizard/page.tsx | 2 +- 14 files changed, 262 insertions(+), 227 deletions(-) create mode 100644 web/src/app/home/components/settings-dialog/panel-layout.tsx diff --git a/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx b/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx index 42b86626..f8c7efbd 100644 --- a/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx +++ b/web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx @@ -48,7 +48,6 @@ interface PipelineOption { } interface RoutingRulesEditorProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any form: UseFormReturn; pipelineNameList: PipelineOption[]; } diff --git a/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx b/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx index 5cf7e4c8..0795f413 100644 --- a/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx +++ b/web/src/app/home/components/account-settings-dialog/AccountSettingsPanel.tsx @@ -14,6 +14,7 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { systemInfo } from '@/app/infra/http'; import { Loader2, ExternalLink, KeyRound, Layers } from 'lucide-react'; import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog'; +import { PanelBody } from '../settings-dialog/panel-layout'; interface AccountSettingsPanelProps { // True when this panel is the active section and the dialog is open. @@ -86,7 +87,7 @@ export default function AccountSettingsPanel({ }; return ( -
+ {userEmail && (

{userEmail}

)} @@ -165,6 +166,6 @@ export default function AccountSettingsPanel({ onOpenChange={handlePasswordDialogClose} hasPassword={hasPassword} /> -
+ ); } diff --git a/web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx b/web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx index e45d5f50..9cb14a69 100644 --- a/web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx +++ b/web/src/app/home/components/api-integration-dialog/ApiIntegrationPanel.tsx @@ -36,6 +36,7 @@ import { } from '@/components/ui/alert-dialog'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import { backendClient } from '@/app/infra/http'; +import { PanelToolbar } from '../settings-dialog/panel-layout'; interface ApiKey { id: number; @@ -252,216 +253,209 @@ export default function ApiIntegrationPanel({ return ( <> -
- - - - {t('common.apiKeys')} - - - {t('common.webhooks')} - + + + + {t('common.apiKeys')} + {t('common.webhooks')} + {activeTab === 'apikeys' ? ( + + ) : ( + + )} + - {/* API Keys Tab */} - -
- {t('common.apiKeyHint')} + {/* API Keys Tab */} + +

+ {t('common.apiKeyHint')} +

+ + {loading ? ( +
+ {t('common.loading')}
- -
- + ) : apiKeys.length === 0 ? ( +
+ {t('common.noApiKeys')}
- - {loading ? ( -
- {t('common.loading')} -
- ) : apiKeys.length === 0 ? ( -
- {t('common.noApiKeys')} -
- ) : ( -
- - - - - {t('common.name')} - - - {t('common.apiKeyValue')} - - - {t('common.actions')} - - - - - {apiKeys.map((item) => ( - - -
-
{item.name}
- {item.description && ( -
- {item.description} -
- )} -
-
- - - {maskApiKey(item.key)} - - - -
- - -
-
-
- ))} -
-
-
- )} - - - {/* Webhooks Tab */} - -
- {t('common.webhookHint')} -
- -
- -
- - {loading ? ( -
- {t('common.loading')} -
- ) : webhooks.length === 0 ? ( -
- {t('common.noWebhooks')} -
- ) : ( -
- - - - - {t('common.name')} - - - {t('common.webhookUrl')} - - - {t('common.webhookEnabled')} - - - {t('common.actions')} - - - - - {webhooks.map((webhook) => ( - - -
-
- {webhook.name} + ) : ( +
+
+ + + + {t('common.name')} + + + {t('common.apiKeyValue')} + + + {t('common.actions')} + + + + + {apiKeys.map((item) => ( + + +
+
{item.name}
+ {item.description && ( +
+ {item.description}
- {webhook.description && ( -
- {webhook.description} -
- )} -
-
- -
- - {webhook.url} - -
-
- - handleToggleWebhook(webhook)} - /> - - + )} + + + + + {maskApiKey(item.key)} + + + +
+ - - - ))} - -
-
- )} -
- -
+
+ + + ))} + + +
+ )} + + + {/* Webhooks Tab */} + +

+ {t('common.webhookHint')} +

+ + {loading ? ( +
+ {t('common.loading')} +
+ ) : webhooks.length === 0 ? ( +
+ {t('common.noWebhooks')} +
+ ) : ( +
+ + + + + {t('common.name')} + + + {t('common.webhookUrl')} + + + {t('common.webhookEnabled')} + + + {t('common.actions')} + + + + + {webhooks.map((webhook) => ( + + +
+
+ {webhook.name} +
+ {webhook.description && ( +
+ {webhook.description} +
+ )} +
+
+ +
+ + {webhook.url} + +
+
+ + handleToggleWebhook(webhook)} + /> + + + + +
+ ))} +
+
+
+ )} +
+ {/* Create API Key Dialog */} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts index b11e09d2..62f1dbf0 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts @@ -47,7 +47,6 @@ export function parseDynamicFormItemType(value: string): DynamicFormItemType { export function getDefaultValues( itemConfigList: IDynamicFormItemSchema[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { return itemConfigList.reduce( (acc, item) => { @@ -59,7 +58,7 @@ export function getDefaultValues( acc[item.name] = item.default; return acc; }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as Record, ); } diff --git a/web/src/app/home/components/models-dialog/ModelsPanel.tsx b/web/src/app/home/components/models-dialog/ModelsPanel.tsx index 7dd32c13..a71bf758 100644 --- a/web/src/app/home/components/models-dialog/ModelsPanel.tsx +++ b/web/src/app/home/components/models-dialog/ModelsPanel.tsx @@ -23,6 +23,7 @@ import { LANGBOT_MODELS_PROVIDER_REQUESTER, } from './types'; import { CustomApiError } from '@/app/infra/entities/common'; +import { PanelBody } from '../settings-dialog/panel-layout'; interface ModelsPanelProps { // True when this panel is the active section and the dialog is open. @@ -611,12 +612,13 @@ export default function ModelsPanel({ return ( <> -
- {/* LangBot Models Card */} + + {/* LangBot Models (Space) provider card is intentionally pinned to the + top, above the "add custom provider" action row. */} {langbotProvider && renderProviderCard(langbotProvider, true)} - {/* Add Provider Button */} -
+ {/* Add-provider row: stays below the pinned card by design. */} +
{otherProviders.length === 0 ? t( @@ -626,12 +628,10 @@ export default function ModelsPanel({ ) : t('models.providerCount', { count: otherProviders.length })} -
- -
+
{/* Provider List */} @@ -643,7 +643,7 @@ export default function ModelsPanel({ ) : ( otherProviders.map((p) => renderProviderCard(p)) )} -
+
diff --git a/web/src/app/home/components/settings-dialog/panel-layout.tsx b/web/src/app/home/components/settings-dialog/panel-layout.tsx new file mode 100644 index 00000000..27ad7a3c --- /dev/null +++ b/web/src/app/home/components/settings-dialog/panel-layout.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +/** + * Shared layout primitives for the settings-dialog panels. + * + * Every section renders under the dialog's unified header, so the panels + * themselves should share the same vertical rhythm: an optional top toolbar + * (meta on the left, primary action on the right) followed by a scrollable + * body with consistent padding. Keeping these in one place is what makes the + * tabs feel like one cohesive surface instead of four separately-styled views. + */ + +export function PanelToolbar({ + className, + children, +}: { + className?: string; + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} + +export function PanelBody({ + className, + children, +}: { + className?: string; + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/web/src/app/home/components/storage-analysis-dialog/StorageAnalysisPanel.tsx b/web/src/app/home/components/storage-analysis-dialog/StorageAnalysisPanel.tsx index 0488616c..833f5e85 100644 --- a/web/src/app/home/components/storage-analysis-dialog/StorageAnalysisPanel.tsx +++ b/web/src/app/home/components/storage-analysis-dialog/StorageAnalysisPanel.tsx @@ -21,6 +21,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { backendClient } from '@/app/infra/http'; +import { PanelToolbar } from '../settings-dialog/panel-layout'; interface StorageSection { key: string; @@ -137,7 +138,7 @@ export default function StorageAnalysisPanel({ return (
-
+
{analysis ? t('storageAnalysis.generatedAt', { @@ -156,7 +157,7 @@ export default function StorageAnalysisPanel({ /> {t('storageAnalysis.refresh')} -
+
diff --git a/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx b/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx index 8b2f65ea..46be50a9 100644 --- a/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx +++ b/web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx @@ -82,7 +82,6 @@ export default function SystemStatusCard({ fetchStatus(); const interval = setInterval(fetchStatus, 30_000); return () => clearInterval(interval); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetchStatus, refreshKey]); const pluginOk = pluginStatus diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 863c2202..5df4baa8 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -323,7 +323,6 @@ export default function PipelineFormComponent({ const isFirstEmission = !initializedStagesRef.current.has(stageKey); const currentValues = - // eslint-disable-next-line @typescript-eslint/no-explicit-any (form.getValues(formName) as Record) || {}; form.setValue(formName, { ...currentValues, @@ -368,7 +367,6 @@ export default function PipelineFormComponent({ )?.[stage.name] || {} } @@ -402,7 +400,6 @@ export default function PipelineFormComponent({ )?.[stage.name] || {} } @@ -445,7 +442,7 @@ export default function PipelineFormComponent({ // make the locked selector display a scope that is NOT the one actually in // effect. Coerce the displayed/saved value to the forced template so the UI // truthfully reflects runtime behavior. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stageInitialValues: Record = (form.watch(formName) as Record)?.[stage.name] || {}; const effectiveInitialValues = diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts index 279161b4..c1546886 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts @@ -9,7 +9,7 @@ export interface IPluginCardVO { enabled: boolean; priority: number; install_source: string; - install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + install_info: Record; status: string; components: PluginComponent[]; debug: boolean; @@ -27,7 +27,7 @@ export class PluginCardVO implements IPluginCardVO { priority: number; debug: boolean; install_source: string; - install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + install_info: Record; status: string; components: PluginComponent[]; hasUpdate?: boolean; diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts index 4f04c2ef..942fc545 100644 --- a/web/src/app/infra/entities/common.ts +++ b/web/src/app/infra/entities/common.ts @@ -21,7 +21,7 @@ export interface ComponentManifest { version?: string; author?: string; }; - spec: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + spec: Record; } export interface CustomApiError { diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts index e2dca5c3..44fed3ac 100644 --- a/web/src/app/infra/entities/form/dynamic.ts +++ b/web/src/app/infra/entities/form/dynamic.ts @@ -8,7 +8,7 @@ export const SYSTEM_FIELD_PREFIX = '__system.'; export interface IShowIfCondition { field: string; operator: 'eq' | 'neq' | 'in'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; } diff --git a/web/src/app/infra/entities/plugin/index.ts b/web/src/app/infra/entities/plugin/index.ts index ad661211..9431fae5 100644 --- a/web/src/app/infra/entities/plugin/index.ts +++ b/web/src/app/infra/entities/plugin/index.ts @@ -10,7 +10,7 @@ export interface Plugin { debug: boolean; enabled: boolean; install_source: string; - install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + install_info: Record; components: PluginComponent[]; } diff --git a/web/src/app/wizard/page.tsx b/web/src/app/wizard/page.tsx index 1fd39375..a3afd07c 100644 --- a/web/src/app/wizard/page.tsx +++ b/web/src/app/wizard/page.tsx @@ -86,7 +86,7 @@ export default function WizardPage() { const [selectedAdapter, setSelectedAdapter] = useState(null); const [selectedRunner, setSelectedRunner] = useState(null); const [botName, setBotName] = useState(''); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [botDescription, _setBotDescription] = useState(''); const [adapterConfig, setAdapterConfig] = useState>( {},