fix(launch): reliable Windows launcher; feat(ui): compact sidebar, Vite toggle, chat Think toggle
Ported from the private repo via bin/publish.sh, plus a manual catch-up on files that had drifted out of sync before today: - launch_nexus.ps1: health-check based restart decisions instead of a bare port-listen check (a wedged leftover process squatting a port used to look "already running" and block the real service from starting), a script-path quoting fix for Start-Process, hidden console via a wscript.exe wrapper (bin/launch_nexus_hidden.vbs), and a taskbar/window icon for the native app window. - Sidebar: slim icon+text nav rows instead of bulky bordered buttons, tighter spacing throughout. - Settings: full-width layout, a Vite dev-server Start/Stop toggle (synapse/frontend_manager.py + /frontend/* endpoints), and the Linux-only Icon Branding section now gated on the new /status `platform` field instead of always rendering. - Chatbot: a Think toggle next to the model picker, so extended thinking can be flipped without leaving the chat page. - management/ncp.py: faster start/stop polling (0.25s steps instead of 1s), Vite no longer blocks `ncp start` on Linux and is skipped outright on Windows. Note: the private repo also has a Mail (IMAP/SMTP) feature; it's intentionally not included here, so the Mail-only pieces of main.py, App.jsx, and requirements-windows.txt were left out of this port.
This commit is contained in:
+65
-52
@@ -155,20 +155,20 @@ function App() {
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ key: "chatbot", label: "💬 Chat" },
|
||||
{ key: "playbook", label: "📖 Playbooks" },
|
||||
{ key: "models", label: "🤖 Models", badge: isModelPulling },
|
||||
{ key: "memory", label: "🧠 Memory" },
|
||||
{ key: "documents", label: "📄 Documents" },
|
||||
{ key: "logs", label: "📜 Logs" },
|
||||
{ key: "settings", label: "⚙️ Settings" },
|
||||
{ key: "chatbot", icon: "💬", label: "Chat" },
|
||||
{ key: "playbook", icon: "📖", label: "Playbooks" },
|
||||
{ key: "models", icon: "🤖", label: "Models", badge: isModelPulling },
|
||||
{ key: "memory", icon: "🧠", label: "Memory" },
|
||||
{ key: "documents", icon: "📄", label: "Documents" },
|
||||
{ key: "logs", icon: "📜", label: "Logs" },
|
||||
{ key: "settings", icon: "⚙️", label: "Settings" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ background: "#111", color: "#eee", height: "100vh", overflow: "hidden", fontFamily: "system-ui", display: "flex" }}>
|
||||
{/* Sidebar */}
|
||||
<aside style={{
|
||||
width: "260px",
|
||||
width: "220px",
|
||||
flexShrink: 0,
|
||||
background: "#0a0a0a",
|
||||
borderRight: "1px solid #333",
|
||||
@@ -178,18 +178,18 @@ function App() {
|
||||
}}>
|
||||
{/* Top cell: brand + nav */}
|
||||
<div style={{
|
||||
padding: "1.25rem 1rem 1rem",
|
||||
padding: "0.75rem 0.6rem 0.65rem",
|
||||
borderBottom: "1px solid #1f1f1f",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
gap: "0.4rem",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<img src="/n small.png" alt="Logo" style={{ width: "48px", height: "48px", objectFit: "contain", borderRadius: "8px" }} />
|
||||
<h1 style={{ fontSize: "1rem", margin: 0, color: "#007acc", textAlign: "center" }}>NexusOS</h1>
|
||||
<img src="/n small.png" alt="Logo" style={{ width: "36px", height: "36px", objectFit: "contain", borderRadius: "6px" }} />
|
||||
<h1 style={{ fontSize: "0.9rem", margin: 0, color: "#007acc", textAlign: "center" }}>NexusOS</h1>
|
||||
{version && (
|
||||
<span style={{ fontSize: "0.7rem", color: "#666", marginTop: "-0.5rem", letterSpacing: "0.02em" }}>v{version}</span>
|
||||
<span style={{ fontSize: "0.65rem", color: "#666", marginTop: "-0.35rem", letterSpacing: "0.02em" }}>v{version}</span>
|
||||
)}
|
||||
|
||||
{/* Status Indicator */}
|
||||
@@ -243,13 +243,13 @@ function App() {
|
||||
title={ollamaStatus === "unavailable" ? "Ollama binary not found" : "Start or stop the AI"}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.5rem 0.75rem",
|
||||
padding: "0.4rem 0.6rem",
|
||||
background: ollamaStatus === "running" ? "#2a1a1a" : "#152a15",
|
||||
color: ollamaStatus === "running" ? "#ff8a80" : "#8aff8a",
|
||||
border: "1px solid " + (ollamaStatus === "running" ? "#5a2a2a" : "#2a5a2a"),
|
||||
borderRadius: "8px",
|
||||
borderRadius: "6px",
|
||||
cursor: (ollamaBusy || ollamaStatus === "unavailable" || status === "Offline") ? "not-allowed" : "pointer",
|
||||
fontSize: "0.8rem",
|
||||
fontSize: "0.78rem",
|
||||
opacity: (ollamaStatus === "unavailable" || status === "Offline") ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
@@ -260,39 +260,46 @@ function App() {
|
||||
: ollamaStatus === "running" ? "⏹ Stop AI" : "▶ Start AI"}
|
||||
</button>
|
||||
|
||||
<nav style={{ display: "flex", flexDirection: "column", gap: "0.4rem", width: "100%", marginTop: "0.25rem" }}>
|
||||
{navItems.map(item => (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setCurrentPage(item.key)}
|
||||
style={{
|
||||
padding: "0.65rem 0.85rem",
|
||||
background: currentPage === item.key ? "#007acc" : "#161616",
|
||||
color: "#fff",
|
||||
border: "1px solid " + (currentPage === item.key ? "#0099ff" : "#2a2a2a"),
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.85rem",
|
||||
textAlign: "left",
|
||||
transition: "all 0.15s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
{item.badge && (
|
||||
<span style={{
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
borderRadius: "50%",
|
||||
background: "#28a745",
|
||||
flexShrink: 0,
|
||||
animation: "pulse 1.2s ease-in-out infinite",
|
||||
}} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<nav style={{ display: "flex", flexDirection: "column", width: "100%", marginTop: "0.1rem" }}>
|
||||
{navItems.map(item => {
|
||||
const isActive = currentPage === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setCurrentPage(item.key)}
|
||||
style={{
|
||||
padding: "0.32rem 0.5rem",
|
||||
background: isActive ? "#1c2733" : "transparent",
|
||||
color: isActive ? "#fff" : "#bbb",
|
||||
border: "none",
|
||||
borderRadius: "5px",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.8rem",
|
||||
textAlign: "left",
|
||||
transition: "background 0.12s, color 0.12s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.45rem",
|
||||
width: "100%",
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!isActive) e.currentTarget.style.background = "#161616"; }}
|
||||
onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.background = "transparent"; }}
|
||||
>
|
||||
<span style={{ fontSize: "0.85rem", width: "1rem", textAlign: "center", flexShrink: 0 }}>{item.icon}</span>
|
||||
<span style={{ flexGrow: 1 }}>{item.label}</span>
|
||||
{item.badge && (
|
||||
<span style={{
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
background: "#28a745",
|
||||
flexShrink: 0,
|
||||
animation: "pulse 1.2s ease-in-out infinite",
|
||||
}} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -302,7 +309,7 @@ function App() {
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "0.75rem 0.75rem 0.75rem",
|
||||
padding: "0.6rem 0.6rem 0.6rem",
|
||||
}}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.5rem", flexShrink: 0 }}>
|
||||
<span style={{ fontSize: "0.75rem", color: "#888", textTransform: "uppercase", letterSpacing: "0.05em" }}>Chats</span>
|
||||
@@ -470,13 +477,19 @@ function App() {
|
||||
}}>
|
||||
<Models onPullStateChange={setIsModelPulling} />
|
||||
</div>
|
||||
{currentPage === "chatbot" && (
|
||||
{/* Chatbot stays mounted so an in-flight reply survives page navigation */}
|
||||
<div style={{
|
||||
display: currentPage === "chatbot" ? "flex" : "none",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
}}>
|
||||
<Chatbot
|
||||
conversationId={activeConversationId}
|
||||
setConversationId={setActiveConversationId}
|
||||
onConversationChanged={() => loadConversations(search)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{currentPage === "playbook" && <Playbook />}
|
||||
{currentPage === "memory" && <Memory />}
|
||||
{currentPage === "documents" && <Documents />}
|
||||
|
||||
@@ -10,6 +10,7 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
const [modelList, setModelList] = useState([]);
|
||||
const [selectedModel, setSelectedModel] = useState(""); // "" = auto
|
||||
const [autoModel, setAutoModel] = useState(null);
|
||||
const [think, setThink] = useState(false); // extended thinking, mirrors Settings
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const [copiedIdx, setCopiedIdx] = useState(null);
|
||||
const [lastStats, setLastStats] = useState(null);
|
||||
@@ -138,7 +139,10 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
]).then(([models, settings]) => {
|
||||
if (models?.models) setModelList(models.models);
|
||||
if (models?.selected) setAutoModel(models.selected);
|
||||
if (settings) setSelectedModel(settings.model || "");
|
||||
if (settings) {
|
||||
setSelectedModel(settings.model || "");
|
||||
setThink(!!settings.think);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
@@ -163,6 +167,18 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
} catch { /* persisting the model choice is best-effort */ }
|
||||
};
|
||||
|
||||
const toggleThink = async () => {
|
||||
const next = !think;
|
||||
setThink(next);
|
||||
try {
|
||||
await fetch(`${API_BASE}/settings`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ think: next }),
|
||||
});
|
||||
} catch { /* persisting the toggle is best-effort */ }
|
||||
};
|
||||
|
||||
const startNewChat = () => {
|
||||
if (abortRef.current) abortRef.current.abort();
|
||||
setInput("");
|
||||
@@ -202,6 +218,7 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
history,
|
||||
think,
|
||||
...(imgs && imgs.length ? { images: imgs } : {}),
|
||||
}),
|
||||
signal: controller.signal,
|
||||
@@ -522,6 +539,28 @@ export function Chatbot({ conversationId, setConversationId, onConversationChang
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleThink}
|
||||
title={think ? "Extended thinking is on — click to turn off" : "Extended thinking is off — click to turn on"}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
color: think ? "#c4b5fd" : "#888",
|
||||
background: think ? "#1e1a2e" : "#1a1a1a",
|
||||
border: "1px solid " + (think ? "#7c3aed" : "#2a2a2a"),
|
||||
borderRadius: "4px",
|
||||
padding: "0.15rem 0.5rem",
|
||||
cursor: "pointer",
|
||||
letterSpacing: "0.03em",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.3rem",
|
||||
}}
|
||||
>
|
||||
🧠 Think
|
||||
<span style={{ fontSize: "0.6rem", color: think ? "#8aff8a" : "#555" }}>
|
||||
{think ? "on" : "off"}
|
||||
</span>
|
||||
</button>
|
||||
{lastStats && (
|
||||
<span style={{
|
||||
fontSize: "0.7rem",
|
||||
|
||||
@@ -33,6 +33,34 @@ export function Settings() {
|
||||
const [brandQueue, setBrandQueue] = useState([]);
|
||||
const [isBranding, setIsBranding] = useState(false);
|
||||
const [modelList, setModelList] = useState([]);
|
||||
const [platform, setPlatform] = useState("");
|
||||
|
||||
// Vite dev server toggle
|
||||
const [viteRunning, setViteRunning] = useState(null);
|
||||
const [viteBusy, setViteBusy] = useState(false);
|
||||
const [viteError, setViteError] = useState("");
|
||||
|
||||
const loadViteStatus = () => {
|
||||
fetch(`${API_BASE}/frontend/status`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d) setViteRunning(!!d.running); })
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const toggleVite = async () => {
|
||||
setViteBusy(true);
|
||||
setViteError("");
|
||||
try {
|
||||
const action = viteRunning ? "stop" : "start";
|
||||
const r = await fetch(`${API_BASE}/frontend/${action}`, { method: "POST" });
|
||||
const d = await r.json().catch(() => null);
|
||||
if (d && d.status === "error") setViteError(d.detail || "Failed to start Vite");
|
||||
} catch (e) {
|
||||
setViteError(e.message || "Failed");
|
||||
}
|
||||
setViteBusy(false);
|
||||
loadViteStatus();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/models`).then(r => r.ok ? r.json() : null)
|
||||
@@ -46,6 +74,13 @@ export function Settings() {
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d) setIconApps(d.apps || []); })
|
||||
.catch(() => {});
|
||||
|
||||
fetch(`${API_BASE}/status`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d) setPlatform(d.platform || ""); })
|
||||
.catch(() => {});
|
||||
|
||||
loadViteStatus();
|
||||
}, []);
|
||||
|
||||
const update = (key, value) => setForm(f => ({ ...f, [key]: value }));
|
||||
@@ -167,7 +202,7 @@ export function Settings() {
|
||||
const displayQueue = isBranding ? brandQueue : selectedApps;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "720px" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<h2 style={{ margin: "0 0 1.25rem", fontSize: "1.1rem", color: "#eee" }}>Settings</h2>
|
||||
|
||||
{/* Auto model routing */}
|
||||
@@ -480,7 +515,42 @@ export function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Icon Branding */}
|
||||
{/* Vite Dev Server */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ margin: "0 0 0.5rem", fontSize: "0.95rem", color: "#bbb" }}>Vite Dev Server</h3>
|
||||
<p style={{ margin: "0 0 1rem", fontSize: "0.75rem", color: "#555" }}>
|
||||
Hot-reload server for editing the frontend directly (interface/web). Not
|
||||
needed for normal use — this backend already serves the built UI.
|
||||
</p>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", flexWrap: "wrap" }}>
|
||||
<button
|
||||
onClick={toggleVite}
|
||||
disabled={viteBusy || viteRunning === null}
|
||||
style={{
|
||||
padding: "0.6rem 1.25rem",
|
||||
background: viteRunning ? "#2a1a1a" : "#152a15",
|
||||
color: viteRunning ? "#ff8a80" : "#8aff8a",
|
||||
border: "1px solid " + (viteRunning ? "#5a2a2a" : "#2a5a2a"),
|
||||
borderRadius: "8px",
|
||||
cursor: (viteBusy || viteRunning === null) ? "not-allowed" : "pointer",
|
||||
fontSize: "0.88rem",
|
||||
fontWeight: 500,
|
||||
opacity: (viteBusy || viteRunning === null) ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{viteBusy ? (viteRunning ? "Stopping…" : "Starting…")
|
||||
: viteRunning ? "⏹ Stop Vite" : "▶ Start Vite"}
|
||||
</button>
|
||||
<span style={{ fontSize: "0.82rem", color: viteRunning ? "#8aff8a" : "#666" }}>
|
||||
{viteRunning === null ? "Checking…" : viteRunning ? "Running at :5173" : "Stopped"}
|
||||
</span>
|
||||
{viteError && <span style={{ color: "#f44336", fontSize: "0.85rem" }}>{viteError}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Icon Branding — Linux-only: composites icons onto XFCE/GTK app tiles,
|
||||
which don't exist on Windows/macOS. */}
|
||||
{platform === "linux" && (
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ margin: "0 0 0.75rem", fontSize: "0.95rem", color: "#bbb" }}>App Icon Branding</h3>
|
||||
<p style={{ margin: "0 0 1rem", fontSize: "0.78rem", color: "#555" }}>
|
||||
@@ -638,6 +708,7 @@ export function Settings() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
|
||||
|
||||
Reference in New Issue
Block a user