396 lines
14 KiB
JavaScript
396 lines
14 KiB
JavaScript
function getConfig() {
|
|
return {
|
|
apiKey: document.getElementById("apiKey").value.trim(),
|
|
releaseName: document.getElementById("releaseName").value.trim(),
|
|
};
|
|
}
|
|
|
|
function saveConfig() {
|
|
const cfg = getConfig();
|
|
cfg.chatSessionId = document.getElementById("chatSessionId").value.trim();
|
|
localStorage.setItem("assistant_ui_cfg", JSON.stringify(cfg));
|
|
}
|
|
|
|
function loadConfig() {
|
|
try {
|
|
const raw = localStorage.getItem("assistant_ui_cfg");
|
|
if (!raw) return;
|
|
const cfg = JSON.parse(raw);
|
|
document.getElementById("apiKey").value = cfg.apiKey || "";
|
|
document.getElementById("releaseName").value = cfg.releaseName || "";
|
|
document.getElementById("chatSessionId").value = cfg.chatSessionId || "main";
|
|
} catch (_) {}
|
|
}
|
|
|
|
async function apiGet(path, params) {
|
|
const cfg = getConfig();
|
|
const url = new URL(path, window.location.origin);
|
|
Object.entries(params || {}).forEach(([k, v]) => {
|
|
if (v !== null && v !== undefined && String(v).length > 0) url.searchParams.set(k, String(v));
|
|
});
|
|
const r = await fetch(url, {
|
|
headers: { "X-Admin-Api-Key": cfg.apiKey },
|
|
});
|
|
if (!r.ok) throw new Error(await r.text());
|
|
return r.json();
|
|
}
|
|
|
|
async function apiPost(path, payload) {
|
|
const cfg = getConfig();
|
|
const r = await fetch(path, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Admin-Api-Key": cfg.apiKey,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (!r.ok) throw new Error(await r.text());
|
|
return r.json();
|
|
}
|
|
|
|
async function loadMeta() {
|
|
const status = document.getElementById("metaStatus");
|
|
status.textContent = "backend: checking...";
|
|
try {
|
|
const data = await apiGet("/meta", {});
|
|
status.textContent = `backend: ${data.version} @ ${new Date(data.started_at_utc).toLocaleTimeString()}`;
|
|
} catch (e) {
|
|
status.textContent = "backend: unreachable/old";
|
|
}
|
|
}
|
|
|
|
function renderRows(target, rows, formatter) {
|
|
target.innerHTML = "";
|
|
if (!rows || rows.length === 0) {
|
|
target.innerHTML = '<div class="row">No rows.</div>';
|
|
return;
|
|
}
|
|
rows.forEach((row) => {
|
|
const el = document.createElement("div");
|
|
el.className = "row";
|
|
el.innerHTML = formatter(row);
|
|
target.appendChild(el);
|
|
});
|
|
}
|
|
|
|
async function loadInbox() {
|
|
const cfg = getConfig();
|
|
const q = document.getElementById("inboxQuery").value.trim();
|
|
const out = document.getElementById("inboxResults");
|
|
out.innerHTML = '<div class="row">Loading...</div>';
|
|
try {
|
|
const data = await apiGet("/assistant/inbox", { release_name: cfg.releaseName, q, limit: 20 });
|
|
renderRows(out, data.rows || [], (r) => {
|
|
const text = (r.text || r.summary || r.description || "").slice(0, 280);
|
|
return `
|
|
<div><strong>${r.display_name || r.concept_id || "message"}</strong></div>
|
|
<div>${text || "(no text)"}</div>
|
|
<div class="meta">${r.source_pk || ""} | ${r.release_name || ""}</div>
|
|
`;
|
|
});
|
|
} catch (e) {
|
|
out.innerHTML = `<div class="row">Error: ${String(e)}</div>`;
|
|
}
|
|
}
|
|
|
|
async function loadTasks() {
|
|
const cfg = getConfig();
|
|
const onlyPending = document.getElementById("onlyPending").checked;
|
|
const out = document.getElementById("taskResults");
|
|
out.innerHTML = '<div class="row">Loading...</div>';
|
|
try {
|
|
const data = await apiGet("/assistant/tasks", {
|
|
release_name: cfg.releaseName,
|
|
only_pending: onlyPending,
|
|
limit: 30,
|
|
});
|
|
renderRows(out, data.rows || [], (r) => {
|
|
const safeTodo = (r.todo || "").replace(/"/g, """);
|
|
return `
|
|
<div><strong>${r.todo || "(empty task)"}</strong></div>
|
|
<div class="meta">status=${r.status} | due=${r.due_hint || "-"} | who=${r.who || "-"}</div>
|
|
<div class="meta">source=${r.source_pk || ""} | release=${r.release_name || ""}</div>
|
|
<div style="margin-top:6px"><button data-goal="${safeTodo}" class="use-goal">Use as goal</button></div>
|
|
`;
|
|
});
|
|
document.querySelectorAll(".use-goal").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const goal = btn.getAttribute("data-goal") || "";
|
|
document.getElementById("goalText").value = goal;
|
|
});
|
|
});
|
|
} catch (e) {
|
|
out.innerHTML = `<div class="row">Error: ${String(e)}</div>`;
|
|
}
|
|
}
|
|
|
|
async function makeDraft() {
|
|
const cfg = getConfig();
|
|
const goal = document.getElementById("goalText").value.trim();
|
|
const recipient = document.getElementById("recipient").value.trim();
|
|
const out = document.getElementById("draftOutput");
|
|
if (!goal) {
|
|
out.textContent = "Provide goal text first.";
|
|
return;
|
|
}
|
|
out.textContent = "Generating...";
|
|
try {
|
|
const data = await apiPost("/assistant/draft", {
|
|
task_type: "message",
|
|
goal,
|
|
recipient: recipient || null,
|
|
tone: "friendly-professional",
|
|
constraints: ["keep it concise"],
|
|
release_name: cfg.releaseName || null,
|
|
max_sources: 5,
|
|
});
|
|
const sourceLine = (data.sources || []).map((s) => s.concept_id).filter(Boolean).slice(0, 5).join(", ");
|
|
out.textContent = `${data.draft || ""}\n\nconfidence=${data.confidence}\nneeds_review=${data.needs_review}\nsources=${sourceLine}`;
|
|
} catch (e) {
|
|
out.textContent = `Error: ${String(e)}`;
|
|
}
|
|
}
|
|
|
|
async function saveLearn() {
|
|
const cfg = getConfig();
|
|
const title = document.getElementById("learnTitle").value.trim();
|
|
const tags = document.getElementById("learnTags").value
|
|
.split(",")
|
|
.map((x) => x.trim())
|
|
.filter(Boolean);
|
|
const text = document.getElementById("learnText").value.trim();
|
|
const out = document.getElementById("learnOutput");
|
|
if (!text) {
|
|
out.textContent = "Provide note text first.";
|
|
return;
|
|
}
|
|
out.textContent = "Saving...";
|
|
try {
|
|
const data = await apiPost("/assistant/learn", {
|
|
text,
|
|
title: title || null,
|
|
tags,
|
|
release_name: cfg.releaseName || null,
|
|
});
|
|
out.textContent = `saved=${data.stored}\nconcept_id=${data.concept_id}\ntitle=${data.title}`;
|
|
document.getElementById("learnText").value = "";
|
|
} catch (e) {
|
|
out.textContent = `Error: ${String(e)}`;
|
|
}
|
|
}
|
|
|
|
async function saveLearnAndWriteCode() {
|
|
const cfg = getConfig();
|
|
const title = document.getElementById("learnTitle").value.trim();
|
|
const tags = document.getElementById("learnTags").value
|
|
.split(",")
|
|
.map((x) => x.trim())
|
|
.filter(Boolean);
|
|
const text = document.getElementById("learnText").value.trim();
|
|
const codeObjective = document.getElementById("learnCodeObjective").value.trim();
|
|
const codeFiles = document.getElementById("learnCodeFiles").value
|
|
.split(",")
|
|
.map((x) => x.trim())
|
|
.filter(Boolean);
|
|
const codeDryRun = document.getElementById("learnCodeDryRun").checked;
|
|
const codeCommit = document.getElementById("learnCodeCommit").checked;
|
|
const out = document.getElementById("learnOutput");
|
|
if (!text) {
|
|
out.textContent = "Provide note text first.";
|
|
return;
|
|
}
|
|
if (codeFiles.length === 0) {
|
|
out.textContent = "Provide at least one code file path.";
|
|
return;
|
|
}
|
|
out.textContent = "Saving note and writing code...";
|
|
try {
|
|
const data = await apiPost("/assistant/learn", {
|
|
text,
|
|
title: title || null,
|
|
tags,
|
|
release_name: cfg.releaseName || null,
|
|
write_code: true,
|
|
code_objective: codeObjective || null,
|
|
code_files: codeFiles,
|
|
code_dry_run: codeDryRun,
|
|
code_commit: codeCommit,
|
|
});
|
|
const cw = data.code_write || {};
|
|
out.textContent =
|
|
`saved=${data.stored}\nconcept_id=${data.concept_id}\ntitle=${data.title}\n\n` +
|
|
`write_code.attempted=${cw.attempted}\n` +
|
|
`dry_run=${cw.dry_run}\n` +
|
|
`branch=${cw.branch_name || "-"}\n` +
|
|
`changed_files=${(cw.changed_files || []).join(", ") || "-"}\n` +
|
|
`skipped_files=${(cw.skipped_files || []).map((x) => `${x.file}:${x.reason}`).join(" | ") || "-"}\n` +
|
|
`commit=${cw.commit || "-"}`;
|
|
} catch (e) {
|
|
out.textContent = `Error: ${String(e)}`;
|
|
}
|
|
}
|
|
|
|
function appendChat(role, text, meta) {
|
|
const target = document.getElementById("chatTranscript");
|
|
const el = document.createElement("div");
|
|
el.className = "row";
|
|
el.innerHTML = `
|
|
<div><strong>${role}</strong></div>
|
|
<div>${(text || "").replace(/\n/g, "<br/>")}</div>
|
|
${meta ? `<div class="meta">${meta}</div>` : ""}
|
|
`;
|
|
target.prepend(el);
|
|
}
|
|
|
|
async function sendChat() {
|
|
const cfg = getConfig();
|
|
const sessionInput = document.getElementById("chatSessionId");
|
|
const session_id = (sessionInput.value || "main").trim();
|
|
sessionInput.value = session_id;
|
|
const messageEl = document.getElementById("chatMessage");
|
|
const message = messageEl.value.trim();
|
|
if (!message) return;
|
|
appendChat("user", message, `session=${session_id}`);
|
|
messageEl.value = "";
|
|
try {
|
|
const data = await apiPost("/assistant/chat", {
|
|
session_id,
|
|
message,
|
|
release_name: cfg.releaseName || null,
|
|
max_sources: 6,
|
|
});
|
|
const sourceLine = (data.sources || []).map((s) => s.concept_id).filter(Boolean).slice(0, 4).join(", ");
|
|
appendChat("assistant", data.answer || "", `confidence=${data.confidence} | sources=${sourceLine || "-"}`);
|
|
} catch (e) {
|
|
appendChat("assistant", `Error: ${String(e)}`, "");
|
|
}
|
|
}
|
|
|
|
async function runSelfImprove() {
|
|
const cfg = getConfig();
|
|
const objective = document.getElementById("improveObjective").value.trim() || "Improve assistant quality and reliability";
|
|
const maxRaw = Number(document.getElementById("improveMax").value || 5);
|
|
const maxProposals = Math.max(1, Math.min(20, Number.isFinite(maxRaw) ? maxRaw : 5));
|
|
const summary = document.getElementById("improveSummary");
|
|
const list = document.getElementById("improveProposals");
|
|
summary.textContent = "Generating proposals...";
|
|
list.innerHTML = "";
|
|
try {
|
|
const data = await apiPost("/assistant/self-improve", {
|
|
objective,
|
|
release_name: cfg.releaseName || null,
|
|
max_proposals: maxProposals,
|
|
feedback_limit: 50,
|
|
action_limit: 50,
|
|
include_edited_feedback: true,
|
|
include_rejected_feedback: true,
|
|
include_blocked_actions: true,
|
|
apply: false,
|
|
});
|
|
summary.textContent =
|
|
`${data.summary || ""}\n\nproposal_set_id=${data.proposal_set_id}\n` +
|
|
`signals: feedback=${data.signals?.feedback_rows ?? 0}, blocked_actions=${data.signals?.blocked_action_rows ?? 0}`;
|
|
renderProposalSets(
|
|
[{ proposal_set_id: data.proposal_set_id, created_at_utc: data.created_at_utc, objective, release_name: cfg.releaseName || "", summary: data.summary, signals: data.signals || {}, proposals: data.proposals || [] }],
|
|
list,
|
|
summary,
|
|
cfg.releaseName || null
|
|
);
|
|
} catch (e) {
|
|
summary.textContent = `Error: ${String(e)}`;
|
|
}
|
|
}
|
|
|
|
function renderProposalSets(sets, listEl, summaryEl, releaseName) {
|
|
listEl.innerHTML = "";
|
|
if (!sets || sets.length === 0) {
|
|
listEl.innerHTML = '<div class="row">No proposal sets.</div>';
|
|
return;
|
|
}
|
|
sets.forEach((setObj) => {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "row";
|
|
const proposals = setObj.proposals || [];
|
|
const proposalsHtml = proposals.map((p) => `
|
|
<div class="row" style="margin-top:8px">
|
|
<div><strong>${p.proposal_id}: ${p.title}</strong></div>
|
|
<div>${p.problem || ""}</div>
|
|
<div class="meta">risk=${p.risk} | auto_apply_safe=${p.auto_apply_safe}</div>
|
|
<div class="meta">files=${(p.files || []).join(", ") || "-"}</div>
|
|
<div class="meta">tests=${(p.tests || []).join(" | ") || "-"}</div>
|
|
<div style="margin-top:6px">${p.change || ""}</div>
|
|
<div style="margin-top:6px">
|
|
<button class="apply-proposal" data-proposal-set-id="${setObj.proposal_set_id}" data-proposal-id="${p.proposal_id}">Apply as branch</button>
|
|
</div>
|
|
</div>
|
|
`).join("");
|
|
wrap.innerHTML = `
|
|
<div><strong>Set ${setObj.proposal_set_id}</strong></div>
|
|
<div class="meta">${setObj.created_at_utc || ""} | release=${setObj.release_name || ""}</div>
|
|
<div>${setObj.summary || ""}</div>
|
|
${proposalsHtml}
|
|
`;
|
|
listEl.appendChild(wrap);
|
|
});
|
|
|
|
document.querySelectorAll(".apply-proposal").forEach((btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const proposalSetId = btn.getAttribute("data-proposal-set-id");
|
|
const proposalId = btn.getAttribute("data-proposal-id");
|
|
if (!proposalSetId || !proposalId) return;
|
|
const dryRun = document.getElementById("improveDryRun").checked;
|
|
summaryEl.textContent = `Applying ${proposalId} from set ${proposalSetId}...`;
|
|
try {
|
|
const applied = await apiPost("/assistant/self-improve/apply", {
|
|
objective: document.getElementById("improveObjective").value.trim() || null,
|
|
release_name: releaseName || null,
|
|
proposal_set_id: proposalSetId,
|
|
proposal_id: proposalId,
|
|
dry_run: dryRun,
|
|
});
|
|
summaryEl.textContent =
|
|
`apply result: applied=${applied.applied} dry_run=${applied.dry_run}\n` +
|
|
`branch=${applied.branch_name}\n` +
|
|
`proposal_file=${applied.proposal_file}\n` +
|
|
`commit=${applied.commit || "-"}\n` +
|
|
`${applied.detail || ""}`;
|
|
} catch (e) {
|
|
summaryEl.textContent = `Apply error: ${String(e)}`;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadImproveHistory() {
|
|
const cfg = getConfig();
|
|
const summary = document.getElementById("improveSummary");
|
|
const list = document.getElementById("improveProposals");
|
|
summary.textContent = "Loading proposal history...";
|
|
try {
|
|
const data = await apiGet("/assistant/self-improve/history", {
|
|
release_name: cfg.releaseName || null,
|
|
limit: 200,
|
|
});
|
|
summary.textContent = `history sets=${data.count || 0}`;
|
|
renderProposalSets(data.rows || [], list, summary, cfg.releaseName || null);
|
|
} catch (e) {
|
|
summary.textContent = `Error: ${String(e)}`;
|
|
}
|
|
}
|
|
|
|
document.getElementById("saveConfig").addEventListener("click", saveConfig);
|
|
document.getElementById("refreshMeta").addEventListener("click", loadMeta);
|
|
document.getElementById("loadInbox").addEventListener("click", loadInbox);
|
|
document.getElementById("loadTasks").addEventListener("click", loadTasks);
|
|
document.getElementById("makeDraft").addEventListener("click", makeDraft);
|
|
document.getElementById("saveLearn").addEventListener("click", saveLearn);
|
|
document.getElementById("saveLearnWrite").addEventListener("click", saveLearnAndWriteCode);
|
|
document.getElementById("sendChat").addEventListener("click", sendChat);
|
|
document.getElementById("runImprove").addEventListener("click", runSelfImprove);
|
|
document.getElementById("loadImproveHistory").addEventListener("click", loadImproveHistory);
|
|
|
|
loadConfig();
|
|
loadMeta();
|
|
loadImproveHistory();
|