mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-19 12:10:17 +09:00
feat: improve async operations and UI feedback
This commit is contained in:
parent
4d535b1cd0
commit
65810d918b
@ -85,6 +85,50 @@
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: loading-pulse 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background-color: #16a34a;
|
||||
color: #fff;
|
||||
animation: pulse 1.5s infinite;
|
||||
|
||||
&[data-status="stopping"] {
|
||||
background-color: #9ca3af;
|
||||
}
|
||||
|
||||
&[data-status="starting"] {
|
||||
background-color: #4ade80;
|
||||
}
|
||||
|
||||
&[data-status="error"] {
|
||||
background-color: #f87171;
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-market-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -585,3 +629,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-pulse {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,9 @@ export function McpMarketPage() {
|
||||
>({});
|
||||
const [loadingPresets, setLoadingPresets] = useState(true);
|
||||
const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
|
||||
const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPresetServers = async () => {
|
||||
@ -141,8 +144,12 @@ export function McpMarketPage() {
|
||||
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||
if (!preset || !preset.configSchema || !editingServerId) return;
|
||||
|
||||
// 先关闭模态框
|
||||
const savingServerId = editingServerId;
|
||||
setEditingServerId(undefined);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
updateLoadingState(savingServerId, "Updating configuration...");
|
||||
// 构建服务器配置
|
||||
const args = [...preset.baseArgs];
|
||||
const env: Record<string, string> = {};
|
||||
@ -172,25 +179,38 @@ export function McpMarketPage() {
|
||||
...(Object.keys(env).length > 0 ? { env } : {}),
|
||||
};
|
||||
|
||||
// 检查是否是新增还是编辑
|
||||
const isNewServer = !isServerAdded(savingServerId);
|
||||
|
||||
// 如果是编辑现有服务器,保持原有状态
|
||||
if (!isNewServer) {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const currentStatus = currentConfig.mcpServers[savingServerId]?.status;
|
||||
if (currentStatus) {
|
||||
serverConfig.status = currentStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置并初始化新服务器
|
||||
const newConfig = await addMcpServer(editingServerId, serverConfig);
|
||||
const newConfig = await addMcpServer(savingServerId, serverConfig);
|
||||
setConfig(newConfig);
|
||||
|
||||
// 更新状态
|
||||
const status = await getClientStatus(editingServerId);
|
||||
// 只有新增的服务器才需要获取状态(因为会自动启动)
|
||||
if (isNewServer) {
|
||||
const status = await getClientStatus(savingServerId);
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[editingServerId]: status,
|
||||
[savingServerId]: status,
|
||||
}));
|
||||
}
|
||||
|
||||
setEditingServerId(undefined);
|
||||
showToast("Server configuration saved successfully");
|
||||
showToast("Server configuration updated successfully");
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error ? error.message : "Failed to save configuration",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
updateLoadingState(savingServerId, null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -210,36 +230,24 @@ export function McpMarketPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 重启所有客户端
|
||||
const handleRestartAll = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const newConfig = await restartAllClients();
|
||||
setConfig(newConfig);
|
||||
|
||||
// 更新所有客户端状态
|
||||
const statuses: Record<string, any> = {};
|
||||
for (const clientId of Object.keys(newConfig.mcpServers)) {
|
||||
statuses[clientId] = await getClientStatus(clientId);
|
||||
}
|
||||
setClientStatuses(statuses);
|
||||
|
||||
showToast("Successfully restarted all clients");
|
||||
} catch (error) {
|
||||
showToast("Failed to restart clients");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// 更新加载状态的辅助函数
|
||||
const updateLoadingState = (id: string, message: string | null) => {
|
||||
setLoadingStates((prev) => {
|
||||
if (message === null) {
|
||||
const { [id]: _, ...rest } = prev;
|
||||
return rest;
|
||||
}
|
||||
return { ...prev, [id]: message };
|
||||
});
|
||||
};
|
||||
|
||||
// 添加服务器
|
||||
// 修改添加服务器函数
|
||||
const addServer = async (preset: PresetServer) => {
|
||||
if (!preset.configurable) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
showToast("Creating MCP client...");
|
||||
// 如果服务器不需要配置,直接添加
|
||||
const serverId = preset.id;
|
||||
updateLoadingState(serverId, "Creating MCP client...");
|
||||
|
||||
const serverConfig: ServerConfig = {
|
||||
command: preset.command,
|
||||
args: [...preset.baseArgs],
|
||||
@ -254,7 +262,7 @@ export function McpMarketPage() {
|
||||
[preset.id]: status,
|
||||
}));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
updateLoadingState(preset.id, null);
|
||||
}
|
||||
} else {
|
||||
// 如果需要配置,打开配置对话框
|
||||
@ -263,33 +271,13 @@ export function McpMarketPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 移除服务器
|
||||
// const removeServer = async (id: string) => {
|
||||
// try {
|
||||
// setIsLoading(true);
|
||||
// const newConfig = await removeMcpServer(id);
|
||||
// setConfig(newConfig);
|
||||
|
||||
// // 移除状态
|
||||
// setClientStatuses((prev) => {
|
||||
// const newStatuses = { ...prev };
|
||||
// delete newStatuses[id];
|
||||
// return newStatuses;
|
||||
// });
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// 暂停服务器
|
||||
// 修改暂停服务器函数
|
||||
const pauseServer = async (id: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
showToast("Stopping server...");
|
||||
updateLoadingState(id, "Stopping server...");
|
||||
const newConfig = await pauseMcpServer(id);
|
||||
setConfig(newConfig);
|
||||
|
||||
// 更新状态为暂停
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[id]: { status: "paused", errorMsg: null },
|
||||
@ -299,27 +287,22 @@ export function McpMarketPage() {
|
||||
showToast("Failed to stop server");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
updateLoadingState(id, null);
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复服务器
|
||||
// 修改恢复服务器函数
|
||||
const resumeServer = async (id: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
showToast("Starting server...");
|
||||
updateLoadingState(id, "Starting server...");
|
||||
|
||||
// 尝试启动服务器
|
||||
const success = await resumeMcpServer(id);
|
||||
|
||||
// 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态)
|
||||
const status = await getClientStatus(id);
|
||||
setClientStatuses((prev) => ({
|
||||
...prev,
|
||||
[id]: status,
|
||||
}));
|
||||
|
||||
// 根据启动结果显示消息
|
||||
if (success) {
|
||||
showToast("Server started successfully");
|
||||
} else {
|
||||
@ -333,7 +316,29 @@ export function McpMarketPage() {
|
||||
);
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
updateLoadingState(id, null);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改重启所有客户端函数
|
||||
const handleRestartAll = async () => {
|
||||
try {
|
||||
updateLoadingState("all", "Restarting all servers...");
|
||||
const newConfig = await restartAllClients();
|
||||
setConfig(newConfig);
|
||||
|
||||
const statuses: Record<string, any> = {};
|
||||
for (const clientId of Object.keys(newConfig.mcpServers)) {
|
||||
statuses[clientId] = await getClientStatus(clientId);
|
||||
}
|
||||
setClientStatuses(statuses);
|
||||
|
||||
showToast("Successfully restarted all clients");
|
||||
} catch (error) {
|
||||
showToast("Failed to restart clients");
|
||||
console.error(error);
|
||||
} finally {
|
||||
updateLoadingState("all", null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -445,6 +450,14 @@ export function McpMarketPage() {
|
||||
return statusMap[status.status];
|
||||
};
|
||||
|
||||
// 获取操作状态的类型
|
||||
const getOperationStatusType = (message: string) => {
|
||||
if (message.toLowerCase().includes("stopping")) return "stopping";
|
||||
if (message.toLowerCase().includes("starting")) return "starting";
|
||||
if (message.toLowerCase().includes("error")) return "error";
|
||||
return "default";
|
||||
};
|
||||
|
||||
// 渲染服务器列表
|
||||
const renderServerList = () => {
|
||||
if (loadingPresets) {
|
||||
@ -478,29 +491,46 @@ export function McpMarketPage() {
|
||||
.sort((a, b) => {
|
||||
const aStatus = checkServerStatus(a.id).status;
|
||||
const bStatus = checkServerStatus(b.id).status;
|
||||
const aLoading = loadingStates[a.id];
|
||||
const bLoading = loadingStates[b.id];
|
||||
|
||||
// 定义状态优先级
|
||||
const statusPriority: Record<string, number> = {
|
||||
error: 0, // 最高优先级
|
||||
active: 1, // 运行中
|
||||
paused: 2, // 已暂停
|
||||
undefined: 3, // 未配置/未找到
|
||||
error: 0, // 错误状态最高优先级
|
||||
active: 1, // 已启动次之
|
||||
starting: 2, // 正在启动
|
||||
stopping: 3, // 正在停止
|
||||
paused: 4, // 已暂停
|
||||
undefined: 5, // 未配置最低优先级
|
||||
};
|
||||
|
||||
// 获取实际状态(包括加载状态)
|
||||
const getEffectiveStatus = (status: string, loading?: string) => {
|
||||
if (loading) {
|
||||
const operationType = getOperationStatusType(loading);
|
||||
return operationType === "default" ? status : operationType;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
|
||||
const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
|
||||
|
||||
// 首先按状态排序
|
||||
if (aStatus !== bStatus) {
|
||||
if (aEffectiveStatus !== bEffectiveStatus) {
|
||||
return (
|
||||
(statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3)
|
||||
(statusPriority[aEffectiveStatus] ?? 5) -
|
||||
(statusPriority[bEffectiveStatus] ?? 5)
|
||||
);
|
||||
}
|
||||
|
||||
// 然后按名称排序
|
||||
// 状态相同时按名称排序
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((server) => (
|
||||
<div
|
||||
className={clsx(styles["mcp-market-item"], {
|
||||
[styles["disabled"]]: isLoading,
|
||||
[styles["loading"]]: loadingStates[server.id],
|
||||
})}
|
||||
key={server.id}
|
||||
>
|
||||
@ -508,7 +538,17 @@ export function McpMarketPage() {
|
||||
<div className={styles["mcp-market-title"]}>
|
||||
<div className={styles["mcp-market-name"]}>
|
||||
{server.name}
|
||||
{getServerStatusDisplay(server.id)}
|
||||
{loadingStates[server.id] && (
|
||||
<span
|
||||
className={styles["operation-status"]}
|
||||
data-status={getOperationStatusType(
|
||||
loadingStates[server.id],
|
||||
)}
|
||||
>
|
||||
{loadingStates[server.id]}
|
||||
</span>
|
||||
)}
|
||||
{!loadingStates[server.id] && getServerStatusDisplay(server.id)}
|
||||
{server.repo && (
|
||||
<a
|
||||
href={server.repo}
|
||||
@ -605,8 +645,10 @@ export function McpMarketPage() {
|
||||
<div className="window-header-title">
|
||||
<div className="window-header-main-title">
|
||||
MCP Market
|
||||
{isLoading && (
|
||||
<span className={styles["loading-indicator"]}>Loading...</span>
|
||||
{loadingStates["all"] && (
|
||||
<span className={styles["loading-indicator"]}>
|
||||
{loadingStates["all"]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="window-header-sub-title">
|
||||
|
@ -98,6 +98,9 @@ async function initializeSingleClient(
|
||||
try {
|
||||
const client = await createClient(clientId, serverConfig);
|
||||
const tools = await listTools(client);
|
||||
logger.info(
|
||||
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
|
||||
);
|
||||
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||
logger.success(`Client [${clientId}] initialized successfully`);
|
||||
} catch (error) {
|
||||
@ -130,6 +133,13 @@ export async function initializeMcpSystem() {
|
||||
export async function addMcpServer(clientId: string, config: ServerConfig) {
|
||||
try {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const isNewServer = !(clientId in currentConfig.mcpServers);
|
||||
|
||||
// 如果是新服务器,设置默认状态为 active
|
||||
if (isNewServer && !config.status) {
|
||||
config.status = "active";
|
||||
}
|
||||
|
||||
const newConfig = {
|
||||
...currentConfig,
|
||||
mcpServers: {
|
||||
@ -138,8 +148,12 @@ export async function addMcpServer(clientId: string, config: ServerConfig) {
|
||||
},
|
||||
};
|
||||
await updateMcpConfig(newConfig);
|
||||
// 只初始化新添加的服务器
|
||||
|
||||
// 只有新服务器或状态为 active 的服务器才初始化
|
||||
if (isNewServer || config.status === "active") {
|
||||
await initializeSingleClient(clientId, config);
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to add server [${clientId}]: ${error}`);
|
||||
|
Loading…
Reference in New Issue
Block a user