OXYGENHUB
# 71e65fcf95ab oxygenhub / assets / hub.js
25.3 KiB · JavaScript · last changed Adding an assistant: provider presets and the endpoint's own model list 1 month ago source blame history raw
1
/* Progressive enhancement only: every page works without this file. */
2
(function () {
3
  "use strict";
4
  /* The band's active tab: the longest link that prefixes the path. */
5
  var p = location.pathname, best = null;
6
  document.querySelectorAll(".band a").forEach(function (a) {
7
    var h = a.getAttribute("href");
8
    if (h !== "/" && (p === h || p.indexOf(h + "/") === 0) && (!best || h.length > best.getAttribute("href").length)) best = a;
9
  });
10
  if (best) best.classList.add("on");
11
12
  /* Off-the-shelf editors, loaded only where a page asks for them:
13
     CodeMirror 6 over textarea.code, EasyMDE over textarea.md. */
14
  function load(src, css) {
15
    return new Promise(function (ok) {
16
      if (css) { var l = document.createElement("link"); l.rel = "stylesheet"; l.href = css; document.head.appendChild(l); }
17
      var s = document.createElement("script"); s.src = src; s.onload = ok; document.head.appendChild(s);
18
    });
19
  }
20
  if (document.querySelector("textarea.code")) load("/static/vendor/editor.js");
21
  var mds = document.querySelectorAll("textarea.md");
22
  if (mds.length) load("/static/vendor/easymde.min.js", "/static/vendor/easymde.min.css").then(function () {
23
    var E = window.EasyMDE;
24
    function b(name, action, text, title) { return { name: name, action: action, text: text, title: title, className: "mde-btn" }; }
25
    var toolbar = [b("bold", E.toggleBold, "B", "Bold"), b("italic", E.toggleItalic, "I", "Italic"), b("heading", E.toggleHeadingSmaller, "H", "Heading"), "|",
26
      b("code", E.toggleCodeBlock, "</>", "Code"), b("quote", E.toggleBlockquote, "“", "Quote"), b("ul", E.toggleUnorderedList, "•", "List"), b("ol", E.toggleOrderedList, "1.", "Numbered list"), "|",
27
      b("link", E.drawLink, "link", "Link"), "|", b("preview", E.togglePreview, "preview", "Preview")];
28
    mds.forEach(function (t) { t._mde = new E({ element: t, spellChecker: false, status: false, forceSync: true, autoDownloadFontAwesome: false, toolbar: toolbar }); });
29
    /* An editor shown later (a :target edit form) lays itself out again. */
30
    window.addEventListener("hashchange", function () { mds.forEach(function (t) { if (t._mde) setTimeout(function () { t._mde.codemirror.refresh(); }, 0); }); });
31
  });
32
33
  /* Theme switch in the header: dark, light, auto (follows the system); kept in the theme cookie. */
34
  var toggle = document.querySelector("[data-theme-toggle]");
35
  if (toggle) {
36
    var root = document.documentElement, order = ["light", "dark", "auto"], glyph = { dark: "◐", light: "☀", auto: "◑" };
37
    var show = function () {
38
      var p = root.dataset.themePref || "light";
39
      toggle.title = "Theme: " + p + " (click to change)";
40
      toggle.setAttribute("aria-label", toggle.title);
41
      toggle.firstElementChild.textContent = glyph[p];
42
    };
43
    show();
44
    toggle.addEventListener("click", function () {
45
      var p = order[(order.indexOf(root.dataset.themePref || "light") + 1) % order.length];
46
      document.cookie = "theme=" + p + "; path=/; max-age=31536000; samesite=lax";
47
      root.dataset.themePref = p;
48
      root.dataset.theme = p === "auto" ? (window.matchMedia && matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
49
      show();
50
    });
51
  }
52
53
  /* Diffs: unified or split (remembered in the diff cookie), collapse all. */
54
  var diffs = document.querySelectorAll("table.code.diff");
55
  if (diffs.length) {
56
    var pane = function (tr, col, side) {
57
      var n = document.createElement("td"), t = document.createElement("td");
58
      n.className = "n"; t.className = "t" + (side === "right" ? " mid" : "");
59
      if (!tr) { n.className += " none"; t.className += " none"; return [n, t]; }
60
      var kind = tr.classList.contains("add") ? "add" : tr.classList.contains("del") ? "del" : "ctx";
61
      if (kind !== "ctx") { n.className += " " + kind; t.className += " " + kind; }
62
      n.textContent = tr.cells[col].textContent;
63
      t.innerHTML = tr.cells[3].innerHTML;
64
      // Line comments belong to the projected (right) side only.
65
      if (side === "left") t.querySelectorAll(".linecomment").forEach(function (d) { d.remove(); });
66
      return [n, t];
67
    };
68
    var split = function (table) {
69
      var s = document.createElement("table");
70
      s.className = "code diff split";
71
      s.innerHTML = '<colgroup><col class="n"><col><col class="n"><col></colgroup>';
72
      var rows = [].slice.call(table.rows), i = 0;
73
      var add = function (l, r) {
74
        var tr = s.insertRow();
75
        pane(l, 0, "left").concat(pane(r, 1, "right")).forEach(function (c) { tr.appendChild(c); });
76
      };
77
      while (i < rows.length) {
78
        var r = rows[i];
79
        if (r.classList.contains("del") || r.classList.contains("add")) {
80
          var dels = [], adds = [];
81
          while (i < rows.length && rows[i].classList.contains("del")) dels.push(rows[i++]);
82
          while (i < rows.length && rows[i].classList.contains("add")) adds.push(rows[i++]);
83
          for (var k = 0; k < Math.max(dels.length, adds.length); k++) add(dels[k], adds[k]);
84
          continue;
85
        }
86
        if (r.classList.contains("gap") || r.querySelector("td.thread")) {
87
          var tr = s.insertRow(), td = tr.insertCell();
88
          tr.className = r.className; td.colSpan = 4; td.className = r.cells[r.cells.length - 1].className;
89
          td.innerHTML = r.cells[r.cells.length - 1].innerHTML;
90
        } else {
91
          add(r, r);
92
        }
93
        i++;
94
      }
95
      return s;
96
    };
97
    var cookieMode = (document.cookie.match(/(?:^|; )diff=(split|unified)/) || [])[1] || "unified";
98
    var apply = function (mode) {
99
      diffs.forEach(function (t) {
100
        if (mode === "split" && !t._split) { t._split = split(t); t.parentNode.insertBefore(t._split, t.nextSibling); }
101
        t.hidden = mode === "split";
102
        if (t._split) t._split.hidden = mode !== "split";
103
      });
104
      document.querySelectorAll("[data-diff-mode]").forEach(function (a) { a.classList.toggle("on", a.getAttribute("data-diff-mode") === mode); });
105
    };
106
    document.querySelectorAll("[data-diff-modes], [data-diff-collapse]").forEach(function (e) { e.hidden = false; });
107
    document.querySelectorAll("[data-diff-mode]").forEach(function (a) {
108
      a.addEventListener("click", function (ev) {
109
        ev.preventDefault();
110
        var mode = a.getAttribute("data-diff-mode");
111
        document.cookie = "diff=" + mode + "; path=/; max-age=31536000; samesite=lax";
112
        apply(mode);
113
      });
114
    });
115
    document.querySelectorAll("[data-diff-collapse]").forEach(function (a) {
116
      a.addEventListener("click", function (ev) {
117
        ev.preventDefault();
118
        var files = document.querySelectorAll("details.file-diff"), anyOpen = [].some.call(files, function (d) { return d.open; });
119
        files.forEach(function (d) { d.open = !anyOpen; });
120
        a.textContent = anyOpen ? "expand all" : "collapse all";
121
      });
122
    });
123
    apply(cookieMode);
124
  }
125
126
  /* Settings pages: mark the section in view in the list of sections. */
127
  var navLinks = document.querySelectorAll(".section-nav a[href^='#']");
128
  if (navLinks.length && "IntersectionObserver" in window) {
129
    var byId = {};
130
    navLinks.forEach(function (a) { byId[a.getAttribute("href").slice(1)] = a; });
131
    var io = new IntersectionObserver(function (entries) {
132
      entries.forEach(function (e) {
133
        if (e.isIntersecting && byId[e.target.id]) {
134
          navLinks.forEach(function (a) { a.classList.remove("on"); });
135
          byId[e.target.id].classList.add("on");
136
        }
137
      });
138
    }, { rootMargin: "0px 0px -70% 0px" });
139
    Object.keys(byId).forEach(function (id) { var el = document.getElementById(id); if (el) io.observe(el); });
140
  }
141
142
  /* Copy buttons. */
143
  document.querySelectorAll("[data-copy]").forEach(function (b) {
144
    b.addEventListener("click", function () {
145
      navigator.clipboard.writeText(b.getAttribute("data-copy")).then(function () {
146
        var t = b.textContent; b.textContent = "copied"; setTimeout(function () { b.textContent = t; }, 1200);
147
      });
148
    });
149
  });
150
151
  /* Tree rows: the last change per entry, fetched after the page paints. */
152
  var tree = document.querySelector("table.tree[data-last]");
153
  if (tree) {
154
    var paths = [].map.call(tree.querySelectorAll("tr[data-path]"), function (r) { return r.getAttribute("data-path"); });
155
    if (paths.length) {
156
      fetch(tree.getAttribute("data-last") + "?paths=" + encodeURIComponent(paths.join(",")), { credentials: "same-origin" })
157
        .then(function (r) { return r.json(); })
158
        .then(function (rows) {
159
          rows.forEach(function (c) {
160
            var tr = tree.querySelector('tr[data-path="' + CSS.escape(c.path) + '"]');
161
            if (!tr) return;
162
            var a = tr.querySelector("td.last a"), t = tr.querySelector("td.age");
163
            if (a) { a.textContent = c.title; a.href = tree.getAttribute("data-change") + c.hash; }
164
            if (t) { t.textContent = c.ago; t.title = c.when; }
165
          });
166
        }).catch(function () {});
167
    }
168
  }
169
170
  /* Line anchors: #L12 or #L12-L20 highlight a range and clicking a number selects. */
171
  function mark() {
172
    var m = location.hash.match(/^#L(\d+)(?:-L(\d+))?$/);
173
    document.querySelectorAll("tr.hl").forEach(function (r) { r.classList.remove("hl"); });
174
    if (!m) { showBar(0, 0); return; }
175
    var a = +m[1], b = +(m[2] || m[1]);
176
    for (var i = Math.min(a, b); i <= Math.max(a, b); i++) {
177
      var row = document.getElementById("L" + i);
178
      if (row) row.classList.add("hl");
179
    }
180
    var first = document.getElementById("L" + Math.min(a, b));
181
    if (first && !mark.done) { first.scrollIntoView({ block: "center" }); mark.done = true; }
182
    showBar(Math.min(a, b), Math.max(a, b));
183
  }
184
  /* The bar for a selected range: copy its text, or a permalink to it. */
185
  var bar = document.querySelector(".linebar");
186
  function showBar(a, b) {
187
    if (!bar) return;
188
    bar.hidden = !a;
189
    if (!a) return;
190
    bar.querySelector("[data-linebar-label]").textContent = a === b ? "Line " + a : "Lines " + a + "–" + b;
191
    bar.setAttribute("data-range", a + "-" + b);
192
  }
193
  function flash(btn) { var t = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = t; }, 1200); }
194
  if (bar) {
195
    bar.querySelector("[data-copy-lines]").addEventListener("click", function (ev) {
196
      var r = bar.getAttribute("data-range").split("-"), out = [];
197
      for (var i = +r[0]; i <= +r[1]; i++) { var row = document.getElementById("L" + i); if (row) out.push(row.querySelector("td.t").textContent); }
198
      navigator.clipboard.writeText(out.join("\n") + "\n").then(function () { flash(ev.target); });
199
    });
200
    var pl = bar.querySelector("[data-copy-permalink]");
201
    if (pl) pl.addEventListener("click", function () {
202
      navigator.clipboard.writeText(new URL(document.body.getAttribute("data-permalink"), location.href).href + location.hash).then(function () { flash(pl); });
203
    });
204
  }
205
  function permalink() { var p = document.body.getAttribute("data-permalink"); return p ? p + location.hash : null; }
206
  window.addEventListener("hashchange", mark);
207
  mark();
208
  document.querySelectorAll("table.code td.n").forEach(function (td) {
209
    td.addEventListener("click", function (e) {
210
      var n = td.parentNode.id;
211
      if (e.shiftKey && location.hash.match(/^#L(\d+)/)) location.hash = location.hash.replace(/^#(L\d+).*/, "$1-" + n);
212
      else location.hash = n;
213
    });
214
  });
215
216
  /* Flow run page: statuses and new log lines while the run is live. */
217
  var liveUrl = document.body.getAttribute("data-flow-live");
218
  if (liveUrl) {
219
    var tick = function () {
220
      var after = [].map.call(document.querySelectorAll("details[data-job]"), function (d) {
221
        return d.getAttribute("data-job") + ":" + d.querySelector("[data-log]").getAttribute("data-seq");
222
      }).join(",");
223
      fetch(liveUrl + "?after=" + after, { headers: { accept: "application/json" } })
224
        .then(function (r) { return r.json(); })
225
        .then(function (s) {
226
          s.jobs.forEach(function (j) {
227
            var cls = "st-" + j.status;
228
            var node = document.querySelector("[data-job-node='" + j.id + "']");
229
            if (node) {
230
              node.setAttribute("class", "node " + cls);
231
              node.querySelector("[data-glyph]").textContent = j.glyph;
232
              node.querySelector("[data-meta]").textContent = j.status + " · " + j.duration;
233
            }
234
            var d = document.querySelector("details[data-job='" + j.id + "']");
235
            if (!d) return;
236
            d.className = "panel flow-job " + cls;
237
            var g = d.querySelector("summary [data-glyph]");
238
            g.className = "flow-glyph " + cls; g.textContent = j.glyph;
239
            d.querySelector("summary [data-meta]").textContent = j.status + (j.exit_code !== null ? " · exit " + j.exit_code : "") + " · " + j.duration;
240
            if (j.status === "running" || j.status === "failed") d.open = true;
241
            var log = d.querySelector("[data-log]");
242
            var stick = log.scrollTop + log.clientHeight >= log.scrollHeight - 4;
243
            j.lines.forEach(function (l) {
244
              var ln = document.createElement("span"); ln.className = "ln s-" + l.stream;
245
              var t = document.createElement("span"); t.className = "t"; t.textContent = (l.t / 1000).toFixed(1) + "s";
246
              ln.appendChild(t); ln.appendChild(document.createTextNode(l.text));
247
              log.appendChild(ln); log.appendChild(document.createTextNode("\n"));
248
              log.setAttribute("data-seq", l.seq);
249
            });
250
            if (stick && j.lines.length) log.scrollTop = log.scrollHeight;
251
          });
252
          var st = document.querySelector("[data-run-status]"); if (st) st.textContent = s.status;
253
          if (s.live) setTimeout(tick, 1500); else setTimeout(function () { location.reload(); }, 800);
254
        })
255
        .catch(function () { setTimeout(tick, 5000); });
256
    };
257
    setTimeout(tick, 1500);
258
  }
259
260
  /* Keyboard: / focuses search; g c / g p / g i jump; ? shows the sheet. */
261
  var pending = null;
262
  document.addEventListener("keydown", function (e) {
263
    if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
264
    if (e.key === "/") { var s = document.querySelector("input[name=q]"); if (s) { e.preventDefault(); s.focus(); } return; }
265
    if (e.key === "y" && !e.ctrlKey && !e.metaKey && !e.altKey && permalink()) { history.replaceState(null, "", permalink()); pending = null; return; }
266
    if (e.key === "?") { var k = document.getElementById("keys"); if (k) k.hidden = !k.hidden; return; }
267
    var repo = document.body.getAttribute("data-repo");
268
    if (pending === "g" && repo) {
269
      var to = { c: "", p: "/proposals", i: "/issues", h: "/changes/" + (document.body.getAttribute("data-ref") || "main"), b: "/bookmarks", t: "/trust" }[e.key];
270
      if (to !== undefined) location.href = repo + to;
271
      pending = null; return;
272
    }
273
    pending = e.key === "g" ? "g" : null;
274
  });
275
276
  // Live conflict resolution (hub spec §6.10): who else is resolving this
277
  // conflict, their choices region by region (the last one wins), and the
278
  // resolution when someone commits it.
279
  var live = document.querySelector("[data-resolve-live]");
280
  if (live && window.EventSource && window.fetch) {
281
    var url = live.getAttribute("data-resolve-live"), csrf = live.getAttribute("data-csrf");
282
    var client = Math.random().toString(36).slice(2) + Date.now().toString(36);
283
    var present = document.querySelector("[data-live-present]"), landed = document.querySelector("[data-live-landed]");
284
    var applying = false;
285
    var fieldsOf = function (region) {
286
      return region === "keep"
287
        ? { radios: live.querySelectorAll('input[name="keep"]'), text: null }
288
        : { radios: live.querySelectorAll('input[name="r' + region + '"]'), text: live.querySelector('textarea[name="t' + region + '"]') };
289
    };
290
    var apply = function (region, c) {
291
      var f = fieldsOf(region);
292
      applying = true;
293
      f.radios.forEach(function (r) { r.checked = r.value === c.choice; });
294
      if (f.text && c.choice === "text") f.text.value = c.text;
295
      applying = false;
296
      var sec = f.radios.length ? f.radios[0].closest("section") : null;
297
      if (sec) sec.setAttribute("title", "chosen by " + c.by);
298
    };
299
    var send = function (region, choice, text) {
300
      fetch(url, { method: "POST", headers: { "content-type": "application/json" }, credentials: "same-origin",
301
        body: JSON.stringify({ _csrf: csrf, client: client, region: region, choice: choice, text: text || "" }) });
302
    };
303
    var timers = {};
304
    live.addEventListener("change", function (ev) {
305
      var t = ev.target;
306
      if (applying || !t.name || t.type !== "radio") return;
307
      var region = t.name === "keep" ? "keep" : t.name.slice(1);
308
      var ta = live.querySelector('textarea[name="t' + region + '"]');
309
      send(region, t.value, t.value === "text" && ta ? ta.value : "");
310
    });
311
    live.addEventListener("input", function (ev) {
312
      var t = ev.target;
313
      if (applying || t.tagName !== "TEXTAREA") return;
314
      var region = t.name.slice(1);
315
      live.querySelectorAll('input[name="r' + region + '"]').forEach(function (r) { r.checked = r.value === "text"; });
316
      clearTimeout(timers[region]);
317
      timers[region] = setTimeout(function () { send(region, "text", t.value); }, 400);
318
    });
319
    var es = new EventSource(url + "?client=" + encodeURIComponent(client));
320
    var showPresent = function (names) {
321
      present.hidden = names.length < 2;
322
      present.textContent = "Resolving together: " + names.join(", ") + ". Choices are shared as you make them; the last choice for a conflict wins.";
323
    };
324
    es.addEventListener("snapshot", function (e) {
325
      var d = JSON.parse(e.data);
326
      showPresent(d.present);
327
      Object.keys(d.choices).forEach(function (r) { apply(r, d.choices[r]); });
328
      if (d.keep) apply("keep", d.keep);
329
    });
330
    es.addEventListener("presence", function (e) { showPresent(JSON.parse(e.data).present); });
331
    es.addEventListener("choice", function (e) {
332
      var d = JSON.parse(e.data);
333
      if (d.client !== client) apply(d.region, d.choice);
334
    });
335
    es.addEventListener("landed", function (e) {
336
      var d = JSON.parse(e.data);
337
      es.close();
338
      landed.hidden = false;
339
      landed.textContent = "";
340
      landed.appendChild(document.createTextNode(d.by + " committed the resolution: "));
341
      var a = document.createElement("a");
342
      a.href = d.url;
343
      a.textContent = "see the change";
344
      landed.appendChild(a);
345
      live.querySelectorAll("input, textarea, button").forEach(function (x) { x.disabled = true; });
346
    });
347
  }
348
349
  // Swimlanes of a proposal's bookmark (hub spec §6.7): drag a change onto
350
  // another to plan a squash or a rebase. The hub only writes the plan;
351
  // the author's client carries it out and signs it.
352
  var plan = document.querySelector("[data-plan]");
353
  if (plan && window.fetch) {
354
    var panel = document.querySelector("[data-plan-panel]"), from = null, moved = false;
355
    plan.querySelectorAll("[data-change]").forEach(function (a) {
356
      a.style.cursor = "grab";
357
      a.addEventListener("pointerdown", function (ev) { from = a.getAttribute("data-change"); moved = false; ev.preventDefault(); });
358
      a.addEventListener("pointermove", function () { if (from) moved = true; });
359
      a.addEventListener("click", function (ev) { if (moved) ev.preventDefault(); });
360
    });
361
    document.addEventListener("pointerup", function (ev) {
362
      var start = from;
363
      from = null;
364
      if (!start) return;
365
      var el = document.elementFromPoint(ev.clientX, ev.clientY), onto = el && el.closest ? el.closest("[data-change]") : null;
366
      if (!onto || onto.getAttribute("data-change") === start) return;
367
      var body = new URLSearchParams({ _csrf: plan.getAttribute("data-csrf"), change: start, target: onto.getAttribute("data-change") });
368
      fetch(plan.getAttribute("data-plan"), { method: "POST", credentials: "same-origin", body: body })
369
        .then(function (r) { return r.json(); })
370
        .then(function (d) {
371
          panel.hidden = false;
372
          panel.textContent = "";
373
          if (d.error) { panel.textContent = d.error; return; }
374
          d.options.forEach(function (o) {
375
            var p = document.createElement("p");
376
            p.appendChild(document.createTextNode(o.summary + " "));
377
            var a = document.createElement("a");
378
            a.href = o.vscode;
379
            a.textContent = "Apply in VS Code";
380
            p.appendChild(a);
381
            var code = document.createElement("pre");
382
            code.className = "snippet";
383
            code.textContent = o.command;
384
            panel.appendChild(p);
385
            panel.appendChild(code);
386
          });
387
        });
388
    });
389
  }
390
391
  /* Sidebar menus (oxygenhub#82): type to filter a menu's items; a click
392
     outside an open menu closes it. */
393
  document.querySelectorAll("[data-menu-filter]").forEach(function (input) {
394
    var items = input.closest(".menu-pop").querySelectorAll(".menu-item");
395
    input.addEventListener("input", function () {
396
      var q = input.value.trim().toLowerCase();
397
      items.forEach(function (it) { it.hidden = q !== "" && it.textContent.toLowerCase().indexOf(q) < 0; });
398
    });
399
  });
400
  document.querySelectorAll("details.menu").forEach(function (d) {
401
    d.addEventListener("toggle", function () {
402
      var f = d.querySelector("[data-menu-filter]");
403
      if (d.open && f) f.focus();
404
    });
405
  });
406
  document.addEventListener("click", function (ev) {
407
    document.querySelectorAll("details.menu[open]").forEach(function (d) { if (!d.contains(ev.target)) d.open = false; });
408
  });
409
410
  /* ··· menus: copy a link to a part of the page, or some text. */
411
  function copied(btn, text) {
412
    if (navigator.clipboard) navigator.clipboard.writeText(text).then(function () {
413
      var was = btn.textContent; btn.textContent = "Copied"; setTimeout(function () { btn.textContent = was; }, 1200);
414
    });
415
    var d = btn.closest("details"); if (d) d.open = false;
416
  }
417
  document.querySelectorAll("[data-copy-link]").forEach(function (b) {
418
    b.addEventListener("click", function () { copied(b, location.origin + location.pathname + location.search + b.getAttribute("data-copy-link")); });
419
  });
420
  document.querySelectorAll("[data-copy-text]").forEach(function (b) {
421
    b.addEventListener("click", function () { copied(b, b.getAttribute("data-copy-text")); });
422
  });
423
  document.querySelectorAll('.menu-actions a[href^="#"]').forEach(function (a) {
424
    a.addEventListener("click", function () { var d = a.closest("details"); if (d) d.open = false; });
425
  });
426
427
  /* Settings pages: a form posts and the page comes back from the top.
428
     Remember which section's form was sent, and on the same page's next
429
     load scroll back to it, bringing the notice along. */
430
  document.addEventListener("submit", function (e) {
431
    var sec = e.target.closest ? e.target.closest("section[id], form[id], details[id], div[id]") : null;
432
    if (!sec || !sec.id) return;
433
    try { sessionStorage.setItem("oxy-return", location.pathname + "#" + sec.id); } catch (err) { /* private mode */ }
434
  });
435
  try {
436
    var back = sessionStorage.getItem("oxy-return");
437
    if (back && back.indexOf(location.pathname + "#") === 0) {
438
      sessionStorage.removeItem("oxy-return");
439
      var target = document.getElementById(back.split("#")[1]);
440
      if (target) {
441
        var flash = document.querySelector("main > .note, main > .error, .sections > .note, .sections > .error");
442
        if (flash && !target.contains(flash)) target.parentNode.insertBefore(flash, target);
443
        target.scrollIntoView({ block: "start" });
444
      }
445
    }
446
  } catch (err) { /* no storage: nothing to restore */ }
447
448
  /* AI assistants (#109): a provider fills the name, endpoint and API;
449
     List models asks the endpoint what it offers and fills the model
450
     field's suggestions. Without this script, both are typed by hand. */
451
  document.querySelectorAll("[data-preset-select]").forEach(function (sel) {
452
    var form = sel.form;
453
    function apply() {
454
      var o = sel.options[sel.selectedIndex];
455
      if (!o || !o.value) return;
456
      var name = form.querySelector("[name=name]"), ep = form.querySelector("[name=endpoint]"), api = form.querySelector("[name=api]");
457
      name.placeholder = o.dataset.name; ep.value = o.dataset.endpoint; api.value = o.dataset.api;
458
    }
459
    sel.addEventListener("change", apply);
460
    apply();
461
  });
462
  document.querySelectorAll("[data-list-models]").forEach(function (b) {
463
    b.addEventListener("click", function () {
464
      var form = b.form, input = document.getElementById(b.dataset.for);
465
      var list = document.getElementById(b.dataset.for + "-list"), note = document.querySelector("[data-models-note='" + b.dataset.for + "']");
466
      var body = new URLSearchParams();
467
      body.set("_csrf", b.dataset.csrf);
468
      ["endpoint", "api", "token", "preset"].forEach(function (k) { var el = form.querySelector("[name=" + k + "]"); if (el) body.set(k, el.value); });
469
      if (b.dataset.id) body.set("id", b.dataset.id);
470
      if (note) note.textContent = "Asking the provider…";
471
      fetch(b.dataset.listModels, { method: "POST", body: body, credentials: "same-origin" })
472
        .then(function (r) { return r.json(); })
473
        .then(function (j) {
474
          if (j.models) {
475
            list.innerHTML = "";
476
            j.models.forEach(function (m) { var o = document.createElement("option"); o.value = m; list.appendChild(o); });
477
            if (note) note.textContent = j.models.length + " models: pick one in the Model field.";
478
            input.focus();
479
          } else if (note) {
480
            note.textContent = "No list: " + (j.error || "unknown") + ". Type the model's name.";
481
          }
482
        })
483
        .catch(function () { if (note) note.textContent = "The hub could not ask; type the model's name."; });
484
    });
485
  });
486
})();