@enigmax/dashboard 0.1.63 → 0.1.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/index.html +154 -7
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -321,6 +321,12 @@
|
|
|
321
321
|
/* Per-memory delete in the Recall list: kept to the right and revealed on row hover/focus. */
|
|
322
322
|
.recall-del { margin-left: auto; opacity: 0; transition: opacity .12s; }
|
|
323
323
|
.set-row:hover .recall-del, .recall-del:focus-visible { opacity: 1; }
|
|
324
|
+
/* Recall multi-select: a checkbox per row, a highlight for selected rows, and a bulk-action
|
|
325
|
+
bar that appears only while something is selected. Ctrl/Cmd+click toggles, Shift+click ranges. */
|
|
326
|
+
.rc-check { flex: 0 0 auto; width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); margin: 0; }
|
|
327
|
+
.set-row.sel { background: rgba(224, 164, 88, .10); }
|
|
328
|
+
.rc-bulk { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
329
|
+
.rc-bulk .rc-seln { font-size: 12px; color: var(--muted); }
|
|
324
330
|
/* Per-agent skill toggles: one pill per app (the app's icon), green when the skill is on for it; the app name is the tooltip. */
|
|
325
331
|
.set-agents { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
326
332
|
.achip { display: inline-flex; align-items: center; justify-content: center; padding: 5px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); cursor: pointer; text-decoration: none; transition: color .15s, background .15s, border-color .15s; }
|
|
@@ -454,6 +460,10 @@
|
|
|
454
460
|
.modal-fl { display: block; margin-top: 14px; margin-bottom: 5px; font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
455
461
|
.modal-fl .modal-opt { text-transform: none; letter-spacing: 0; opacity: .75; }
|
|
456
462
|
.modal-fl + .modal-input { margin-top: 0; }
|
|
463
|
+
/* Provider-override editor labels + key hint. */
|
|
464
|
+
.prov-l { display: block; margin-top: 12px; margin-bottom: 4px; font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
465
|
+
.prov-hint { margin-top: 8px; font-size: 12px; color: var(--muted); word-break: break-word; }
|
|
466
|
+
.prov-hint a { color: var(--accent); }
|
|
457
467
|
/* Usage view provider/account pickers. */
|
|
458
468
|
.useg-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
|
|
459
469
|
.useg-pick select { width: auto; min-width: 170px; }
|
|
@@ -3460,7 +3470,7 @@ export {
|
|
|
3460
3470
|
|
|
3461
3471
|
// --- accounts & profiles subpage (mirrors the TUI hub over /api/accounts) ---
|
|
3462
3472
|
let accountsLoaded = false;
|
|
3463
|
-
let accTools = [], accAccounts = [], accProfiles = [];
|
|
3473
|
+
let accTools = [], accAccounts = [], accProfiles = [], accPresets = [];
|
|
3464
3474
|
// Real-time name validator mirroring the server (accounts.ts validateAccountName + the
|
|
3465
3475
|
// duplicate checks), so the modal blocks an invalid or already-used name as the user types.
|
|
3466
3476
|
// `taken` = names already in scope (per-tool for accounts, global for profiles); `current`
|
|
@@ -3483,16 +3493,23 @@ export {
|
|
|
3483
3493
|
+ (a.loggedIn ? '<span class="tag">' + esc(a.email || "logged in") + "</span>" : '<span class="tag warn">not logged in</span>');
|
|
3484
3494
|
const btn = (act, label, cls) => '<button type="button" class="' + (cls || "toggle") + '" data-acc="' + act + '" data-tool="' + esc(a.tool) + '" data-name="' + esc(a.name) + '">' + label + "</button>";
|
|
3485
3495
|
const removeBtn = '<button type="button" class="btn-danger icon" title="Remove" aria-label="Remove account ' + esc(a.name) + '" data-acc="remove" data-tool="' + esc(a.tool) + '" data-name="' + esc(a.name) + '">' + TRASH_ICON + "</button>";
|
|
3496
|
+
// Provider override (e.g. MiniMax) only for tools that support it, and never the
|
|
3497
|
+
// built-in "default" account (kept on the vendor default). Managed accounts only.
|
|
3498
|
+
const provBtn = (a.supportsProvider && a.removable) ? btn("provider", "Provider") : "";
|
|
3486
3499
|
const acts = '<div class="set-ctl" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
|
|
3487
3500
|
+ (a.active ? "" : btn("activate", "Use"))
|
|
3488
3501
|
+ (a.loggedIn ? "" : btn("login", "Log in..."))
|
|
3502
|
+
+ provBtn
|
|
3489
3503
|
+ (a.removable ? btn("rename", "Rename") + removeBtn : "")
|
|
3490
3504
|
+ "</div>";
|
|
3491
3505
|
const toolIc = agentSvg(a.tool);
|
|
3492
3506
|
const toolBadge = toolIc
|
|
3493
3507
|
? '<span class="aicon set-ic" title="' + esc(a.toolLabel) + '" aria-label="' + esc(a.toolLabel) + '">' + toolIc + "</span>"
|
|
3494
3508
|
: '<span class="tag ext">' + esc(a.toolLabel) + "</span> ";
|
|
3495
|
-
|
|
3509
|
+
const provTag = a.provider
|
|
3510
|
+
? ' <span class="tag meas" title="' + esc(a.provider.baseUrl) + '">via ' + esc(a.provider.preset && a.provider.preset !== "custom" ? a.provider.preset : "custom") + (a.provider.model ? " · " + esc(a.provider.model) : "") + "</span>"
|
|
3511
|
+
: "";
|
|
3512
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(a.name) + " " + toolBadge + badges + provTag
|
|
3496
3513
|
+ '</div><div class="set-hint">' + esc(a.dir) + "</div></div>" + acts + "</div>";
|
|
3497
3514
|
}
|
|
3498
3515
|
function profileRow(p, tools, accounts) {
|
|
@@ -3515,7 +3532,7 @@ export {
|
|
|
3515
3532
|
function renderAccounts(d) {
|
|
3516
3533
|
d = d || {};
|
|
3517
3534
|
const accounts = d.accounts || [], profiles = d.profiles || [], tools = d.tools || [];
|
|
3518
|
-
accTools = tools; accAccounts = accounts; accProfiles = profiles;
|
|
3535
|
+
accTools = tools; accAccounts = accounts; accProfiles = profiles; accPresets = d.presets || [];
|
|
3519
3536
|
$("accountsBody").innerHTML = accounts.length ? accounts.map(accountRow).join("") : '<div class="empty" style="padding:20px 0">No accounts.</div>';
|
|
3520
3537
|
$("profilesBody").innerHTML = profiles.length ? profiles.map((p) => profileRow(p, tools, accounts)).join("")
|
|
3521
3538
|
: '<div class="empty" style="padding:20px 0">No profiles yet. Add one to pin an account per tool.</div>';
|
|
@@ -3534,6 +3551,69 @@ export {
|
|
|
3534
3551
|
setNote("accountsNote", "Error: " + (out.error || "failed"), "err");
|
|
3535
3552
|
} catch { if (rollback) rollback(); setNote("accountsNote", "Could not reach the server.", "err"); }
|
|
3536
3553
|
}
|
|
3554
|
+
// Provider-override editor: a small multi-field modal (backend select + base/model/token)
|
|
3555
|
+
// since openModal only does single fields. Resolves to { clear:true } to drop the override,
|
|
3556
|
+
// an object to set it ({ baseUrl, model?, preset?, token? } - token only when typed), or null.
|
|
3557
|
+
function editProvider(a) {
|
|
3558
|
+
return new Promise((resolve) => {
|
|
3559
|
+
const toolPresets = accPresets.filter((p) => p.tool === a.tool);
|
|
3560
|
+
const cur = a.provider; // { baseUrl, model?, preset?, hasToken } | null
|
|
3561
|
+
const opts = ['<option value="default">Default (Anthropic)</option>']
|
|
3562
|
+
.concat(toolPresets.map((p) => '<option value="' + esc(p.id) + '">' + esc(p.label) + "</option>"))
|
|
3563
|
+
.concat(['<option value="custom">Custom (Anthropic-compatible)</option>']).join("");
|
|
3564
|
+
const back = document.createElement("div");
|
|
3565
|
+
back.className = "modal-back";
|
|
3566
|
+
back.innerHTML = '<div class="modal" role="dialog" aria-modal="true" style="max-width:480px;text-align:left">'
|
|
3567
|
+
+ '<div class="modal-title">Provider for ' + esc(a.name) + "</div>"
|
|
3568
|
+
+ '<div class="modal-msg">Point ' + esc(a.toolLabel) + " at another Anthropic-compatible backend (e.g. MiniMax), or keep the default.</div>"
|
|
3569
|
+
+ '<label class="prov-l">Backend</label><select class="modal-input" id="provSel">' + opts + "</select>"
|
|
3570
|
+
+ '<div id="provFields" style="margin-top:8px">'
|
|
3571
|
+
+ '<label class="prov-l">Base URL</label><input class="modal-input" id="provBase" placeholder="https://...">'
|
|
3572
|
+
+ '<label class="prov-l">Model</label><input class="modal-input" id="provModel" placeholder="(provider default)">'
|
|
3573
|
+
+ '<label class="prov-l">API key</label><input class="modal-input" id="provToken" type="password" autocomplete="off" placeholder="' + (cur && cur.hasToken ? "set - leave blank to keep" : "API key") + '">'
|
|
3574
|
+
+ '<div class="prov-hint" id="provHint"></div></div>'
|
|
3575
|
+
+ '<div class="modal-acts"><button type="button" class="toggle modal-cancel">Cancel</button>'
|
|
3576
|
+
+ '<button type="button" class="toggle on modal-ok">Save</button></div></div>';
|
|
3577
|
+
document.body.appendChild(back);
|
|
3578
|
+
requestAnimationFrame(() => back.classList.add("show"));
|
|
3579
|
+
const sel = back.querySelector("#provSel"), fields = back.querySelector("#provFields");
|
|
3580
|
+
const base = back.querySelector("#provBase"), model = back.querySelector("#provModel"), token = back.querySelector("#provToken"), hint = back.querySelector("#provHint");
|
|
3581
|
+
sel.value = cur ? (cur.preset && cur.preset !== "custom" ? cur.preset : "custom") : "default";
|
|
3582
|
+
function applySel() {
|
|
3583
|
+
const v = sel.value;
|
|
3584
|
+
fields.style.display = v === "default" ? "none" : "";
|
|
3585
|
+
if (v === "default") return;
|
|
3586
|
+
const preset = toolPresets.find((p) => p.id === v);
|
|
3587
|
+
if (preset) {
|
|
3588
|
+
base.value = (cur && cur.preset === v && cur.baseUrl) || preset.baseUrl;
|
|
3589
|
+
model.value = (cur && cur.preset === v && cur.model) || preset.model || "";
|
|
3590
|
+
hint.innerHTML = preset.tokenUrl ? 'Get a key at <a href="' + esc(preset.tokenUrl) + '" target="_blank" rel="noopener">' + esc(preset.tokenUrl) + "</a>" : "";
|
|
3591
|
+
} else {
|
|
3592
|
+
base.value = (cur && (!cur.preset || cur.preset === "custom") && cur.baseUrl) || "";
|
|
3593
|
+
model.value = (cur && cur.model) || "";
|
|
3594
|
+
hint.textContent = "";
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
applySel();
|
|
3598
|
+
sel.addEventListener("change", applySel);
|
|
3599
|
+
let done = false;
|
|
3600
|
+
const close = (val) => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); resolve(val); };
|
|
3601
|
+
back.querySelector(".modal-cancel").addEventListener("click", () => close(null));
|
|
3602
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back) close(null); });
|
|
3603
|
+
back.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); close(null); } });
|
|
3604
|
+
back.querySelector(".modal-ok").addEventListener("click", () => {
|
|
3605
|
+
if (sel.value === "default") { close({ clear: true }); return; }
|
|
3606
|
+
const b = base.value.trim();
|
|
3607
|
+
if (!/^https?:\/\//i.test(b)) { hint.textContent = "Enter a base URL starting with http:// or https://"; return; }
|
|
3608
|
+
const out = { baseUrl: b };
|
|
3609
|
+
if (model.value.trim()) out.model = model.value.trim();
|
|
3610
|
+
if (sel.value !== "custom") out.preset = sel.value;
|
|
3611
|
+
if (token.value) out.token = token.value; // only send a typed key (blank keeps the stored one)
|
|
3612
|
+
close(out);
|
|
3613
|
+
});
|
|
3614
|
+
sel.focus();
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3537
3617
|
function wireAccounts() {
|
|
3538
3618
|
$("accAdd").addEventListener("click", async () => {
|
|
3539
3619
|
const tool = accTools.length === 1 ? accTools[0].name
|
|
@@ -3551,6 +3631,12 @@ export {
|
|
|
3551
3631
|
else if (op === "remove") { if (await askConfirm("Remove account", "Remove account '" + name + "' and delete its local config dir?", true)) accAction("account.remove", { tool, name }); }
|
|
3552
3632
|
else if (op === "rename") { const taken = accAccounts.filter((a) => a.tool === tool).map((a) => a.name); const nn = await askText("Rename account", name, "Rename '" + name + "' to:", nameValidator(taken, name)); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
|
|
3553
3633
|
else if (op === "login") { setNote("accountsNote", "To log in, run this in a terminal: enigma " + tool + " " + name); }
|
|
3634
|
+
else if (op === "provider") {
|
|
3635
|
+
const a = accAccounts.find((x) => x.tool === tool && x.name === name);
|
|
3636
|
+
if (!a) return;
|
|
3637
|
+
const out = await editProvider(a);
|
|
3638
|
+
if (out) accAction("account.provider", { tool, name, provider: out.clear ? null : out });
|
|
3639
|
+
}
|
|
3554
3640
|
});
|
|
3555
3641
|
$("profilesBody").addEventListener("click", async (e) => {
|
|
3556
3642
|
const b = e.target.closest("button[data-prof-act]"); if (!b) return;
|
|
@@ -3765,6 +3851,9 @@ export {
|
|
|
3765
3851
|
llmBtn.setAttribute("aria-checked", p.llm ? "true" : "false");
|
|
3766
3852
|
}
|
|
3767
3853
|
let recallItemsById = {};
|
|
3854
|
+
// Multi-select state for the Recall list: a Set of selected ids, the ordered ids as
|
|
3855
|
+
// rendered (for Shift range selection), and the anchor index for the next Shift+click.
|
|
3856
|
+
let recallSel = new Set(), recallOrderIds = [], recallAnchorIdx = null;
|
|
3768
3857
|
function recallRowHtml(it, anchorId) {
|
|
3769
3858
|
const ic = '<span class="aicon" title="' + esc(it.source) + '" aria-label="' + esc(it.source) + '">' + (agentSvg(it.source) || esc(it.source)) + "</span>";
|
|
3770
3859
|
const date = it.createdAt ? new Date(it.createdAt).toISOString().slice(0, 10) : "";
|
|
@@ -3786,13 +3875,45 @@ export {
|
|
|
3786
3875
|
}
|
|
3787
3876
|
function renderRecallList(items) {
|
|
3788
3877
|
recallItemsById = {};
|
|
3878
|
+
// A fresh list invalidates the previous selection (ids may have changed).
|
|
3879
|
+
recallSel = new Set(); recallOrderIds = []; recallAnchorIdx = null;
|
|
3789
3880
|
if (!items || !items.length) { $("recallList").innerHTML = '<div class="empty" style="padding:16px 0">No memories yet - run a sync, or adjust your search.</div>'; return; }
|
|
3790
|
-
|
|
3791
|
-
|
|
3881
|
+
const rows = items.map((it) => {
|
|
3882
|
+
// data-idx aligns with recallOrderIds (id'd rows only) so Shift ranges are exact.
|
|
3883
|
+
let dataIdx = "";
|
|
3884
|
+
if (it.id) { recallItemsById[it.id] = it; recallOrderIds.push(it.id); dataIdx = recallOrderIds.length - 1; }
|
|
3792
3885
|
const del = it.id ? '<button type="button" class="btn-danger icon recall-del" title="Delete memory" aria-label="Delete this memory" data-del="' + it.id + '">' + TRASH_ICON + "</button>" : "";
|
|
3793
|
-
|
|
3886
|
+
const check = it.id ? '<input type="checkbox" class="rc-check" data-id="' + it.id + '" aria-label="Select this memory">' : '<span style="width:15px;flex:0 0 auto"></span>';
|
|
3887
|
+
return '<div class="set-row" style="cursor:pointer" data-rid="' + (it.id || "") + '"' + (it.id ? ' data-idx="' + dataIdx + '"' : "") + '>' + check + recallRowHtml(it) + del + "</div>"
|
|
3794
3888
|
+ '<div id="rd-' + (it.id || "") + '" style="display:none"></div>';
|
|
3795
|
-
}).join("")
|
|
3889
|
+
}).join("");
|
|
3890
|
+
// Bulk-action bar (hidden until something is selected) + the rows.
|
|
3891
|
+
$("recallList").innerHTML = '<div id="recallBulk" class="rc-bulk" style="display:none">'
|
|
3892
|
+
+ '<span class="rc-seln" id="recallSelN"></span>'
|
|
3893
|
+
+ '<button type="button" class="btn-danger" data-bulk-del>Delete selected</button>'
|
|
3894
|
+
+ '<button type="button" class="toggle" data-bulk-clear>Clear selection</button></div>'
|
|
3895
|
+
+ '<div class="panel">' + rows + "</div>";
|
|
3896
|
+
}
|
|
3897
|
+
/** Reflect the current selection in the DOM: row highlight, checkboxes, and the bulk bar. */
|
|
3898
|
+
function updateRecallSelUI() {
|
|
3899
|
+
document.querySelectorAll("#recallList .set-row[data-rid]").forEach((row) => {
|
|
3900
|
+
const id = Number(row.dataset.rid);
|
|
3901
|
+
const on = recallSel.has(id);
|
|
3902
|
+
row.classList.toggle("sel", on);
|
|
3903
|
+
const cb = row.querySelector(".rc-check");
|
|
3904
|
+
if (cb) cb.checked = on;
|
|
3905
|
+
});
|
|
3906
|
+
const bar = $("recallBulk");
|
|
3907
|
+
if (bar) {
|
|
3908
|
+
bar.style.display = recallSel.size ? "" : "none";
|
|
3909
|
+
const n = $("recallSelN");
|
|
3910
|
+
if (n) n.textContent = recallSel.size + (recallSel.size === 1 ? " memory selected" : " memories selected");
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
/** Select every id between two row indices (inclusive), for Shift+click range selection. */
|
|
3914
|
+
function recallSelectRange(fromIdx, toIdx) {
|
|
3915
|
+
const lo = Math.min(fromIdx, toIdx), hi = Math.max(fromIdx, toIdx);
|
|
3916
|
+
for (let i = lo; i <= hi; i++) { const id = recallOrderIds[i]; if (id) recallSel.add(id); }
|
|
3796
3917
|
}
|
|
3797
3918
|
async function loadRecallTimeline(id) {
|
|
3798
3919
|
const el = $("rtl-" + id);
|
|
@@ -3887,6 +4008,16 @@ export {
|
|
|
3887
4008
|
$("recallType").addEventListener("change", loadRecall);
|
|
3888
4009
|
$("recallProject").addEventListener("change", loadRecall);
|
|
3889
4010
|
$("recallList").addEventListener("click", async (e) => {
|
|
4011
|
+
// Bulk actions on the current multi-selection.
|
|
4012
|
+
if (e.target.closest("[data-bulk-clear]")) { recallSel.clear(); recallAnchorIdx = null; updateRecallSelUI(); return; }
|
|
4013
|
+
if (e.target.closest("[data-bulk-del]")) {
|
|
4014
|
+
const ids = [...recallSel];
|
|
4015
|
+
if (!ids.length) return;
|
|
4016
|
+
const noun = ids.length === 1 ? "memory" : "memories";
|
|
4017
|
+
if (!(await askConfirm("Delete memories", "Delete " + ids.length + " selected " + noun + "? This cannot be undone.", true))) return;
|
|
4018
|
+
recallOpPost({ op: "delete", ids }, "Deleting " + ids.length + "...", ids.length + " " + noun + " deleted.");
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
3890
4021
|
const del = e.target.closest("[data-del]");
|
|
3891
4022
|
if (del) {
|
|
3892
4023
|
if (!(await askConfirm("Delete memory", "Delete this memory? This cannot be undone.", true))) return;
|
|
@@ -3897,6 +4028,22 @@ export {
|
|
|
3897
4028
|
if (tl) { loadRecallTimeline(tl.dataset.tl); return; }
|
|
3898
4029
|
const row = e.target.closest("[data-rid]");
|
|
3899
4030
|
if (!row || !row.dataset.rid) return;
|
|
4031
|
+
const id = Number(row.dataset.rid);
|
|
4032
|
+
const cb = e.target.closest(".rc-check");
|
|
4033
|
+
// Selection shortcuts: a checkbox click, Ctrl/Cmd+click (toggle one), or Shift+click
|
|
4034
|
+
// (select the range from the last-clicked anchor through this row, inclusive).
|
|
4035
|
+
if (cb || e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
4036
|
+
const idx = row.dataset.idx === undefined ? -1 : Number(row.dataset.idx);
|
|
4037
|
+
if (e.shiftKey && recallAnchorIdx !== null && idx >= 0) {
|
|
4038
|
+
recallSelectRange(recallAnchorIdx, idx);
|
|
4039
|
+
} else {
|
|
4040
|
+
if (recallSel.has(id)) recallSel.delete(id); else recallSel.add(id);
|
|
4041
|
+
if (idx >= 0) recallAnchorIdx = idx;
|
|
4042
|
+
}
|
|
4043
|
+
updateRecallSelUI();
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
// Plain click expands/collapses the detail (unchanged).
|
|
3900
4047
|
const detail = $("rd-" + row.dataset.rid);
|
|
3901
4048
|
if (!detail) return;
|
|
3902
4049
|
if (detail.innerHTML === "") detail.innerHTML = recallDetailHtml(recallItemsById[row.dataset.rid]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.64",
|
|
4
4
|
"description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|