moelove's picture
refactor: completely remove PWA functionality
ce51672
import { useState, useEffect } from 'react';
function Settings({
profiles,
activeProfileId,
onSaveProfiles,
onChangeActiveProfile,
onCloseSettings,
onSaveSummarizationProfile,
summarizationProfile,
mcpServers,
onSaveMcpServers
}) {
const [localProfiles, setLocalProfiles] = useState(profiles);
const [localSummarizationProfile, setLocalSummarizationProfile] = useState(summarizationProfile || {
id: 'default-summarization-profile',
apiEndpoint: '',
apiKey: '',
model: 'DeepSeek-R1'
});
const [localMcpServers, setLocalMcpServers] = useState(mcpServers || []);
// 当 mcpServers 变化时更新本地状态
useEffect(() => {
console.log('mcpServers changed:', mcpServers);
setLocalMcpServers(mcpServers || []);
}, [mcpServers]);
const [editingProfileId, setEditingProfileId] = useState(activeProfileId);
const [editingMcpServerId, setEditingMcpServerId] = useState(null);
const [isHintExpanded, setIsHintExpanded] = useState(false);
const [isMcpHintExpanded, setIsMcpHintExpanded] = useState(false);
const [builtInServers, setBuiltInServers] = useState([]);
const [isLoadingServers, setIsLoadingServers] = useState(false);
const [serverTestResult, setServerTestResult] = useState(null);
const [isTestingConnection, setIsTestingConnection] = useState(false);
const editingProfile = localProfiles.find(p => p.id === editingProfileId) || localProfiles[0];
const editingMcpServer = localMcpServers.find(s => s.id === editingMcpServerId) || null;
// 获取可用的内置 MCP 服务器
useEffect(() => {
const fetchBuiltInServers = async () => {
setIsLoadingServers(true);
try {
console.log('Fetching built-in MCP servers...');
const response = await fetch('/api/mcp/servers/available', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (response.ok) {
const data = await response.json();
console.log('Received built-in MCP servers:', data);
setBuiltInServers(data.servers || []);
} else {
console.error('Failed to fetch built-in MCP servers:', response.status, response.statusText);
// 尝试读取错误消息
const errorText = await response.text();
console.error('Error details:', errorText);
}
} catch (error) {
console.error('Error fetching built-in MCP servers:', error);
} finally {
setIsLoadingServers(false);
}
};
fetchBuiltInServers();
// 每 5 秒刷新一次服务器状态
const intervalId = setInterval(fetchBuiltInServers, 5000);
// 清理定时器
return () => clearInterval(intervalId);
}, []);
const handleProfileChange = (updatedProfile) => {
setLocalProfiles(localProfiles.map(p =>
p.id === updatedProfile.id ? updatedProfile : p
));
};
const handleSummarizationProfileChange = (updatedProfile) => {
setLocalSummarizationProfile(updatedProfile);
};
const handleMcpServerChange = (updatedServer) => {
setLocalMcpServers(localMcpServers.map(s =>
s.id === updatedServer.id ? updatedServer : s
));
};
const handleAddProfile = () => {
const newId = `profile-${Date.now()}`;
const newProfile = {
id: newId,
name: `New Profile ${localProfiles.length + 1}`,
apiEndpoint: '',
apiKey: '',
model: 'DeepSeek-R1'
};
const updatedProfiles = [...localProfiles, newProfile];
setLocalProfiles(updatedProfiles);
setEditingProfileId(newId);
};
const handleDeleteProfile = (profileId) => {
if (localProfiles.length <= 1) {
alert("Cannot delete the last profile");
return;
}
const updatedProfiles = localProfiles.filter(p => p.id !== profileId);
setLocalProfiles(updatedProfiles);
if (editingProfileId === profileId) {
setEditingProfileId(updatedProfiles[0].id);
}
};
// 测试 MCP 服务器连接
const testMcpServerConnection = async (endpoint, authToken) => {
if (!endpoint) return;
setIsTestingConnection(true);
setServerTestResult(null);
try {
const response = await fetch('/api/mcp/servers/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ endpoint, authToken })
});
const result = await response.json();
setServerTestResult(result);
return result.success;
} catch (error) {
console.error('Error testing MCP server connection:', error);
setServerTestResult({
success: false,
message: `Connection error: ${error.message}`,
error: error.message
});
return false;
} finally {
setIsTestingConnection(false);
}
};
// 启动内置 MCP 服务器
const startBuiltInServer = async (serverId) => {
try {
const response = await fetch('/api/mcp/servers/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ serverId })
});
const result = await response.json();
if (result.success) {
// 检查是否已经添加了这个服务器
const existingServer = localMcpServers.find(s =>
s.isBuiltIn && s.builtInId === serverId
);
if (!existingServer) {
// 添加到本地 MCP 服务器列表
const builtInServer = builtInServers.find(s => s.id === serverId);
const newServer = {
id: `built-in-${serverId}-${Date.now()}`,
name: builtInServer ? builtInServer.name : `Built-in Server ${serverId}`,
endpoint: result.endpoint,
authToken: '',
description: builtInServer ? builtInServer.description : 'Built-in MCP server',
isBuiltIn: true,
builtInId: serverId,
isRunning: true
};
const updatedServers = [...localMcpServers, newServer];
setLocalMcpServers(updatedServers);
setEditingMcpServerId(newServer.id);
} else {
// 更新现有服务器的端点
const updatedServers = localMcpServers.map(s =>
s.id === existingServer.id
? { ...s, endpoint: result.endpoint, isRunning: true }
: s
);
setLocalMcpServers(updatedServers);
setEditingMcpServerId(existingServer.id);
}
// 刷新内置服务器列表
const response = await fetch('/api/mcp/servers/available');
if (response.ok) {
const data = await response.json();
setBuiltInServers(data.servers || []);
}
return true;
} else {
console.error('Failed to start built-in MCP server:', result.error);
return false;
}
} catch (error) {
console.error('Error starting built-in MCP server:', error);
return false;
}
};
// 停止内置 MCP 服务器
const stopBuiltInServer = async (serverId) => {
try {
const response = await fetch('/api/mcp/servers/stop', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ serverId })
});
const result = await response.json();
if (result.success) {
// 更新内置服务器的状态
const updatedServers = localMcpServers.map(s =>
s.isBuiltIn && s.builtInId === serverId
? { ...s, isRunning: false }
: s
);
setLocalMcpServers(updatedServers);
// 刷新内置服务器列表
const response = await fetch('/api/mcp/servers/available');
if (response.ok) {
const data = await response.json();
setBuiltInServers(data.servers || []);
}
return true;
} else {
console.error('Failed to stop built-in MCP server:', result.error);
return false;
}
} catch (error) {
console.error('Error stopping built-in MCP server:', error);
return false;
}
};
const handleAddMcpServer = () => {
const newId = `mcp-server-${Date.now()}`;
const newServer = {
id: newId,
name: `MCP Server ${localMcpServers.length + 1}`,
endpoint: '',
authToken: '',
description: '',
isBuiltIn: false
};
const updatedServers = [...localMcpServers, newServer];
setLocalMcpServers(updatedServers);
setEditingMcpServerId(newId);
};
const handleDeleteMcpServer = (serverId) => {
const updatedServers = localMcpServers.filter(s => s.id !== serverId);
setLocalMcpServers(updatedServers);
if (editingMcpServerId === serverId) {
setEditingMcpServerId(updatedServers.length > 0 ? updatedServers[0].id : null);
}
};
const handleSubmit = (e) => {
e.preventDefault();
onSaveProfiles(localProfiles);
onSaveSummarizationProfile(localSummarizationProfile);
if (onSaveMcpServers) {
onSaveMcpServers(localMcpServers);
}
onChangeActiveProfile(editingProfileId);
onCloseSettings();
};
return (
<div className="settings-panel">
<h2>Settings</h2>
<div className="profiles-section">
<div className="profiles-header">
<h3>Chat Profiles</h3>
<button
type="button"
className="add-profile-button"
onClick={handleAddProfile}
>
+ Add Profile
</button>
</div>
<div className="profiles-list">
{localProfiles.map(profile => (
<div
key={profile.id}
className={`profile-item ${profile.id === editingProfileId ? 'active' : ''}`}
onClick={() => setEditingProfileId(profile.id)}
>
<span>{profile.name}</span>
{localProfiles.length > 1 && (
<button
className="delete-profile-button"
onClick={(e) => {
e.stopPropagation();
handleDeleteProfile(profile.id);
}}
>
×
</button>
)}
</div>
))}
</div>
</div>
<form onSubmit={handleSubmit}>
{editingProfile && (
<div className="current-profile-section">
<h3>Edit Chat Profile: {editingProfile.name}</h3>
<div className="setting-item">
<label>Profile Name:</label>
<input
type="text"
value={editingProfile.name}
onChange={(e) => handleProfileChange({
...editingProfile,
name: e.target.value
})}
placeholder="Enter profile name"
/>
</div>
<div className="setting-item">
<label>API Endpoint:</label>
<input
type="text"
value={editingProfile.apiEndpoint}
onChange={(e) => handleProfileChange({
...editingProfile,
apiEndpoint: e.target.value
})}
placeholder="Enter API endpoint"
/>
<div className="setting-hint">
<button
type="button"
className="hint-toggle"
onClick={() => setIsHintExpanded(!isHintExpanded)}
>
{isHintExpanded ? 'Hide' : 'Show'} API Endpoint Format Examples
</button>
<div className={`hint-content ${isHintExpanded ? 'expanded' : ''}`}>
<p>API Endpoint format examples:</p>
<ul>
<li>Ends with / → /chat/completions will be appended</li>
<li>Ends with # → # will be removed</li>
<li>Other cases → /v1/chat/completions will be appended</li>
</ul>
</div>
</div>
</div>
<div className="setting-item">
<label>API Key:</label>
<input
type="password"
value={editingProfile.apiKey}
onChange={(e) => handleProfileChange({
...editingProfile,
apiKey: e.target.value
})}
placeholder="Enter your API key"
/>
</div>
<div className="setting-item">
<label>Model:</label>
<input
type="text"
value={editingProfile.model}
onChange={(e) => handleProfileChange({
...editingProfile,
model: e.target.value
})}
placeholder="Enter model name (e.g., DeepSeek-R1)"
/>
</div>
</div>
)}
<div className="profiles-section">
<h3>Summarization Profile</h3>
<div className="current-profile-section">
<div className="setting-item">
<label>API Endpoint:</label>
<input
type="text"
value={localSummarizationProfile.apiEndpoint}
onChange={(e) => handleSummarizationProfileChange({
...localSummarizationProfile,
apiEndpoint: e.target.value
})}
placeholder="Enter API endpoint"
/>
</div>
<div className="setting-item">
<label>API Key:</label>
<input
type="password"
value={localSummarizationProfile.apiKey}
onChange={(e) => handleSummarizationProfileChange({
...localSummarizationProfile,
apiKey: e.target.value
})}
placeholder="Enter your API key"
/>
</div>
<div className="setting-item">
<label>Model:</label>
<input
type="text"
value={localSummarizationProfile.model}
onChange={(e) => handleSummarizationProfileChange({
...localSummarizationProfile,
model: e.target.value
})}
placeholder="Enter model name (e.g., DeepSeek-R1)"
/>
</div>
</div>
</div>
{/* MCP Servers Section */}
<div className="profiles-section">
<div className="profiles-header">
<h3>MCP Servers</h3>
<button
type="button"
className="add-profile-button"
onClick={handleAddMcpServer}
>
+ Add MCP Server
</button>
</div>
{/* Built-in MCP Servers */}
<div className="built-in-servers-section">
<h4>Built-in MCP Servers</h4>
<div className="built-in-servers-list">
{isLoadingServers ? (
<div className="loading-indicator">Loading built-in servers...</div>
) : builtInServers.length > 0 ? (
builtInServers.map(server => {
// 检查这个内置服务器是否已经添加到用户的服务器列表中
const isAdded = localMcpServers.some(s => s.isBuiltIn && s.builtInId === server.id);
return (
<div key={server.id} className="built-in-server-item">
<div className="server-info">
<span className="server-name">{server.name}</span>
<span className={`server-status ${server.isRunning ? 'running' : 'stopped'}`}>
{server.isRunning ? 'Running' : 'Stopped'}
</span>
</div>
<div className="server-actions">
{isAdded ? (
<button
className="server-action-button"
onClick={() => {
const addedServer = localMcpServers.find(s => s.isBuiltIn && s.builtInId === server.id);
if (addedServer) {
setEditingMcpServerId(addedServer.id);
}
}}
>
View
</button>
) : (
<button
className="server-action-button"
onClick={() => startBuiltInServer(server.id)}
disabled={server.isRunning}
>
Add & Start
</button>
)}
{server.isRunning ? (
<button
className="server-action-button stop"
onClick={() => stopBuiltInServer(server.id)}
>
Stop
</button>
) : (
<button
className="server-action-button start"
onClick={() => startBuiltInServer(server.id)}
>
Start
</button>
)}
</div>
</div>
);
})
) : (
<div className="empty-state">
<p>No built-in MCP servers available.</p>
</div>
)}
</div>
</div>
{/* User-added MCP Servers */}
<h4>Your MCP Servers</h4>
<div className="profiles-list">
{localMcpServers.map(server => (
<div
key={server.id}
className={`profile-item ${server.id === editingMcpServerId ? 'active' : ''} ${server.isBuiltIn && server.isRunning ? 'running' : ''}`}
onClick={() => setEditingMcpServerId(server.id)}
>
<span>{server.name}</span>
{server.isBuiltIn && server.isRunning && (
<span className="server-badge running">Running</span>
)}
{server.isBuiltIn && !server.isRunning && (
<span className="server-badge stopped">Stopped</span>
)}
<button
className="delete-profile-button"
onClick={(e) => {
e.stopPropagation();
handleDeleteMcpServer(server.id);
}}
>
×
</button>
</div>
))}
{localMcpServers.length === 0 && (
<div className="empty-state">
<p>No MCP servers added yet. Add a server to enable Model Context Protocol capabilities.</p>
</div>
)}
</div>
{editingMcpServer && (
<div className="current-profile-section">
<h3>Edit MCP Server: {editingMcpServer.name}</h3>
<div className="setting-item">
<label>Server Name:</label>
<input
type="text"
value={editingMcpServer.name}
onChange={(e) => handleMcpServerChange({
...editingMcpServer,
name: e.target.value
})}
placeholder="Enter server name"
disabled={editingMcpServer.isBuiltIn}
/>
</div>
<div className="setting-item">
<label>Endpoint URL:</label>
<div className="input-with-button">
<input
type="text"
value={editingMcpServer.endpoint}
onChange={(e) => handleMcpServerChange({
...editingMcpServer,
endpoint: e.target.value
})}
placeholder="Enter MCP server endpoint URL"
disabled={editingMcpServer.isBuiltIn}
/>
<button
type="button"
className="test-connection-button"
onClick={() => testMcpServerConnection(editingMcpServer.endpoint, editingMcpServer.authToken)}
disabled={!editingMcpServer.endpoint || isTestingConnection}
>
{isTestingConnection ? 'Testing...' : 'Test Connection'}
</button>
</div>
{serverTestResult && (
<div className={`connection-test-result ${serverTestResult.success ? 'success' : 'error'}`}>
{serverTestResult.message}
{serverTestResult.success && serverTestResult.tools && (
<div className="available-tools">
<p>Available tools: {serverTestResult.tools.length}</p>
<ul>
{serverTestResult.tools.map((tool, index) => (
<li key={index}>{tool.name} - {tool.description}</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
<div className="setting-item">
<label>Authentication Token:</label>
<input
type="password"
value={editingMcpServer.authToken}
onChange={(e) => handleMcpServerChange({
...editingMcpServer,
authToken: e.target.value
})}
placeholder="Enter authentication token (if required)"
disabled={editingMcpServer.isBuiltIn}
/>
</div>
<div className="setting-item">
<label>Description:</label>
<textarea
value={editingMcpServer.description}
onChange={(e) => handleMcpServerChange({
...editingMcpServer,
description: e.target.value
})}
placeholder="Enter server description"
rows="3"
disabled={editingMcpServer.isBuiltIn}
/>
</div>
{editingMcpServer.isBuiltIn && (
<div className="built-in-server-controls">
<p className="built-in-server-note">
This is a built-in MCP server managed by the application.
{editingMcpServer.isRunning
? ' It is currently running.'
: ' It is currently stopped.'}
</p>
{editingMcpServer.isRunning ? (
<button
type="button"
className="server-control-button stop"
onClick={() => stopBuiltInServer(editingMcpServer.builtInId)}
>
Stop Server
</button>
) : (
<button
type="button"
className="server-control-button start"
onClick={() => startBuiltInServer(editingMcpServer.builtInId)}
>
Start Server
</button>
)}
</div>
)}
<div className="setting-hint">
<button
type="button"
className="hint-toggle"
onClick={() => setIsMcpHintExpanded(!isMcpHintExpanded)}
>
{isMcpHintExpanded ? 'Hide' : 'Show'} MCP Information
</button>
<div className={`hint-content ${isMcpHintExpanded ? 'expanded' : ''}`}>
<p>Model Context Protocol (MCP) allows AI models to access external tools and data sources.</p>
<ul>
<li>MCP servers provide specialized capabilities to AI models</li>
<li>Each server can offer different tools like web search, data retrieval, etc.</li>
<li>The model will automatically use available MCP servers when needed</li>
</ul>
</div>
</div>
</div>
)}
</div>
<div className="settings-actions">
<button type="submit" className="save-button">Save Settings</button>
<button type="button" className="cancel-button" onClick={onCloseSettings}>Cancel</button>
</div>
</form>
</div>
);
}
export default Settings;