summaryrefslogtreecommitdiff
path: root/src/ollama_web/static
diff options
context:
space:
mode:
Diffstat (limited to 'src/ollama_web/static')
-rw-r--r--src/ollama_web/static/settings-actions.js151
-rw-r--r--src/ollama_web/static/settings.css52
2 files changed, 203 insertions, 0 deletions
diff --git a/src/ollama_web/static/settings-actions.js b/src/ollama_web/static/settings-actions.js
new file mode 100644
index 0000000..3411c54
--- /dev/null
+++ b/src/ollama_web/static/settings-actions.js
@@ -0,0 +1,151 @@
+"use strict";
+(function () {
+ const $ = (id) => document.getElementById(id);
+ const modal = $("settings-modal");
+ const screens = [...document.querySelectorAll("[data-settings-screen]")];
+ const advanced = [...document.querySelectorAll("[data-option]")];
+ const ctxValues = [4096, 8192, 16384, 32768, 65536, 131072, 262144];
+ const ctxLabels = ["4k", "8k", "16k", "32k", "64k", "128k", "256k"];
+ let screen = "general";
+
+ function cookie(name) {
+ const found = document.cookie.split(";").map((v) => v.trim())
+ .find((v) => v.startsWith(`${name}=`));
+ return found ? decodeURIComponent(found.slice(name.length + 1)) : "";
+ }
+ async function request(url, options = {}) {
+ const headers = { ...(options.headers || {}) };
+ if ((options.method || "GET").toUpperCase() !== "GET") {
+ headers["X-CSRF-Token"] = cookie("ollama_web_csrf");
+ }
+ const response = await fetch(url, { ...options, headers });
+ if (response.status === 401) location.assign(`/login?next=${encodeURIComponent(location.pathname)}`);
+ return response;
+ }
+ function status(message, error = false) {
+ $("settings-status").textContent = message;
+ $("settings-status").classList.toggle("error", error);
+ }
+ function show(name) {
+ screen = name;
+ screens.forEach((el) => { el.hidden = el.dataset.settingsScreen !== name; });
+ $("settings-back-btn").hidden = name === "general";
+ $("settings-modal-footer").hidden = name === "general";
+ const key = name === "ui" ? "settings.ui.title"
+ : name === "ollama" ? "settings.ollama.title" : "settings.general.title";
+ $("settings-modal-title").textContent = t(key);
+ status("");
+ }
+ function sliders() {
+ $("temperature-value").textContent = Number($("settings-temperature").value).toFixed(1);
+ $("num-ctx-value").textContent = ctxLabels[Number($("settings-num-ctx").value)];
+ }
+ function seedMode() {
+ const fixed = $("settings-seed-mode").value === "fixed";
+ $("settings-seed").hidden = !fixed;
+ $("settings-seed").required = fixed;
+ if (!fixed) $("settings-seed").value = "";
+ }
+ function populate(data) {
+ const options = data.ollama.options;
+ $("settings-language").value = data.ui.language;
+ $("settings-system-prompt").value = data.ollama.system_prompt;
+ $("settings-temperature").value = String(options.temperature);
+ $("settings-num-ctx").value = String(Math.max(0, ctxValues.indexOf(options.num_ctx)));
+ $("settings-seed-mode").value = options.seed === undefined ? "random" : "fixed";
+ $("settings-seed").value = options.seed === undefined ? "" : String(options.seed);
+ advanced.forEach((input) => {
+ input.value = options[input.dataset.option] === undefined
+ ? "" : String(options[input.dataset.option]);
+ });
+ $("settings-stop").value = Array.isArray(options.stop) ? options.stop.join("\n") : "";
+ sliders();
+ seedMode();
+ }
+ function number(input, integer) {
+ const raw = input.value.trim();
+ if (!raw) return null;
+ const valid = integer ? /^-?\d+$/.test(raw) : Number.isFinite(Number(raw));
+ input.setCustomValidity(valid ? "" : t(integer
+ ? "settings.messages.invalid_integer" : "settings.messages.invalid_number"));
+ if (!input.reportValidity()) throw new Error(input.validationMessage);
+ return integer ? Number.parseInt(raw, 10) : Number(raw);
+ }
+ function collect() {
+ const options = {
+ temperature: Number($("settings-temperature").value),
+ num_ctx: ctxValues[Number($("settings-num-ctx").value)],
+ };
+ if ($("settings-seed-mode").value === "fixed") {
+ const value = number($("settings-seed"), true);
+ if (value === null) {
+ $("settings-seed").setCustomValidity(t("settings.messages.invalid_integer"));
+ $("settings-seed").reportValidity();
+ throw new Error("seed");
+ }
+ options.seed = value;
+ }
+ advanced.forEach((input) => {
+ input.setCustomValidity("");
+ const value = number(input, input.dataset.type === "int");
+ if (value !== null) options[input.dataset.option] = value;
+ });
+ const stop = $("settings-stop").value.split(/\r?\n/).filter((line) => line.length);
+ if (stop.length) options.stop = stop;
+ return {
+ ui: { language: $("settings-language").value },
+ ollama: { system_prompt: $("settings-system-prompt").value, options },
+ };
+ }
+ async function open() {
+ modal.classList.add("open");
+ modal.setAttribute("aria-hidden", "false");
+ show("general");
+ try {
+ const response = await request("/api/settings");
+ if (!response.ok) throw new Error(response.statusText);
+ populate(await response.json());
+ } catch (error) {
+ console.error(error);
+ status(t("settings.messages.load_failed"), true);
+ }
+ }
+ function close() {
+ modal.classList.remove("open");
+ modal.setAttribute("aria-hidden", "true");
+ }
+ async function save() {
+ let payload;
+ try { payload = collect(); } catch { return; }
+ $("settings-save-btn").disabled = true;
+ try {
+ const response = await request("/api/settings", {
+ method: "PUT", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ const body = await response.json().catch(() => ({}));
+ if (!response.ok) throw new Error(body.error || response.statusText);
+ if (body.ui.language !== document.documentElement.lang) {
+ location.reload();
+ return;
+ }
+ status(t("settings.messages.saved"));
+ } catch (error) {
+ status(t("settings.messages.save_failed").replace("{error}", error.message), true);
+ } finally { $("settings-save-btn").disabled = false; }
+ }
+
+ $("general-settings-btn").addEventListener("click", open);
+ $("settings-modal-close").addEventListener("click", close);
+ $("settings-back-btn").addEventListener("click", () => show("general"));
+ $("open-ui-settings").addEventListener("click", () => show("ui"));
+ $("open-ollama-settings").addEventListener("click", () => show("ollama"));
+ $("settings-save-btn").addEventListener("click", save);
+ $("settings-temperature").addEventListener("input", sliders);
+ $("settings-num-ctx").addEventListener("input", sliders);
+ $("settings-seed-mode").addEventListener("change", seedMode);
+ modal.addEventListener("click", (event) => { if (event.target === modal) close(); });
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape" && modal.classList.contains("open")) close();
+ });
+})();
diff --git a/src/ollama_web/static/settings.css b/src/ollama_web/static/settings.css
new file mode 100644
index 0000000..c2c778e
--- /dev/null
+++ b/src/ollama_web/static/settings.css
@@ -0,0 +1,52 @@
+.sidebar-footer { display: flex; flex-direction: column; gap: 8px; }
+.settings-modal-content { width: min(760px, 100%); }
+.settings-heading { display: flex; align-items: center; gap: 8px; }
+.settings-back-btn {
+ background: transparent; color: var(--text); border: 1px solid var(--border);
+ border-radius: 6px; padding: 3px 9px; cursor: pointer;
+}
+.settings-menu {
+ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
+}
+.settings-menu-card {
+ display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
+ min-height: 100px; padding: 16px; text-align: left; color: var(--text);
+ background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
+ cursor: pointer;
+}
+.settings-menu-card:hover { border-color: var(--accent); }
+.settings-menu-card span, .field-help { color: var(--muted); font-size: 13px; }
+.settings-field {
+ display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px;
+}
+.settings-field > span { color: var(--muted); font-size: 13px; }
+.settings-field input:not([type="range"]),
+.settings-field textarea, .settings-field select,
+.seed-controls input, .seed-controls select {
+ width: 100%; padding: 8px; color: var(--text); background: var(--panel);
+ border: 1px solid var(--border); border-radius: 6px; font: inherit;
+}
+.settings-field textarea { resize: vertical; }
+.settings-field input[type="range"] { width: 100%; accent-color: var(--accent); }
+.range-labels {
+ display: flex; justify-content: space-between; color: var(--muted); font-size: 11px;
+}
+.seed-controls {
+ display: grid; grid-template-columns: minmax(180px, 1fr) minmax(120px, 1fr); gap: 8px;
+}
+.advanced-settings {
+ margin-top: 8px; padding: 12px; background: var(--bg);
+ border: 1px solid var(--border); border-radius: 8px;
+}
+.advanced-settings summary { cursor: pointer; font-weight: 600; }
+.option-grid {
+ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0 12px; margin-top: 14px;
+}
+.settings-status { min-height: 20px; color: var(--muted); font-size: 13px; }
+.settings-status.error { color: var(--danger); }
+@media (max-width: 600px) {
+ .settings-menu, .option-grid, .seed-controls { grid-template-columns: 1fr; }
+ .settings-modal-content { max-height: 95vh; }
+ .range-labels { font-size: 9px; }
+}