From 6a362d58a952c2de0d3097b03b212388a6bc35e4 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sat, 27 Jun 2026 17:00:42 +0900 Subject: 一般設定の追加(UI/Ollama設定) settings.jsonに設定を保存 UIから言語設定対応 Ollama設定でシステムプロンプト, temperature, コンテキスト長などのパラメータを設定可能 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ollama_web/static/settings-actions.js | 151 ++++++++++++++++++++++++++++++ src/ollama_web/static/settings.css | 52 ++++++++++ 2 files changed, 203 insertions(+) create mode 100644 src/ollama_web/static/settings-actions.js create mode 100644 src/ollama_web/static/settings.css (limited to 'src/ollama_web/static') 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; } +} -- cgit v1.2.3