From 12e18e7dbc30029499c7a829f442047c6b26986b Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 18 Aug 2026 18:54:21 +1000 Subject: [PATCH] Show Ollama chat processing status --- README.md | 2 +- dashboard/dist/index.js | 30 ++++++++++++++++++++++++++++-- dashboard/dist/style.css | 1 + dashboard/manifest.json | 2 +- plugin.yaml | 2 +- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 50390fe..0e0e74e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Native-like Hermes dashboard plugin for local Ollama model management and chat. - View Ollama's loaded-model memory split: total, GPU VRAM, and normal RAM/offload - View NVIDIA GPU telemetry when `nvidia-smi` is available -The chat transcript and selected model are persisted in this browser, so navigating away from the plugin or reloading the dashboard does not clear the conversation. Use **Clear chat** to remove the saved transcript. Uploaded files remain temporary and are not stored in browser persistence. +The chat transcript and selected model are persisted in this browser, so navigating away from the plugin or reloading the dashboard does not clear the conversation. Use **Clear chat** to remove the saved transcript. The UI also shows a prominent live processing status while Ollama is working, including an animated indicator, elapsed time, request preparation, and response-generation stages. This is operational progress only; private model chain-of-thought is not exposed. Uploaded files remain temporary and are not stored in browser persistence. ## Security limits diff --git a/dashboard/dist/index.js b/dashboard/dist/index.js index 56276d1..4f9bad9 100644 --- a/dashboard/dist/index.js +++ b/dashboard/dist/index.js @@ -38,6 +38,11 @@ if (!value) return "Unknown"; try { return new Date(value * 1000 || value).toLocaleString(); } catch (_) { return value; } } + function fmtElapsed(seconds) { + var total = Math.max(0, Number(seconds) || 0); + if (total < 60) return total + "s"; + return Math.floor(total / 60) + "m " + String(total % 60).padStart(2, "0") + "s"; + } function Badge(props) { return h("span", { className: "ollama-badge " + (props.tone || "") }, props.children); } function Button(props) { var buttonProps = Object.assign({}, props); @@ -131,6 +136,17 @@ ); } + function ThinkingStatus(props) { + return h("div", { className: "ollama-thinking-status", role: "status", "aria-live": "polite" }, + h("div", { className: "ollama-thinking-spinner", "aria-hidden": "true" }, h("span", null), h("span", null), h("span", null)), + h("div", { className: "ollama-thinking-copy" }, + h("strong", null, "Ollama is thinking"), + h("span", null, props.stage), + h("small", null, "Elapsed ", fmtElapsed(props.elapsed), " · RAM telemetry is updating live") + ) + ); + } + function readFileAsDataURL(file) { return new Promise(function (resolve, reject) { var reader = new FileReader(); reader.onload = function () { resolve({ name: file.name, mime_type: file.type || "application/octet-stream", data_url: reader.result }); }; reader.onerror = reject; reader.readAsDataURL(file); }); } @@ -147,9 +163,18 @@ var samplesState = React.useState([]), samples = samplesState[0], setSamples = samplesState[1]; var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1]; var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1]; + var thinkingState = React.useState(null), thinking = thinkingState[0], setThinking = thinkingState[1]; + var thinkingElapsedState = React.useState(0), thinkingElapsed = thinkingElapsedState[0], setThinkingElapsed = thinkingElapsedState[1]; React.useEffect(function () { if (!model && models[0]) setModel(models[0].name); }, [models, model]); React.useEffect(function () { saveChat(model, history); }, [model, history]); + React.useEffect(function () { + if (!thinking) { setThinkingElapsed(0); return; } + function tick() { setThinkingElapsed(Math.floor((Date.now() - thinking.startedAt) / 1000)); } + tick(); + var timer = setInterval(tick, 1000); + return function () { clearInterval(timer); }; + }, [thinking]); function clearChat() { setHistory([]); try { window.localStorage.removeItem(CHAT_STORAGE_KEY); } catch (_) {} @@ -168,12 +193,13 @@ function send() { if (busy === "send" || !model || (!message.trim() && !attachments.length)) return; var outgoing = { role: "user", content: message.trim() || "[Attachments]" }, body = { model: model, message: message, history: history, attachments: attachments }; - setHistory(function (old) { return old.concat([outgoing]); }); setMessage(""); setBusy("send"); setNotice(null); - fetchJSON(API + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(function (result) { var answer = result.message && result.message.content ? result.message.content : "(No response text returned.)"; setHistory(function (old) { return old.concat([{ role: "assistant", content: answer }]); }); setAttachments([]); setRuntime(result.runtime || runtime); setNotice({ ok: "Response complete. Live placement is shown below." }); pollRuntime(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); }); + setHistory(function (old) { return old.concat([outgoing]); }); setMessage(""); setBusy("send"); setThinking({ startedAt: Date.now(), stage: attachments.length ? "Preparing attachments and sending request to Ollama" : "Sending request to Ollama" }); setNotice(null); + fetchJSON(API + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(function (result) { var answer = result.message && result.message.content ? result.message.content : "(No response text returned.)"; setHistory(function (old) { return old.concat([{ role: "assistant", content: answer }]); }); setAttachments([]); setRuntime(result.runtime || runtime); setNotice({ ok: "Response complete. Live placement is shown below." }); pollRuntime(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setThinking(null); setBusy(""); }); } return h("section", { className: "ollama-chat" }, h("div", { className: "ollama-chat-header" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "LOCAL OLLAMA CHAT"), h("h2", null, "Chat with your selected model"), h("p", null, "Images are sent as Ollama vision inputs; PDFs and web pages are extracted as untrusted document text.")), h("div", { className: "ollama-chat-model" }, h("label", null, "Model", h("select", { value: model, onChange: function (event) { setModel(event.target.value); } }, models.map(function (item) { return h("option", { key: item.name, value: item.name }, item.name + (item.loaded ? " · loaded" : "")); }))), h(Button, { disabled: !model || busy === "load", onClick: loadModel }, busy === "load" ? "Loading…" : "Load model"), h(Button, { className: "secondary", disabled: !history.length || busy === "send", onClick: clearChat }, "Clear chat"))), notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : "ok") }, notice.error || notice.ok), + thinking && h(ThinkingStatus, { stage: thinkingElapsed < 1 ? thinking.stage : "Ollama is generating the response", elapsed: thinkingElapsed }), h(RuntimePanel, { runtime: runtime, samples: samples }), h("div", { className: "ollama-chat-layout" }, h("div", { className: "ollama-conversation" }, history.length ? history.map(function (item, index) { return h("div", { className: "ollama-message " + item.role, key: index }, h("small", null, item.role === "assistant" ? "Ollama" : "You"), h("div", null, item.content)); }) : h(Empty, null, "Start a conversation. The selected model will be loaded into Ollama memory when you load it or send the first message.")), diff --git a/dashboard/dist/style.css b/dashboard/dist/style.css index 0ddbd59..3bc7e41 100644 --- a/dashboard/dist/style.css +++ b/dashboard/dist/style.css @@ -4,3 +4,4 @@ .ollama-chat{margin-top:18px}.ollama-chat-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:18px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(23,52,48,.42)}.ollama-chat-header h2{margin:6px 0;color:#effcf8}.ollama-chat-header p{margin:0;color:#a5bfba;font-size:12px;max-width:720px}.ollama-chat-model{display:flex;align-items:flex-end;gap:10px;min-width:260px}.ollama-chat-model label{display:flex;flex-direction:column;gap:6px;color:#a5bfba;font-size:11px}.ollama-chat-model select,.ollama-url-input{border:1px solid rgba(155,205,194,.28);border-radius:7px;background:#102d29;color:#e8f2ef;padding:9px;font:inherit;font-size:12px}.ollama-runtime-panel{margin-top:14px;padding:16px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,31,28,.7)}.ollama-runtime-heading{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.ollama-runtime-heading h3{margin:0 0 3px}.ollama-runtime-heading p{margin:0;color:#8fa9a4;font-size:11px}.ollama-badge.muted{color:#a7b7b4;border-color:rgba(167,183,180,.25)}.ollama-runtime-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px}.ollama-runtime-stat{display:flex;flex-direction:column;gap:5px;min-height:70px;padding:11px;border-radius:8px;background:rgba(71,117,108,.11);color:#a5bfba}.ollama-runtime-stat strong{color:#effcf8;font-size:15px}.ollama-runtime-stat small{font-size:10px}.ollama-meter{height:5px;border-radius:99px;background:#173c36;overflow:hidden}.ollama-meter span{display:block;height:100%;border-radius:inherit;background:#75d2b7}.ollama-memory-chart{height:70px;display:flex;align-items:flex-end;gap:2px;margin-top:14px;padding:5px 0;border-bottom:1px solid rgba(164,211,199,.18)}.ollama-memory-chart span{flex:1;min-width:2px;background:#55a995;border-radius:2px 2px 0 0;opacity:.8}.ollama-loaded-memory h4{margin:15px 0 8px}.ollama-loaded-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:9px 0;border-top:1px solid rgba(164,211,199,.1);font-size:11px;color:#a5bfba}.ollama-loaded-row strong{color:#effcf8;min-width:180px}.ollama-chat-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,420px);gap:14px;margin-top:14px}.ollama-conversation{min-height:320px;max-height:620px;overflow:auto;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(10,25,23,.62)}.ollama-message{margin:0 0 14px;padding:11px 13px;border-radius:9px;white-space:pre-wrap;line-height:1.5;font-size:13px}.ollama-message small{display:block;margin-bottom:5px;color:#8fc7ba;font-size:10px;text-transform:uppercase;letter-spacing:.1em}.ollama-message.user{margin-left:12%;background:rgba(54,105,94,.3)}.ollama-message.assistant{margin-right:8%;background:rgba(34,63,62,.6)}.ollama-composer{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid rgba(164,211,199,.16);border-radius:12px;background:rgba(23,52,48,.42)}.ollama-composer textarea{min-height:160px;resize:vertical;border:1px solid rgba(155,205,194,.25);border-radius:8px;background:#0d2522;color:#e8f2ef;padding:11px;font:inherit;font-size:13px}.ollama-attachment-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.ollama-file-button{display:inline-flex;align-items:center;border:1px solid rgba(155,205,194,.24);border-radius:7px;background:rgba(70,117,108,.18);color:#dbebe7;padding:8px 11px;cursor:pointer;font-size:11px}.ollama-file-button input{display:none}.ollama-url-input{flex:1;min-width:160px}.ollama-attachments{display:flex;gap:6px;flex-wrap:wrap}.ollama-attachment{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:rgba(80,125,115,.16);color:#c5dfd8;font-size:10px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.ollama-attachment button{border:0;background:none;color:#ffb3b3;cursor:pointer}.ollama-chat-footnote{margin:0;color:#819b96;font-size:10px;line-height:1.45} @media(max-width:900px){.ollama-chat-header,.ollama-chat-layout{display:block}.ollama-chat-model{margin-top:15px;align-items:center}.ollama-composer{margin-top:14px}.ollama-runtime-grid{grid-template-columns:1fr 1fr}} @media(max-width:600px){.ollama-runtime-grid{grid-template-columns:1fr}.ollama-chat-model{display:block}.ollama-chat-model select{width:100%;margin-bottom:8px}.ollama-message.user{margin-left:0}.ollama-message.assistant{margin-right:0}} +.ollama-thinking-status{display:flex;align-items:center;gap:14px;margin-top:14px;padding:14px 16px;border:1px solid rgba(117,210,183,.42);border-radius:10px;background:linear-gradient(90deg,rgba(46,111,96,.34),rgba(24,64,57,.5));box-shadow:0 0 20px rgba(74,190,158,.08)}.ollama-thinking-spinner{display:flex;align-items:center;gap:4px;min-width:28px}.ollama-thinking-spinner span{width:7px;height:7px;border-radius:50%;background:#75d2b7;animation:ollama-thinking-pulse 1.1s ease-in-out infinite}.ollama-thinking-spinner span:nth-child(2){animation-delay:.18s}.ollama-thinking-spinner span:nth-child(3){animation-delay:.36s}.ollama-thinking-copy{display:flex;flex-direction:column;gap:3px}.ollama-thinking-copy strong{color:#effcf8;font-size:13px}.ollama-thinking-copy span{color:#b9d8d0;font-size:12px}.ollama-thinking-copy small{color:#8fb5ac;font-size:10px}@keyframes ollama-thinking-pulse{0%,80%,100%{opacity:.35;transform:scale(.8)}40%{opacity:1;transform:scale(1.2)}} \ No newline at end of file diff --git a/dashboard/manifest.json b/dashboard/manifest.json index 7b299e4..0b27643 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -3,7 +3,7 @@ "label": "Ollama Models", "description": "Inspect, manage, and chat with local Ollama models, including images, PDFs, URLs, and live memory telemetry.", "icon": "Cpu", - "version": "1.3.2", + "version": "1.3.3", "tab": {"path": "/ollama-manager", "position": "after:models"}, "entry": "dist/index.js", "css": "dist/style.css", diff --git a/plugin.yaml b/plugin.yaml index 40c71b9..f2fc43c 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: ollama-manager -version: 1.3.2 +version: 1.3.3 description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, and live runtime telemetry. python_dependencies: - "pypdf>=6.0,<7.0"