feat(ui): bulk-clear conversations, fold Start/Stop AI into the status dot

- Clear button beside Export deletes every conversation currently listed. It
  respects the search filter, so it doubles as "delete these matches"; the
  confirm text names which of the two is about to happen. Starts a fresh
  conversation id if the active one was among those deleted.
- The separate Start/Stop AI button is gone. The status dot was already
  colour-coding the same state and sitting right above it, so clicking it now
  toggles Ollama and the tooltip carries the phase text ("loading model…"),
  which is where the eye already is during the wait.
- Active nav/conversation items are marked with a blue left border and
  weighted text instead of a filled background, and the sidebar is wider so
  conversation titles stop truncating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
janvanwan
2026-08-18 14:39:30 -05:00
co-authored by Claude Opus 5
parent 4bfa979ee8
commit fe5d18afa7
+51 -32
View File
@@ -154,6 +154,21 @@ function App() {
} }
}; };
// Bulk delete of everything currently listed (respects the search filter).
const clearConversations = async () => {
const n = conversations.length;
if (!n) return;
const what = search ? `${n} matching conversation${n > 1 ? "s" : ""}` : `all ${n} conversation${n > 1 ? "s" : ""}`;
if (!window.confirm(`Delete ${what}? This cannot be undone.`)) return;
const ids = conversations.map(c => c.id);
await Promise.all(
ids.map(id => fetch(`${API_BASE}/conversations/${id}`, { method: "DELETE" }).catch(err =>
console.error("Failed to delete conversation:", err)))
);
if (ids.includes(activeConversationId)) setActiveConversationId(crypto.randomUUID());
loadConversations(search);
};
const navItems = [ const navItems = [
{ key: "chatbot", icon: "💬", label: "Chat" }, { key: "chatbot", icon: "💬", label: "Chat" },
{ key: "playbook", icon: "📖", label: "Playbooks" }, { key: "playbook", icon: "📖", label: "Playbooks" },
@@ -168,7 +183,7 @@ function App() {
<div style={{ background: "#111", color: "#eee", height: "100vh", overflow: "hidden", fontFamily: "system-ui", display: "flex" }}> <div style={{ background: "#111", color: "#eee", height: "100vh", overflow: "hidden", fontFamily: "system-ui", display: "flex" }}>
{/* Sidebar */} {/* Sidebar */}
<aside style={{ <aside style={{
width: "220px", width: "400px",
flexShrink: 0, flexShrink: 0,
background: "#0a0a0a", background: "#0a0a0a",
borderRight: "1px solid #333", borderRight: "1px solid #333",
@@ -201,7 +216,7 @@ function App() {
borderRadius: "50%", borderRadius: "50%",
background: status === "Offline" || ollamaStatus === "unavailable" ? "#f44336" background: status === "Offline" || ollamaStatus === "unavailable" ? "#f44336"
: ollamaStatus === "running" ? "#4caf50" : "#ff9800", : ollamaStatus === "running" ? "#4caf50" : "#ff9800",
cursor: "pointer", cursor: (ollamaBusy || ollamaStatus === "unavailable" || status === "Offline") ? "not-allowed" : "pointer",
transition: "all 0.3s" transition: "all 0.3s"
}} }}
onMouseEnter={() => { onMouseEnter={() => {
@@ -209,7 +224,11 @@ function App() {
setShowStatusTooltip(true); setShowStatusTooltip(true);
}} }}
onMouseLeave={() => setShowStatusTooltip(false)} onMouseLeave={() => setShowStatusTooltip(false)}
title="Hover to refresh" onClick={() => {
if (ollamaBusy || ollamaStatus === "unavailable" || status === "Offline") return;
toggleOllama();
}}
title={ollamaStatus === "unavailable" ? "Ollama binary not found" : "Click to start/stop the AI"}
/> />
{showStatusTooltip && ( {showStatusTooltip && (
<div style={{ <div style={{
@@ -230,35 +249,15 @@ function App() {
Synapse: <strong>{status}</strong> Synapse: <strong>{status}</strong>
</div> </div>
<div style={{ color: ollamaStatus === "unavailable" ? "#f44336" : ollamaStatus === "running" ? "#4caf50" : "#aaa" }}> <div style={{ color: ollamaStatus === "unavailable" ? "#f44336" : ollamaStatus === "running" ? "#4caf50" : "#aaa" }}>
Ollama: <strong>{ollamaStatus}</strong> {/* Naming the phase matters: nearly all of the wait is the model
being read off disk (GBs), not the server booting. */}
Ollama: <strong>{ollamaBusy ? (ollamaStatus === "running" ? "stopping…" : "loading model…") : ollamaStatus}</strong>
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Manual AI start/stop — Ollama is off until the user turns it on */} {/* Start/Stop AI moved onto the status dot above — click it to toggle. */}
<button
onClick={toggleOllama}
disabled={ollamaBusy || ollamaStatus === "unavailable" || status === "Offline"}
title={ollamaStatus === "unavailable" ? "Ollama binary not found" : "Start or stop the AI"}
style={{
width: "100%",
padding: "0.4rem 0.6rem",
background: ollamaStatus === "running" ? "#2a1a1a" : "#152a15",
color: ollamaStatus === "running" ? "#ff8a80" : "#8aff8a",
border: "1px solid " + (ollamaStatus === "running" ? "#5a2a2a" : "#2a5a2a"),
borderRadius: "6px",
cursor: (ollamaBusy || ollamaStatus === "unavailable" || status === "Offline") ? "not-allowed" : "pointer",
fontSize: "0.78rem",
opacity: (ollamaStatus === "unavailable" || status === "Offline") ? 0.5 : 1,
}}
>
{/* Naming the phase matters: nearly all of the wait is the model
being read off disk (GBs), not the server booting. "…" for a
minute reads as hung. */}
{ollamaBusy ? (ollamaStatus === "running" ? "Stopping…" : "Loading model…")
: ollamaStatus === "running" ? "⏹ Stop AI" : "▶ Start AI"}
</button>
<nav style={{ display: "flex", flexDirection: "column", width: "100%", marginTop: "0.1rem" }}> <nav style={{ display: "flex", flexDirection: "column", width: "100%", marginTop: "0.1rem" }}>
{navItems.map(item => { {navItems.map(item => {
@@ -269,9 +268,11 @@ function App() {
onClick={() => setCurrentPage(item.key)} onClick={() => setCurrentPage(item.key)}
style={{ style={{
padding: "0.32rem 0.5rem", padding: "0.32rem 0.5rem",
background: isActive ? "#1c2733" : "transparent", background: "transparent",
color: isActive ? "#fff" : "#bbb", color: isActive ? "#4aa3e0" : "#bbb",
fontWeight: isActive ? 600 : 400,
border: "none", border: "none",
borderLeft: `2px solid ${isActive ? "#007acc" : "transparent"}`,
borderRadius: "5px", borderRadius: "5px",
cursor: "pointer", cursor: "pointer",
fontSize: "0.8rem", fontSize: "0.8rem",
@@ -330,6 +331,22 @@ function App() {
> >
Export Export
</button> </button>
<button
onClick={clearConversations}
title={search ? "Delete the conversations matching this search" : "Delete all conversations"}
disabled={conversations.length === 0}
style={{
padding: "0.2rem 0.5rem",
background: "#161616",
color: conversations.length === 0 ? "#555" : "#c66",
border: "1px solid #2a2a2a",
borderRadius: "6px",
cursor: conversations.length === 0 ? "default" : "pointer",
fontSize: "0.75rem",
}}
>
🗑 Clear
</button>
<button <button
onClick={startNewChat} onClick={startNewChat}
title="New chat" title="New chat"
@@ -385,8 +402,9 @@ function App() {
marginBottom: "0.3rem", marginBottom: "0.3rem",
borderRadius: "6px", borderRadius: "6px",
cursor: "pointer", cursor: "pointer",
background: isActive ? "#1c2a3a" : (isHover ? "#161616" : "transparent"), background: isHover ? "#161616" : "transparent",
border: `1px solid ${isActive ? "#2a4a6a" : "transparent"}`, border: "1px solid transparent",
borderLeft: `2px solid ${isActive ? "#007acc" : "transparent"}`,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "0.4rem", gap: "0.4rem",
@@ -395,7 +413,8 @@ function App() {
<div style={{ flexGrow: 1, minWidth: 0 }}> <div style={{ flexGrow: 1, minWidth: 0 }}>
<div style={{ <div style={{
fontSize: "0.82rem", fontSize: "0.82rem",
color: isActive ? "#eee" : "#ccc", color: isActive ? "#4aa3e0" : "#ccc",
fontWeight: isActive ? 600 : 400,
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",