重构客户端左侧导航
This commit is contained in:
@@ -9,26 +9,13 @@ import React, {
|
||||
} from "react";
|
||||
import {
|
||||
ConfigProvider,
|
||||
Menu,
|
||||
Badge,
|
||||
Spin,
|
||||
Button,
|
||||
Modal,
|
||||
notification,
|
||||
message,
|
||||
} from "antd";
|
||||
import type { PresetStatusColorType } from "antd/es/_util/colors";
|
||||
import {
|
||||
SettingOutlined,
|
||||
DashboardOutlined,
|
||||
FolderOutlined,
|
||||
InfoCircleOutlined,
|
||||
SafetyOutlined,
|
||||
FileTextOutlined,
|
||||
TeamOutlined,
|
||||
ArrowLeftOutlined,
|
||||
ReloadOutlined,
|
||||
ApiOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ArrowLeftOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
setupService,
|
||||
authService,
|
||||
@@ -39,15 +26,11 @@ import {
|
||||
syncConfigToServer,
|
||||
normalizeServerHost,
|
||||
loginAndRegister,
|
||||
logout,
|
||||
isLoggedIn as checkIsLoggedIn,
|
||||
} from "./services/core/auth";
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
AUTH_KEYS,
|
||||
normalizeAgentEngine,
|
||||
} from "@shared/constants";
|
||||
import { AUTH_KEYS, normalizeAgentEngine } from "@shared/constants";
|
||||
import type { QuickInitConfig } from "@shared/types/quickInit";
|
||||
import type { UpdateState } from "@shared/types/updateTypes";
|
||||
import { t, getCurrentLang } from "./services/core/i18n";
|
||||
import SetupWizard from "./components/setup/SetupWizard";
|
||||
import LoginPage from "./components/setup/LoginPage";
|
||||
@@ -60,6 +43,10 @@ import LogViewer from "./components/pages/LogViewer";
|
||||
import PermissionsPage from "./components/pages/PermissionsPage";
|
||||
import SessionsPage from "./components/pages/SessionsPage";
|
||||
import MCPSettings from "./components/settings/MCPSettings";
|
||||
import AppSidebar, {
|
||||
NavKey,
|
||||
LegacyTabKey,
|
||||
} from "./components/layout/AppSidebar";
|
||||
import type { WebviewHeaderActions } from "./components/pages/SessionsPage";
|
||||
import { createLogger } from "./services/utils/rendererLog";
|
||||
import styles from "./styles/components/App.module.css";
|
||||
@@ -112,27 +99,7 @@ type TabKey =
|
||||
| "dependencies"
|
||||
| "permissions"
|
||||
| "logs"
|
||||
| "about"
|
||||
| "model";
|
||||
|
||||
// 状态配置(对齐 Tauri 客户端)
|
||||
// 就绪、繁忙使用橙色(warning)、小点展示
|
||||
const STATUS_CONFIG: Record<
|
||||
string,
|
||||
{ status: PresetStatusColorType; textKey: string }
|
||||
> = {
|
||||
idle: { status: "warning", textKey: "Claw.Agent.Status.idle" },
|
||||
starting: { status: "processing", textKey: "Claw.Agent.Status.starting" },
|
||||
running: { status: "success", textKey: "Claw.Agent.Status.running" },
|
||||
busy: { status: "warning", textKey: "Claw.Agent.Status.busy" },
|
||||
stopped: { status: "default", textKey: "Claw.Agent.Status.stopped" },
|
||||
error: { status: "error", textKey: "Claw.Agent.Status.error" },
|
||||
};
|
||||
|
||||
/** macOS/Linux 无 download-progress 时,header tag 用本地模拟进度避免长期显示 0%。 */
|
||||
const HEADER_SIMULATED_PROGRESS_CAP = 90;
|
||||
const HEADER_SIMULATED_PROGRESS_INTERVAL_MS = 500;
|
||||
const HEADER_SIMULATED_DURATION_MS = 45_000;
|
||||
| "about";
|
||||
|
||||
// 服务状态接口(与 ClientPage 共享)
|
||||
export interface ServiceItem {
|
||||
@@ -345,36 +312,21 @@ function App() {
|
||||
// 核心状态
|
||||
// ============================================
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("client");
|
||||
const [activeNavKey, setActiveNavKey] = useState<NavKey>("overview");
|
||||
const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false);
|
||||
const [webviewActions, setWebviewActions] =
|
||||
useState<WebviewHeaderActions | null>(null);
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [userEmail, setUserEmail] = useState<string>("");
|
||||
const [onlineStatus, setOnlineStatus] = useState<boolean | null>(null);
|
||||
const [agentStatus, setAgentStatus] = useState<string>("idle");
|
||||
const [services, setServices] = useState<ServiceItem[]>([]);
|
||||
const [servicesLoading, setServicesLoading] = useState(true);
|
||||
const [guiMcpEnabled, setGuiMcpEnabled] = useState(false);
|
||||
const [pollFailCount, setPollFailCount] = useState(0);
|
||||
const [startingServices, setStartingServices] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const servicesPollTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
/** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */
|
||||
const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0);
|
||||
const [updateState, setUpdateState] = useState<UpdateState>({
|
||||
status: "idle",
|
||||
});
|
||||
const [headerSimulatedPercent, setHeaderSimulatedPercent] = useState(0);
|
||||
const headerSimulatedIntervalRef = useRef<ReturnType<
|
||||
typeof setInterval
|
||||
> | null>(null);
|
||||
const statusExpectedKeys = useMemo(() => {
|
||||
const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"];
|
||||
if (FEATURES.ENABLE_GUI_AGENT_SERVER && guiMcpEnabled) {
|
||||
keys.splice(3, 0, "guiServer");
|
||||
}
|
||||
return keys;
|
||||
}, [guiMcpEnabled]);
|
||||
const getStartupServiceKeys = useCallback(async (): Promise<string[]> => {
|
||||
const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"];
|
||||
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) return keys;
|
||||
@@ -439,6 +391,7 @@ function App() {
|
||||
setUsername(
|
||||
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
||||
);
|
||||
setUserEmail(user.email || user.phone || user.username || "");
|
||||
}
|
||||
|
||||
// 加载在线状态
|
||||
@@ -461,8 +414,10 @@ function App() {
|
||||
setUsername(
|
||||
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
||||
);
|
||||
setUserEmail(user.email || user.phone || user.username || "");
|
||||
} else {
|
||||
setUsername("");
|
||||
setUserEmail("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -596,7 +551,6 @@ function App() {
|
||||
]);
|
||||
const isGuiEnabled =
|
||||
FEATURES.ENABLE_GUI_AGENT_SERVER && (guiEnabledRes?.enabled ?? false);
|
||||
setGuiMcpEnabled(isGuiEnabled);
|
||||
items.push({
|
||||
key: "mcpProxy",
|
||||
label: t("Claw.Service.mcp"),
|
||||
@@ -649,10 +603,8 @@ function App() {
|
||||
error: lpStatus?.error,
|
||||
});
|
||||
setServices(items);
|
||||
setPollFailCount(0);
|
||||
} catch (error) {
|
||||
console.error("[App] pollServicesStatus failed:", error);
|
||||
setPollFailCount((count) => count + 1);
|
||||
} finally {
|
||||
setServicesLoading(false);
|
||||
}
|
||||
@@ -838,57 +790,6 @@ function App() {
|
||||
getStartupServiceKeys,
|
||||
]);
|
||||
|
||||
// ============================================
|
||||
// 根据服务状态计算 Agent 状态
|
||||
// ============================================
|
||||
// 根据服务状态计算 Agent 状态(对齐 Tauri 客户端逻辑)
|
||||
useEffect(() => {
|
||||
// 如果正在加载,保持当前状态不变(避免初始加载时的闪烁)
|
||||
if (servicesLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusExpectedKeys.length === 0) {
|
||||
setAgentStatus("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceMap = new Map(services.map((s) => [s.key, s]));
|
||||
const trackedServices = statusExpectedKeys.map((key) =>
|
||||
serviceMap.get(key),
|
||||
);
|
||||
const runningCount = trackedServices.filter((s) => s?.running).length;
|
||||
const totalCount = statusExpectedKeys.length;
|
||||
const hasErrors = trackedServices.some((s) => !!s?.error);
|
||||
const hasStartingServices = Array.from(startingServices).some((key) =>
|
||||
statusExpectedKeys.includes(key),
|
||||
);
|
||||
const hasStaleServiceStatus = pollFailCount >= 2;
|
||||
|
||||
if (hasStaleServiceStatus) {
|
||||
// 连续轮询失败时,避免继续展示可能过期的 running 状态。
|
||||
setAgentStatus("busy");
|
||||
} else if (hasErrors) {
|
||||
setAgentStatus("error");
|
||||
} else if (hasStartingServices) {
|
||||
setAgentStatus("starting");
|
||||
} else if (runningCount === totalCount && runningCount > 0) {
|
||||
setAgentStatus("running");
|
||||
} else if (runningCount > 0 && runningCount < totalCount) {
|
||||
setAgentStatus("busy");
|
||||
} else if (runningCount === 0) {
|
||||
setAgentStatus("stopped");
|
||||
} else {
|
||||
setAgentStatus("idle");
|
||||
}
|
||||
}, [
|
||||
services,
|
||||
servicesLoading,
|
||||
startingServices,
|
||||
statusExpectedKeys,
|
||||
pollFailCount,
|
||||
]);
|
||||
|
||||
// 启动服务状态轮询
|
||||
useEffect(() => {
|
||||
if (isSetupComplete !== true) return;
|
||||
@@ -906,55 +807,6 @@ function App() {
|
||||
};
|
||||
}, [isSetupComplete]);
|
||||
|
||||
// ============================================
|
||||
// 监听更新状态(header tag 展示)
|
||||
// ============================================
|
||||
useEffect(() => {
|
||||
const handler = (state: UpdateState) => {
|
||||
if (state) setUpdateState(state);
|
||||
};
|
||||
window.electronAPI?.on("update:status", handler as any);
|
||||
window.electronAPI?.app?.getUpdateState?.()?.then((state) => {
|
||||
if (state) setUpdateState(state);
|
||||
});
|
||||
return () => {
|
||||
window.electronAPI?.off("update:status", handler as any);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isDownloading = updateState.status === "downloading";
|
||||
const hasRealProgress = updateState.progress != null;
|
||||
|
||||
if (isDownloading && !hasRealProgress) {
|
||||
setHeaderSimulatedPercent(0);
|
||||
const increment =
|
||||
(HEADER_SIMULATED_PROGRESS_CAP / HEADER_SIMULATED_DURATION_MS) *
|
||||
HEADER_SIMULATED_PROGRESS_INTERVAL_MS;
|
||||
const id = setInterval(() => {
|
||||
setHeaderSimulatedPercent((prev) => {
|
||||
const next = prev + increment;
|
||||
return next >= HEADER_SIMULATED_PROGRESS_CAP
|
||||
? HEADER_SIMULATED_PROGRESS_CAP
|
||||
: next;
|
||||
});
|
||||
}, HEADER_SIMULATED_PROGRESS_INTERVAL_MS);
|
||||
headerSimulatedIntervalRef.current = id;
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
headerSimulatedIntervalRef.current = null;
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDownloading || hasRealProgress) {
|
||||
if (headerSimulatedIntervalRef.current) {
|
||||
clearInterval(headerSimulatedIntervalRef.current);
|
||||
headerSimulatedIntervalRef.current = null;
|
||||
}
|
||||
setHeaderSimulatedPercent(0);
|
||||
}
|
||||
}, [updateState.status, updateState.progress]);
|
||||
|
||||
// ============================================
|
||||
// 监听托盘/菜单事件
|
||||
// ============================================
|
||||
@@ -1069,65 +921,103 @@ function App() {
|
||||
setIsSetupComplete(true);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 状态 Badge
|
||||
// ============================================
|
||||
const badge = STATUS_CONFIG[agentStatus] || STATUS_CONFIG.idle;
|
||||
const stopServicesForLogout = useCallback(async () => {
|
||||
const toStop = services.filter((svc) => svc.running || !!svc.error);
|
||||
for (const svc of toStop) {
|
||||
try {
|
||||
if (svc.key === "agent") await window.electronAPI?.agent.destroy();
|
||||
else if (svc.key === "fileServer")
|
||||
await window.electronAPI?.fileServer.stop();
|
||||
else if (svc.key === "lanproxy")
|
||||
await window.electronAPI?.lanproxy.stop();
|
||||
else if (svc.key === "mcpProxy") await window.electronAPI?.mcp.stop();
|
||||
else if (svc.key === "guiServer")
|
||||
await window.electronAPI?.guiServer?.stop();
|
||||
} catch (e) {
|
||||
console.error(`[App] Failed to stop ${svc.label}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
await window.electronAPI?.computerServer.stop().catch((e: unknown) => {
|
||||
console.error("[App] Failed to stop computerServer:", e);
|
||||
});
|
||||
}, [services]);
|
||||
|
||||
const handleSidebarLogout = useCallback(() => {
|
||||
Modal.confirm({
|
||||
title: t("Claw.Client.logoutConfirm"),
|
||||
content: t("Claw.Client.logoutConfirmDetail"),
|
||||
okText: t("Claw.Client.logout"),
|
||||
cancelText: t("Claw.Client.cancel"),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
await stopServicesForLogout();
|
||||
await logout();
|
||||
await handleAuthChange();
|
||||
setIsLoggedIn(false);
|
||||
setActiveTab("client");
|
||||
setActiveNavKey("overview");
|
||||
setServices([]);
|
||||
setServicesLoading(false);
|
||||
setStartingServices(new Set());
|
||||
setOnlineStatus(false);
|
||||
message.success(t("Claw.Auth.loggedOut"));
|
||||
} catch {
|
||||
message.error(t("Claw.Client.logoutFailed"));
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [handleAuthChange, stopServicesForLogout]);
|
||||
|
||||
const handleNavSelect = useCallback((key: NavKey) => {
|
||||
setActiveNavKey(key);
|
||||
|
||||
if (key === "overview") {
|
||||
setActiveTab("client");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "newTask") {
|
||||
setSessionsAutoOpen(true);
|
||||
setActiveTab("sessions");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "aiChat") {
|
||||
setActiveTab("sessions");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "mcpLibrary") {
|
||||
setActiveTab("mcp");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "toolIntegration" || key === "systemSettings") {
|
||||
setActiveTab("settings");
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTab("client");
|
||||
}, []);
|
||||
|
||||
const handleLegacySelect = useCallback((key: LegacyTabKey) => {
|
||||
setActiveTab(key);
|
||||
const navByLegacyTab: Partial<Record<LegacyTabKey, NavKey>> = {
|
||||
client: "overview",
|
||||
sessions: "aiChat",
|
||||
mcp: "mcpLibrary",
|
||||
settings: "systemSettings",
|
||||
};
|
||||
setActiveNavKey(navByLegacyTab[key] || "systemSettings");
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// 平台检测
|
||||
// ============================================
|
||||
const isMacOS = navigator.platform.toUpperCase().includes("MAC");
|
||||
|
||||
// ============================================
|
||||
// 菜单配置(对齐 Tauri 客户端)
|
||||
// ============================================
|
||||
const menuItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
key: "client",
|
||||
icon: <DashboardOutlined />,
|
||||
label: t("Claw.Menu.client"),
|
||||
},
|
||||
{
|
||||
key: "sessions",
|
||||
icon: <TeamOutlined />,
|
||||
label: t("Claw.Menu.session"),
|
||||
},
|
||||
{
|
||||
key: "mcp",
|
||||
icon: <ApiOutlined />,
|
||||
label: t("Claw.Menu.mcp"),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
icon: <SettingOutlined />,
|
||||
label: t("Claw.Menu.settings"),
|
||||
},
|
||||
{
|
||||
key: "dependencies",
|
||||
icon: <FolderOutlined />,
|
||||
label: t("Claw.Menu.dependencies"),
|
||||
},
|
||||
];
|
||||
if (isMacOS) {
|
||||
items.push({
|
||||
key: "permissions",
|
||||
icon: <SafetyOutlined />,
|
||||
label: t("Claw.Menu.authorization"),
|
||||
});
|
||||
}
|
||||
items.push(
|
||||
{ key: "logs", icon: <FileTextOutlined />, label: t("Claw.Menu.logs") },
|
||||
{
|
||||
key: "about",
|
||||
icon: <InfoCircleOutlined />,
|
||||
label: t("Claw.Menu.about"),
|
||||
},
|
||||
);
|
||||
return items;
|
||||
}, [isMacOS, i18nLang]);
|
||||
|
||||
// ============================================
|
||||
// i18n Context value
|
||||
// ============================================
|
||||
@@ -1212,9 +1102,8 @@ function App() {
|
||||
value={{ themeMode, isDarkMode, setThemeMode: handleSetThemeMode }}
|
||||
>
|
||||
<div className="app-container">
|
||||
{/* 顶部栏 */}
|
||||
<div className="app-header">
|
||||
{webviewActions ? (
|
||||
{webviewActions && (
|
||||
<div className="app-header">
|
||||
<div className={styles.headerWebviewActions}>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -1231,143 +1120,23 @@ function App() {
|
||||
{t("Claw.App.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="app-header-logo">
|
||||
<img
|
||||
src="./32x32.png"
|
||||
alt=""
|
||||
style={{ width: 16, height: 16 }}
|
||||
/>
|
||||
<span className="app-header-title">{APP_DISPLAY_NAME}</span>
|
||||
{updateState.status === "available" && (
|
||||
<span
|
||||
className="app-header-update-tag app-header-update-tag--available"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (updateState.canAutoUpdate === false) {
|
||||
await window.electronAPI?.app?.openReleasesPage?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setUpdateState((prev) => ({
|
||||
...prev,
|
||||
status: "downloading",
|
||||
progress: undefined,
|
||||
}));
|
||||
const res =
|
||||
await window.electronAPI?.app?.downloadUpdate?.();
|
||||
if (!res || !res.success) {
|
||||
message.error(
|
||||
res?.error || t("Claw.About.downloadFailed"),
|
||||
);
|
||||
setUpdateState((prev) => ({
|
||||
...prev,
|
||||
status: "available",
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
message.error(t("Claw.About.downloadFailed"));
|
||||
setUpdateState((prev) => ({
|
||||
...prev,
|
||||
status: "available",
|
||||
}));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) =>
|
||||
(e.key === "Enter" || e.key === " ") &&
|
||||
(e.target as HTMLElement).click()
|
||||
}
|
||||
>
|
||||
{updateState.canAutoUpdate === false
|
||||
? t("Claw.App.UpdateTag.newVersion", {
|
||||
version: updateState.version,
|
||||
})
|
||||
: t("Claw.App.UpdateTag.download", {
|
||||
version: updateState.version,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{updateState.status === "downloading" && (
|
||||
<span className="app-header-update-tag app-header-update-tag--downloading">
|
||||
{t("Claw.App.UpdateTag.downloading", {
|
||||
percent: Math.round(
|
||||
updateState.progress?.percent ??
|
||||
headerSimulatedPercent,
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{updateState.status === "downloaded" && (
|
||||
<span
|
||||
className="app-header-update-tag app-header-update-tag--install"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res =
|
||||
await window.electronAPI?.app?.installUpdate?.();
|
||||
if (res && !res.success) {
|
||||
message.error(
|
||||
res.error || t("Claw.About.installFailed"),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
message.error(t("Claw.About.installFailed"));
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) =>
|
||||
(e.key === "Enter" || e.key === " ") &&
|
||||
(e.target as HTMLElement).click()
|
||||
}
|
||||
>
|
||||
{t("Claw.About.installUpdate")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.headerRight}>
|
||||
{username && (
|
||||
<span className={styles.username}>{username}</span>
|
||||
)}
|
||||
<Badge
|
||||
status={badge.status}
|
||||
className={
|
||||
agentStatus === "idle" || agentStatus === "busy"
|
||||
? styles.badgeIdle
|
||||
: undefined
|
||||
}
|
||||
text={
|
||||
<span className={styles.badgeText}>{t(badge.textKey)}</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 主体部分 */}
|
||||
<div className="app-body">
|
||||
{/* 左侧边栏 (hidden when webview is active) */}
|
||||
{!webviewActions && (
|
||||
<div
|
||||
className={
|
||||
// 英文菜单文案通常更长,侧边栏适当加宽以减少截断;其他语言保持默认宽度
|
||||
i18nLang.toLowerCase().startsWith("en")
|
||||
? "app-sider app-sider-en"
|
||||
: "app-sider"
|
||||
}
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineIndent={0}
|
||||
selectedKeys={[activeTab]}
|
||||
items={menuItems.map((item) => ({
|
||||
key: item.key,
|
||||
icon: item.icon,
|
||||
label: item.label,
|
||||
onClick: () => setActiveTab(item.key as TabKey),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<AppSidebar
|
||||
activeNavKey={activeNavKey}
|
||||
username={username}
|
||||
email={userEmail}
|
||||
onlineStatus={onlineStatus}
|
||||
isMacOS={isMacOS}
|
||||
onNavSelect={handleNavSelect}
|
||||
onLegacySelect={handleLegacySelect}
|
||||
onLogout={handleSidebarLogout}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 主内容区:flex 子撑满,便于日志等页占满高度 */}
|
||||
@@ -1392,6 +1161,9 @@ function App() {
|
||||
onNavigate={(tab) => {
|
||||
if (tab === "sessions") setSessionsAutoOpen(true);
|
||||
setActiveTab(tab);
|
||||
setActiveNavKey(
|
||||
tab === "sessions" ? "aiChat" : "overview",
|
||||
);
|
||||
}}
|
||||
services={services}
|
||||
servicesLoading={servicesLoading}
|
||||
|
||||
Reference in New Issue
Block a user