feat(settings): 统一设置中心内页与代理交互

- 将主题设置、AI 设置和关于页纳入设置中心详情页,补齐返回和取消交互

- 重构全局代理设置为草稿应用模式,新增常用预设、URL 测试入口和测试结果展示

- 按参考样式重做关于页版本与项目入口布局,并收紧顶部品牌卡尺寸

- 提取 AISettingsContent 供设置中心内嵌复用,控制详情页内部滚动边界

- 补齐多语言文案和设置中心静态回归断言
This commit is contained in:
Syngnat
2026-07-08 17:37:05 +08:00
parent b94168957e
commit da895ea502
10 changed files with 2446 additions and 1195 deletions

View File

@@ -44,6 +44,14 @@ interface AISettingsModalProps {
onBeforeExternalMCPUse?: () => Promise<void>;
}
export interface AISettingsContentProps {
active: boolean;
darkMode: boolean;
overlayTheme: OverlayWorkbenchTheme;
focusProviderId?: string;
onBeforeExternalMCPUse?: () => Promise<void>;
}
const DEFAULT_MCP_HTTP_SERVER_STATUS: AIMCPHTTPServerStatus = {
running: false,
addr: '127.0.0.1:8765',
@@ -80,7 +88,7 @@ const normalizeMCPHTTPAuthorizationToken = (value: string): string => {
return withoutHeaderName.replace(/^Bearer\s+/i, '').trim();
};
const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMode, overlayTheme, focusProviderId, onBeforeExternalMCPUse }) => {
export const AISettingsContent: React.FC<AISettingsContentProps> = ({ active, darkMode, overlayTheme, focusProviderId, onBeforeExternalMCPUse }) => {
const { t } = useI18n();
const defaultMCPHTTPServerStatus = useMemo<AIMCPHTTPServerStatus>(() => ({
...DEFAULT_MCP_HTTP_SERVER_STATUS,
@@ -251,16 +259,16 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
} catch (e) { console.warn('Failed to load AI config', e); }
}, [defaultMCPHTTPServerStatus, resolveAIService, syncMCPClientStatuses]);
useEffect(() => { if (open) void loadConfig(); }, [open, loadConfig]);
useEffect(() => { if (active) void loadConfig(); }, [active, loadConfig]);
useEffect(() => {
if (open) {
if (active) {
resetMCPClientSelectionTouched();
}
}, [open, resetMCPClientSelectionTouched]);
}, [active, resetMCPClientSelectionTouched]);
useEffect(() => {
if (!open || !focusProviderId) {
if (!active || !focusProviderId) {
return;
}
if (!providers.some((provider) => provider.id === focusProviderId)) {
@@ -268,7 +276,7 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
}
setActiveSection('providers');
setActiveProviderId(focusProviderId);
}, [focusProviderId, open, providers]);
}, [active, focusProviderId, providers]);
const applyProviderEditorSession = useCallback((session: ProviderEditorSession) => {
setEditingProvider(session.editingProvider as AIProviderConfig | null);
@@ -285,16 +293,11 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
applyProviderEditorSession(buildClosedProviderEditorSession());
}, [applyProviderEditorSession]);
const handleModalClose = useCallback(() => {
resetProviderEditorSession();
onClose();
}, [onClose, resetProviderEditorSession]);
useEffect(() => {
if (!open) {
if (!active) {
resetProviderEditorSession();
}
}, [open, resetProviderEditorSession]);
}, [active, resetProviderEditorSession]);
const handleAddProvider = () => {
const preset = findPreset('openai');
applyProviderEditorSession(buildAddProviderEditorSession({
@@ -707,6 +710,148 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
});
};
return (
<div ref={modalBodyRef} className="ai-settings-body" style={{ display: 'grid', gridTemplateColumns: '180px minmax(0, 1fr)', gap: 16, padding: '12px 0', height: '100%', minHeight: 0, overflow: 'hidden', alignItems: 'stretch', position: 'relative', boxSizing: 'border-box' }}>
{messageContextHolder}
<AISettingsSidebar
activeSection={activeSection}
darkMode={darkMode}
overlayTheme={overlayTheme}
onSelectSection={setActiveSection}
/>
<div style={{ minWidth: 0, minHeight: 0, height: '100%', overflowY: 'auto', overflowX: 'hidden', overscrollBehavior: 'contain', paddingRight: 8, paddingBottom: 28 }}>
{activeSection === 'providers' && (
<AISettingsProvidersSection
providers={providers}
activeProviderId={activeProviderId}
editingProvider={editingProvider}
isEditing={isEditing}
form={form}
providerPresets={localizedProviderPresets}
watchedPresetKey={watchedPresetKey}
watchedApiFormat={watchedApiFormat}
loading={loading}
testStatus={testStatus}
primaryPasswordVisible={primaryPasswordVisible}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
onPrimaryPasswordVisibleChange={setPrimaryPasswordVisible}
resolveProviderPreset={matchLocalizedProviderPreset}
resolvePresetByKey={findLocalizedPreset}
onAddProvider={handleAddProvider}
onEditProvider={handleEditProvider}
onDeleteProvider={handleDeleteProvider}
onSetActiveProvider={handleSetActive}
onCancelEdit={resetProviderEditorSession}
onPresetChange={handlePresetChange}
onTestProvider={handleTestProvider}
onSaveProvider={handleSaveProvider}
/>
)}
{activeSection === 'safety' && (
<AISettingsSafetySection
safetyLevel={safetyLevel}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
onChange={handleSafetyChange}
/>
)}
{activeSection === 'context' && (
<AISettingsContextSection
contextLevel={contextLevel}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
onChange={handleContextChange}
/>
)}
{activeSection === 'mcp' && (
<AISettingsMCPSection
mcpClientStatuses={mcpClientStatuses}
selectedMCPClient={selectedMCPClient}
selectedMCPClientStatus={selectedMCPClientStatus}
selectedMCPClientCommandText={selectedMCPClientCommandText}
mcpHTTPServerStatus={mcpHTTPServerStatus}
mcpHTTPServerDraft={mcpHTTPServerDraft}
mcpServers={mcpServers}
mcpTools={mcpTools}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
loading={loading}
mcpClientStatusLoading={mcpClientStatusLoading}
mcpHTTPServerLoading={mcpHTTPServerLoading}
onUpdateHTTPServerDraft={handleUpdateMCPHTTPServerDraft}
onToggleHTTPServer={handleToggleMCPHTTPServer}
onCopyHTTPServerURL={() => void handleCopyMCPHTTPServerURL()}
onCopyHTTPServerAuthorization={() => void handleCopyMCPHTTPServerAuthorization()}
onSelectClient={handleSelectMCPClient}
onRefreshStatus={() => void loadMCPClientStatuses()}
onCopyConfigPath={() => void handleCopySelectedMCPConfigPath()}
onCopyLaunchCommand={() => void handleCopySelectedMCPLaunchCommand()}
onInstallSelectedClient={handleInstallSelectedMCPClient}
onAddServer={handleAddMCPServer}
onUpdateServerDraft={updateMCPServerDraft}
onTestServer={handleTestMCPServer}
onSaveServer={handleSaveMCPServer}
onDeleteServer={handleDeleteMCPServer}
/>
)}
{activeSection === 'skills' && (
<AISettingsSkillsSection
skills={skills}
skillRequiredToolOptions={skillRequiredToolOptions}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
loading={loading}
onAddSkill={handleAddSkill}
onUpdateSkillDraft={updateSkillDraft}
onSaveSkill={handleSaveSkill}
onDeleteSkill={handleDeleteSkill}
/>
)}
{activeSection === 'tools' && (
<AIBuiltinToolsCatalog
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
/>
)}
{activeSection === 'prompts' && (
<AISettingsPromptsSection
builtinPrompts={builtinPrompts}
userPromptSettings={userPromptSettings}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
darkMode={darkMode}
loading={loading}
onChangeUserPrompt={(key, value) => setUserPromptSettings((prev) => ({
...prev,
[key]: value,
}))}
onSave={handleSaveUserPromptSettings}
/>
)}
</div>
</div>
);
};
const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMode, overlayTheme, focusProviderId, onBeforeExternalMCPUse }) => {
const { t } = useI18n();
const modalShellStyle = {
background: overlayTheme.shellBg, border: overlayTheme.shellBorder,
boxShadow: overlayTheme.shellShadow, backdropFilter: overlayTheme.shellBackdropFilter,
@@ -731,7 +876,7 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
</div>
}
open={open}
onCancel={handleModalClose}
onCancel={onClose}
footer={null}
width={820}
styles={{
@@ -740,142 +885,13 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
body: { paddingTop: 8, height: 620, overflow: 'hidden' },
}}
>
<div ref={modalBodyRef} className="ai-settings-body" style={{ display: 'grid', gridTemplateColumns: '180px minmax(0, 1fr)', gap: 16, padding: '12px 0', height: '100%', minHeight: 0, overflow: 'hidden', alignItems: 'stretch', position: 'relative' }}>
{messageContextHolder}
<AISettingsSidebar
activeSection={activeSection}
darkMode={darkMode}
overlayTheme={overlayTheme}
onSelectSection={setActiveSection}
/>
<div style={{ minWidth: 0, minHeight: 0, height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 8, paddingBottom: 28 }}>
{activeSection === 'providers' && (
<AISettingsProvidersSection
providers={providers}
activeProviderId={activeProviderId}
editingProvider={editingProvider}
isEditing={isEditing}
form={form}
providerPresets={localizedProviderPresets}
watchedPresetKey={watchedPresetKey}
watchedApiFormat={watchedApiFormat}
loading={loading}
testStatus={testStatus}
primaryPasswordVisible={primaryPasswordVisible}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
onPrimaryPasswordVisibleChange={setPrimaryPasswordVisible}
resolveProviderPreset={matchLocalizedProviderPreset}
resolvePresetByKey={findLocalizedPreset}
onAddProvider={handleAddProvider}
onEditProvider={handleEditProvider}
onDeleteProvider={handleDeleteProvider}
onSetActiveProvider={handleSetActive}
onCancelEdit={resetProviderEditorSession}
onPresetChange={handlePresetChange}
onTestProvider={handleTestProvider}
onSaveProvider={handleSaveProvider}
/>
)}
{activeSection === 'safety' && (
<AISettingsSafetySection
safetyLevel={safetyLevel}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
onChange={handleSafetyChange}
/>
)}
{activeSection === 'context' && (
<AISettingsContextSection
contextLevel={contextLevel}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
onChange={handleContextChange}
/>
)}
{activeSection === 'mcp' && (
<AISettingsMCPSection
mcpClientStatuses={mcpClientStatuses}
selectedMCPClient={selectedMCPClient}
selectedMCPClientStatus={selectedMCPClientStatus}
selectedMCPClientCommandText={selectedMCPClientCommandText}
mcpHTTPServerStatus={mcpHTTPServerStatus}
mcpHTTPServerDraft={mcpHTTPServerDraft}
mcpServers={mcpServers}
mcpTools={mcpTools}
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
loading={loading}
mcpClientStatusLoading={mcpClientStatusLoading}
mcpHTTPServerLoading={mcpHTTPServerLoading}
onUpdateHTTPServerDraft={handleUpdateMCPHTTPServerDraft}
onToggleHTTPServer={handleToggleMCPHTTPServer}
onCopyHTTPServerURL={() => void handleCopyMCPHTTPServerURL()}
onCopyHTTPServerAuthorization={() => void handleCopyMCPHTTPServerAuthorization()}
onSelectClient={handleSelectMCPClient}
onRefreshStatus={() => void loadMCPClientStatuses()}
onCopyConfigPath={() => void handleCopySelectedMCPConfigPath()}
onCopyLaunchCommand={() => void handleCopySelectedMCPLaunchCommand()}
onInstallSelectedClient={handleInstallSelectedMCPClient}
onAddServer={handleAddMCPServer}
onUpdateServerDraft={updateMCPServerDraft}
onTestServer={handleTestMCPServer}
onSaveServer={handleSaveMCPServer}
onDeleteServer={handleDeleteMCPServer}
/>
)}
{activeSection === 'skills' && (
<AISettingsSkillsSection
skills={skills}
skillRequiredToolOptions={skillRequiredToolOptions}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
loading={loading}
onAddSkill={handleAddSkill}
onUpdateSkillDraft={updateSkillDraft}
onSaveSkill={handleSaveSkill}
onDeleteSkill={handleDeleteSkill}
/>
)}
{activeSection === 'tools' && (
<AIBuiltinToolsCatalog
darkMode={darkMode}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
/>
)}
{activeSection === 'prompts' && (
<AISettingsPromptsSection
builtinPrompts={builtinPrompts}
userPromptSettings={userPromptSettings}
overlayTheme={overlayTheme}
cardBg={cardBg}
cardBorder={cardBorder}
inputBg={inputBg}
darkMode={darkMode}
loading={loading}
onChangeUserPrompt={(key, value) => setUserPromptSettings((prev) => ({
...prev,
[key]: value,
}))}
onSave={handleSaveUserPromptSettings}
/>
)}
</div>
</div>
<AISettingsContent
active={open}
darkMode={darkMode}
overlayTheme={overlayTheme}
focusProviderId={focusProviderId}
onBeforeExternalMCPUse={onBeforeExternalMCPUse}
/>
</Modal>
);
};