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

765 lines
79 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";
var PLACEMENT_STORAGE_KEY = "hermes.ollama-manager.placement.v1";
var PERFORMANCE_STORAGE_KEY = "hermes.ollama-manager.performance.v1";
var PERFORMANCE_WINDOWS = [
{ value: "1", label: "Last 1 hour", seconds: 60 * 60 },
{ value: "6", label: "Last 6 hours", seconds: 6 * 60 * 60 },
{ value: "9", label: "Last 9 hours", seconds: 9 * 60 * 60 },
{ value: "12", label: "Last 12 hours", seconds: 12 * 60 * 60 },
{ value: "24", label: "Last 24 hours", seconds: 24 * 60 * 60 }
];
function readSavedPlacements() {
try {
var raw = window.localStorage.getItem(PLACEMENT_STORAGE_KEY);
var value = raw ? JSON.parse(raw) : {};
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
} catch (_) { return {}; }
}
function savePlacements(value) {
try { window.localStorage.setItem(PLACEMENT_STORAGE_KEY, JSON.stringify(value || {})); } catch (_) {}
}
function readSavedPerformanceSamples() {
try {
var raw = window.localStorage.getItem(PERFORMANCE_STORAGE_KEY);
var value = raw ? JSON.parse(raw) : [];
return Array.isArray(value) ? value.filter(function (sample) { return sample && typeof sample.captured_at === "number"; }).slice(-120) : [];
} catch (_) { return []; }
}
function savePerformanceSamples(value) {
try { window.localStorage.setItem(PERFORMANCE_STORAGE_KEY, JSON.stringify((value || []).slice(-120))); } catch (_) {}
}
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 : "",
primaryModel: typeof value.primaryModel === "string" ? value.primaryModel : (typeof value.model === "string" ? value.model : ""),
models: Array.isArray(value.models) ? value.models.filter(function (item) { return typeof item === "string"; }).slice(0, 12) : [],
validatorModels: Array.isArray(value.validatorModels) ? value.validatorModels.filter(function (item) { return typeof item === "string"; }).slice(0, 11) : [],
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 || "", primaryModel: model || "", models: (models || []).slice(0, 12), validatorModels: (models || []).slice(1, 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 renderInline(text, keyPrefix) {
var tokens = String(text || "").split(/(`[^`]*`|\*\*[^*]+\*\*|\*[^*]+\*)/g).filter(function (token) { return token !== ""; });
return tokens.map(function (token, index) {
var key = keyPrefix + "-" + index;
if (token.charAt(0) === "`" && token.charAt(token.length - 1) === "`") return h("code", { key: key, className: "ollama-inline-code" }, token.slice(1, -1));
if (token.indexOf("**") === 0 && token.lastIndexOf("**") === token.length - 2) return h("strong", { key: key }, token.slice(2, -2));
if (token.charAt(0) === "*" && token.charAt(token.length - 1) === "*") return h("em", { key: key }, token.slice(1, -1));
return token;
});
}
function RichText(props) {
var lines = String(props.content || "").split("\\n"), nodes = [], code = [], language = "", inCode = false;
lines.forEach(function (line, index) {
var fence = line.match(/^```(.*)$/);
if (fence) {
if (inCode) {
nodes.push(h("pre", { key: "code-" + index, className: "ollama-code-block" }, h("code", { className: language ? "language-" + language : "" }, code.join("\\n"))));
code = []; language = ""; inCode = false;
} else {
language = String(fence[1] || "").trim().replace(/[^A-Za-z0-9_-]/g, ""); inCode = true;
}
return;
}
if (inCode) { code.push(line); return; }
if (!line.trim()) { nodes.push(h("br", { key: "break-" + index })); return; }
nodes.push(h("div", { key: "line-" + index }, renderInline(line, "line-" + index)));
});
if (inCode) nodes.push(h("pre", { key: "code-final", className: "ollama-code-block" }, h("code", null, code.join("\\n"))));
return h("div", { className: "ollama-rich-text" }, nodes);
}
function copyText(value, onCopied) {
if (!navigator.clipboard || !navigator.clipboard.writeText) return;
navigator.clipboard.writeText(String(value || "")).then(function () { if (onCopied) onCopied(); }).catch(function () {});
}
function MessageBubble(props) {
var item = props.item || {}, assistant = item.role === "assistant", label = item.variant === "initial" ? "Initial output" : item.variant === "enhanced" ? "Enhanced output" : "";
return h("div", { className: "ollama-message " + item.role + (item.variant ? " " + item.variant : ""), key: props.messageKey },
h("div", { className: "ollama-message-meta" }, h("small", null, label || (assistant ? (item.model || "Ollama") : "You")), assistant && label && h("small", null, item.model || "Ollama"), item.created_at && h("small", null, fmtDate(item.created_at))),
h(RichText, { content: item.content }),
h("div", { className: "ollama-message-actions" },
h("button", { type: "button", onClick: function () { copyText(item.content, props.onCopied); } }, "Copy"),
assistant && props.onRetry && h("button", { type: "button", onClick: props.onRetry }, "Retry")
)
);
}
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"));
if (model.memory_fit === false) badges.push(h(Badge, { key: "memory", tone: "danger" }, "RAM estimate exceeds host"));
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"), model.activated_parameter_size && h("small", { className: "ollama-activated-parameters" }, model.activated_parameter_size, " activated"))
),
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()), model.modified_at ? h("span", null, "Last updated " + fmtDate(model.modified_at)) : h("span", { className: "ollama-date-unavailable" }, "Last updated unavailable")),
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 graphNumber(sample, key) {
if (!sample || sample[key] === null || sample[key] === undefined) return null;
var value = Number(sample[key]);
return isFinite(value) ? value : null;
}
function PerformanceGraph(props) {
var samples = props.samples || [], values = samples.map(function (sample) { return graphNumber(sample, props.valueKey); });
var max = Number(props.max || 0), observed = values.reduce(function (result, value) { return value == null ? result : Math.max(result, value); }, 0);
if (!max) max = observed > 0 ? observed * 1.15 : 1;
var width = 240, height = 84, pad = 5, points = [], latest = null;
values.forEach(function (value, index) {
if (value == null) return;
latest = value;
var x = pad + (values.length > 1 ? index * (width - pad * 2) / (values.length - 1) : (width / 2));
var y = height - pad - Math.max(0, Math.min(1, value / max)) * (height - pad * 2);
points.push(x.toFixed(1) + "," + y.toFixed(1));
});
var line = points.join(" "), area = points.length ? pad + "," + (height - pad) + " " + line + " " + (width - pad) + "," + (height - pad) : "";
var formatted = latest == null ? "No samples yet" : (props.format ? props.format(latest) : Number(latest).toFixed(1));
return h("article", { className: "ollama-performance-graph" },
h("div", { className: "ollama-performance-graph-heading" }, h("div", null, h("strong", null, props.title), h("small", null, props.subtitle)), h("b", null, formatted)),
h("svg", { className: "ollama-performance-svg", viewBox: "0 0 240 84", role: "img", "aria-label": props.title + " history" },
h("line", { x1: pad, y1: height - pad, x2: width - pad, y2: height - pad, className: "ollama-graph-grid" }),
h("line", { x1: pad, y1: height / 2, x2: width - pad, y2: height / 2, className: "ollama-graph-grid" }),
h("line", { x1: pad, y1: pad, x2: width - pad, y2: pad, className: "ollama-graph-grid" }),
area && h("polygon", { points: area, className: "ollama-graph-area" }),
line && h("polyline", { points: line, className: "ollama-graph-line" })
),
h("div", { className: "ollama-performance-graph-scale" }, h("span", null, "0"), h("span", null, props.range || "dynamic"))
);
}
function PerformanceGraphs(props) {
var samples = props.samples || [];
var rangeState = React.useState("1"), range = rangeState[0], setRange = rangeState[1];
var selectedWindow = PERFORMANCE_WINDOWS.find(function (item) { return item.value === range; }) || PERFORMANCE_WINDOWS[0];
var cutoff = Date.now() / 1000 - selectedWindow.seconds;
var visibleSamples = samples.filter(function (sample) { return Number(sample.captured_at || 0) >= cutoff; });
function pct(value) { return value == null ? "n/a" : Number(value).toFixed(1) + "%"; }
function gib(value) { return value == null ? "n/a" : Number(value).toFixed(2) + " GiB"; }
function count(value) { return value == null ? "n/a" : String(Math.round(value)); }
return h("section", { className: "ollama-performance-graphs" },
h("div", { className: "ollama-performance-graphs-heading" },
h("div", null, h("h3", null, "Performance history"), h("p", null, "CPU, GPU, memory, storage, swap, and Ollama residency over the selected window. ", visibleSamples.length, " minute samples available.")),
h("div", { className: "ollama-performance-controls" },
h("label", { className: "ollama-performance-range" }, "History window", h("select", { value: range, onChange: function (event) { setRange(event.target.value); }, "aria-label": "Performance history window" }, PERFORMANCE_WINDOWS.map(function (item) { return h("option", { key: item.value, value: item.value }, item.label); }))),
h("span", null, samples.length ? "Live · 1 second" : "Waiting for telemetry")
)
),
h("div", { className: "ollama-performance-grid" },
h(PerformanceGraph, { title: "CPU usage", subtitle: "Total processor utilization", valueKey: "cpu_usage_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "CPU load", subtitle: "1-minute load average", valueKey: "cpu_load_1m", range: "dynamic", format: function (value) { return Number(value).toFixed(2); }, samples: visibleSamples }),
h(PerformanceGraph, { title: "System memory", subtitle: "Used RAM", valueKey: "memory_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "GPU usage", subtitle: "Aggregate GPU utilization", valueKey: "gpu_usage_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "GPU VRAM", subtitle: "Used video memory", valueKey: "gpu_vram_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "Disk usage", subtitle: "Root filesystem", valueKey: "disk_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "Swap usage", subtitle: "Used swap memory", valueKey: "swap_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "Ollama model weights", subtitle: "Resident model bytes", valueKey: "ollama_model_gib", range: "dynamic", format: gib, samples: visibleSamples }),
h(PerformanceGraph, { title: "Resident models", subtitle: "Loaded Ollama model count", valueKey: "resident_model_count", range: "dynamic", format: count, samples: visibleSamples })
)
);
}
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 || {}, cpu = runtime.cpu || {}, disk = runtime.disk || {}, models = runtime.model_memory || [], loading = runtime.model_loading || [], cores = cpu.cores || [], gpus = gpu.gpus || [];
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;
function percent(value) { return value == null ? "n/a" : Number(value).toFixed(1) + "%"; }
function meter(value) { return value == null ? 0 : Math.max(0, Math.min(100, Number(value))); }
return h("section", { className: "ollama-runtime-panel" },
h("div", { className: "ollama-runtime-heading" }, h("div", null, h("h3", null, "Live runtime memory and hardware"), h("p", null, "Updates every second while this panel is open. Multiple CPUs and GPUs expand into individual cards.")), h(Badge, { tone: loading.length ? "live" : (gpu.detected ? "live" : "muted") }, loading.length ? "MODEL LOADING" : (gpu.detected ? "GPU detected" : "CPU 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 ollama-cpu-stat" }, h("small", null, "CPU usage"), h("strong", null, percent(cpu.usage_percent)), h("div", { className: "ollama-meter" }, h("span", { style: { width: meter(cpu.usage_percent) + "%" } })), h("small", null, cpu.count ? cpu.count + " logical CPUs · load " + (cpu.load_average || []).map(function (value) { return Number(value).toFixed(2); }).join(" / ") : "Unavailable")),
h("div", { className: "ollama-runtime-stat ollama-disk-stat" }, h("small", null, "Disk usage"), h("strong", null, percent(disk.used_percent)), h("div", { className: "ollama-meter" }, h("span", { style: { width: meter(disk.used_percent) + "%" } })), h("small", null, disk.available ? fmtBytes(disk.used_bytes) + " used · " + fmtBytes(disk.free_bytes) + " free" : "Unavailable")),
h("div", { className: "ollama-runtime-stat ollama-gpu-stat" }, h("small", null, "GPU usage"), h("strong", null, percent(gpu.utilization_percent), " · ", gpu.count || 0, " GPU", (gpu.count || 0) === 1 ? "" : "s"), h("div", { className: "ollama-meter" }, h("span", { style: { width: meter(gpu.utilization_percent) + "%" } })), h("small", null, gpu.telemetry_available && gpus.length ? gpus.map(function (item) { return item.name + " · " + fmtBytes(item.used_bytes) + " / " + fmtBytes(item.total_bytes); }).join("; ") : "Unavailable")),
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"))
),
cores.length > 1 && h("div", { className: "ollama-device-section" }, h("div", { className: "ollama-device-heading" }, h("h4", null, "CPU cores · ", cores.length), h("small", null, "Per-core usage")), h("div", { className: "ollama-device-grid" }, cores.map(function (core) { return h("div", { className: "ollama-device-card", key: core.name }, h("strong", null, core.name.toUpperCase()), h("span", null, percent(core.usage_percent)), h("div", { className: "ollama-meter" }, h("span", { style: { width: meter(core.usage_percent) + "%" } }))); }))),
gpus.length > 1 && h("div", { className: "ollama-device-section" }, h("div", { className: "ollama-device-heading" }, h("h4", null, "GPUs · ", gpus.length), h("small", null, "Per-GPU telemetry")), h("div", { className: "ollama-device-grid" }, gpus.map(function (item) { return h("div", { className: "ollama-device-card ollama-gpu-device-card", key: item.index }, h("strong", null, "GPU ", item.index, " · ", item.name), h("span", null, "Usage ", percent(item.utilization_percent)), h("div", { className: "ollama-meter" }, h("span", { style: { width: meter(item.utilization_percent) + "%" } })), h("small", null, "VRAM ", fmtBytes(item.used_bytes), " / ", fmtBytes(item.total_bytes), " · Free ", fmtBytes(item.free_bytes)), h("small", null, item.temperature_c == null ? "Temperature n/a" : "Temperature " + item.temperature_c.toFixed(0) + "°C", " · ", item.power_watts == null ? "Power n/a" : "Power " + item.power_watts.toFixed(0) + " W")); }))),
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 StoragePanel(props) {
var storage = props.storage || {};
var postgres = storage.postgres || {};
var backend = storage.backend || "sqlite";
return h("section", { className: "ollama-storage-panel" },
h("div", { className: "ollama-pool-heading" }, h("div", null, h("h3", null, "Chat storage"), h("p", null, "SQLite is the default. PostgreSQL is optional and remains local-only.")), h(Badge, { tone: backend === "postgres" ? "live" : "muted" }, backend === "postgres" ? "PostgreSQL" : "SQLite")),
h("div", { className: "ollama-storage-copy" }, h("strong", null, backend === "postgres" ? "Using PostgreSQL chat storage" : "Using SQLite chat storage"), h("small", null, postgres.available ? "Native PostgreSQL detected: " + (postgres.version || "version available") : "PostgreSQL is not currently linked. Switching storage does not delete existing conversations.")),
h("div", { className: "ollama-storage-actions" },
backend === "postgres" ? h(Button, { className: "secondary", onClick: function () { props.onConfigure("sqlite"); } }, "Use SQLite") : h(Button, { disabled: !postgres.available, onClick: function () { props.onConfigure("postgres"); } }, "Link PostgreSQL"),
!postgres.available && h(Button, { className: "secondary", onClick: props.onInstall }, "Install native PostgreSQL"),
h(Button, { className: "secondary", onClick: props.onRefresh }, "Refresh storage status")
),
h("small", { className: "ollama-storage-note" }, "PostgreSQL installation is an explicit host change. Chat data is not moved until you choose Link PostgreSQL.")
);
}
function ModelPoolPanel(props) {
var models = props.models || [], loaded = models.filter(function (item) { return item.loaded; }), primary = props.primaryModel || "", validators = props.validatorModels || [];
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, placement = props.placements[item.name] || "gpu_ram"; 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("span", { className: "ollama-placement-control" }, h("small", null, "Placement"), h("select", { value: placement, onClick: function (event) { event.stopPropagation(); }, onChange: function (event) { event.stopPropagation(); props.onPlacementChange(item.name, event.target.value); } }, h("option", { value: "gpu_ram" }, "GPU + RAM (automatic offload)"), h("option", { value: "ram_only" }, "RAM only (CPU)"))))); })),
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("div", null, h("strong", null, "Two-stage answer workflow"), h("small", null, "The primary model writes the initial output. One enhancement model rewrites it with improvements and returns a complete enhanced output.")),
loaded.length ? h("div", { className: "ollama-harness-primary" }, h("label", null, "Primary model", h("select", { value: primary, onChange: function (event) { props.onPrimaryChange(event.target.value); } }, loaded.map(function (item) { return h("option", { key: item.name, value: item.name }, item.name); })))) : h("span", null, "Load one or more models above first."),
loaded.length > 1 && h("div", { className: "ollama-harness-validators" }, h("strong", null, "Enhancement model"), loaded.filter(function (item) { return item.name !== primary; }).map(function (item) { return h("label", { className: "loaded", key: item.name }, h("input", { type: "radio", name: "ollama-enhancement-model", checked: validators[0] === item.name, onChange: function () { props.onToggleChat(item.name); } }), item.name, " · ", (item.capabilities || []).join(", ")); })),
loaded.length > 1 && h("small", { className: validators.length >= 1 ? "ollama-harness-ready" : "ollama-harness-warning" }, validators.length >= 1 ? "Workflow ready: initial output will be followed by a complete enhanced output." : "Select one enhancement model to produce the enhanced output."))
);
}
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.slice(0, 2) : (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 placementState = React.useState(readSavedPlacements()), placements = placementState[0], setPlacements = placementState[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(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(readSavedPerformanceSamples()), samples = samplesState[0], setSamples = samplesState[1];
var performanceHistoryAt = React.useRef(0);
var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1];
var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1];
var validationState = React.useState(null), validationReports = validationState[0], setValidationReports = validationState[1];
var storageState = React.useState(null), storage = storageState[0], setStorage = storageState[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 || {}, outputs = value.harness_outputs || {}, restored = [];
(value.messages || []).forEach(function (message) {
var output = outputs[String(message.request_id || "")];
if (message.role === "assistant" && output && output.initial_output) {
restored.push({ id: String(message.id) + ":initial", request_id: message.request_id, role: "assistant", content: output.initial_output, created_at: message.created_at, model: output.primary_model, variant: "initial", attachments: message.attachments || [] });
restored.push({ id: String(message.id) + ":enhanced", request_id: message.request_id, role: "assistant", content: output.enhanced_output || output.initial_output, created_at: message.created_at, model: output.enhancement_model, variant: "enhanced", attachments: message.attachments || [] });
} else {
restored.push({ id: message.id, request_id: message.request_id, role: message.role, content: message.content, created_at: message.created_at, model: message.model, attachments: message.attachments || [] });
}
});
setConversationId(item.id || id);
setHistory(restored);
setMetrics(value.metrics || []);
if (item.model) setModel(item.model);
if (Array.isArray(item.models) && item.models.length) setSelectedModels(item.models.slice(0, 2));
}).catch(function (err) { setNotice({ error: err.message || String(err) }); });
}
function refreshStorage() { fetchJSON(API + "/storage").then(setStorage).catch(function (err) { setNotice({ error: "Storage status unavailable: " + (err.message || String(err)) }); }); }
function configureStorage(backend) {
var confirmation = backend === "postgres" ? "enable-postgresql" : "use-sqlite";
if (!window.confirm(backend === "postgres" ? "Link PostgreSQL chat storage now? Existing SQLite conversations will be migrated." : "Switch new chat writes back to SQLite?")) return;
fetchJSON(API + "/storage/configure", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ backend: backend, confirm: confirmation }) }).then(function (value) { setNotice({ ok: value.message || "Chat storage updated." }); refreshStorage(); refreshConversations(); }).catch(function (err) { setNotice({ error: err.message || String(err) }); });
}
function installPostgres() {
if (!window.confirm("Install native PostgreSQL on this host? This changes host packages and services.")) return;
setNotice({ ok: "Native PostgreSQL installation started…" });
fetchJSON(API + "/storage/postgresql/install", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ backend: "postgres", confirm: "install-postgresql", install: true }) }).then(function (value) { setNotice({ ok: value.message || "PostgreSQL installation completed." }); refreshStorage(); }).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([]); setValidationReports(null); setNotice({ ok: "New conversation ready." }); }
React.useEffect(function () { refreshConversations(); refreshStorage(); resumeActiveJobs(); }, []);
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 valid.length ? Array.from(new Set(valid)).slice(0, 2) : (loadedNames.length ? [loadedNames[0]] : []); });
} else {
setSelectedModels(function (old) { return old.filter(function (name) { return loadedNames.indexOf(name) >= 0; }).slice(0, 2); });
}
}, [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, 1000);
return function () { clearInterval(timer); };
}, [thinkingId]);
function clearChat() {
var id = conversationId;
setHistory([]); setMetrics([]); setValidationReports(null); 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 pollPerformanceHistory() {
performanceHistoryAt.current = Date.now();
fetchJSON(API + "/runtime/history?hours=24").then(function (value) {
setSamples(Array.isArray(value.samples) ? value.samples : []);
}).catch(function () {});
}
function pollRuntime() {
fetchJSON(API + "/runtime").then(function (value) {
setRuntime(value);
if (Date.now() - performanceHistoryAt.current >= 5000) pollPerformanceHistory();
}).catch(function () {});
}
React.useEffect(function () { pollRuntime(); pollPerformanceHistory(); var timer = setInterval(pollRuntime, 1000); return function () { clearInterval(timer); }; }, []);
React.useEffect(function () { savePerformanceSamples(samples); }, [samples]);
React.useEffect(function () { savePlacements(placements); }, [placements]);
function toggleIn(setter, name) { setter(function (old) { return old.indexOf(name) >= 0 ? old.filter(function (item) { return item !== name; }) : old.concat([name]); }); }
function setPlacement(name, value) { setPlacements(function (old) { var next = Object.assign({}, old); next[name] = value === "ram_only" ? "ram_only" : "gpu_ram"; return next; }); }
function manageModels(endpoint, label) {
if (!poolSelection.length) { setNotice({ error: "Select one or more installed models first." }); return; }
var requested = poolSelection.slice();
var requestedPlacements = requested.reduce(function (result, name) { result[name] = placements[name] || "gpu_ram"; return result; }, {});
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, placements: requestedPlacements }) }).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; });
if (result.memory_safety && result.memory_safety.triggered) {
setNotice({ warning: result.message + " Observed RAM: " + (result.memory_safety.usage_percent == null ? "unknown" : result.memory_safety.usage_percent + "%") + "." });
} else {
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: result.message || ("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 setPrimaryModel(name) { if (!loadedModels.some(function (item) { return item.name === name; })) return; setModel(name); setSelectedModels(function (old) { return [name].concat(old.filter(function (item) { return item !== name; })).slice(0, 2); }); }
function toggleChatModel(name) {
if (name === selectedModels[0] || !loadedModels.some(function (item) { return item.name === name; })) return;
setSelectedModels(function (old) { return old.length > 1 && old[1] === name ? [old[0]] : [old[0], 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 pollChatJob(requestId, current) {
return fetchJSON(API + "/chat/status/" + encodeURIComponent(requestId)).then(function (status) {
setThinkingDetails(status);
if (current.stopped) throw new Error("Chat stopped by user");
if (status.done) {
if (status.status !== "completed") throw new Error(status.error || "Server-side chat job did not complete");
return status;
}
return new Promise(function (resolve) { setTimeout(resolve, 1200); }).then(function () { return pollChatJob(requestId, current); });
});
}
function applyChatResult(result) {
var answer = result.message && result.message.content ? result.message.content : "(No response text returned.)";
var resolvedConversationId = result.conversation_id || conversationId;
setConversationId(resolvedConversationId);
setHistory(function (old) {
if (result.initial_output) {
var requestId = result.request_id || "", alreadyShown = old.some(function (item) { return item.request_id === requestId && item.variant === "enhanced"; });
if (alreadyShown) return old;
return old.concat([
{ role: "assistant", content: result.initial_output, model: result.primary_model || "Ollama", request_id: requestId, variant: "initial" },
{ role: "assistant", content: result.enhanced_output || answer, model: result.enhancement_model || "Ollama", request_id: requestId, variant: "enhanced" }
]);
}
var last = old.length ? old[old.length - 1] : null;
return last && last.role === "assistant" && last.content === answer ? old : old.concat([{ role: "assistant", content: answer, model: result.primary_model || "Ollama", request_id: result.request_id }]);
});
setMetrics(result.metrics || []);
setValidationReports(null);
setAttachments([]);
setRuntime(result.runtime || runtime);
setNotice({ ok: result.mode === "harness" ? "Initial output and enhanced output generated by " + result.primary_model + " and " + (result.enhancement_model || (result.validator_models || [])[0] || "the enhancement model") + "." : "Response complete. Shared conversation and performance metrics saved." });
pollRuntime(); refreshConversations();
return result;
}
function resumeActiveJobs() {
return fetchJSON(API + "/chat/jobs?active=true&limit=20").then(function (value) {
var jobs = value.jobs || [];
var active = jobs.find(function (job) { return !job.done && (job.status === "queued" || job.status === "running" || job.status === "stopping"); });
if (!active) return null;
var current = { id: active.request_id, controller: null, stopped: false };
setConversationId(active.conversation_id || "");
setActiveRequest(current);
setBusy("send");
setThinking({ request_id: active.request_id, startedAt: (Number(active.started_at || active.updated_at || Date.now() / 1000) * 1000), stage: active.stage || "Resuming server-side chat job" });
setThinkingDetails(active);
if (active.conversation_id) openConversation(active.conversation_id);
return pollChatJob(active.request_id, current).then(applyChatResult).catch(function (err) { if (!current.stopped) setNotice({ error: err.message || String(err) }); }).finally(function () { if (!current.stopped) { setThinking(null); setActiveRequest(null); } setBusy(""); });
}).catch(function () { return null; });
}
function retryMessage(index) {
if (busy === "send" || busy === "stop") return;
var prior = history.slice(0, index).reverse().find(function (item) { return item.role === "user"; });
if (prior && prior.content) send(prior.content);
}
function send(messageOverride) {
var outgoingMessage = messageOverride == null ? message : String(messageOverride);
if (busy === "send" || busy === "stop" || !selectedModels.length || (!outgoingMessage.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: outgoingMessage.trim() || "[Attachments]" }, body = { model: selectedModels[0], models: selectedModels, primary_model: selectedModels[0], validator_models: selectedModels.slice(1), harness: selectedModels.length > 1, placements: placements, message: outgoingMessage, 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) { return result.done ? result : pollChatJob(requestId, current); }).then(applyChatResult).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 Ollama"), h("p", null, "Direct chat is the default. For two-stage writing, select a primary and one enhancement model to receive both outputs.")), h(Button, { className: "secondary", disabled: !history.length || busy === "send", onClick: clearChat }, "Clear chat")),
notice && h("div", { className: "ollama-notice " + (notice.error ? "error" : notice.warning ? "warning" : "ok") }, notice.error || notice.warning || notice.ok),
h("div", { className: "ollama-chat-shell" },
h("aside", { className: "ollama-conversation-rail" },
h("div", { className: "ollama-rail-heading" }, h("div", null, h("h3", null, "Conversations"), h("small", null, "Shared on this Hermes server")), h(Button, { className: "secondary", onClick: newConversation }, "New")),
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); } }, h("span", null, (item.title || "New conversation").slice(0, 52)), h("small", null, item.message_count || 0, " messages")); }) : h("small", null, "No saved conversations yet.")),
aggregate && h("div", { className: "ollama-metrics-summary" }, h("strong", null, aggregate.sample_count || 0, " samples"), h("span", null, "TTFT ", 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, "Errors ", aggregate.error_count || 0))
),
h("div", { className: "ollama-chat-main" },
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("div", { className: "ollama-conversation" }, history.length ? history.map(function (item, index) { return h(MessageBubble, { item: item, messageKey: item.id || item.request_id || index, key: item.id || item.request_id || index, onCopied: function () { setNotice({ ok: "Message copied." }); }, onRetry: item.role === "assistant" && item.variant !== "initial" ? function () { retryMessage(index); } : null }); }) : h(Empty, null, "Start a conversation. Select a loaded model in the controls, then send a 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 in Chat controls…", 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.")
)
),
h("aside", { className: "ollama-chat-controls" },
h("div", { className: "ollama-controls-heading" }, h("h3", null, "Chat controls"), h("small", null, "Model, quality, and runtime")),
h(ModelPoolPanel, { models: models, loadedModels: loadedModels, selectedModels: selectedModels, primaryModel: selectedModels[0] || "", validatorModels: selectedModels.slice(1), poolSelection: poolSelection, placements: placements, busy: busy, onTogglePool: togglePoolModel, onPlacementChange: setPlacement, onToggleChat: toggleChatModel, onPrimaryChange: setPrimaryModel, onLoad: loadModel, onUnload: unloadModels }),
h("details", { className: "ollama-advanced-control" }, h("summary", null, "Chat storage"), h(StoragePanel, { storage: storage, onConfigure: configureStorage, onInstall: installPostgres, onRefresh: refreshStorage })),
h("div", { className: "ollama-advanced-note" }, "Runtime cards and historical performance graphs are shown in Operations / Performance below.")
)
),
h("section", { className: "ollama-operations-section" },
h("div", { className: "ollama-operations-heading" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "OPERATIONS / PERFORMANCE"), h("h3", null, "Runtime telemetry and historical graphs"), h("p", null, "The complete operational view is preserved below the chat so it remains available without competing with the conversation.")), h("span", null, "CPU · GPU · RAM · disk · swap")),
h(RuntimePanel, { runtime: runtime, samples: samples }),
h(PerformanceGraphs, { samples: samples })
)
);
}
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 recentOnlyState = React.useState(true), recentOnly = recentOnlyState[0], setRecentOnly = recentOnlyState[1];
var showOversizedState = React.useState(false), showOversized = showOversizedState[0], setShowOversized = showOversizedState[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 catalogState = React.useState({ rows: [], total: 0, page: 1, has_more: false, capabilities: [] }), catalogData = catalogState[0], setCatalogData = catalogState[1];
var catalogLoadingState = React.useState(false), catalogLoading = catalogLoadingState[0], setCatalogLoading = catalogLoadingState[1];
var loadSequence = React.useRef(0);
var catalogSequence = 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); } });
}
function loadCatalog(nextPage) {
var page = nextPage || 1;
var sequence = ++catalogSequence.current;
var params = new URLSearchParams({ q: query, page: String(page), page_size: "60", model_type: catalogType, capability: catalogCapability, sort: catalogSort, recent_only: recentOnly ? "true" : "false", show_oversized: showOversized ? "true" : "false" });
setCatalogLoading(true);
return fetchJSON(API + "/catalog?" + params.toString()).then(function (value) {
if (sequence !== catalogSequence.current) return value;
setCatalogData(function (old) { return page > 1 ? Object.assign({}, value, { rows: (old.rows || []).concat(value.catalog || []), capabilities: old.capabilities || ((value.catalog_filter_options || {}).capabilities || []) }) : Object.assign({}, value, { rows: value.catalog || [], capabilities: (value.catalog_filter_options || {}).capabilities || [] }); });
return value;
}).catch(function (err) { if (sequence === catalogSequence.current) setNotice({ error: err.message || String(err) }); }).finally(function () { if (sequence === catalogSequence.current) setCatalogLoading(false); });
}
React.useEffect(function () { load(); var timer = setInterval(load, 10000); return function () { clearInterval(timer); }; }, []);
React.useEffect(function () { if (tab !== "catalog") return; var timer = setTimeout(function () { loadCatalog(1); }, 250); return function () { clearTimeout(timer); }; }, [tab, query, catalogType, catalogCapability, catalogSort, recentOnly, showOversized]);
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" ? catalogData.rows || [] : []) : [];
var models = baseModels;
if (tab !== "catalog") {
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 = catalogData.capabilities && catalogData.capabilities.length ? catalogData.capabilities : (data && data.catalog_filter_options ? data.catalog_filter_options.capabilities || [] : []);
var jobs = data && data.jobs ? data.jobs.filter(function (job) { return job.state === "running"; }) : [];
var disk = data && data.disk ? data.disk : {};
var navTabs = h("nav", { className: "ollama-tabs", "aria-label": "Ollama views" },
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 (" + (tab === "catalog" ? (catalogData.total || 0) : ((data && data.catalog_count) || 0)) + ")")
);
var catalogControls = 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")
)),
h("label", { className: "ollama-catalog-checkbox", title: "Models with no published source date remain visible." }, h("input", { type: "checkbox", checked: recentOnly, onChange: function (event) { setRecentOnly(event.target.checked); } }), h("span", null, "Hide models older than 12 months")),
h("label", { className: "ollama-catalog-checkbox ollama-catalog-memory-bypass", title: "This only bypasses the catalog display filter; loading remains protected by the 95% RAM safety guard." }, h("input", { type: "checkbox", checked: showOversized, onChange: function (event) { setShowOversized(event.target.checked); } }), h("span", null, "Show models above estimated RAM"))
);
var browseToolbar = tab !== "chat" && h("div", { className: "ollama-browse-row" },
h("input", { className: "ollama-search", value: query, placeholder: "Search models, capabilities, or strengths…", onChange: function (event) { setQuery(event.target.value); } }),
catalogControls
);
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-nav-row" }, navTabs, h("div", { className: "ollama-toolbar-disk" }, h("span", null, "Disk"), h("strong", null, disk.used_percent == null ? "n/a" : Number(disk.used_percent).toFixed(1) + "%"), h("small", null, disk.available ? fmtBytes(disk.free_bytes) + " free" : "Unavailable"))), browseToolbar),
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, tab === "catalog" ? (catalogData.total || 0) + " matching downloads · showing " + models.length : "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 === "catalog" && catalogLoading && !models.length && h(Empty, null, "Searching the Ollama catalog…"), tab !== "chat" && !loading && !catalogLoading && !models.length && h(Empty, null, tab === "installed" ? "No local models found." : tab === "popular" ? "No popular catalog entries available." : "No catalog entries match these filters."), 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 }); })), tab === "catalog" && catalogData.has_more && h("div", { className: "ollama-catalog-more" }, h(Button, { className: "secondary", disabled: catalogLoading, onClick: function () { loadCatalog((catalogData.page || 1) + 1); } }, catalogLoading ? "Loading more…" : "Load more models"))
);
}
registry.register("ollama-manager", Page);
})();