diff --git a/traderai/agent.py b/traderai/agent.py index 6422f3d..2d9b207 100644 --- a/traderai/agent.py +++ b/traderai/agent.py @@ -53,7 +53,7 @@ class OllamaAgent: "online": False, "model": self.model, "base_url": self.base_url, - "message": f"Ollama is offline or unreachable at {self.base_url}. Start Ollama and make sure the model is pulled.", + "message": f"Ollama is offline or unreachable at {self.base_url}. Open the Ollama tab and use the recommended action.", "detail": str(exc), } diff --git a/web/app.js b/web/app.js index 6f0169b..9ab49ee 100644 --- a/web/app.js +++ b/web/app.js @@ -52,6 +52,8 @@ let ollamaOnline = true; let latestUpdate = null; let currentThreadId = "default"; let currentNegotiationId = null; +let latestOllamaStatus = null; +const clickedOllamaActions = new Set(); if (window.lucide) { window.lucide.createIcons(); @@ -585,6 +587,7 @@ async function refreshOllamaStatus() { function renderOllamaStatus(status) { if (!ollamaStatusEl) return; + latestOllamaStatus = status; const models = status.models?.length ? status.models.join(", ") : "None detected"; const pillClass = status.installed && status.running && status.model_available ? "status-pill" : "status-pill warning"; ollamaStatusEl.innerHTML = ` @@ -595,14 +598,18 @@ function renderOllamaStatus(status) { ${ollamaStatusItem("Model", status.configured_model || "")} ${ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No")} ${ollamaStatusItem("URL", status.base_url || "")} - ${ollamaStatusItem("Auto Install", status.can_auto_install ? "Available" : "Unavailable")} + ${status.can_auto_install ? ollamaStatusItem("Auto Install", "Available") : ""} ${ollamaStatusItem("Installed Models", models)} ${status.detail ? ollamaStatusItem("Detail", status.detail) : ""} `; - if (ollamaInstallButton) ollamaInstallButton.disabled = Boolean(status.installed); + if (ollamaInstallButton) { + ollamaInstallButton.hidden = !status.can_auto_install; + ollamaInstallButton.disabled = Boolean(status.installed) || !status.can_auto_install; + } if (ollamaLaunchButton) ollamaLaunchButton.disabled = !status.installed || Boolean(status.running); if (ollamaPullButton) ollamaPullButton.disabled = !status.running || Boolean(status.model_available); + updateOllamaAttention(status); } function ollamaStatusItem(label, value) { @@ -630,6 +637,29 @@ async function postOllamaAction(endpoint, options = {}) { } } +function markOllamaActionClicked(action) { + if (action) clickedOllamaActions.add(action); + updateOllamaAttention(); +} + +function setOllamaButtonAttention(button, action, active) { + if (!button) return; + const shouldPulse = active && !clickedOllamaActions.has(action) && !button.disabled && !button.hidden; + button.classList.toggle("attention-pulse", shouldPulse); +} + +function updateOllamaAttention(status = null) { + const currentStatus = status || latestOllamaStatus; + if (!currentStatus) return; + const ready = Boolean(currentStatus.installed && currentStatus.running && currentStatus.model_available); + ollamaToggle?.classList.toggle("attention-pulse", !ready); + setOllamaButtonAttention(ollamaDownloadButton, "download", !currentStatus.installed); + setOllamaButtonAttention(ollamaInstallButton, "install", !currentStatus.installed && currentStatus.can_auto_install); + setOllamaButtonAttention(ollamaLaunchButton, "launch", currentStatus.installed && !currentStatus.running); + setOllamaButtonAttention(ollamaPullButton, "pull", currentStatus.running && !currentStatus.model_available); + if (ready) clickedOllamaActions.clear(); +} + function configuredOllamaModel() { return document.getElementById("ollama-model")?.value || ""; } @@ -980,20 +1010,24 @@ async function checkHealth() { ollamaOnline = Boolean(health.online); if (!ollamaOnline) { statusEl.textContent = "Offline"; - setWarning(health.message || "Ollama is offline. Start Ollama before chatting."); + setWarning("Ollama needs attention. Open the Ollama tab and use the pulsing action button."); + ollamaToggle?.classList.add("attention-pulse"); return false; } if (health.model_available === false) { - setWarning(`Ollama is online, but model "${health.model}" is not pulled. Run: ollama pull ${health.model}`); + setWarning(`Ollama needs the configured model "${health.model}". Open the Ollama tab and use Install Model.`); + ollamaToggle?.classList.add("attention-pulse"); } else { setWarning(""); + ollamaToggle?.classList.remove("attention-pulse"); } statusEl.textContent = "Ready"; return true; } catch (error) { ollamaOnline = false; statusEl.textContent = "Offline"; - setWarning(`Could not check Ollama health: ${error.message}`); + setWarning("Could not check Ollama health. Open the Ollama tab and use the pulsing action button."); + ollamaToggle?.classList.add("attention-pulse"); return false; } } @@ -1174,10 +1208,22 @@ memoryToggle?.addEventListener("click", () => toggleSidebarPanel("memory")); ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama")); ollamaForm?.addEventListener("submit", saveOllamaConfig); ollamaRefreshButton?.addEventListener("click", refreshOllamaStatus); -ollamaDownloadButton?.addEventListener("click", () => postOllamaAction("/api/ollama/download")); -ollamaInstallButton?.addEventListener("click", () => postOllamaAction("/api/ollama/install")); -ollamaLaunchButton?.addEventListener("click", () => postOllamaAction("/api/ollama/launch")); -ollamaPullButton?.addEventListener("click", () => postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } })); +ollamaDownloadButton?.addEventListener("click", () => { + markOllamaActionClicked("download"); + postOllamaAction("/api/ollama/download"); +}); +ollamaInstallButton?.addEventListener("click", () => { + markOllamaActionClicked("install"); + postOllamaAction("/api/ollama/install"); +}); +ollamaLaunchButton?.addEventListener("click", () => { + markOllamaActionClicked("launch"); + postOllamaAction("/api/ollama/launch"); +}); +ollamaPullButton?.addEventListener("click", () => { + markOllamaActionClicked("pull"); + postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } }); +}); updateCheckButton?.addEventListener("click", checkForUpdate); updateInstallButton?.addEventListener("click", installUpdate); updateOpenReleasesButton?.addEventListener("click", openReleasesPage); @@ -1194,7 +1240,7 @@ async function sendMessage() { if (!message || input.disabled) return; const healthy = await checkHealth(); if (!healthy) { - addMessage("assistant warning-message", "Ollama is offline. Start Ollama, then try again."); + addMessage("assistant warning-message", "Ollama needs attention before chat can continue. Open the Ollama tab and press the pulsing action button, then try again."); return; } input.value = ""; @@ -1257,7 +1303,7 @@ async function sendMessage() { } } catch (error) { const message = error.message.includes("503") - ? "Ollama is offline or unreachable. Start Ollama, then try again." + ? "Ollama needs attention before chat can continue. Open the Ollama tab and press the pulsing action button, then try again." : `Chat failed: ${error.message}`; setWarning(message); setMessageMarkdown(assistantNode, message); diff --git a/web/styles.css b/web/styles.css index 5e4008e..37bd486 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1078,6 +1078,31 @@ button.secondary { width: 100%; } +.attention-pulse { + position: relative; + border-color: rgba(47, 125, 50, 0.72) !important; + box-shadow: 0 0 0 0 rgba(55, 148, 61, 0.54); + animation: green-attention-pulse 1.35s ease-out infinite; +} + +.attention-pulse:disabled, +.attention-pulse[hidden] { + animation: none; + box-shadow: none; +} + +@keyframes green-attention-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(55, 148, 61, 0.58); + } + 70% { + box-shadow: 0 0 0 10px rgba(55, 148, 61, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(55, 148, 61, 0); + } +} + .update-box { display: grid; gap: 10px;