File size: 6,113 Bytes
dfb775d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"use strict";
// Standalone Ollama Modelfile builder. Fetches the PARAMETER catalogue, renders a
// toggle + input per parameter, and POSTs the assembled spec to /coach/api/modelfile/build.

const $ = (s) => document.querySelector(s);

async function getJSON(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`${url} → ${r.status}`);
  return r.json();
}
async function postJSON(url, body) {
  const r = await fetch(url, {
    method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
  });
  if (!r.ok) throw new Error(`${url} → ${r.status}`);
  return r.json();
}

// Toggleable text/area instructions: SYSTEM, TEMPLATE, ADAPTER, LICENSE, REQUIRES.
const INSTRUCTIONS = [
  { key: "system", label: "SYSTEM", area: true, ph: "You are Codephreak, augmentic intelligence orchestrator." },
  { key: "template", label: "TEMPLATE", area: true, ph: "{{ .System }}\n{{ .Prompt }}" },
  { key: "adapter", label: "ADAPTER", area: false, ph: "./out/runs/<run>/checkpoint" },
  { key: "license", label: "LICENSE", area: true, ph: "Apache-2.0" },
  { key: "requires", label: "REQUIRES", area: false, ph: "0.5.0" },
];

function renderInstructions(prefill) {
  const host = $("#mf-instructions");
  for (const ins of INSTRUCTIONS) {
    const wrap = document.createElement("div");
    wrap.className = "mf-row";
    const pre = (prefill[ins.key] || "");
    const field = ins.area
      ? `<textarea id="mf-${ins.key}" rows="2" placeholder="${ins.ph}">${pre}</textarea>`
      : `<input id="mf-${ins.key}" type="text" placeholder="${ins.ph}" value="${pre}">`;
    wrap.innerHTML =
      `<label><input type="checkbox" class="mf-ins-toggle" data-key="${ins.key}" ${pre ? "checked" : ""}> ${ins.label}</label>` +
      `<div class="mf-toggle-field" id="mf-field-${ins.key}" ${pre ? "" : "hidden"}>${field}</div>`;
    host.appendChild(wrap);
  }
  host.querySelectorAll(".mf-ins-toggle").forEach((cb) => {
    cb.addEventListener("change", () => {
      $(`#mf-field-${cb.dataset.key}`).hidden = !cb.checked;
    });
  });
}

async function renderParams() {
  const host = $("#mf-params");
  let params = [];
  try { params = (await getJSON("/coach/api/modelfile/params")).parameters; }
  catch (e) { host.textContent = "could not load parameters"; return; }
  for (const p of params) {
    const div = document.createElement("div");
    div.className = "mf-param";
    const inputType = (p.type === "int" || p.type === "float") ? "number" : "text";
    const step = p.type === "float" ? "0.05" : "1";
    div.innerHTML =
      `<input type="checkbox" class="mf-param-toggle" data-name="${p.name}" data-type="${p.type}">` +
      `<code>${p.name}</code>` +
      `<input class="mf-param-val" data-name="${p.name}" type="${inputType}" step="${step}" ` +
      `value="${p.default ?? ""}" title="${p.description}">` +
      `<span class="mf-desc">${p.description}</span>`;
    host.appendChild(div);
  }
}

function buildSpec() {
  const spec = { from_model: $("#mf-from").value.trim(), parameters: {} };
  // Toggled instructions.
  for (const ins of INSTRUCTIONS) {
    const cb = document.querySelector(`.mf-ins-toggle[data-key="${ins.key}"]`);
    if (cb && cb.checked) {
      const v = $(`#mf-${ins.key}`).value.trim();
      if (v) spec[ins.key] = v;
    }
  }
  // Toggled parameters.
  document.querySelectorAll(".mf-param-toggle").forEach((cb) => {
    if (!cb.checked) return;
    const name = cb.dataset.name;
    const raw = document.querySelector(`.mf-param-val[data-name="${name}"]`).value.trim();
    if (raw === "") return;
    spec.parameters[name] = (cb.dataset.type === "int") ? parseInt(raw, 10)
      : (cb.dataset.type === "float") ? parseFloat(raw) : raw;
  });
  // Stop sequences.
  const stop = $("#mf-stop").value.split("\n").map((s) => s.trim()).filter(Boolean);
  if (stop.length) spec.stop = stop;
  // Messages: "role: content".
  const messages = [];
  for (const line of $("#mf-messages").value.split("\n")) {
    const i = line.indexOf(":");
    if (i < 0) continue;
    const role = line.slice(0, i).trim().toLowerCase();
    const content = line.slice(i + 1).trim();
    if (["system", "user", "assistant"].includes(role) && content) messages.push({ role, content });
  }
  if (messages.length) spec.messages = messages;
  return spec;
}

async function buildModelfile() {
  const status = $("#mf-status");
  const spec = buildSpec();
  if (!spec.from_model) { status.textContent = "FROM is required"; return; }
  status.textContent = "building…";
  try {
    const body = await postJSON("/coach/api/modelfile/build", spec);
    $("#mf-output").hidden = false;
    $("#mf-output").textContent = body.modelfile;
    status.textContent = "built ✓";
  } catch (e) { status.textContent = `build failed: ${e}`; }
}

async function createModel() {
  const s = $("#mf-create-status");
  const tag = $("#mf-tag").value.trim();
  if (!tag) { s.textContent = "tag required"; return; }
  const spec = buildSpec();
  if (!spec.from_model) { s.textContent = "FROM is required"; return; }
  s.textContent = `running ollama create ${tag}…`;
  try {
    const res = await postJSON("/coach/api/modelfile/create", { tag, spec });
    s.textContent = `${res.status}: ${(res.output || "").slice(-200)}`;
  } catch (e) { s.textContent = `create failed: ${e}`; }
}

function prefillFromQuery() {
  const q = new URLSearchParams(location.search);
  const out = {};
  for (const k of ["system", "template", "adapter", "license", "requires"]) {
    if (q.get(k)) out[k] = q.get(k);
  }
  if (q.get("from")) $("#mf-from").value = q.get("from");
  if (q.get("tag")) $("#mf-tag").value = q.get("tag");
  return out;
}

window.addEventListener("DOMContentLoaded", async () => {
  const prefill = prefillFromQuery();
  renderInstructions(prefill);
  await renderParams();
  $("#mf-build").addEventListener("click", buildModelfile);
  $("#mf-create").addEventListener("click", createModel);
  $("#mf-copy").addEventListener("click", () => {
    const t = $("#mf-output").textContent;
    if (t) navigator.clipboard && navigator.clipboard.writeText(t);
  });
});