mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-20 12:21:54 +08:00
✨ feat(settings): 统一设置中心内页与代理交互
- 将主题设置、AI 设置和关于页纳入设置中心详情页,补齐返回和取消交互 - 重构全局代理设置为草稿应用模式,新增常用预设、URL 测试入口和测试结果展示 - 按参考样式重做关于页版本与项目入口布局,并收紧顶部品牌卡尺寸 - 提取 AISettingsContent 供设置中心内嵌复用,控制详情页内部滚动边界 - 补齐多语言文案和设置中心静态回归断言
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user