@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.
Files changed (2) hide show
  1. package/assets/index.html +154 -7
  2. 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
- return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(a.name) + " " + toolBadge + badges
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
- $("recallList").innerHTML = '<div class="panel">' + items.map((it) => {
3791
- if (it.id) recallItemsById[it.id] = it;
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
- return '<div class="set-row" style="cursor:pointer" data-rid="' + (it.id || "") + '">' + recallRowHtml(it) + del + "</div>"
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("") + "</div>";
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.63",
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": [