Persist Ollama chat history across navigation
This commit is contained in:
Vendored
+36
-5
@@ -7,6 +7,26 @@
|
||||
var h = React.createElement;
|
||||
var fetchJSON = SDK.fetchJSON;
|
||||
var API = "/api/plugins/ollama-manager";
|
||||
var CHAT_STORAGE_KEY = "hermes.ollama-manager.chat.v1";
|
||||
|
||||
function readSavedChat() {
|
||||
try {
|
||||
var raw = window.localStorage.getItem(CHAT_STORAGE_KEY);
|
||||
if (!raw) return { model: "", history: [] };
|
||||
var value = JSON.parse(raw);
|
||||
return {
|
||||
model: typeof value.model === "string" ? value.model : "",
|
||||
history: Array.isArray(value.history) ? value.history.filter(function (item) { return item && (item.role === "user" || item.role === "assistant") && typeof item.content === "string"; }).slice(-100) : []
|
||||
};
|
||||
} catch (_) {
|
||||
return { model: "", history: [] };
|
||||
}
|
||||
}
|
||||
function saveChat(model, history) {
|
||||
try {
|
||||
window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify({ model: model || "", history: history.slice(-100) }));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function fmtBytes(bytes) {
|
||||
if (bytes === null || bytes === undefined || Number(bytes) === 0) return "0 B";
|
||||
@@ -19,7 +39,11 @@
|
||||
try { return new Date(value * 1000 || value).toLocaleString(); } catch (_) { return value; }
|
||||
}
|
||||
function Badge(props) { return h("span", { className: "ollama-badge " + (props.tone || "") }, props.children); }
|
||||
function Button(props) { return h("button", Object.assign({ className: "ollama-button" }, props), props.children); }
|
||||
function Button(props) {
|
||||
var buttonProps = Object.assign({}, props);
|
||||
buttonProps.className = "ollama-button" + (props.className ? " " + props.className : "");
|
||||
return h("button", buttonProps, props.children);
|
||||
}
|
||||
function Empty(props) { return h("div", { className: "ollama-empty" }, props.children); }
|
||||
|
||||
function CapabilityList(props) {
|
||||
@@ -113,17 +137,24 @@
|
||||
|
||||
function ChatPanel(props) {
|
||||
var models = props.models || [];
|
||||
var modelState = React.useState(models[0] ? models[0].name : ""), model = modelState[0], setModel = modelState[1];
|
||||
var savedChatState = React.useState(function () { return readSavedChat(); })[0];
|
||||
var modelState = React.useState(savedChatState.model || (models[0] ? models[0].name : "")), model = modelState[0], setModel = modelState[1];
|
||||
var messageState = React.useState(""), message = messageState[0], setMessage = messageState[1];
|
||||
var urlState = React.useState(""), url = urlState[0], setUrl = urlState[1];
|
||||
var attachState = React.useState([]), attachments = attachState[0], setAttachments = attachState[1];
|
||||
var historyState = React.useState([]), history = historyState[0], setHistory = historyState[1];
|
||||
var historyState = React.useState(savedChatState.history), history = historyState[0], setHistory = historyState[1];
|
||||
var runtimeState = React.useState(null), runtime = runtimeState[0], setRuntime = runtimeState[1];
|
||||
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];
|
||||
|
||||
React.useEffect(function () { if (!model && models[0]) setModel(models[0].name); }, [models, model]);
|
||||
React.useEffect(function () { saveChat(model, history); }, [model, history]);
|
||||
function clearChat() {
|
||||
setHistory([]);
|
||||
try { window.localStorage.removeItem(CHAT_STORAGE_KEY); } catch (_) {}
|
||||
setNotice({ ok: "Chat history cleared from this browser." });
|
||||
}
|
||||
function pollRuntime() { fetchJSON(API + "/runtime").then(function (value) { setRuntime(value); setSamples(function (old) { return old.concat([{ used: Number(value.memory_used_bytes || 0), total: Number(value.memory_total_bytes || 0) }]).slice(-60); }); }).catch(function () {}); }
|
||||
React.useEffect(function () { pollRuntime(); var timer = setInterval(pollRuntime, 1000); return function () { clearInterval(timer); }; }, []);
|
||||
|
||||
@@ -141,7 +172,7 @@
|
||||
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(""); });
|
||||
}
|
||||
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("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),
|
||||
h(RuntimePanel, { runtime: runtime, samples: samples }),
|
||||
h("div", { className: "ollama-chat-layout" },
|
||||
@@ -170,7 +201,7 @@
|
||||
var jobs = data && data.jobs ? data.jobs.filter(function (job) { return job.state === "running"; }) : [];
|
||||
return h("main", { className: "ollama-page" }, h("header", { className: "ollama-hero" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "LOCAL MODEL OPERATIONS"), h("h1", null, "Ollama Models"), h("p", null, "Inspect, chat with, download, update, and remove models from the local Ollama runtime.")), h("div", { className: "ollama-health" }, h(Badge, { tone: data && data.ollama && data.ollama.available ? "live" : "danger" }, data && data.ollama && data.ollama.available ? "Ollama online" : "Ollama unavailable"), data && data.ollama && h("span", null, "v" + (data.ollama.version || "unknown")), h(Button, { disabled: busy === "catalog", onClick: refreshCatalog }, busy === "catalog" ? "Refreshing…" : "Refresh catalog"))),
|
||||
notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : "ok") }, notice.error || notice.ok),
|
||||
h("section", { className: "ollama-toolbar" }, h("div", { className: "ollama-tabs" }, h(Button, { className: tab === "chat" ? "selected" : "", onClick: function () { setTab("chat"); } }, "Chat"), h(Button, { className: tab === "installed" ? "selected" : "", onClick: function () { setTab("installed"); } }, "Installed (" + ((data && data.models) || []).length + ")"), h(Button, { className: tab === "popular" ? "selected" : "", onClick: function () { setTab("popular"); } }, "Top 20 popular (" + ((data && data.popular) || []).length + ")"), h(Button, { className: tab === "catalog" ? "selected" : "", onClick: function () { setTab("catalog"); } }, "Available downloads (" + ((data && data.catalog) || []).length + ")")), tab !== "chat" && h("input", { className: "ollama-search", value: query, placeholder: "Search models, capabilities, or strengths…", onChange: function (event) { setQuery(event.target.value); } })),
|
||||
h("section", { className: "ollama-toolbar" }, h("div", { className: "ollama-tabs" }, h(Button, { className: tab === "chat" ? "selected" : "", onClick: function () { setTab("chat"); } }, "Ollama Chat"), h(Button, { className: tab === "installed" ? "selected" : "", onClick: function () { setTab("installed"); } }, "Installed (" + ((data && data.models) || []).length + ")"), h(Button, { className: tab === "popular" ? "selected" : "", onClick: function () { setTab("popular"); } }, "Top 20 popular (" + ((data && data.popular) || []).length + ")"), h(Button, { className: tab === "catalog" ? "selected" : "", onClick: function () { setTab("catalog"); } }, "Available downloads (" + ((data && data.catalog) || []).length + ")")), tab !== "chat" && h("input", { className: "ollama-search", value: query, placeholder: "Search models, capabilities, or strengths…", onChange: function (event) { setQuery(event.target.value); } })),
|
||||
tab !== "chat" && h("div", { className: "ollama-info-strip" }, h("span", null, data && data.models ? data.models.filter(function (m) { return m.loaded; }).length + " currently loaded" : "Loading runtime state…"), h("span", null, "Catalog checked " + (data && data.catalog_updated_at ? fmtDate(data.catalog_updated_at) : "not yet")), h("span", null, "Next daily check " + (data && data.next_catalog_refresh ? fmtDate(data.next_catalog_refresh) : "01:00 Melbourne time") + " (1:00 AM Melbourne time)")),
|
||||
tab === "chat" && h(ChatPanel, { models: data && data.models ? data.models : [] }),
|
||||
tab === "popular" && h("p", { className: "ollama-popular-note" }, "Popular is limited to models with known size and RAM estimates at or below 30 GiB. Oversized families are represented by a smaller fitting variant when available."), jobs.length > 0 && h("section", { className: "ollama-jobs" }, jobs.map(function (job) { return h("div", { key: job.id }, h("strong", null, job.action + " · " + job.name), h("span", null, job.percent == null ? job.status : job.percent + "%")); })),
|
||||
|
||||
Reference in New Issue
Block a user