diff --git a/electron/src/api.ts b/electron/src/api.ts index e66d6f734..2cc66c343 100644 --- a/electron/src/api.ts +++ b/electron/src/api.ts @@ -1,6 +1,12 @@ -import { Workflow } from "./types"; +import { Workflow, BasicSystemInfo } from "./types"; import { logMessage } from "./logger"; import { serverState } from "./state"; +import { app } from "electron"; +import { spawn } from "child_process"; +import { getPythonPath } from "./config"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; export let isConnected = false; let healthCheckTimer: NodeJS.Timeout | null = null; @@ -75,3 +81,304 @@ export function stopPeriodicHealthCheck(): void { healthCheckTimer = null; } } + +/** + * Gets Python version locally by executing python --version + * @returns {Promise} Python version or null if unavailable + */ +async function getLocalPythonVersion(): Promise { + try { + const pythonPath = getPythonPath(); + + // Check if Python executable exists + if (!fs.existsSync(pythonPath)) { + return null; + } + + return new Promise((resolve) => { + const child = spawn(pythonPath, ["--version"], { + timeout: 3000, // 3 second timeout + stdio: ["ignore", "pipe", "pipe"], + }); + + let output = ""; + let errorOutput = ""; + + child.stdout?.on("data", (data) => { + output += data.toString(); + }); + + child.stderr?.on("data", (data) => { + errorOutput += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0) { + // Python version is usually in format "Python 3.x.x" + const version = (output || errorOutput) + .trim() + .replace(/^Python\s+/, ""); + resolve(version || null); + } else { + resolve(null); + } + }); + + child.on("error", () => { + resolve(null); + }); + }); + } catch (error) { + logMessage(`Failed to get local Python version: ${error}`, "error"); + return null; + } +} + +/** + * Gets NodeTool package versions from local package.json files + * @returns {Promise<{core?: string, base?: string}>} Package versions + */ +async function getLocalNodeToolVersions(): Promise<{ + core?: string; + base?: string; +}> { + const versions: { core?: string; base?: string } = {}; + + try { + // Try to find package.json files in common locations + const appPath = app.getAppPath(); + const possiblePaths = [ + path.join(appPath, "..", "..", "package.json"), // From electron app to root + path.join(appPath, "..", "package.json"), + path.join(process.cwd(), "package.json"), + ]; + + for (const pkgPath of possiblePaths) { + try { + if (fs.existsSync(pkgPath)) { + const pkgContent = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + + // Check dependencies for nodetool packages + const allDeps = { + ...pkgContent.dependencies, + ...pkgContent.devDependencies, + ...pkgContent.peerDependencies, + }; + + if (allDeps["nodetool-core"]) { + versions.core = allDeps["nodetool-core"]; + } + if (allDeps["nodetool-base"]) { + versions.base = allDeps["nodetool-base"]; + } + } + } catch (e) { + // Continue to next path + } + } + } catch (error) { + logMessage(`Failed to get local NodeTool versions: ${error}`, "error"); + } + + return versions; +} + +/** + * Gets CUDA version locally by checking nvcc or nvidia-smi + * @returns {Promise} CUDA version or null if unavailable + */ +async function getLocalCudaVersion(): Promise { + try { + // Try nvcc first to get actual CUDA toolkit version + const nvccVersion = await new Promise((resolve) => { + const child = spawn("nvcc", ["--version"], { + timeout: 3000, + stdio: ["ignore", "pipe", "pipe"], + }); + + let output = ""; + child.stdout?.on("data", (data) => { + output += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0) { + // Extract CUDA version from nvcc output (e.g., "release 12.1") + const match = output.match(/release (\d+\.\d+)/); + resolve(match ? match[1] : null); + } else { + resolve(null); + } + }); + + child.on("error", () => { + resolve(null); + }); + }); + + if (nvccVersion) { + return nvccVersion; + } + + // Try nvidia-smi to get CUDA runtime version as fallback + const nvidiaSmiCudaVersion = await new Promise((resolve) => { + const child = spawn( + "nvidia-smi", + ["--query-gpu=cuda_version", "--format=csv,noheader,nounits"], + { + timeout: 3000, + stdio: ["ignore", "pipe", "pipe"], + } + ); + + let output = ""; + child.stdout?.on("data", (data) => { + output += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0 && output.trim()) { + const cudaVersion = output.trim().split("\n")[0]; + // nvidia-smi sometimes returns "N/A" for CUDA version + resolve(cudaVersion !== "N/A" ? cudaVersion : null); + } else { + resolve(null); + } + }); + + child.on("error", () => { + resolve(null); + }); + }); + + return nvidiaSmiCudaVersion; + } catch (error) { + logMessage(`Failed to get local CUDA version: ${error}`, "error"); + return null; + } +} + +/** + * Fetches basic system information for diagnostics + * @returns {Promise} Basic system info or null if unavailable + */ +export async function fetchBasicSystemInfo(): Promise { + logMessage("Fetching basic system information..."); + + try { + const port = serverState.serverPort ?? 8000; + const baseUrl = `http://127.0.0.1:${port}`; + + // Try to fetch system info from backend + const [systemResponse, healthResponse] = await Promise.allSettled([ + fetch(`${baseUrl}/api/system/`, { + method: "GET", + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(5000), // 5 second timeout + }), + fetch(`${baseUrl}/api/system/health`, { + method: "GET", + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(5000), + }), + ]); + + let systemData: any = null; + let serverStatus: "connected" | "disconnected" | "checking" = + "disconnected"; + + // Process system info response + if (systemResponse.status === "fulfilled" && systemResponse.value.ok) { + systemData = await systemResponse.value.json(); + serverStatus = "connected"; + logMessage("Successfully fetched system information from backend"); + } + + // Process health response + if (healthResponse.status === "fulfilled" && healthResponse.value.ok) { + serverStatus = "connected"; + } + + // Get local fallback information when backend is unavailable + let localPythonVersion: string | null = null; + let localNodeToolVersions: { core?: string; base?: string } = {}; + let localCudaVersion: string | null = null; + + if (serverStatus === "disconnected") { + logMessage("Backend unavailable, fetching local system information..."); + [localPythonVersion, localNodeToolVersions, localCudaVersion] = + await Promise.all([ + getLocalPythonVersion(), + getLocalNodeToolVersions(), + getLocalCudaVersion(), + ]); + } + + // Build system info object with enhanced fallbacks + const systemInfo: BasicSystemInfo = { + os: { + platform: systemData?.os?.platform || process.platform, + release: systemData?.os?.release || os.release(), + arch: systemData?.os?.arch || process.arch, + }, + versions: { + python: systemData?.versions?.python || localPythonVersion || undefined, + nodetool_core: + systemData?.versions?.nodetool_core || + localNodeToolVersions.core || + undefined, + nodetool_base: + systemData?.versions?.nodetool_base || + localNodeToolVersions.base || + undefined, + cuda: systemData?.versions?.cuda || localCudaVersion || undefined, + }, + paths: { + data_dir: systemData?.paths?.data_dir || app.getPath("userData"), + core_logs_dir: systemData?.paths?.core_logs_dir || "-", + electron_logs_dir: app.getPath("logs"), + }, + server: { + status: serverStatus, + port: serverStatus === "connected" ? port : undefined, + }, + }; + + return systemInfo; + } catch (error) { + logMessage(`Failed to fetch system information: ${error}`, "error"); + + // Enhanced fallback with local information + const [localPythonVersion, localNodeToolVersions, localCudaVersion] = + await Promise.all([ + getLocalPythonVersion().catch(() => null), + getLocalNodeToolVersions().catch(() => ({ + core: undefined, + base: undefined, + })), + getLocalCudaVersion().catch(() => null), + ]); + + return { + os: { + platform: process.platform, + release: os.release(), + arch: process.arch, + }, + versions: { + python: localPythonVersion || undefined, + nodetool_core: localNodeToolVersions.core || undefined, + nodetool_base: localNodeToolVersions.base || undefined, + cuda: localCudaVersion || undefined, + }, + paths: { + data_dir: app.getPath("userData"), + core_logs_dir: "-", + electron_logs_dir: app.getPath("logs"), + }, + server: { + status: "disconnected", + }, + }; + } +} diff --git a/electron/src/components/BootMessage.tsx b/electron/src/components/BootMessage.tsx index 2d7a7be9b..9a44e03e9 100644 --- a/electron/src/components/BootMessage.tsx +++ b/electron/src/components/BootMessage.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useState } from "react"; +import SystemDiagnostics from "./SystemDiagnostics"; interface UpdateProgressData { componentName: string; @@ -13,11 +14,26 @@ interface BootMessageProps { progressData: UpdateProgressData; } -const BootMessage: React.FC = ({ message, showUpdateSteps, progressData }) => { +const BootMessage: React.FC = ({ + message, + showUpdateSteps, + progressData, +}) => { + const [showSystemInfo, setShowSystemInfo] = useState(false); + return (
-
NodeTool
+
+
NodeTool
+ +
@@ -85,8 +101,14 @@ const BootMessage: React.FC = ({ message, showUpdateSteps, pro
)}
+ + {/* System Diagnostics Overlay */} + setShowSystemInfo(!showSystemInfo)} + /> ); }; -export default BootMessage; \ No newline at end of file +export default BootMessage; diff --git a/electron/src/components/SystemDiagnostics.tsx b/electron/src/components/SystemDiagnostics.tsx new file mode 100644 index 000000000..2b85f064f --- /dev/null +++ b/electron/src/components/SystemDiagnostics.tsx @@ -0,0 +1,256 @@ +import React, { useState, useEffect } from "react"; + +interface BasicSystemInfo { + os: { + platform: string; + release: string; + arch: string; + }; + versions: { + python?: string; + nodetool_core?: string; + nodetool_base?: string; + cuda?: string; + gpu_name?: string; + vram_total_gb?: string; + driver_version?: string; + }; + paths: { + data_dir: string; + core_logs_dir: string; + electron_logs_dir: string; + }; + server: { + status: "connected" | "disconnected" | "checking"; + port?: number; + }; +} + +interface SystemDiagnosticsProps { + isVisible: boolean; + onToggle: () => void; +} + +const SystemDiagnostics: React.FC = ({ + isVisible, + onToggle, +}) => { + const [systemInfo, setSystemInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (isVisible && !systemInfo) { + loadSystemInfo(); + } + }, [isVisible, systemInfo]); + + const loadSystemInfo = async () => { + setLoading(true); + setError(null); + + try { + // Try to get system info from backend + const info = await fetchBasicSystemInfo(); + setSystemInfo(info); + } catch (err) { + console.error("Failed to load system info:", err); + setError("Unable to load system information"); + + // Fallback to basic local info + setSystemInfo({ + os: { + platform: window.api.platform, + release: "Unknown", + arch: "Unknown", + }, + versions: {}, + paths: { + data_dir: "Unknown", + core_logs_dir: "Unknown", + electron_logs_dir: "Unknown", + }, + server: { + status: "disconnected", + }, + }); + } finally { + setLoading(false); + } + }; + + const fetchBasicSystemInfo = async (): Promise => { + // Use the real API integration + const systemInfo = await window.api.getSystemInfo(); + if (!systemInfo) { + throw new Error("Failed to fetch system information"); + } + return systemInfo; + }; + + const copySystemInfo = async () => { + if (!systemInfo) return; + + const info = [ + `OS: ${systemInfo.os.platform} ${systemInfo.os.release} (${systemInfo.os.arch})`, + `Python: ${systemInfo.versions.python || "-"}`, + `NodeTool Core: ${systemInfo.versions.nodetool_core || "-"}`, + `NodeTool Base: ${systemInfo.versions.nodetool_base || "-"}`, + `CUDA: ${systemInfo.versions.cuda || "-"}`, + ...(systemInfo.versions.gpu_name || + systemInfo.versions.vram_total_gb || + systemInfo.versions.driver_version + ? [ + `GPU: ${systemInfo.versions.gpu_name || "N/A"}`, + `VRAM: ${ + systemInfo.versions.vram_total_gb + ? systemInfo.versions.vram_total_gb + "GB" + : "N/A" + }`, + `Driver: ${systemInfo.versions.driver_version || "N/A"}`, + ] + : []), + `Server Status: ${systemInfo.server.status}${ + systemInfo.server.port ? ` (port ${systemInfo.server.port})` : "" + }`, + `Data Directory: ${systemInfo.paths.data_dir}`, + `Logs: ${systemInfo.paths.electron_logs_dir}`, + ].join("\n"); + + try { + await window.api.clipboardWriteText(info); + // Could add a brief success indicator here + } catch (err) { + console.error("Failed to copy to clipboard:", err); + } + }; + + if (!isVisible) return null; + + return ( +
+
+

System Information

+ +
+ +
+ {loading && ( +
+ Loading system information... +
+ )} + + {error &&
{error}
} + + {systemInfo && !loading && ( + <> +
+
Operating System
+
+ Platform: + {systemInfo.os.platform} +
+
+ Release: + {systemInfo.os.release} +
+
+ Architecture: + {systemInfo.os.arch} +
+
+ +
+
Versions
+
+ Python: + + {systemInfo.versions.python || "-"} + +
+
+ NodeTool Core: + + {systemInfo.versions.nodetool_core || "-"} + +
+
+ NodeTool Base: + + {systemInfo.versions.nodetool_base || "-"} + +
+
+ CUDA: + {systemInfo.versions.cuda || "-"} +
+ {(systemInfo.versions.gpu_name || + systemInfo.versions.vram_total_gb || + systemInfo.versions.driver_version) && ( + <> +
+ GPU: + + {systemInfo.versions.gpu_name || "N/A"} + +
+
+ VRAM: + + {systemInfo.versions.vram_total_gb + ? systemInfo.versions.vram_total_gb + "GB" + : "N/A"} + +
+
+ Driver: + + {systemInfo.versions.driver_version || "N/A"} + +
+ + )} +
+ +
+
Server Status
+
+ Connection: + + {systemInfo.server.status} + {systemInfo.server.port && + ` (port ${systemInfo.server.port})`} + +
+
+ +
+
Key Paths
+
+ Data Directory: + {systemInfo.paths.data_dir} +
+
+ Logs: + + {systemInfo.paths.electron_logs_dir} + +
+
+ +
+ +
+ + )} +
+
+ ); +}; + +export default SystemDiagnostics; diff --git a/electron/src/index.css b/electron/src/index.css index 87cc1ab0c..58950bb27 100644 --- a/electron/src/index.css +++ b/electron/src/index.css @@ -44,9 +44,17 @@ padding: 24px; height: 100vh; overflow-y: auto; - background: radial-gradient(1200px 800px at 10% 10%, rgba(94, 160, 255, 0.08), transparent), - radial-gradient(1000px 600px at 80% 30%, rgba(118, 229, 184, 0.06), transparent), - var(--c_bg_primary); + background: radial-gradient( + 1200px 800px at 10% 10%, + rgba(94, 160, 255, 0.08), + transparent + ), + radial-gradient( + 1000px 600px at 80% 30%, + rgba(118, 229, 184, 0.06), + transparent + ), + var(--c_bg_primary); color: var(--c_text_primary); } @@ -91,9 +99,15 @@ } @keyframes pulse { - 0% { opacity: 0.5; } - 50% { opacity: 1; } - 100% { opacity: 0.5; } + 0% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.5; + } } .skip-button { @@ -179,7 +193,12 @@ align-items: center; padding: 14px 16px; margin-bottom: 10px; - background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0) 40%), var(--c_bg_secondary); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.02), + rgba(255, 255, 255, 0) 40% + ), + var(--c_bg_secondary); border-radius: 12px; border: 1px solid var(--c_border); transition: all 0.2s ease; @@ -188,7 +207,7 @@ .package-item:hover { border-color: var(--c_border_hover); transform: translateY(-1px); - box-shadow: 0 6px 20px rgba(0,0,0,0.25); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); } .package-item.installed { @@ -260,7 +279,7 @@ } .installed-indicator { - background: rgba(255,255,255,0.06); + background: rgba(255, 255, 255, 0.06); color: var(--c_text_secondary); cursor: default; } @@ -338,10 +357,22 @@ html { position: fixed; inset: 0; padding: 24px; - background: radial-gradient(1200px 800px at 10% 10%, rgba(94, 160, 255, 0.08), transparent), - radial-gradient(1000px 600px at 80% 30%, rgba(118, 229, 184, 0.06), transparent), - radial-gradient(1000px 600px at 50% 100%, rgba(255,255,255,0.04), transparent), - #12161c; + background: radial-gradient( + 1200px 800px at 10% 10%, + rgba(94, 160, 255, 0.08), + transparent + ), + radial-gradient( + 1000px 600px at 80% 30%, + rgba(118, 229, 184, 0.06), + transparent + ), + radial-gradient( + 1000px 600px at 50% 100%, + rgba(255, 255, 255, 0.04), + transparent + ), + #12161c; border: 12px solid #0b0e13; border-radius: 36px; overflow: hidden; @@ -355,7 +386,8 @@ html { border-radius: 24px; background: var(--c_panel_bg); border: 1px solid var(--c_panel_border); - box-shadow: 0 20px 80px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(255,255,255,0.03); + box-shadow: 0 20px 80px rgba(0, 0, 0, 0.35), + inset 0 0 0 1px rgba(255, 255, 255, 0.03); display: flex; flex-direction: column; align-items: center; @@ -396,9 +428,9 @@ html { } .welcome-actions .secondary { - background: rgba(255,255,255,0.06); + background: rgba(255, 255, 255, 0.06); color: var(--c_text_secondary); - border: 1px solid rgba(255,255,255,0.12); + border: 1px solid rgba(255, 255, 255, 0.12); padding: 10px 16px; border-radius: 10px; cursor: pointer; @@ -415,7 +447,12 @@ html { } .feature-card { - background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0) 40%), var(--c_bg_secondary); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.02), + rgba(255, 255, 255, 0) 40% + ), + var(--c_bg_secondary); border-radius: 12px; border: 1px solid var(--c_border); padding: 14px; @@ -442,9 +479,9 @@ html { } .pill { - background: rgba(255,255,255,0.06); + background: rgba(255, 255, 255, 0.06); color: var(--c_text_secondary); - border: 1px solid rgba(255,255,255,0.12); + border: 1px solid rgba(255, 255, 255, 0.12); padding: 6px 10px; border-radius: 999px; font-size: 12px; @@ -615,10 +652,22 @@ ul li { position: fixed; inset: 0; padding: 24px; - background: radial-gradient(1200px 800px at 10% 10%, rgba(94, 160, 255, 0.08), transparent), - radial-gradient(1000px 600px at 80% 30%, rgba(118, 229, 184, 0.06), transparent), - radial-gradient(1000px 600px at 50% 100%, rgba(255,255,255,0.04), transparent), - #12161c; + background: radial-gradient( + 1200px 800px at 10% 10%, + rgba(94, 160, 255, 0.08), + transparent + ), + radial-gradient( + 1000px 600px at 80% 30%, + rgba(118, 229, 184, 0.06), + transparent + ), + radial-gradient( + 1000px 600px at 50% 100%, + rgba(255, 255, 255, 0.04), + transparent + ), + #12161c; border: 12px solid #0b0e13; border-radius: 36px; overflow: hidden; @@ -632,7 +681,8 @@ ul li { border-radius: 24px; background: var(--c_panel_bg); border: 1px solid var(--c_panel_border); - box-shadow: 0 20px 80px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(255,255,255,0.03); + box-shadow: 0 20px 80px rgba(0, 0, 0, 0.35), + inset 0 0 0 1px rgba(255, 255, 255, 0.03); display: flex; flex-direction: column; align-items: center; @@ -646,6 +696,7 @@ ul li { text-transform: uppercase; letter-spacing: 0.28em; color: var(--c_text_secondary); + flex: 1; } .brand-ring { @@ -653,8 +704,16 @@ ul li { inset: -1px; pointer-events: none; border-radius: inherit; - background: radial-gradient(400px 200px at 50% -20%, rgba(94,160,255,0.25), transparent 60%), - radial-gradient(500px 280px at 50% 120%, rgba(118,229,184,0.18), transparent 60%); + background: radial-gradient( + 400px 200px at 50% -20%, + rgba(94, 160, 255, 0.25), + transparent 60% + ), + radial-gradient( + 500px 280px at 50% 120%, + rgba(118, 229, 184, 0.18), + transparent 60% + ); } .boot-text { @@ -755,17 +814,26 @@ ul li { .progress-bar { width: 100%; height: 8px; - background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.06), + rgba(255, 255, 255, 0.02) + ); border-radius: 999px; overflow: hidden; margin: 0 auto; - border: 1px solid rgba(255,255,255,0.06); + border: 1px solid rgba(255, 255, 255, 0.06); } .progress { width: 0; height: 100%; - background: linear-gradient(90deg, rgba(118,229,184,0.1), rgba(118,229,184,0.85), rgba(118,229,184,0.1)); + background: linear-gradient( + 90deg, + rgba(118, 229, 184, 0.1), + rgba(118, 229, 184, 0.85), + rgba(118, 229, 184, 0.1) + ); transition: width 0.4s ease; border-radius: 999px; position: relative; @@ -775,7 +843,11 @@ ul li { content: ""; position: absolute; inset: 0; - background: radial-gradient(60px 60px at right center, rgba(255,255,255,0.65), transparent 60%); + background: radial-gradient( + 60px 60px at right center, + rgba(255, 255, 255, 0.65), + transparent 60% + ); mix-blend-mode: overlay; } @@ -830,8 +902,7 @@ ul li { background: rgba(16, 19, 19, 0.95); border-radius: 18px; padding: 0; - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.45), + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(118, 229, 184, 0.15) inset; border: 1px solid rgba(118, 229, 184, 0.25); backdrop-filter: blur(10px) saturate(120%); @@ -859,11 +930,13 @@ ul li { flex-direction: column; } -.installer-shell { padding: 20px 20px 24px; } +.installer-shell { + padding: 20px 20px 24px; +} .installer-header { padding: 12px 12px 16px; - border-bottom: 1px solid rgba(255,255,255,0.08); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .installer-brand { @@ -884,7 +957,7 @@ ul li { } .wizard-rail { - border-right: 1px solid rgba(255,255,255,0.08); + border-right: 1px solid rgba(255, 255, 255, 0.08); padding-right: 12px; } @@ -896,8 +969,12 @@ ul li { color: var(--c_text_tertiary); } -.wizard-step.active { color: var(--c_text_secondary); } -.wizard-step.current { color: var(--c_text_primary); } +.wizard-step.active { + color: var(--c_text_secondary); +} +.wizard-step.current { + color: var(--c_text_primary); +} .step-index { width: 22px; @@ -905,11 +982,15 @@ ul li { border-radius: 50%; display: grid; place-items: center; - background: rgba(255,255,255,0.08); + background: rgba(255, 255, 255, 0.08); font-size: 12px; } -.wizard-content { padding-left: 4px; min-width: 0; min-height: 0; } +.wizard-content { + padding-left: 4px; + min-width: 0; + min-height: 0; +} #environment-info { display: flex; @@ -1083,9 +1164,9 @@ ul li { } .chip-button { - background: rgba(255,255,255,0.06); + background: rgba(255, 255, 255, 0.06); color: var(--c_text_secondary); - border: 1px solid rgba(255,255,255,0.12); + border: 1px solid rgba(255, 255, 255, 0.12); padding: 8px 10px; border-radius: 999px; font-size: 12px; @@ -1093,7 +1174,7 @@ ul li { } .chip-button:hover { - background: rgba(255,255,255,0.1); + background: rgba(255, 255, 255, 0.1); } .toolbar-count { @@ -1104,11 +1185,15 @@ ul li { .package-group { width: 100%; margin-bottom: 20px; - background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)); - border: 1px solid rgba(255,255,255,0.08); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.02), + rgba(255, 255, 255, 0) + ); + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 14px; padding: 12px 12px 10px; - box-shadow: 0 6px 24px rgba(0,0,0,0.25); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25); } .package-group h4 { @@ -1122,7 +1207,7 @@ ul li { align-items: center; justify-content: space-between; padding: 2px 4px 8px; - border-bottom: 1px solid rgba(255,255,255,0.08); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .group-actions { @@ -1142,18 +1227,23 @@ ul li { display: grid; grid-template-columns: 40px 1fr; align-items: start; - transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; + transition: transform 0.18s ease, box-shadow 0.18s ease, + background-color 0.18s ease; cursor: pointer; - background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)); - border: 1px solid rgba(255,255,255,0.08); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.02), + rgba(255, 255, 255, 0) + ); + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; - box-shadow: 0 4px 14px rgba(0,0,0,0.22); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22); } .package-option:hover { background-color: rgba(255, 255, 255, 0.05); transform: translateY(-1px); - box-shadow: 0 8px 18px rgba(0,0,0,0.28); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.28); } .checkbox-wrapper { @@ -1199,10 +1289,14 @@ ul li { padding: 2px 8px; border-radius: 999px; font-size: 11px; - background: linear-gradient(180deg, rgba(118,229,184,0.18), rgba(118,229,184,0.08)); - border: 1px solid rgba(118,229,184,0.35); + background: linear-gradient( + 180deg, + rgba(118, 229, 184, 0.18), + rgba(118, 229, 184, 0.08) + ); + border: 1px solid rgba(118, 229, 184, 0.35); color: var(--c_text_primary); - box-shadow: 0 0 0 1px rgba(255,255,255,0.03) inset; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03) inset; } .package-info { @@ -1225,7 +1319,7 @@ ul li { /* selected visual emphasis */ .package-option input[type="checkbox"]:checked ~ .package-content { - box-shadow: 0 0 0 2px rgba(94,158,255,0.45); + box-shadow: 0 0 0 2px rgba(94, 158, 255, 0.45); border-radius: 10px; } @@ -1298,19 +1392,31 @@ input[type="checkbox"]:checked::after { } .location-button.default-location { - background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); - border: 1px solid rgba(94,158,255,0.45); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.04), + rgba(255, 255, 255, 0.02) + ); + border: 1px solid rgba(94, 158, 255, 0.45); border-radius: 14px; color: #fff; - box-shadow: 0 8px 24px rgba(0,0,0,0.28), 0 0 0 1px rgba(255,255,255,0.03) inset; - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28), + 0 0 0 1px rgba(255, 255, 255, 0.03) inset; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, + background 0.2s ease; } .location-button.default-location:hover { transform: translateY(-1px); - border-color: rgba(94,158,255,0.7); - background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03)); - box-shadow: 0 12px 28px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.05) inset, 0 0 0 4px rgba(94,158,255,0.08); + border-color: rgba(94, 158, 255, 0.7); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.06), + rgba(255, 255, 255, 0.03) + ); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35), + 0 0 0 1px rgba(255, 255, 255, 0.05) inset, + 0 0 0 4px rgba(94, 158, 255, 0.08); } @keyframes subtlePulse { @@ -1379,16 +1485,43 @@ body::before { z-index: 1; opacity: 0.18; pointer-events: none; - background: - radial-gradient(1200px 800px at 15% 10%, rgba(94,160,255,0.12), transparent 60%), - radial-gradient(900px 700px at 85% 30%, rgba(118,229,184,0.10), transparent 60%), - radial-gradient(800px 600px at 50% 100%, rgba(255,255,255,0.06), transparent 60%), - conic-gradient(from 180deg at 50% 50%, rgba(255,255,255,0.02), rgba(0,0,0,0.02) 50%, rgba(255,255,255,0.02)), - repeating-linear-gradient(45deg, rgba(255,255,255,0.04) 0 1px, transparent 1px 12px); + background: radial-gradient( + 1200px 800px at 15% 10%, + rgba(94, 160, 255, 0.12), + transparent 60% + ), + radial-gradient( + 900px 700px at 85% 30%, + rgba(118, 229, 184, 0.1), + transparent 60% + ), + radial-gradient( + 800px 600px at 50% 100%, + rgba(255, 255, 255, 0.06), + transparent 60% + ), + conic-gradient( + from 180deg at 50% 50%, + rgba(255, 255, 255, 0.02), + rgba(0, 0, 0, 0.02) 50%, + rgba(255, 255, 255, 0.02) + ), + repeating-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.04) 0 1px, + transparent 1px 12px + ); animation: backgroundFloat 80s linear infinite; } -@keyframes backgroundFloat { from { transform: translateY(0); } to { transform: translateY(-2%); } } +@keyframes backgroundFloat { + from { + transform: translateY(0); + } + to { + transform: translateY(-2%); + } +} .setup-step { display: none; @@ -1457,3 +1590,199 @@ body::before { .nav-button.back { background: transparent; } + +/* System Diagnostics Styles */ +.system-diagnostics { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--c_panel_bg); + border: 1px solid var(--c_panel_border); + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + backdrop-filter: blur(15px); + max-width: 500px; + width: 90vw; + z-index: 1200; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.system-diagnostics-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--c_border); +} + +.system-diagnostics-header h4 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--c_text_primary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.system-diagnostics-close { + background: none; + border: none; + color: var(--c_text_tertiary); + font-size: 18px; + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: all 0.2s ease; +} + +.system-diagnostics-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--c_text_primary); +} + +.system-diagnostics-content { + padding: 16px; + max-height: 60vh; + overflow-y: auto; +} + +.system-diagnostics-loading, +.system-diagnostics-error { + text-align: center; + padding: 20px; + color: var(--c_text_secondary); + font-size: 14px; +} + +.system-diagnostics-error { + color: var(--c_danger); +} + +.system-info-section { + margin-bottom: 16px; +} + +.system-info-section:last-of-type { + margin-bottom: 0; +} + +.system-info-section h5 { + margin: 0 0 8px 0; + font-size: 12px; + font-weight: 600; + color: var(--c_text_tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.system-info-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 4px 0; + gap: 12px; +} + +.system-info-item .label { + font-size: 13px; + color: var(--c_text_secondary); + min-width: 120px; + flex-shrink: 0; +} + +.system-info-item .value { + font-size: 13px; + color: var(--c_text_primary); + font-family: "Courier New", monospace; + word-break: break-all; + text-align: right; + flex: 1; +} + +.system-info-item .value.path { + font-size: 12px; + color: var(--c_text_secondary); +} + +.system-info-item .value.status-connected { + color: var(--c_success); +} + +.system-info-item .value.status-disconnected { + color: var(--c_danger); +} + +.system-info-item .value.status-checking { + color: var(--c_accent_alt); +} + +.system-diagnostics-actions { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--c_border); + text-align: center; +} + +.copy-button { + background: linear-gradient(135deg, var(--c_accent), #4caf50); + color: white; + border: none; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(118, 229, 184, 0.25); +} + +.copy-button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(118, 229, 184, 0.35); +} + +/* Boot Message System Info Integration */ +.boot-panel-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + width: 100%; + margin-bottom: 16px; +} + +.system-info-toggle { + background: rgba(255, 255, 255, 0.06); + color: var(--c_text_secondary); + border: 1px solid rgba(255, 255, 255, 0.12); + padding: 8px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.system-info-toggle:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--c_text_primary); + border-color: rgba(255, 255, 255, 0.2); + transform: translateY(-1px); +} diff --git a/electron/src/ipc.ts b/electron/src/ipc.ts index 5ae38f166..b6f90ace1 100644 --- a/electron/src/ipc.ts +++ b/electron/src/ipc.ts @@ -1,19 +1,32 @@ -import { ipcMain, BrowserWindow, clipboard, globalShortcut, shell } from "electron"; -import { getServerState, openLogFile, runApp, initializeBackendServer, stopServer } from "./server"; +import { + ipcMain, + BrowserWindow, + clipboard, + globalShortcut, + shell, +} from "electron"; +import { + getServerState, + openLogFile, + runApp, + initializeBackendServer, + stopServer, +} from "./server"; import { logMessage } from "./logger"; import { IpcChannels, IpcEvents, IpcResponse } from "./types.d"; import { createPackageManagerWindow } from "./window"; import { IpcRequest } from "./types.d"; import { registerWorkflowShortcut, setupWorkflowShortcuts } from "./shortcuts"; import { updateTrayMenu } from "./tray"; -import { - fetchAvailablePackages, - listInstalledPackages, - installPackage, - uninstallPackage, +import { + fetchAvailablePackages, + listInstalledPackages, + installPackage, + uninstallPackage, updatePackage, - validateRepoId + validateRepoId, } from "./packageManager"; +import { fetchBasicSystemInfo } from "./api"; /** * This module handles Inter-Process Communication (IPC) between the Electron main process @@ -86,6 +99,10 @@ export function initializeIpcHandlers(): void { return getServerState(); }); + createIpcMainHandler(IpcChannels.GET_SYSTEM_INFO, async () => { + return await fetchBasicSystemInfo(); + }); + createIpcMainHandler(IpcChannels.OPEN_LOG_FILE, async () => { openLogFile(); }); @@ -194,36 +211,27 @@ export function initializeIpcHandlers(): void { ); // Package manager handlers - createIpcMainHandler( - IpcChannels.PACKAGE_LIST_AVAILABLE, - async () => { - logMessage("Fetching available packages"); - return await fetchAvailablePackages(); - } - ); + createIpcMainHandler(IpcChannels.PACKAGE_LIST_AVAILABLE, async () => { + logMessage("Fetching available packages"); + return await fetchAvailablePackages(); + }); - createIpcMainHandler( - IpcChannels.PACKAGE_LIST_INSTALLED, - async () => { - logMessage("Listing installed packages"); - return await listInstalledPackages(); - } - ); + createIpcMainHandler(IpcChannels.PACKAGE_LIST_INSTALLED, async () => { + logMessage("Listing installed packages"); + return await listInstalledPackages(); + }); - createIpcMainHandler( - IpcChannels.PACKAGE_INSTALL, - async (_event, request) => { - logMessage(`Installing package: ${request.repo_id}`); - const validation = validateRepoId(request.repo_id); - if (!validation.valid) { - return { - success: false, - message: validation.error || "Invalid repository ID" - }; - } - return await installPackage(request.repo_id); + createIpcMainHandler(IpcChannels.PACKAGE_INSTALL, async (_event, request) => { + logMessage(`Installing package: ${request.repo_id}`); + const validation = validateRepoId(request.repo_id); + if (!validation.valid) { + return { + success: false, + message: validation.error || "Invalid repository ID", + }; } - ); + return await installPackage(request.repo_id); + }); createIpcMainHandler( IpcChannels.PACKAGE_UNINSTALL, @@ -233,27 +241,24 @@ export function initializeIpcHandlers(): void { if (!validation.valid) { return { success: false, - message: validation.error || "Invalid repository ID" + message: validation.error || "Invalid repository ID", }; } return await uninstallPackage(request.repo_id); } ); - createIpcMainHandler( - IpcChannels.PACKAGE_UPDATE, - async (_event, repoId) => { - logMessage(`Updating package: ${repoId}`); - const validation = validateRepoId(repoId); - if (!validation.valid) { - return { - success: false, - message: validation.error || "Invalid repository ID" - }; - } - return await updatePackage(repoId); + createIpcMainHandler(IpcChannels.PACKAGE_UPDATE, async (_event, repoId) => { + logMessage(`Updating package: ${repoId}`); + const validation = validateRepoId(repoId); + if (!validation.valid) { + return { + success: false, + message: validation.error || "Invalid repository ID", + }; } - ); + return await updatePackage(repoId); + }); createIpcMainHandler( IpcChannels.PACKAGE_OPEN_EXTERNAL, diff --git a/electron/src/preload.ts b/electron/src/preload.ts index c82714e9e..63651c7a9 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -87,6 +87,7 @@ function unregisterEventHandler( contextBridge.exposeInMainWorld("api", { // Request-response methods getServerState: () => ipcRenderer.invoke(IpcChannels.GET_SERVER_STATE), + getSystemInfo: () => ipcRenderer.invoke(IpcChannels.GET_SYSTEM_INFO), clipboardWriteText: (text: string) => ipcRenderer.invoke(IpcChannels.CLIPBOARD_WRITE_TEXT, text), clipboardReadText: () => ipcRenderer.invoke(IpcChannels.CLIPBOARD_READ_TEXT), @@ -95,12 +96,9 @@ contextBridge.exposeInMainWorld("api", { ipcRenderer.invoke(IpcChannels.INSTALL_TO_LOCATION, { location, packages }), selectCustomInstallLocation: () => ipcRenderer.invoke(IpcChannels.SELECT_CUSTOM_LOCATION), - continueToApp: () => - ipcRenderer.invoke(IpcChannels.START_SERVER), - startServer: () => - ipcRenderer.invoke(IpcChannels.START_SERVER), - restartServer: () => - ipcRenderer.invoke(IpcChannels.RESTART_SERVER), + continueToApp: () => ipcRenderer.invoke(IpcChannels.START_SERVER), + startServer: () => ipcRenderer.invoke(IpcChannels.START_SERVER), + restartServer: () => ipcRenderer.invoke(IpcChannels.RESTART_SERVER), showPackageManager: () => ipcRenderer.invoke(IpcChannels.SHOW_PACKAGE_MANAGER), runApp: (workflowId: string) => @@ -145,14 +143,14 @@ contextBridge.exposeInMainWorld("electronAPI", { packages: { listAvailable: () => ipcRenderer.invoke(IpcChannels.PACKAGE_LIST_AVAILABLE), listInstalled: () => ipcRenderer.invoke(IpcChannels.PACKAGE_LIST_INSTALLED), - install: (repo_id: string) => + install: (repo_id: string) => ipcRenderer.invoke(IpcChannels.PACKAGE_INSTALL, { repo_id }), - uninstall: (repo_id: string) => + uninstall: (repo_id: string) => ipcRenderer.invoke(IpcChannels.PACKAGE_UNINSTALL, { repo_id }), - update: (repo_id: string) => + update: (repo_id: string) => ipcRenderer.invoke(IpcChannels.PACKAGE_UPDATE, repo_id), }, openExternal: (url: string) => { ipcRenderer.invoke(IpcChannels.PACKAGE_OPEN_EXTERNAL, url); }, -}); \ No newline at end of file +}); diff --git a/electron/src/types.d.ts b/electron/src/types.d.ts index 8828086ab..d09f32a05 100644 --- a/electron/src/types.d.ts +++ b/electron/src/types.d.ts @@ -3,6 +3,7 @@ declare global { api: { platform: string; getServerState: () => Promise; + getSystemInfo: () => Promise; clipboardWriteText: (text: string) => void; clipboardReadText: () => string; openLogFile: () => Promise; @@ -131,19 +132,20 @@ export interface Workflow { export interface MenuEventData { type: - | "cut" - | "copy" - | "paste" - | "selectAll" - | "undo" - | "redo" - | "close" - | "fitView"; + | "cut" + | "copy" + | "paste" + | "selectAll" + | "undo" + | "redo" + | "close" + | "fitView"; } // IPC Channel names as const enum for type safety export enum IpcChannels { GET_SERVER_STATE = "get-server-state", + GET_SYSTEM_INFO = "get-system-info", OPEN_LOG_FILE = "open-log-file", INSTALL_TO_LOCATION = "install-to-location", SELECT_CUSTOM_LOCATION = "select-custom-location", @@ -184,6 +186,7 @@ export interface InstallToLocationData { // Request/Response types for each IPC channel export interface IpcRequest { [IpcChannels.GET_SERVER_STATE]: void; + [IpcChannels.GET_SYSTEM_INFO]: void; [IpcChannels.OPEN_LOG_FILE]: void; [IpcChannels.INSTALL_TO_LOCATION]: InstallToLocationData; [IpcChannels.SELECT_CUSTOM_LOCATION]: void; @@ -210,6 +213,7 @@ export interface IpcRequest { export interface IpcResponse { [IpcChannels.GET_SERVER_STATE]: ServerState; + [IpcChannels.GET_SYSTEM_INFO]: BasicSystemInfo | null; [IpcChannels.OPEN_LOG_FILE]: void; [IpcChannels.INSTALL_TO_LOCATION]: void; [IpcChannels.SELECT_CUSTOM_LOCATION]: string | null; @@ -259,6 +263,29 @@ export interface UpdateInfo { releaseUrl: string; } +export interface BasicSystemInfo { + os: { + platform: string; + release: string; + arch: string; + }; + versions: { + python?: string; + nodetool_core?: string; + nodetool_base?: string; + cuda?: string; + }; + paths: { + data_dir: string; + core_logs_dir: string; + electron_logs_dir: string; + }; + server: { + status: "connected" | "disconnected" | "checking"; + port?: number; + }; +} + export interface InstallLocationData { defaultPath: string; downloadSize?: string; diff --git a/web/src/components/content/Help/Help.tsx b/web/src/components/content/Help/Help.tsx index b43afc32c..ee71e42ea 100644 --- a/web/src/components/content/Help/Help.tsx +++ b/web/src/components/content/Help/Help.tsx @@ -21,6 +21,8 @@ import KeyboardShortcutsView from "./KeyboardShortcutsView"; import { NODE_EDITOR_SHORTCUTS } from "../../../config/shortcuts"; import { getShortcutTooltip } from "../../../config/shortcuts"; import ControlsShortcutsTab from "./ControlsShortcutsTab"; +import SystemTab from "./SystemTab"; +import { isProduction } from "../../../stores/ApiClient"; interface HelpItem { text: string; @@ -246,10 +248,13 @@ const Help = ({ value={helpIndex} onChange={handleChange} aria-label="help tabs" + variant="scrollable" + scrollButtons="auto" > + {!isProduction && }
@@ -272,6 +277,11 @@ const Help = ({ onChange={handleAccordionChange("comfy")} /> + {!isProduction && ( + + + + )}
diff --git a/web/src/components/content/Help/SystemTab.tsx b/web/src/components/content/Help/SystemTab.tsx new file mode 100644 index 000000000..41790a0fc --- /dev/null +++ b/web/src/components/content/Help/SystemTab.tsx @@ -0,0 +1,489 @@ +/** @jsxImportSource @emotion/react */ +import { css } from "@emotion/react"; +import React, { useEffect, useMemo, useState } from "react"; +import { + Box, + Button, + CircularProgress, + Divider, + Typography +} from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import type { Theme } from "@mui/material/styles"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import ErrorIcon from "@mui/icons-material/Error"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import { CopyToClipboardButton } from "../../common/CopyToClipboardButton"; +import { client } from "../../../stores/ApiClient"; +import { getIsElectronDetails } from "../../../utils/browser"; +import { isPathValid, openInExplorer } from "../../../utils/fileExplorer"; + +type OSInfo = { platform: string; release: string; arch: string }; +type VersionsInfo = { + python?: string | null; + nodetool_core?: string | null; + nodetool_base?: string | null; + cuda?: string | null; + gpu_name?: string | null; + vram_total_gb?: string | null; + driver_version?: string | null; +}; +type PathsInfo = { + settings_path: string; + secrets_path: string; + data_dir: string; + core_logs_dir: string; + core_log_file: string; + core_main_log_file?: string; + ollama_models_dir: string; + huggingface_cache_dir: string; + electron_user_data: string; + electron_log_file: string; + electron_logs_dir: string; + electron_main_log_file?: string; +}; + +type OllamaBasePathResponse = { + path: string; +}; + +type HuggingFaceBasePathResponse = { + path: string; +}; +type SystemInfoResponse = { + os: OSInfo; + versions: VersionsInfo; + paths: PathsInfo; +}; + +type HealthCheck = { + id: string; + status: "ok" | "warn" | "error"; + details?: string | null; + fix_hint?: string | null; +}; +type HealthResponse = { + checks: HealthCheck[]; + summary: { ok: number; warn: number; error: number }; +}; + +const systemTabStyles = (theme: Theme) => + css({ + display: "flex", + flexDirection: "column", + gap: "1rem", + ".section": { + display: "flex", + flexDirection: "column", + gap: ".5rem", + marginBottom: "0.75rem" + }, + ".row": { + display: "flex", + alignItems: "center", + gap: "0.5rem", + flexWrap: "wrap" + }, + ".label": { + width: "220px", + minWidth: "220px", + color: theme.vars.palette.grey[300] + }, + ".value": { + fontFamily: "var(--fontFamily2)", + color: theme.vars.palette.grey[100], + wordBreak: "break-all", + flex: 1 + }, + ".path-actions": { + display: "flex", + alignItems: "center", + gap: ".25rem" + }, + ".status": { + display: "flex", + alignItems: "center", + gap: ".5rem", + padding: ".25rem 0" + } + }); + +function StatusIcon({ status }: { status: HealthCheck["status"] }) { + const theme = useTheme(); + if (status === "ok") + return ; + if (status === "warn") + return ; + return ; +} + +export default function SystemTab() { + const theme = useTheme(); + const [info, setInfo] = useState(null); + const [health, setHealth] = useState(null); + const [loading, setLoading] = useState(true); + const { isElectron } = getIsElectronDetails(); + + useEffect(() => { + let cancelled = false; + async function load() { + try { + // Since /api/system/ endpoint doesn't exist, we'll create a basic system info + // and fetch what we can from available endpoints + const [healthResult, ollamaPathResult, hfPathResult] = + await Promise.all([ + // Use the available /health endpoint instead of /api/system/health + client.GET("/health", {}), + client.GET("/api/models/ollama_base_path", {}), + client.GET("/api/models/huggingface_base_path", {}) + ]); + + console.log("Health result:", healthResult); + console.log("Ollama path result:", ollamaPathResult); + console.log("HF path result:", hfPathResult); + + // Create a basic system info structure since the endpoint doesn't exist + const basicSystemInfo: SystemInfoResponse = { + os: { + platform: navigator.platform || "Unknown", + release: "Unknown", + arch: "Unknown" + }, + versions: { + python: null, + nodetool_core: null, + nodetool_base: null, + cuda: null, + gpu_name: null, + vram_total_gb: null, + driver_version: null + }, + paths: { + settings_path: "Not available", + secrets_path: "Not available", + data_dir: "Not available", + core_logs_dir: "Not available", + core_log_file: "Not available", + ollama_models_dir: + (ollamaPathResult.data as any)?.path || "Not available", + huggingface_cache_dir: + (hfPathResult.data as any)?.path || "Not available", + electron_user_data: "Not available", + electron_log_file: "Not available", + electron_logs_dir: "Not available" + } + }; + + // Create a basic health response since the system health endpoint doesn't exist + const hasHealthError = !healthResult.data; + const basicHealthInfo: HealthResponse = { + checks: [ + { + id: "api_connection", + status: hasHealthError ? "error" : "ok", + details: hasHealthError + ? "API connection failed" + : "API connection successful", + fix_hint: hasHealthError ? "Check if the server is running" : null + } + ], + summary: { + ok: hasHealthError ? 0 : 1, + warn: 0, + error: hasHealthError ? 1 : 0 + } + }; + + if (!cancelled) { + setInfo(basicSystemInfo); + setHealth(basicHealthInfo); + } + } catch (e) { + console.error("Failed to load system info/health:", e); + // Create fallback info even on error + if (!cancelled) { + const fallbackInfo: SystemInfoResponse = { + os: { + platform: navigator.platform || "Unknown", + release: "Unknown", + arch: "Unknown" + }, + versions: { + python: null, + nodetool_core: null, + nodetool_base: null, + cuda: null, + gpu_name: null, + vram_total_gb: null, + driver_version: null + }, + paths: { + settings_path: "Not available", + secrets_path: "Not available", + data_dir: "Not available", + core_logs_dir: "Not available", + core_log_file: "Not available", + ollama_models_dir: "Not available", + huggingface_cache_dir: "Not available", + electron_user_data: "Not available", + electron_log_file: "Not available", + electron_logs_dir: "Not available" + } + }; + + const fallbackHealth: HealthResponse = { + checks: [ + { + id: "system_check", + status: "error", + details: "Unable to retrieve system information", + fix_hint: "Check server connection and try again" + } + ], + summary: { ok: 0, warn: 0, error: 1 } + }; + + setInfo(fallbackInfo); + setHealth(fallbackHealth); + } + } finally { + if (!cancelled) setLoading(false); + } + } + load(); + return () => { + cancelled = true; + }; + }, []); + + const copyAllText = useMemo(() => { + if (!info) return ""; + const lines: string[] = []; + lines.push(`OS: ${info.os.platform} ${info.os.release} (${info.os.arch})`); + lines.push( + `Versions: python=${info.versions.python ?? ""}, core=${ + info.versions.nodetool_core ?? "" + }, base=${info.versions.nodetool_base ?? ""}, cuda=${ + info.versions.cuda ?? "" + }` + ); + if ( + info.versions.gpu_name || + info.versions.vram_total_gb || + info.versions.driver_version + ) { + lines.push( + `GPU: ${info.versions.gpu_name ?? "N/A"}, VRAM=${ + info.versions.vram_total_gb + ? info.versions.vram_total_gb + "GB" + : "N/A" + }, driver=${info.versions.driver_version ?? "N/A"}` + ); + } + lines.push("Paths:"); + const p = info.paths; + lines.push(` settings_path: ${p.settings_path}`); + lines.push(` secrets_path: ${p.secrets_path}`); + lines.push(` data_dir: ${p.data_dir}`); + const chatLogs = isElectron ? p.electron_logs_dir : p.core_logs_dir; + lines.push(` chat_logs_folder: ${chatLogs}`); + // Show only if backend provides it to avoid guessing + if (!isElectron && p.core_main_log_file) { + lines.push(` core_main_log_file: ${p.core_main_log_file}`); + } + lines.push(` ollama_models_dir: ${p.ollama_models_dir}`); + lines.push(` huggingface_cache_dir: ${p.huggingface_cache_dir}`); + if (isElectron) { + lines.push(` electron_user_data: ${p.electron_user_data}`); + lines.push(` electron_log_file: ${p.electron_log_file}`); + lines.push(` electron_logs_dir: ${p.electron_logs_dir}`); + if (p.electron_main_log_file) + lines.push(` electron_main_log_file: ${p.electron_main_log_file}`); + } + if (health) { + lines.push("Health Summary:"); + lines.push( + ` ok=${health.summary.ok} warn=${health.summary.warn} error=${health.summary.error}` + ); + lines.push("Health Checks:"); + for (const c of health.checks) { + lines.push( + ` ${c.id}: ${c.status}${c.details ? ` (${c.details})` : ""}` + ); + } + } + return lines.join("\n"); + }, [info, health, isElectron]); + + // Build paths list unconditionally to keep hooks order stable + const paths: Array<{ label: string; value: string }> = useMemo(() => { + if (!info) return []; + const list: Array<{ label: string; value: string }> = [ + { label: "Settings", value: info.paths.settings_path }, + { label: "Secrets", value: info.paths.secrets_path }, + { label: "Data Dir", value: info.paths.data_dir }, + { + label: "Chat Logs Folder", + value: isElectron + ? info.paths.electron_logs_dir + : info.paths.core_logs_dir + }, + ...(!isElectron && info.paths.core_main_log_file + ? [ + { + label: "Main Log", + value: info.paths.core_main_log_file + } + ] + : []), + { label: "Ollama Models", value: info.paths.ollama_models_dir }, + { label: "HuggingFace Cache", value: info.paths.huggingface_cache_dir } + ]; + if (isElectron) { + list.push( + { label: "Electron UserData", value: info.paths.electron_user_data }, + { label: "Electron Log File", value: info.paths.electron_log_file }, + { label: "Electron Logs Dir", value: info.paths.electron_logs_dir }, + ...(info.paths.electron_main_log_file + ? [ + { + label: "Electron Main Log", + value: info.paths.electron_main_log_file + } + ] + : []) + ); + } + return list; + }, [info, isElectron]); + + if (loading) { + return ( + + + Loading system info… + + ); + } + + if (!info) { + return No system info available.; + } + + return ( +
+
+
+ System +
+ +
+
+ + OS: {info.os.platform} {info.os.release} ({info.os.arch}) + + + Versions: python={info.versions.python ?? ""} core= + {info.versions.nodetool_core ?? ""} base= + {info.versions.nodetool_base ?? ""} cuda= + {info.versions.cuda ?? ""} + + {(info.versions.gpu_name || + info.versions.vram_total_gb || + info.versions.driver_version) && ( + + GPU: {info.versions.gpu_name ?? "N/A"}, VRAM= + {info.versions.vram_total_gb + ? info.versions.vram_total_gb + "GB" + : "N/A"} + , driver= + {info.versions.driver_version ?? "N/A"} + + )} +
+ + + +
+ Paths + {paths.map(({ label, value }) => ( +
+ + {label} + + + {value || "-"} + +
+ + +
+
+ ))} +
+ + {health && ( + <> + +
+ Health + + Summary: ok={health.summary.ok} warn={health.summary.warn} error= + {health.summary.error} + + + {health.checks.map((c) => ( +
+ + + + + {c.id} + + + {c.details || ""} + + + {c.fix_hint && ( + + {c.fix_hint} + + )} + +
+ ))} +
+
+ + )} +
+ ); +} diff --git a/web/src/components/themes/ThemeNodetool.tsx b/web/src/components/themes/ThemeNodetool.tsx index 18e967113..efd29b8a2 100644 --- a/web/src/components/themes/ThemeNodetool.tsx +++ b/web/src/components/themes/ThemeNodetool.tsx @@ -14,13 +14,14 @@ import "@fontsource/jetbrains-mono/300.css"; import "@fontsource/jetbrains-mono/400.css"; import "@fontsource/jetbrains-mono/600.css"; -// Theme augmentation moved to a single global file `theme.d.ts` to avoid duplication +// Theme augmentation in `theme.d.ts` const ThemeNodetool = createTheme({ cssVariables: { cssVarPrefix: "", colorSchemeSelector: "class" }, + defaultColorScheme: "dark", colorSchemes: { light: { palette: paletteLight diff --git a/web/src/styles/vars.css b/web/src/styles/vars.css index 1a229cae5..7d1093acb 100644 --- a/web/src/styles/vars.css +++ b/web/src/styles/vars.css @@ -1,11 +1,14 @@ +/* SHOULD BE REMOVED IN FUTURE */ +/* USE GENERATED CSS VARS INSTEAD */ + :root { --font_family: Inter, Arial, sans-serif; --font_family2: "Jetbrains Mono", Inter, Arial, sans-serif; - + /* General */ --c_background: var(--palette-c_background); --c_node_menu: var(--palette-c_node_menu); - + /* Highlights */ --c_link: var(--palette-c_link); --c_link_visited: var(--palette-c_link_visited); @@ -34,5 +37,15 @@ --c_node_bg_group: var(--palette-c_node_bg_group); --c_node_header_bg: var(--palette-c_node_header_bg); --c_node_header_bg_group: var(--palette-c_node_header_bg_group); -} + /* REACTFLOW */ + --c_editor_bg_color: var(--palette-c_editor_bg_color); + --c_editor_grid_color: var(--palette-c_editor_grid_color); + --c_editor_axis_color: var(--palette-c_editor_axis_color); + --c_selection_rect: var(--palette-c_selection_rect); + + /* PROVIDERS */ + --c_provider_api: var(--palette-c_provider_api); + --c_provider_local: var(--palette-c_provider_local); + --c_provider_hf: var(--palette-c_provider_hf); +} diff --git a/web/src/utils/fileExplorer.ts b/web/src/utils/fileExplorer.ts index 9b33c914e..66525e91d 100644 --- a/web/src/utils/fileExplorer.ts +++ b/web/src/utils/fileExplorer.ts @@ -40,13 +40,16 @@ export function isPathValid(path: string): boolean { // 1. POSIX absolute path starting with '/' // 2. Windows absolute path starting with a drive letter followed by ':' and either \\ or '/' // 3. Home‐relative path starting with '~' + // 4. Windows environment variables like %APPDATA%, %LOCALAPPDATA%, etc. const windowsAbsRegex = /^[a-zA-Z]:[\\/].+/; const posixAbsRegex = /^\/.+/; const homeRegex = /^~[\\/].+/; + const windowsEnvVarRegex = /^%[A-Z_]+%[\\/].*/; return ( windowsAbsRegex.test(path) || posixAbsRegex.test(path) || - homeRegex.test(path) + homeRegex.test(path) || + windowsEnvVarRegex.test(path) ); }