Show Ollama chat processing status

This commit is contained in:
Hermes Agent
2026-08-18 18:54:21 +10:00
parent aaf4790591
commit 12e18e7dbc
5 changed files with 32 additions and 5 deletions
+1 -1
View File
@@ -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
+28 -2
View File
@@ -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.")),
+1
View File
@@ -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)}}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"