ux: ollama flow

This commit is contained in:
2026-05-07 03:03:25 -04:00
parent 3b6e3c34d5
commit 767e929bf9
3 changed files with 83 additions and 12 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ class OllamaAgent:
"online": False, "online": False,
"model": self.model, "model": self.model,
"base_url": self.base_url, "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), "detail": str(exc),
} }
+57 -11
View File
@@ -52,6 +52,8 @@ let ollamaOnline = true;
let latestUpdate = null; let latestUpdate = null;
let currentThreadId = "default"; let currentThreadId = "default";
let currentNegotiationId = null; let currentNegotiationId = null;
let latestOllamaStatus = null;
const clickedOllamaActions = new Set();
if (window.lucide) { if (window.lucide) {
window.lucide.createIcons(); window.lucide.createIcons();
@@ -585,6 +587,7 @@ async function refreshOllamaStatus() {
function renderOllamaStatus(status) { function renderOllamaStatus(status) {
if (!ollamaStatusEl) return; if (!ollamaStatusEl) return;
latestOllamaStatus = status;
const models = status.models?.length ? status.models.join(", ") : "None detected"; const models = status.models?.length ? status.models.join(", ") : "None detected";
const pillClass = status.installed && status.running && status.model_available ? "status-pill" : "status-pill warning"; const pillClass = status.installed && status.running && status.model_available ? "status-pill" : "status-pill warning";
ollamaStatusEl.innerHTML = ` ollamaStatusEl.innerHTML = `
@@ -595,14 +598,18 @@ function renderOllamaStatus(status) {
${ollamaStatusItem("Model", status.configured_model || "")} ${ollamaStatusItem("Model", status.configured_model || "")}
${ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No")} ${ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No")}
${ollamaStatusItem("URL", status.base_url || "")} ${ollamaStatusItem("URL", status.base_url || "")}
${ollamaStatusItem("Auto Install", status.can_auto_install ? "Available" : "Unavailable")} ${status.can_auto_install ? ollamaStatusItem("Auto Install", "Available") : ""}
</div> </div>
${ollamaStatusItem("Installed Models", models)} ${ollamaStatusItem("Installed Models", models)}
${status.detail ? ollamaStatusItem("Detail", status.detail) : ""} ${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 (ollamaLaunchButton) ollamaLaunchButton.disabled = !status.installed || Boolean(status.running);
if (ollamaPullButton) ollamaPullButton.disabled = !status.running || Boolean(status.model_available); if (ollamaPullButton) ollamaPullButton.disabled = !status.running || Boolean(status.model_available);
updateOllamaAttention(status);
} }
function ollamaStatusItem(label, value) { 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() { function configuredOllamaModel() {
return document.getElementById("ollama-model")?.value || ""; return document.getElementById("ollama-model")?.value || "";
} }
@@ -980,20 +1010,24 @@ async function checkHealth() {
ollamaOnline = Boolean(health.online); ollamaOnline = Boolean(health.online);
if (!ollamaOnline) { if (!ollamaOnline) {
statusEl.textContent = "Offline"; 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; return false;
} }
if (health.model_available === 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 { } else {
setWarning(""); setWarning("");
ollamaToggle?.classList.remove("attention-pulse");
} }
statusEl.textContent = "Ready"; statusEl.textContent = "Ready";
return true; return true;
} catch (error) { } catch (error) {
ollamaOnline = false; ollamaOnline = false;
statusEl.textContent = "Offline"; 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; return false;
} }
} }
@@ -1174,10 +1208,22 @@ memoryToggle?.addEventListener("click", () => toggleSidebarPanel("memory"));
ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama")); ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama"));
ollamaForm?.addEventListener("submit", saveOllamaConfig); ollamaForm?.addEventListener("submit", saveOllamaConfig);
ollamaRefreshButton?.addEventListener("click", refreshOllamaStatus); ollamaRefreshButton?.addEventListener("click", refreshOllamaStatus);
ollamaDownloadButton?.addEventListener("click", () => postOllamaAction("/api/ollama/download")); ollamaDownloadButton?.addEventListener("click", () => {
ollamaInstallButton?.addEventListener("click", () => postOllamaAction("/api/ollama/install")); markOllamaActionClicked("download");
ollamaLaunchButton?.addEventListener("click", () => postOllamaAction("/api/ollama/launch")); postOllamaAction("/api/ollama/download");
ollamaPullButton?.addEventListener("click", () => postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } })); });
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); updateCheckButton?.addEventListener("click", checkForUpdate);
updateInstallButton?.addEventListener("click", installUpdate); updateInstallButton?.addEventListener("click", installUpdate);
updateOpenReleasesButton?.addEventListener("click", openReleasesPage); updateOpenReleasesButton?.addEventListener("click", openReleasesPage);
@@ -1194,7 +1240,7 @@ async function sendMessage() {
if (!message || input.disabled) return; if (!message || input.disabled) return;
const healthy = await checkHealth(); const healthy = await checkHealth();
if (!healthy) { 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; return;
} }
input.value = ""; input.value = "";
@@ -1257,7 +1303,7 @@ async function sendMessage() {
} }
} catch (error) { } catch (error) {
const message = error.message.includes("503") 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}`; : `Chat failed: ${error.message}`;
setWarning(message); setWarning(message);
setMessageMarkdown(assistantNode, message); setMessageMarkdown(assistantNode, message);
+25
View File
@@ -1078,6 +1078,31 @@ button.secondary {
width: 100%; 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 { .update-box {
display: grid; display: grid;
gap: 10px; gap: 10px;