Files
Hermes-Ollama_Models/dashboard/dist/index.js
T

451 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
var SDK = window.__HERMES_PLUGIN_SDK__;
var registry = window.__HERMES_PLUGINS__;
if (!SDK || !registry) return;
var React = SDK.React;
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: "", models: [], history: [] };
var value = JSON.parse(raw);
return {
model: typeof value.model === "string" ? value.model : "",
models: Array.isArray(value.models) ? value.models.filter(function (item) { return typeof item === "string"; }).slice(0, 12) : [],
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: "", models: [], history: [] };
}
}
function saveChat(model, models, history) {
try {
window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify({ model: model || "", models: (models || []).slice(0, 12), history: history.slice(-100) }));
} catch (_) {}
}
function fmtBytes(bytes) {
if (bytes === null || bytes === undefined || Number(bytes) === 0) return "0 B";
var units = ["B", "KiB", "MiB", "GiB", "TiB"], value = Number(bytes), index = 0;
while (value >= 1024 && index < units.length - 1) { value /= 1024; index += 1; }
return value.toFixed(index ? 2 : 0) + " " + units[index];
}
function fmtDate(value) {
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);
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 ConnectionPanel(props) {
var data = props.data || {}, ollama = data.ollama || {}, connectionState = React.useState(ollama.endpoint || data.active_url || ""), url = connectionState[0], setUrl = connectionState[1];
var roleState = React.useState("local"), role = roleState[0], setRole = roleState[1];
var resultState = React.useState(null), result = resultState[0], setResult = resultState[1];
var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1];
React.useEffect(function () { if (!url && (ollama.endpoint || data.active_url)) setUrl(ollama.endpoint || data.active_url); }, [ollama.endpoint, data.active_url]);
function test() {
if (!url.trim()) return;
setBusy("test"); setResult(null);
fetchJSON(API + "/connections/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: url.trim(), role: role }) }).then(function (value) { setResult({ ok: true, value: value }); }).catch(function (err) { setResult({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
function save() {
if (!url.trim()) return;
setBusy("save"); setResult(null);
fetchJSON(API + "/connections/configure", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: url.trim(), role: role }) }).then(function (value) { setResult({ ok: true, value: value }); if (props.reload) props.reload(); }).catch(function (err) { setResult({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
function removeSaved(item) {
if (!item || !item.saved || !item.kind) return;
if (!window.confirm("Remove the saved " + item.kind + " Ollama configuration?")) return;
setBusy("remove:" + item.kind); setResult(null);
fetchJSON(API + "/connections/" + encodeURIComponent(item.kind), { method: "DELETE" }).then(function (value) { setResult({ ok: true, value: value }); if (props.reload) props.reload(); }).catch(function (err) { setResult({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
function useDetected(item) { setUrl(item.url || ""); setRole(item.kind || "local"); }
var rows = data.connections || [];
return h("section", { className: "ollama-connection-panel" },
h("div", { className: "ollama-connection-heading" }, h("div", null, h("strong", null, "Ollama connection"), h("small", null, ollama.containerized ? "Docker runtime · local and remote endpoints supported" : "Physical runtime · local and remote endpoints supported"))),
h("div", { className: "ollama-connection-form" }, h("select", { className: "ollama-connection-role", value: role, onChange: function (event) { setRole(event.target.value); } }, h("option", { value: "local" }, "Local"), h("option", { value: "remote" }, "Remote")), h("input", { className: "ollama-connection-input", value: url, placeholder: "http://host-or-container:11434", onChange: function (event) { setUrl(event.target.value); } }), h(Button, { onClick: test, disabled: !url.trim() || !!busy }, busy === "test" ? "Testing…" : "Test"), h(Button, { onClick: save, disabled: !url.trim() || !!busy }, busy === "save" ? "Saving…" : "Save")),
result && h("div", { className: "ollama-connection-result " + (result.error ? "error" : "ok") }, result.error || ((result.value && result.value.version) ? "Online · v" + result.value.version + " · " + (result.value.models || 0) + " models" : "Connection accepted")),
h("div", { className: "ollama-connection-list" }, rows.length ? rows.map(function (item) { return h("div", { className: "ollama-connection-row", key: item.kind + ":" + item.url }, h("button", { type: "button", className: "ollama-connection-row-main", onClick: function () { useDetected(item); } }, h("span", { className: "ollama-connection-dot " + (item.available ? "online" : "offline") }), h("span", null, h("strong", null, (item.kind || "local").toUpperCase(), " · ", item.label || item.url), h("small", null, item.url, item.available ? " · v" + item.version + " · " + item.models + " models" + (item.model_names && item.model_names.length ? " · " + item.model_names.join(", ") : "") + (item.same_endpoint ? " · same endpoint as local" : "") : " · unavailable"))), item.saved && h(Button, { className: "ollama-connection-remove", disabled: !!busy, onClick: function () { removeSaved(item); } }, busy === "remove:" + item.kind ? "Removing…" : "Remove saved")); }) : h("small", null, "No endpoints detected yet. Enter an Ollama URL and test it."))
);
}
function CapabilityList(props) {
var model = props.model;
return h("div", { className: "ollama-capabilities" }, (model.capabilities || []).map(function (cap) {
return h("div", { className: "ollama-capability", key: cap }, h(Badge, null, cap), h("span", null, (model.capability_breakdown || {})[cap] || "Advertised by model metadata"));
}));
}
function VariantTable(props) {
var model = props.model, variants = model.variants || [], action = props.action, busy = props.busy;
if (!variants.length) return null;
return h("div", { className: "ollama-variants" },
h("div", { className: "ollama-variants-heading" }, h("h4", null, "Available ", model.name.split(":")[0], " sizes"), h("span", null, "MLX variants excluded")),
h("div", { className: "ollama-variant-table-wrap" }, h("table", { className: "ollama-variant-table" },
h("thead", null, h("tr", null, h("th", null, "Name"), h("th", null, "Size / RAM"), h("th", null, "Context"), h("th", null, "Input"), h("th", null, "Action"))),
h("tbody", null, variants.map(function (variant) {
var current = variant.name === model.name, installed = variant.installed;
return h("tr", { key: variant.name, className: current ? "current" : "" },
h("td", null, h("strong", null, variant.name), current && h(Badge, { tone: "current" }, "current"), installed && !current && h(Badge, { tone: "installed" }, "installed")),
h("td", null, h("strong", null, variant.size_label || "Unknown"), h("small", null, variant.expected_ram_label || "RAM unknown")),
h("td", null, variant.context_length ? Math.round(Number(variant.context_length) / 1024) + "K" : "Unknown"),
h("td", null, (variant.input_modalities || ["Text"]).join(", ")),
h("td", null, current ? h("span", { className: "ollama-current-label" }, "Current") : installed ? h("span", { className: "ollama-current-label" }, "Installed") : h(Button, { disabled: !!busy, onClick: function () { action("pull", variant.name); } }, busy === variant.name + ":pull" ? "Downloading…" : "Download"))
);
}))
))
);
}
function ModelCard(props) {
var model = props.model, installed = props.installed, busy = props.busy, action = props.action;
var openState = React.useState(false), open = openState[0], setOpen = openState[1];
var badges = [];
if (installed && model.loaded) badges.push(h(Badge, { key: "loaded", tone: "live" }, "loaded"));
if (installed) badges.push(h(Badge, { key: "installed", tone: "installed" }, "installed"));
if (!installed) badges.push(h(Badge, { key: "available", tone: "download" }, "available"));
if (model.popularity_rank) badges.push(h(Badge, { key: "popular", tone: "popular" }, "#" + model.popularity_rank + " popular"));
if (model.is_moe) badges.push(h(Badge, { key: "moe", tone: "moe" }, "MoE"));
return h("article", { className: "ollama-model-card" },
h("div", { className: "ollama-card-top" },
h("div", { className: "ollama-model-title" }, h("h3", null, model.name), h("div", { className: "ollama-badge-row" }, badges)),
h("div", { className: "ollama-card-actions" },
installed && h(Button, { disabled: !!busy, onClick: function () { action("redownload", model.name); } }, busy === model.name + ":redownload" ? "Updating…" : "Update / re-download"),
installed && h(Button, { disabled: !!busy, className: "ollama-button danger", onClick: function () { action("delete", model.name); } }, busy === model.name + ":delete" ? "Removing…" : "Remove"),
!installed && h(Button, { disabled: !!busy, onClick: function () { action("pull", model.name); } }, busy === model.name + ":pull" ? "Downloading…" : "Download")
)
),
h("div", { className: "ollama-model-summary" },
h("div", null, h("small", null, "Size"), h("strong", null, model.size_gb ? model.size_gb + " GiB" : "Unknown")),
h("div", null, h("small", null, "Expected RAM"), h("strong", null, model.expected_ram_label || "Unknown")),
h("div", null, h("small", null, "Type"), h("strong", null, model.architecture || "Unknown")),
h("div", null, h("small", null, "Parameters"), h("strong", null, model.parameter_size || "Unknown"))
),
h("div", { className: "ollama-card-meta" }, h("span", null, (model.quantization || "Unknown") + " · " + (model.format || "Unknown")), model.context_length && h("span", null, "Context " + Number(model.context_length).toLocaleString()), installed && model.modified_at && h("span", null, "Updated " + fmtDate(model.modified_at))),
h("div", { className: "ollama-strengths" }, h("strong", null, "Excels at: "), (model.strengths || []).join(" · ")),
installed && h(VariantTable, { model: model, action: action, busy: busy }),
h(Button, { className: "ollama-details-toggle", onClick: function () { setOpen(!open); } }, open ? "Hide capability breakdown" : "Show capability breakdown"),
open && h("div", { className: "ollama-details" },
h("h4", null, "Capabilities"), h(CapabilityList, { model: model }),
h("h4", null, "Runtime estimate"),
h("p", null, model.expected_ram_basis || "No estimate basis available.", " Actual memory varies with context length, KV cache, GPU offload, and concurrent requests."),
h("div", { className: "ollama-detail-grid" },
h("span", null, "Family: ", h("strong", null, model.family || "unknown")),
h("span", null, "Digest: ", h("strong", null, model.digest ? model.digest.slice(0, 16) + "…" : "unknown")),
h("span", null, "Embedding: ", h("strong", null, model.embedding_length || "unknown")),
h("span", null, "Loaded VRAM: ", h("strong", null, fmtBytes(model.loaded_vram_bytes)))
)
)
);
}
function RuntimePanel(props) {
var runtime = props.runtime || {}, total = Number(runtime.memory_total_bytes || 0), used = Number(runtime.memory_used_bytes || 0), pct = total ? Math.min(100, used * 100 / total) : 0;
var gpu = runtime.gpu || {}, models = runtime.model_memory || [], loading = runtime.model_loading || [];
var ollamaBytes = Number(runtime.ollama_model_bytes || 0), ollamaTargetBytes = Number(runtime.ollama_target_model_bytes || ollamaBytes), ollamaPct = total ? Math.min(100, ollamaTargetBytes * 100 / total) : 0;
return h("section", { className: "ollama-runtime-panel" },
h("div", { className: "ollama-runtime-heading" }, h("div", null, h("h3", null, "Live runtime memory"), h("p", null, "Updates every second while this panel is open.")), h(Badge, { tone: loading.length ? "live" : (gpu.detected ? "live" : "muted") }, loading.length ? "MODEL LOADING" : (gpu.detected ? "GPU detected" : "CPU-only / no supported GPU telemetry"))),
h("div", { className: "ollama-runtime-grid" },
h("div", { className: "ollama-runtime-stat" }, h("small", null, "System RAM used"), h("strong", null, fmtBytes(used), " / ", fmtBytes(total)), h("div", { className: "ollama-meter" }, h("span", { style: { width: pct + "%" } })), h("small", null, fmtBytes(runtime.memory_available_bytes || 0), " available")),
h("div", { className: "ollama-runtime-stat" }, h("small", null, "Swap used"), h("strong", null, fmtBytes(runtime.swap_used_bytes || 0), " / ", fmtBytes(runtime.swap_total_bytes || 0)), h("small", null, "Host-wide live statistic")),
h("div", { className: "ollama-runtime-stat" }, h("small", null, "GPU telemetry"), h("strong", null, gpu.telemetry_available ? (gpu.gpus || []).map(function (item) { return item.name + " · " + fmtBytes(item.used_bytes) + " / " + fmtBytes(item.total_bytes); }).join("; ") : "Unavailable"), h("small", null, gpu.detected ? "Ollama VRAM split is still shown below." : "No supported GPU was detected.")),
h("div", { className: "ollama-runtime-stat ollama-weight-stat" }, h("small", null, "Ollama model weights"), h("strong", null, fmtBytes(ollamaBytes), " resident"), h("div", { className: "ollama-meter" }, h("span", { style: { width: ollamaPct + "%" } })), h("small", null, loading.length ? "Loading target: " + fmtBytes(ollamaTargetBytes) : "Mapped weight bytes; Linux may report them as file cache"))
),
h("div", { className: "ollama-memory-chart" + (loading.length ? " loading" : ""), role: loading.length ? "status" : undefined, "aria-live": loading.length ? "polite" : undefined }, loading.length ? h("div", { className: "ollama-loading-progress" }, h("strong", null, "Loading into Ollama memory"), h("span", null, loading.map(function (item) { return item.name; }).join(", ")), h("small", null, loading.map(function (item) { return item.stage + " · " + fmtElapsed(item.elapsed); }).join(" · ")), h("div", { className: "ollama-loading-track" }, h("span", null))) : (props.samples || []).map(function (sample, index) { var height = sample.total ? Math.max(3, Math.min(100, sample.used * 100 / sample.total)) : 3; return h("span", { key: index, title: fmtBytes(sample.used) + " used", style: { height: height + "%" } }); })),
h("div", { className: "ollama-loaded-memory" }, h("h4", null, "Loaded models and capabilities"), models.length ? models.map(function (model) { return h("div", { className: "ollama-loaded-row", key: model.name }, h("strong", null, model.name), h("span", null, "Total ", fmtBytes(model.total_bytes)), h("span", null, "GPU VRAM ", fmtBytes(model.gpu_bytes)), h("span", null, "Normal RAM ", fmtBytes(model.ram_bytes)), h("span", null, model.gpu_offload_percent + "% GPU offload"), h("span", { className: "ollama-loaded-capabilities" }, "Capabilities: ", (model.capabilities || []).join(", ") || "Unknown", " · Input: ", (model.input_modalities || []).join(", ") || "Text", " · ", model.parameter_size || "unknown", " · ", model.quantization || "unknown", " · Context ", model.context_length || "unknown"), h("span", { className: "ollama-permanent-label" }, model.permanent ? "Permanent keep-alive" : "Runtime-loaded")); }) : loading.length ? h("p", null, "Ollama is loading the selected model. Resident memory will appear here when the runner finishes starting.") : h("p", null, "No model is currently loaded. Use the model pool below to load one or more permanently."))
);
}
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")
),
h(Button, { className: "thinking-toggle", onClick: props.onToggle }, props.expanded ? "Hide details" : "Show details"),
h(Button, { className: "danger thinking-stop", onClick: props.onStop }, "Stop"),
props.expanded && h(ThinkingDetails, { details: props.details, stage: props.stage })
);
}
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); });
}
function makeRequestId() {
if (window.crypto && typeof window.crypto.randomUUID === "function") return window.crypto.randomUUID();
return "chat-" + Date.now() + "-" + Math.random().toString(36).slice(2);
}
function ThinkingDetails(props) {
var details = props.details || {};
return h("div", { className: "ollama-thinking-details" },
h("div", { className: "ollama-thinking-detail-grid" },
h("span", null, "State", h("strong", null, details.state || "working")),
h("span", null, "Stage", h("strong", null, details.stage || props.stage)),
h("span", null, "Events", h("strong", null, String(details.chunks || 0))),
h("span", null, "Response characters", h("strong", null, String(details.response_chars || 0))),
h("span", null, "Model-thinking characters", h("strong", null, String(details.thinking_chars || 0)))
),
h("small", null, "This window shows operational progress and telemetry, not private chain-of-thought.")
);
}
function ModelPoolPanel(props) {
var models = props.models || [], loaded = models.filter(function (item) { return item.loaded; });
return h("section", { className: "ollama-model-pool" },
h("div", { className: "ollama-pool-heading" }, h("div", null, h("h3", null, "Model pool"), h("p", null, "Choose installed models to keep permanently loaded. Loaded models remain available to Hermes Agent through Local Ollama.")), h(Badge, { tone: loaded.length ? "live" : "muted" }, loaded.length + " loaded")),
h("div", { className: "ollama-pool-grid" }, models.map(function (item) { var loaded = !!item.loaded, selected = props.poolSelection.indexOf(item.name) >= 0; return h("label", { className: "ollama-pool-item" + (loaded ? " loaded" : "") + (selected ? " selected" : ""), key: item.name, title: loaded ? "Loaded and resident in Ollama" : "Installed but not resident" }, h("input", { type: "checkbox", checked: selected, onChange: function () { props.onTogglePool(item.name); } }), h("span", null, h("strong", null, item.name), h("small", null, loaded ? "Loaded and resident · keep-alive active" : "Installed · not loaded", " · ", (item.capabilities || []).join(", ") || "capabilities unknown"))); })),
h("div", { className: "ollama-pool-actions" }, h(Button, { disabled: !props.poolSelection.length || !!props.busy, onClick: props.onLoad }, props.busy === "/models/load" ? "Loading " + props.poolSelection.length + " model" + (props.poolSelection.length === 1 ? "" : "s") + "…" : "Load selected permanently"), h(Button, { className: "secondary", disabled: !props.poolSelection.length || !!props.busy, onClick: props.onUnload }, props.busy === "/models/unload" ? "Unloading…" : "Unload selected")),
h("div", { className: "ollama-chat-model-selection" }, h("strong", null, "Models for this answer"), h("small", null, "Select one or more loaded models for parallel perspectives."), loaded.length ? loaded.map(function (item) { return h("label", { className: "loaded", key: item.name }, h("input", { type: "checkbox", checked: props.selectedModels.indexOf(item.name) >= 0, onChange: function () { props.onToggleChat(item.name); } }), item.name, " · ", (item.capabilities || []).join(", ")); }) : h("span", null, "Load one or more models above first."))
);
}
function ChatPanel(props) {
var models = props.models || [];
var loadedModels = models.filter(function (item) { return item.loaded; });
var savedChatState = React.useState(function () { return readSavedChat(); })[0];
var modelState = React.useState(savedChatState.model || (loadedModels[0] ? loadedModels[0].name : (models[0] ? models[0].name : ""))), model = modelState[0], setModel = modelState[1];
var selectedModelsState = React.useState(savedChatState.models && savedChatState.models.length ? savedChatState.models : (loadedModels[0] ? [loadedModels[0].name] : [])), selectedModels = selectedModelsState[0], setSelectedModels = selectedModelsState[1];
var poolState = React.useState(loadedModels.map(function (item) { return item.name; })), poolSelection = poolState[0], setPoolSelection = poolState[1];
var poolLoadedSignature = React.useRef("");
var chatLoadedSignature = React.useRef("");
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(savedChatState.history), history = historyState[0], setHistory = historyState[1];
var conversationIdState = React.useState(""), conversationId = conversationIdState[0], setConversationId = conversationIdState[1];
var conversationsState = React.useState([]), conversations = conversationsState[0], setConversations = conversationsState[1];
var metricsState = React.useState(null), metrics = metricsState[0], setMetrics = metricsState[1];
var aggregateState = React.useState(null), aggregate = aggregateState[0], setAggregate = aggregateState[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];
var thinkingState = React.useState(null), thinking = thinkingState[0], setThinking = thinkingState[1];
var thinkingElapsedState = React.useState(0), thinkingElapsed = thinkingElapsedState[0], setThinkingElapsed = thinkingElapsedState[1];
var thinkingDetailsState = React.useState(null), thinkingDetails = thinkingDetailsState[0], setThinkingDetails = thinkingDetailsState[1];
var thinkingOpenState = React.useState(false), thinkingOpen = thinkingOpenState[0], setThinkingOpen = thinkingOpenState[1];
var draggingState = React.useState(false), dragging = draggingState[0], setDragging = draggingState[1];
var activeRequestState = React.useState(null), activeRequest = activeRequestState[0], setActiveRequest = activeRequestState[1];
var thinkingId = thinking ? thinking.request_id : "";
function openConversation(id) {
if (!id) return;
fetchJSON(API + "/conversations/" + encodeURIComponent(id)).then(function (value) {
var item = value.conversation || {};
setConversationId(item.id || id);
setHistory((value.messages || []).filter(function (message) { return message.role === "user" || message.role === "assistant"; }).map(function (message) { return { role: message.role, content: message.content, created_at: message.created_at, model: message.model }; }));
setMetrics(value.metrics || []);
if (item.model) setModel(item.model);
if (Array.isArray(item.models) && item.models.length) setSelectedModels(item.models);
}).catch(function (err) { setNotice({ error: err.message || String(err) }); });
}
function refreshConversations() {
fetchJSON(API + "/metrics?limit=100").then(function (value) { setAggregate(value.aggregate || null); }).catch(function () {});
return fetchJSON(API + "/conversations").then(function (value) {
var rows = value.conversations || [];
setConversations(rows);
if (!conversationId && rows.length) openConversation(rows[0].id);
return rows;
}).catch(function () { return []; });
}
function newConversation() { setConversationId(""); setHistory([]); setMetrics([]); setNotice({ ok: "New conversation ready." }); }
React.useEffect(function () { refreshConversations(); }, []);
React.useEffect(function () { if (conversationId) saveChat(model, selectedModels, history); }, [model, selectedModels, history, conversationId]);
React.useEffect(function () { if (!model && (loadedModels[0] || models[0])) setModel((loadedModels[0] || models[0]).name); }, [models, loadedModels, model]);
React.useEffect(function () {
var loadedNames = loadedModels.map(function (item) { return item.name; });
var loadedSignature = loadedNames.slice().sort().join("\u001f");
var loadedChanged = loadedSignature !== poolLoadedSignature.current;
if (loadedChanged) {
poolLoadedSignature.current = loadedSignature;
setPoolSelection(function (old) { return Array.from(new Set(old.filter(function (name) { return models.some(function (item) { return item.name === name; }); }).concat(loadedNames))); });
} else {
setPoolSelection(function (old) { return old.filter(function (name) { return models.some(function (item) { return item.name === name; }); }); });
}
if (loadedSignature !== chatLoadedSignature.current) {
chatLoadedSignature.current = loadedSignature;
setSelectedModels(function (old) { var valid = old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }); return Array.from(new Set(valid.concat(loadedNames))); });
} else {
setSelectedModels(function (old) { return old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }); });
}
}, [models]);
React.useEffect(function () { saveChat(model, selectedModels, history); }, [model, selectedModels, 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]);
React.useEffect(function () {
if (!thinkingId) return;
function pollThinking() { fetchJSON(API + "/chat/status/" + encodeURIComponent(thinkingId)).then(setThinkingDetails).catch(function () {}); }
pollThinking();
var timer = setInterval(pollThinking, 500);
return function () { clearInterval(timer); };
}, [thinkingId]);
function clearChat() {
var id = conversationId;
setHistory([]); setMetrics([]); setConversationId("");
if (id) fetchJSON(API + "/conversations/" + encodeURIComponent(id), { method: "DELETE" }).catch(function () {});
try { window.localStorage.removeItem(CHAT_STORAGE_KEY); } catch (_) {}
refreshConversations();
setNotice({ ok: "Conversation deleted from shared storage." });
}
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); }; }, []);
function toggleIn(setter, name) { setter(function (old) { return old.indexOf(name) >= 0 ? old.filter(function (item) { return item !== name; }) : old.concat([name]); }); }
function manageModels(endpoint, label) {
if (!poolSelection.length) { setNotice({ error: "Select one or more installed models first." }); return; }
var requested = poolSelection.slice();
setBusy(endpoint); setNotice({ ok: label + " in progress for " + requested.length + " model" + (requested.length === 1 ? "" : "s") + "…" });
fetchJSON(API + endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ names: requested }) }).then(function (result) {
if (endpoint === "/models/load") {
var resident = Array.isArray(result.resident) ? result.resident : ((result.runtime && result.runtime.model_memory) || []).map(function (item) { return item.name; });
var missing = Array.isArray(result.not_resident) ? result.not_resident : requested.filter(function (name) { return resident.indexOf(name) < 0; });
setNotice(missing.length ? { warning: "Ollama kept resident: " + resident.join(", ") + ". Not resident: " + missing.join(", ") + ". This is an Ollama scheduler/capacity warning, not a plugin error." } : { ok: "Loaded and resident: " + resident.join(", ") });
} else {
setNotice({ ok: label + ": " + requested.join(", ") });
}
pollRuntime(); if (props.refresh) props.refresh();
}).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
function loadModel() { manageModels("/models/load", "Permanently loaded"); }
function unloadModels() { manageModels("/models/unload", "Unloaded"); }
function toggleChatModel(name) { if (!loadedModels.some(function (item) { return item.name === name; })) return; toggleIn(setSelectedModels, name); }
function togglePoolModel(name) { toggleIn(setPoolSelection, name); }
function addUrl() { if (!url.trim()) return; setAttachments(function (old) { return old.concat([{ name: url.trim(), url: url.trim(), mime_type: "" }]); }); setUrl(""); }
function addFiles(files) {
var selected = Array.prototype.slice.call(files || []).filter(function (file) { return file && file.size <= 20 * 1024 * 1024; }).slice(0, 12);
if (!selected.length) { setNotice({ error: "No supported files were added, or a file exceeded the 20 MiB limit." }); return; }
Promise.all(selected.map(readFileAsDataURL)).then(function (items) { setAttachments(function (old) { return old.concat(items); }); setNotice({ ok: selected.length + " file" + (selected.length === 1 ? "" : "s") + " attached." }); }).catch(function (err) { setNotice({ error: err.message || "Could not read the selected files." }); });
}
function onFiles(event) { addFiles(event.target.files || []); event.target.value = ""; }
function onPaste(event) {
var files = [];
Array.prototype.slice.call((event.clipboardData && event.clipboardData.items) || []).forEach(function (item) { if (item.kind === "file") { var file = item.getAsFile(); if (file) files.push(file); } });
if (files.length) { event.preventDefault(); addFiles(files); }
}
function onDragOver(event) { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; setDragging(true); }
function onDragLeave(event) { if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false); }
function onDrop(event) { event.preventDefault(); setDragging(false); addFiles(event.dataTransfer.files || []); }
function stop() {
var current = activeRequest;
if (!current || !thinking) return;
current.stopped = true;
setBusy("stop");
fetchJSON(API + "/chat/stop", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_id: current.id }) }).catch(function () {}).finally(function () {
if (current.controller) current.controller.abort();
setThinking(null); setActiveRequest(null); setBusy(""); setNotice({ ok: "Generation stopped." });
});
}
function send() {
if (busy === "send" || busy === "stop" || !selectedModels.length || (!message.trim() && !attachments.length)) return;
var requestId = makeRequestId();
var controller = typeof AbortController === "function" ? new AbortController() : null;
var current = { id: requestId, controller: controller, stopped: false };
var outgoing = { role: "user", content: message.trim() || "[Attachments]" }, body = { model: selectedModels[0], models: selectedModels, message: message, history: history, attachments: attachments, request_id: requestId, conversation_id: conversationId };
setHistory(function (old) { return old.concat([outgoing]); }); setMessage(""); setBusy("send"); setActiveRequest(current); setThinking({ request_id: requestId, startedAt: Date.now(), stage: attachments.length ? "Preparing attachments and sending request to Ollama" : "Sending request to Ollama" }); setThinkingDetails(null); setThinkingOpen(false); setNotice(null);
var requestOptions = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
if (controller) requestOptions.signal = controller.signal;
fetchJSON(API + "/chat", requestOptions).then(function (result) { var answer = result.message && result.message.content ? result.message.content : "(No response text returned.)"; setConversationId(result.conversation_id || conversationId); setHistory(function (old) { return old.concat([{ role: "assistant", content: answer }]); }); setMetrics(result.metrics || []); setAttachments([]); setRuntime(result.runtime || runtime); setNotice({ ok: "Response complete. Shared conversation and performance metrics saved." }); pollRuntime(); refreshConversations(); }).catch(function (err) { if (!current.stopped) setNotice({ error: err.message || String(err) }); }).finally(function () { if (!current.stopped) { setThinking(null); setActiveRequest(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 selected loaded models"), h("p", null, "Select one or more permanently loaded models below. Multiple models answer in parallel and their labelled perspectives are combined.")), h(Button, { className: "secondary", disabled: !history.length || busy === "send", onClick: clearChat }, "Clear chat")),
h("section", { className: "ollama-persistence-panel" },
h("div", { className: "ollama-persistence-heading" }, h("div", null, h("h3", null, "Shared conversations"), h("p", null, "Saved on this Hermes server; any browser can resume them.")), h(Button, { className: "secondary", onClick: newConversation }, "New conversation")),
h("div", { className: "ollama-conversation-list" }, conversations.length ? conversations.map(function (item) { return h(Button, { key: item.id, className: item.id === conversationId ? "selected" : "", onClick: function () { openConversation(item.id); } }, (item.title || "New conversation").slice(0, 70), " · ", item.message_count || 0, " messages"); }) : h("small", null, "No saved conversations yet.")),
aggregate && h("div", { className: "ollama-metrics-summary" }, h("strong", null, "Model performance · ", aggregate.sample_count || 0, " samples"), h("span", null, "TTFT avg: ", aggregate.avg_time_to_first_token_ms == null ? "n/a" : aggregate.avg_time_to_first_token_ms + " ms"), h("span", null, "Output: ", aggregate.avg_eval_tokens_per_second == null ? "n/a" : aggregate.avg_eval_tokens_per_second + " tok/s"), h("span", null, "Latency: ", aggregate.avg_total_latency_ms == null ? "n/a" : aggregate.avg_total_latency_ms + " ms"), h("span", null, "Errors: ", aggregate.error_count || 0)),
metrics && metrics.length > 0 && h("div", { className: "ollama-metrics-detail" }, (metrics.slice(-3)).map(function (item, index) { return h("span", { key: index }, item.model || "model", " · TTFT ", item.time_to_first_token_ms == null ? "n/a" : item.time_to_first_token_ms + " ms", " · ", item.eval_count == null ? "n/a" : item.eval_count + " output tokens", " · ", item.eval_tokens_per_second == null ? "n/a" : item.eval_tokens_per_second + " tok/s"); }))
),
h(ModelPoolPanel, { models: models, loadedModels: loadedModels, selectedModels: selectedModels, poolSelection: poolSelection, busy: busy, onTogglePool: togglePoolModel, onToggleChat: toggleChatModel, onLoad: loadModel, onUnload: unloadModels }),
notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : notice.warning ? "warning" : "ok") }, notice.error || notice.warning || notice.ok),
thinking && h(ThinkingStatus, { stage: thinkingDetails && thinkingDetails.stage ? thinkingDetails.stage : (thinkingElapsed < 1 ? thinking.stage : "Ollama is generating the response"), elapsed: thinkingDetails && thinkingDetails.elapsed != null ? thinkingDetails.elapsed : thinkingElapsed, details: thinkingDetails, expanded: thinkingOpen, onToggle: function () { setThinkingOpen(!thinkingOpen); }, onStop: stop }),
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.")),
h("div", { className: "ollama-composer" + (dragging ? " drop-active" : ""), onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop }, dragging && h("div", { className: "ollama-drop-hint" }, "Drop files here to attach"), h("textarea", { value: message, placeholder: selectedModels.length ? "Ask " + selectedModels.length + " loaded model" + (selectedModels.length === 1 ? "" : "s") + "… Press Enter to send; Shift+Enter for a new line." : "Load and select at least one model above…", onPaste: onPaste, onChange: function (event) { setMessage(event.target.value); }, onKeyDown: function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); send(); } } }),
h("div", { className: "ollama-attachment-actions" }, h("label", { className: "ollama-file-button" }, "Attach image / PDF / file", h("input", { type: "file", multiple: true, accept: "image/*,application/pdf,text/*,.txt,.md,.csv,.json,.log,.xml,.yaml,.yml", onChange: onFiles })), h("input", { className: "ollama-url-input", value: url, placeholder: "https://example.com/document", onChange: function (event) { setUrl(event.target.value); }, onKeyDown: function (event) { if (event.key === "Enter") addUrl(); } }), h(Button, { onClick: addUrl, disabled: !url.trim() }, "Add URL"), h(Button, { onClick: send, disabled: busy === "send" || busy === "stop" || !selectedModels.length || (!message.trim() && !attachments.length) }, busy === "send" ? "Sending…" : "Send (Enter)")),
attachments.length > 0 && h("div", { className: "ollama-attachments" }, attachments.map(function (item, index) { return h("span", { className: "ollama-attachment", key: index }, item.name || item.url, h("button", { type: "button", onClick: function () { setAttachments(function (old) { return old.filter(function (_, i) { return i !== index; }); }); } }, "×")); })),
h("p", { className: "ollama-chat-footnote" }, "Limits: 20 MiB per uploaded file, 15 MiB per fetched URL. Private/local URL targets are blocked. Remote content is treated as untrusted text."))
)
);
}
function Page() {
var dataState = React.useState(null), data = dataState[0], setData = dataState[1];
var tabState = React.useState("chat"), tab = tabState[0], setTab = tabState[1];
var queryState = React.useState(""), query = queryState[0], setQuery = queryState[1];
var catalogTypeState = React.useState("all"), catalogType = catalogTypeState[0], setCatalogType = catalogTypeState[1];
var catalogCapabilityState = React.useState("all"), catalogCapability = catalogCapabilityState[0], setCatalogCapability = catalogCapabilityState[1];
var catalogSortState = React.useState("popularity"), catalogSort = catalogSortState[0], setCatalogSort = catalogSortState[1];
var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1];
var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1];
var targetDialogState = React.useState(null), targetDialog = targetDialogState[0], setTargetDialog = targetDialogState[1];
var loadingState = React.useState(true), loading = loadingState[0], setLoading = loadingState[1];
var loadSequence = React.useRef(0);
function load() {
var sequence = ++loadSequence.current;
return fetchJSON(API + "/status").then(function (value) { if (sequence !== loadSequence.current) return value; setData(value); setLoading(false); return value; }).catch(function (err) { if (sequence === loadSequence.current) { setNotice({ error: err.message || String(err) }); setLoading(false); } });
}
React.useEffect(function () { load(); var timer = setInterval(load, 5000); return function () { clearInterval(timer); }; }, []);
function action(kind, name, selectedTarget) {
if (kind === "delete" && !window.confirm("Remove " + name + " from Ollama?")) return;
if ((kind === "pull" || kind === "redownload") && !selectedTarget) {
var availableTargets = (data && data.connections ? data.connections : []).filter(function (item) { return item.available && (item.kind === "local" || item.kind === "remote"); });
var uniqueTargets = availableTargets.filter(function (item, index, rows) { return rows.findIndex(function (other) { return other.kind === item.kind || other.url === item.url; }) === index; });
if (uniqueTargets.length > 1) { setTargetDialog({ kind: kind, name: name, targets: uniqueTargets }); return; }
selectedTarget = uniqueTargets.length ? uniqueTargets[0].kind : "local";
}
var key = name + ":" + kind; setBusy(key); setNotice(null);
fetchJSON(API + (kind === "delete" ? "/model" : "/" + kind), { method: kind === "delete" ? "DELETE" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name, target: selectedTarget || "local" }) }).then(function (result) { setNotice({ ok: result.message || "Action started." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); });
}
function refreshCatalog() { setBusy("catalog"); setNotice(null); fetchJSON(API + "/catalog/refresh", { method: "POST" }).then(function (result) { setNotice({ ok: "Catalog refreshed: " + result.count + " models." }); load(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); }).finally(function () { setBusy(""); }); }
var baseModels = data ? (tab === "installed" ? data.models || [] : tab === "popular" ? data.popular || [] : tab === "catalog" ? data.catalog || [] : []) : [];
var models = baseModels;
if (tab === "catalog") {
if (catalogType === "moe") models = models.filter(function (model) { return model.is_moe; });
if (catalogType === "dense") models = models.filter(function (model) { return !model.is_moe; });
if (catalogCapability !== "all") models = models.filter(function (model) { return (model.capabilities || []).indexOf(catalogCapability) >= 0; });
models = models.slice().sort(function (a, b) {
if (catalogSort === "size_asc") return (Number(a.size_bytes) || 0) - (Number(b.size_bytes) || 0);
if (catalogSort === "size_desc") return (Number(b.size_bytes) || 0) - (Number(a.size_bytes) || 0);
if (catalogSort === "newest") return (Date.parse(b.modified_at || "") || 0) - (Date.parse(a.modified_at || "") || 0);
if (catalogSort === "name") return String(a.name).localeCompare(String(b.name));
return (Number(a.popularity_rank) || 999999) - (Number(b.popularity_rank) || 999999);
});
}
var needle = query.toLowerCase().trim(); if (needle) models = models.filter(function (model) { return (model.name + " " + model.family + " " + (model.strengths || []).join(" ") + " " + (model.capabilities || []).join(" ")).toLowerCase().indexOf(needle) >= 0; });
var catalogCapabilities = data && data.catalog_filter_options ? data.catalog_filter_options.capabilities || [] : [];
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")), h(ConnectionPanel, { data: data, reload: load })),
targetDialog && h("div", { className: "ollama-target-modal" }, h("div", { className: "ollama-target-card" }, h("h3", null, "Where should " + targetDialog.name + " be downloaded?"), h("p", null, "Both local and remote Ollama instances are online. Choose the destination for this model."), targetDialog.targets.map(function (item) { return h(Button, { key: item.kind, onClick: function () { var chosen = targetDialog; setTargetDialog(null); action(chosen.kind, chosen.name, item.kind); } }, (item.kind || "local").toUpperCase(), " · ", item.url, " · v", item.version, " · ", item.models, " models"); }), h(Button, { className: "secondary", onClick: function () { setTargetDialog(null); } }, "Cancel"))),
notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : notice.warning ? "warning" : "ok") }, notice.error || notice.warning || notice.ok),
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 === "catalog" && h("div", { className: "ollama-catalog-controls" }, h("label", null, "Type", h("select", { className: "ollama-catalog-select", value: catalogType, onChange: function (event) { setCatalogType(event.target.value); } }, h("option", { value: "all" }, "All types"), h("option", { value: "moe" }, "MoE only"), h("option", { value: "dense" }, "Dense only"))), h("label", null, "Ability", h("select", { className: "ollama-catalog-select", value: catalogCapability, onChange: function (event) { setCatalogCapability(event.target.value); } }, h("option", { value: "all" }, "All abilities"), catalogCapabilities.map(function (capability) { return h("option", { key: capability, value: capability }, capability); }))), h("label", null, "Organize", h("select", { className: "ollama-catalog-select", value: catalogSort, onChange: function (event) { setCatalogSort(event.target.value); } }, h("option", { value: "popularity" }, "Popularity"), h("option", { value: "newest" }, "Newest"), h("option", { value: "size_asc" }, "Size: smallest first"), h("option", { value: "size_desc" }, "Size: largest first"), h("option", { value: "name" }, "Name"))))),
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 : [], refresh: load }),
tab === "popular" && h("p", { className: "ollama-popular-note" }, "Popular is limited to models with known size and RAM estimates at or below the detected system RAM (" + (data && data.popular_filter && data.popular_filter.max_expected_ram_gib ? data.popular_filter.max_expected_ram_gib + " GiB" : "detecting…") + "). 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 + " · " + (job.target || "local") + (job.endpoint ? " · " + job.endpoint : "")), h("span", null, job.percent == null ? job.status : job.percent + "%")); })),
tab !== "chat" && loading && h(Empty, null, "Loading local Ollama inventory…"), tab !== "chat" && !loading && !models.length && h(Empty, null, tab === "installed" ? "No local models found." : tab === "popular" ? "No popular catalog entries available." : "No catalog entries available. Try Refresh catalog."), tab !== "chat" && h("section", { className: "ollama-grid" }, models.map(function (model) { return h(ModelCard, { key: model.name, model: model, installed: tab === "installed" || !!model.installed, busy: busy, action: action }); }))
);
}
registry.register("ollama-manager", Page);
})();