@tiagojct/subsub 0.6.0 → 0.7.0

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/web/app.js CHANGED
@@ -67,6 +67,24 @@ marked.use({
67
67
  image(token) {
68
68
  return escapeHtml(token.text || token.href || "");
69
69
  },
70
+ table(token) {
71
+ let headerHtml = "";
72
+ for (let r = 0; r < token.header.length; r++) {
73
+ headerHtml += this.tablecell(token.header[r]);
74
+ }
75
+ let t = this.tablerow({ text: headerHtml });
76
+ let s = "";
77
+ for (let r = 0; r < token.rows.length; r++) {
78
+ let row = token.rows[r];
79
+ let n = "";
80
+ for (let o = 0; o < row.length; o++) {
81
+ n += this.tablecell(row[o]);
82
+ }
83
+ s += this.tablerow({ text: n });
84
+ }
85
+ if (s) s = `<tbody>${s}</tbody>`;
86
+ return `<div class="md-table-wrap"><table><thead>${t}</thead>${s}</table></div>`;
87
+ },
70
88
  },
71
89
  });
72
90
  function md(text) {
@@ -77,11 +95,65 @@ function md(text) {
77
95
  }
78
96
  }
79
97
 
80
- // ---------------------------------------------------------------- header, side panel
98
+ // ---------------------------------------------------------------- theme, layout & language
99
+
100
+ let currentTheme = localStorage.getItem("subsub-theme") || "auto";
101
+ let currentLayout = localStorage.getItem("subsub-layout") || "standard";
102
+
103
+ function updateLayoutUI() {
104
+ const isWide = currentLayout === "wide";
105
+ document.body.classList.toggle("layout-full", isWide);
106
+ const textEl = $("width-btn-text");
107
+ if (textEl) textEl.textContent = isWide ? t("layoutFocus") : t("layoutWide");
108
+ const btn = $("width-btn");
109
+ if (btn) btn.title = t("toggleWidth");
110
+ }
111
+
112
+ function toggleLayout() {
113
+ currentLayout = currentLayout === "wide" ? "standard" : "wide";
114
+ localStorage.setItem("subsub-layout", currentLayout);
115
+ updateLayoutUI();
116
+ }
117
+
118
+ function themeLabel(theme) {
119
+ if (theme === "light") return t("themeLight");
120
+ if (theme === "dark") return t("themeDark");
121
+ return t("themeAuto");
122
+ }
123
+
124
+ function updateThemeUI() {
125
+ const icon = currentTheme === "light" ? "☀️" : currentTheme === "dark" ? "🌙" : "💻";
126
+ const label = `${icon} ${themeLabel(currentTheme)}`;
127
+ const btn = $("theme-btn");
128
+ if (btn) {
129
+ btn.textContent = label;
130
+ btn.title = t("themeToggle");
131
+ }
132
+ const sideBtn = $("theme-btn-side");
133
+ if (sideBtn) sideBtn.textContent = label;
134
+ }
135
+
136
+ function setTheme(theme) {
137
+ currentTheme = theme;
138
+ localStorage.setItem("subsub-theme", theme);
139
+ if (theme === "auto") {
140
+ delete document.documentElement.dataset.theme;
141
+ } else {
142
+ document.documentElement.dataset.theme = theme;
143
+ }
144
+ updateThemeUI();
145
+ }
146
+
147
+ function toggleTheme() {
148
+ if (currentTheme === "auto") setTheme("light");
149
+ else if (currentTheme === "light") setTheme("dark");
150
+ else setTheme("auto");
151
+ }
81
152
 
82
153
  function applyLang() {
83
154
  document.documentElement.lang = lang === "pt" ? "pt-PT" : "en";
84
155
  for (const e of document.querySelectorAll("[data-i18n]")) e.textContent = t(e.dataset.i18n);
156
+ updateThemeUI();
85
157
  }
86
158
 
87
159
  function mode() {
@@ -92,27 +164,34 @@ function renderHeader() {
92
164
  document.body.dataset.mode = mode();
93
165
  for (const b of document.querySelectorAll(".modes button")) b.setAttribute("aria-pressed", String(b.dataset.mode === mode()));
94
166
  if (state?.profile) $("profile").value = $("profile-side").value = state.profile;
167
+ $("starbuck-toggle").checked = Boolean(state?.starbuck);
168
+ $("starbuck-toggle").parentElement.title = t("starbuckHint");
95
169
  const m = state?.model ?? session?.model;
96
170
  $("model-btn").textContent = $("model-btn-side").textContent = m ? m.id : t("noModel");
97
171
  const lib = $("library");
172
+ const txt = lib.querySelector(".library-text") ?? lib;
98
173
  const L = state?.library;
99
- lib.classList.remove("down");
100
- if (!L || L.zotero === "checking") lib.textContent = t("zoteroChecking");
101
- else if (L.zotero === "down") {
102
- lib.textContent = t("zoteroDown");
174
+ lib.classList.remove("down", "reachable", "checking");
175
+ if (!L || L.zotero === "checking") {
176
+ txt.textContent = t("zoteroChecking");
177
+ lib.classList.add("checking");
178
+ } else if (L.zotero === "down") {
179
+ txt.textContent = t("zoteroDown");
103
180
  lib.classList.add("down");
104
181
  } else {
105
182
  const parts = [t("items", { n: L.items ?? "?" })];
106
183
  if (L.toReview) parts.push(t("toReview", { n: L.toReview }));
107
- lib.textContent = `Zotero: ${parts.join(", ")}`;
184
+ txt.textContent = `Zotero: ${parts.join(", ")}`;
185
+ lib.classList.add("reachable");
108
186
  }
109
187
  if (state?.down?.length) {
110
- lib.textContent += ` | ${t("serversDown", { x: state.down.join(", ") })}`;
188
+ txt.textContent += ` | ${t("serversDown", { x: state.down.join(", ") })}`;
111
189
  lib.classList.add("down");
112
190
  }
113
191
  $("input").placeholder = mode() === "librarian" ? t("placeholderLibrarian") : t("placeholderResearcher");
114
192
  renderActions();
115
193
  renderBanner();
194
+ updateThemeUI();
116
195
  }
117
196
 
118
197
  function hasCommand(name) {
@@ -123,7 +202,9 @@ function hasCommand(name) {
123
202
  function renderActions() {
124
203
  const box = $("actions");
125
204
  box.replaceChildren();
126
- const list = [...ACTIONS[mode()]].filter((a) => hasCommand(a.cmd));
205
+ const list = [...ACTIONS[mode()]].filter(
206
+ (a) => hasCommand(a.cmd) && (!a.needs || state?.[a.needs]) && (!a.profiles || a.profiles.includes(state?.profile ?? "scholar")),
207
+ );
127
208
  for (const a of list) box.append(actionButton(a));
128
209
  box.append(el("hr"));
129
210
  for (const a of ACTIONS.both.filter((x) => hasCommand(x.cmd))) box.append(actionButton(a));
@@ -151,26 +232,26 @@ function renderBanner() {
151
232
  b.replaceChildren();
152
233
  const m = state?.model ?? session?.model;
153
234
  if (!m && session) {
154
- b.append(el("span", { text: t("connectModel") }), el("button", { type: "button", class: "primary small", text: t("connect"), onclick: openModels }));
235
+ b.append(el("span", { text: t("connectModel") }), el("button", { type: "button", class: "primary small", text: t("connect"), onclick: showModels }));
155
236
  b.hidden = false;
156
237
  } else b.hidden = true;
157
238
  }
158
239
 
159
- async function loadSessions() {
160
- let list = [];
161
- try {
162
- list = (await api("/api/sessions")).sessions ?? [];
163
- } catch {
164
- return;
165
- }
240
+ let loadedSessions = [];
241
+
242
+ function renderSessionsList(filterText = "") {
166
243
  const ul = $("sessions");
167
244
  ul.replaceChildren();
168
- if (!list.length) {
245
+ const q = filterText.trim().toLowerCase();
246
+ const filtered = q
247
+ ? loadedSessions.filter((s) => (s.name || s.first || "").toLowerCase().includes(q))
248
+ : loadedSessions;
249
+ if (!filtered.length) {
169
250
  ul.append(el("li", { class: "muted small", text: t("noConversations") }));
170
251
  return;
171
252
  }
172
253
  const fmt = new Intl.DateTimeFormat(lang === "pt" ? "pt-PT" : "en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
173
- for (const s of list) {
254
+ for (const s of filtered) {
174
255
  const current = session?.sessionFile === s.path;
175
256
  ul.append(
176
257
  el(
@@ -192,13 +273,57 @@ async function loadSessions() {
192
273
  },
193
274
  },
194
275
  el("span", { class: "what", text: s.name || s.first || "…" }),
195
- el("span", { class: "when", text: fmt.format(new Date(s.modified)) }),
276
+ el(
277
+ "span",
278
+ { class: "when" },
279
+ el("span", { text: fmt.format(new Date(s.modified)) }),
280
+ s.messages ? el("span", { class: "badge", text: `${s.messages}` }) : null,
281
+ ),
196
282
  ),
197
283
  ),
198
284
  );
199
285
  }
200
286
  }
201
287
 
288
+ async function loadSessions() {
289
+ try {
290
+ loadedSessions = (await api("/api/sessions")).sessions ?? [];
291
+ } catch {
292
+ return;
293
+ }
294
+ renderSessionsList($("session-search")?.value ?? "");
295
+ }
296
+
297
+ function exportConversation() {
298
+ const msgs = [];
299
+ const items = log.querySelectorAll(".msg");
300
+ if (!items.length) {
301
+ note(t("noConversations"), "warning");
302
+ return;
303
+ }
304
+ for (const m of items) {
305
+ if (m.classList.contains("user")) {
306
+ const text = m.querySelector(".bubble")?.innerText ?? "";
307
+ if (text && text !== "/subsub-refresh") msgs.push(`### User\n\n${text}\n`);
308
+ } else if (m.classList.contains("assistant")) {
309
+ const text = Array.from(m.querySelectorAll(".md")).map((d) => d.innerText).join("\n\n");
310
+ if (text) msgs.push(`### Sub-Sub (${mode()})\n\n${text}\n`);
311
+ }
312
+ }
313
+ if (!msgs.length) return;
314
+ const date = new Date().toISOString().slice(0, 10);
315
+ const content = `# Sub-Sub Research Session (${date})\n\n**Mode**: ${mode()} \n**Profile**: ${state?.profile ?? "scholar"} \n\n---\n\n${msgs.join("\n---\n\n")}`;
316
+ const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
317
+ const url = URL.createObjectURL(blob);
318
+ const a = document.createElement("a");
319
+ a.href = url;
320
+ a.download = `Sub-Sub-${mode()}-${date}.md`;
321
+ document.body.append(a);
322
+ a.click();
323
+ a.remove();
324
+ URL.revokeObjectURL(url);
325
+ }
326
+
202
327
  function closeSide() {
203
328
  $("side").classList.remove("open");
204
329
  $("menu-btn").setAttribute("aria-expanded", "false");
@@ -261,6 +386,7 @@ function textOf(content) {
261
386
  const sentTemplates = [];
262
387
 
263
388
  function userMessage(text) {
389
+ if (String(text).trim() === "/subsub-refresh") return;
264
390
  clearEmpty();
265
391
  let bubble;
266
392
  if (text.length > 700) {
@@ -332,6 +458,35 @@ function finishTool(id, result, isError) {
332
458
  if (isError) row.querySelector(".label").textContent = `${toolLabel(row.dataset.name)} (${t("failed")})`;
333
459
  }
334
460
 
461
+ function attachCopyButtons(root) {
462
+ for (const pre of root.querySelectorAll("pre")) {
463
+ if (pre.parentElement?.classList.contains("pre-wrap")) continue;
464
+ const wrap = el("div", { class: "pre-wrap" });
465
+ pre.parentNode.insertBefore(wrap, pre);
466
+ wrap.append(pre);
467
+ const btn = el("button", {
468
+ type: "button",
469
+ class: "copy-btn",
470
+ text: t("copy"),
471
+ onclick: async () => {
472
+ const code = pre.querySelector("code")?.innerText ?? pre.innerText;
473
+ try {
474
+ await navigator.clipboard.writeText(code);
475
+ btn.textContent = t("copied");
476
+ btn.classList.add("copied");
477
+ setTimeout(() => {
478
+ btn.textContent = t("copy");
479
+ btn.classList.remove("copied");
480
+ }, 2000);
481
+ } catch {
482
+ btn.textContent = t("error");
483
+ }
484
+ },
485
+ });
486
+ wrap.append(btn);
487
+ }
488
+ }
489
+
335
490
  /** A container for one assistant message; blocks are keyed by content index. */
336
491
  function startAssistant() {
337
492
  clearEmpty();
@@ -357,6 +512,7 @@ function paint(msg) {
357
512
  msg.raf = 0;
358
513
  for (const b of msg.blocks.values()) if (b.kind === "text" && b.dirty) {
359
514
  b.node.innerHTML = md(b.text);
515
+ attachCopyButtons(b.node);
360
516
  b.dirty = false;
361
517
  }
362
518
  scrollDown();
@@ -366,10 +522,13 @@ function paint(msg) {
366
522
  function renderAssistantFinal(message, msg = startAssistant()) {
367
523
  msg.box.replaceChildren();
368
524
  msg.blocks.clear();
525
+ let fullText = "";
369
526
  for (const c of message.content ?? []) {
370
527
  if (c.type === "text" && c.text?.trim()) {
528
+ fullText += (fullText ? "\n\n" : "") + c.text;
371
529
  const node = el("div", { class: "md" });
372
530
  node.innerHTML = md(c.text);
531
+ attachCopyButtons(node);
373
532
  msg.box.append(node);
374
533
  } else if (c.type === "toolCall") {
375
534
  const row = toolRow(c.id, c.name, c.arguments);
@@ -377,6 +536,32 @@ function renderAssistantFinal(message, msg = startAssistant()) {
377
536
  msg.box.append(row);
378
537
  }
379
538
  }
539
+ if (fullText.trim()) {
540
+ const actions = el(
541
+ "div",
542
+ { class: "msg-actions" },
543
+ el("button", {
544
+ type: "button",
545
+ class: "msg-action-btn",
546
+ text: t("copyNote"),
547
+ onclick: async (e) => {
548
+ const btn = e.currentTarget;
549
+ try {
550
+ await navigator.clipboard.writeText(fullText);
551
+ btn.textContent = t("copied");
552
+ btn.classList.add("copied");
553
+ setTimeout(() => {
554
+ btn.textContent = t("copyNote");
555
+ btn.classList.remove("copied");
556
+ }, 2000);
557
+ } catch {
558
+ btn.textContent = t("error");
559
+ }
560
+ },
561
+ }),
562
+ );
563
+ msg.box.append(actions);
564
+ }
380
565
  if (message.stopReason === "error" && message.errorMessage) msg.box.append(el("div", { class: "note error", text: message.errorMessage }));
381
566
  if (message.stopReason === "aborted") msg.box.append(el("div", { class: "note", text: t("stopped") }));
382
567
  if (!msg.box.children.length) msg.box.remove();
@@ -388,7 +573,11 @@ function renderHistory(messages) {
388
573
  toolRows.clear();
389
574
  currentMsg = null;
390
575
  for (const m of messages ?? []) {
391
- if (m.role === "user") userMessage(textOf(m.content));
576
+ if (m.role === "user") {
577
+ const txt = textOf(m.content).trim();
578
+ if (txt === "/subsub-refresh") continue;
579
+ userMessage(txt);
580
+ }
392
581
  else if (m.role === "assistant") renderAssistantFinal(m);
393
582
  else if (m.role === "toolResult") finishTool(m.toolCallId, m, m.isError);
394
583
  else if (m.role === "custom" && m.display) note(textOf(m.content));
@@ -447,7 +636,10 @@ function onEvent(ev) {
447
636
  refreshSession();
448
637
  break;
449
638
  case "message_start":
450
- if (ev.message?.role === "user") userMessage(sentTemplates.shift() ?? textOf(ev.message.content));
639
+ if (ev.message?.role === "user") {
640
+ const txt = (sentTemplates.shift() ?? textOf(ev.message.content)).trim();
641
+ if (txt !== "/subsub-refresh") userMessage(txt);
642
+ }
451
643
  else if (ev.message?.role === "assistant") startAssistant();
452
644
  else if (ev.message?.role === "custom" && ev.message.display) note(textOf(ev.message.content));
453
645
  if (working) log.append(working);
@@ -599,6 +791,7 @@ function nextDialog() {
599
791
  };
600
792
  if (d.method === "confirm") {
601
793
  buttons.append(
794
+ el("span", { class: "modal-shortcut-hint", text: t("confirmShortcutHint") }),
602
795
  el("button", { type: "button", class: "secondary", text: t("no"), onclick: () => answer({ confirmed: false }) }),
603
796
  el("button", { type: "button", class: "primary", text: t("yes"), onclick: () => answer({ confirmed: true }) }),
604
797
  );
@@ -614,6 +807,17 @@ function nextDialog() {
614
807
  el("button", { type: "button", class: "primary", text: t("ok"), onclick: () => answer({ value: field.value }) }),
615
808
  );
616
809
  }
810
+ dlg.onkeydown = (e) => {
811
+ if (d.method === "confirm") {
812
+ if (e.key === "y" || e.key === "Y" || (e.key === "Enter" && (e.metaKey || e.ctrlKey))) {
813
+ e.preventDefault();
814
+ answer({ confirmed: true });
815
+ } else if (e.key === "n" || e.key === "N") {
816
+ e.preventDefault();
817
+ answer({ confirmed: false });
818
+ }
819
+ }
820
+ };
617
821
  dlg.oncancel = (e) => {
618
822
  e.preventDefault();
619
823
  answer(d.method === "confirm" ? { confirmed: false } : { cancelled: true });
@@ -624,6 +828,11 @@ function nextDialog() {
624
828
 
625
829
  // ---------------------------------------------------------------- models and keys
626
830
 
831
+ /** Open the model dialog; a failure becomes a note, not an unhandled rejection. */
832
+ function showModels() {
833
+ openModels().catch((err) => note(err.message, "warning"));
834
+ }
835
+
627
836
  async function openModels() {
628
837
  const dlg = $("model-dialog");
629
838
  const ul = $("models");
@@ -807,11 +1016,24 @@ for (const b of document.querySelectorAll(".modes button")) {
807
1016
  });
808
1017
  }
809
1018
  $("profile").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
1019
+ $("starbuck-toggle").addEventListener("change", async (e) => {
1020
+ const on = e.target.checked;
1021
+ e.target.disabled = true;
1022
+ try {
1023
+ await api("/api/addons", { starbuck: on });
1024
+ note(t(on ? "starbuckOn" : "starbuckOff"), "info");
1025
+ } catch (err) {
1026
+ e.target.checked = !on;
1027
+ note(err.message, "warning");
1028
+ } finally {
1029
+ e.target.disabled = false;
1030
+ }
1031
+ });
810
1032
  $("profile-side").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
811
- $("model-btn").addEventListener("click", openModels);
1033
+ $("model-btn").addEventListener("click", showModels);
812
1034
  $("model-btn-side").addEventListener("click", () => {
813
1035
  closeSide();
814
- openModels();
1036
+ showModels();
815
1037
  });
816
1038
  $("model-close").addEventListener("click", () => $("model-dialog").close());
817
1039
  $("main").addEventListener("click", () => {
@@ -819,10 +1041,26 @@ $("main").addEventListener("click", () => {
819
1041
  });
820
1042
  $("menu-btn").addEventListener("click", (e) => {
821
1043
  e.stopPropagation();
822
- const side = $("side");
823
- side.classList.toggle("open");
824
- $("menu-btn").setAttribute("aria-expanded", String(side.classList.contains("open")));
1044
+ if (window.innerWidth > 832) {
1045
+ const app = document.querySelector(".app");
1046
+ app.classList.toggle("side-collapsed");
1047
+ const collapsed = app.classList.contains("side-collapsed");
1048
+ $("menu-btn").setAttribute("aria-expanded", String(!collapsed));
1049
+ localStorage.setItem("subsub-side-collapsed", String(collapsed));
1050
+ } else {
1051
+ const side = $("side");
1052
+ side.classList.toggle("open");
1053
+ $("menu-btn").setAttribute("aria-expanded", String(side.classList.contains("open")));
1054
+ }
1055
+ });
1056
+ $("width-btn")?.addEventListener("click", toggleLayout);
1057
+ $("theme-btn")?.addEventListener("click", toggleTheme);
1058
+ $("theme-btn-side")?.addEventListener("click", () => {
1059
+ closeSide();
1060
+ toggleTheme();
825
1061
  });
1062
+ $("session-search")?.addEventListener("input", (e) => renderSessionsList(e.target.value));
1063
+ $("export-btn")?.addEventListener("click", exportConversation);
826
1064
  $("quit-btn").addEventListener("click", async () => {
827
1065
  try {
828
1066
  await api("/api/quit", {});
@@ -855,5 +1093,11 @@ function connect() {
855
1093
  };
856
1094
  }
857
1095
 
1096
+ setTheme(currentTheme);
1097
+ updateLayoutUI();
1098
+ if (localStorage.getItem("subsub-side-collapsed") === "true" && window.innerWidth > 832) {
1099
+ document.querySelector(".app")?.classList.add("side-collapsed");
1100
+ $("menu-btn")?.setAttribute("aria-expanded", "false");
1101
+ }
858
1102
  applyLang();
859
1103
  connect();
package/web/glauca.css CHANGED
@@ -1,23 +1,23 @@
1
- /* Generated from glauca.json. Edit the json, then `make generate`. */
1
+ /* Sub-Sub Glauca tokens: Vintage & Minimal Bookmaking System */
2
2
  :root {
3
- --gl-font-serif: "IBM Plex Serif", "IBM Plex Serif fallback", Georgia, serif;
4
- --gl-font-sans: "IBM Plex Sans", "IBM Plex Sans fallback", system-ui, sans-serif;
5
- --gl-font-mono: "IBM Plex Mono", "IBM Plex Mono fallback", ui-monospace, monospace;
6
- --gl-font-reading: "IBM Plex Sans", "IBM Plex Sans fallback", system-ui, sans-serif;
7
- --gl-text-xs: 0.64rem;
8
- --gl-text-sm: 0.8rem;
9
- --gl-text-base: 1rem;
3
+ --gl-font-serif: "EB Garamond", "Newsreader", "IBM Plex Serif", Georgia, serif;
4
+ --gl-font-sans: "EB Garamond", "IBM Plex Sans", system-ui, -apple-system, sans-serif;
5
+ --gl-font-mono: "JetBrains Mono", "Courier Prime", "IBM Plex Mono", ui-monospace, monospace;
6
+ --gl-font-reading: "EB Garamond", Garamond, Georgia, serif;
7
+ --gl-text-xs: 0.72rem;
8
+ --gl-text-sm: 0.88rem;
9
+ --gl-text-base: 1.05rem;
10
10
  --gl-text-lg: 1.25rem;
11
- --gl-text-xl: 1.563rem;
12
- --gl-text-2xl: 1.953rem;
13
- --gl-text-3xl: 2.441rem;
14
- --gl-text-4xl: 3.052rem;
15
- --gl-text-display: clamp(2.5rem, 6vw, 4rem);
16
- --gl-text-poster: clamp(3rem, 10vw, 6.5rem);
17
- --gl-leading-tight: 1.08;
18
- --gl-leading-snug: 1.3;
19
- --gl-leading-normal: 1.55;
20
- --gl-leading-relaxed: 1.7;
11
+ --gl-text-xl: 1.55rem;
12
+ --gl-text-2xl: 1.95rem;
13
+ --gl-text-3xl: 2.45rem;
14
+ --gl-text-4xl: 3.05rem;
15
+ --gl-text-display: clamp(2.4rem, 5vw, 3.6rem);
16
+ --gl-text-poster: clamp(3rem, 8vw, 5.5rem);
17
+ --gl-leading-tight: 1.15;
18
+ --gl-leading-snug: 1.35;
19
+ --gl-leading-normal: 1.65;
20
+ --gl-leading-relaxed: 1.8;
21
21
  --gl-weight-regular: 400;
22
22
  --gl-weight-medium: 500;
23
23
  --gl-weight-semibold: 600;
@@ -43,40 +43,45 @@
43
43
  --gl-border-width: 1px;
44
44
  }
45
45
 
46
- /* Pruina is the light default; Profundum is dark. */
46
+ /* Light Default: Warm Archival Paper & Printer's Inks */
47
47
  :root,
48
- [data-mode="light"] {
49
- --gl-bg: #f0f4f6;
50
- --gl-surface: #f7fafb;
48
+ [data-mode="light"],
49
+ [data-theme="light"],
50
+ html[data-theme="light"] {
51
+ --gl-bg: #f8f6f0;
52
+ --gl-surface: #f2ede2;
51
53
  --gl-surface-raised: #ffffff;
52
- --gl-text: #16222a;
53
- --gl-text-muted: #55646d;
54
- --gl-border: #cdd7dc;
55
- --gl-accent: #0b62cf;
56
- --gl-accent-bright: #007aff;
57
- --gl-accent-deep: #084b96;
58
- --gl-on-accent: #f2f7fb;
59
- --gl-tint-deep: #16303f;
60
- --gl-tint: #35576b;
61
- --gl-tint-bright: #3e6d84;
62
- --gl-tint-pale: #b7cdd6;
63
- --gl-on-tint: #f2f7fb;
54
+ --gl-text: #211e1b;
55
+ --gl-text-muted: #5c554a;
56
+ --gl-border: #dfd7c6;
57
+ --gl-accent: #234b6e;
58
+ --gl-accent-bright: #2f648e;
59
+ --gl-accent-deep: #15344e;
60
+ --gl-on-accent: #f8f6f0;
61
+ --gl-tint-deep: #182a36;
62
+ --gl-tint: #2c4659;
63
+ --gl-tint-bright: #486980;
64
+ --gl-tint-pale: #c4d3dc;
65
+ --gl-on-tint: #f8f6f0;
64
66
  }
65
67
 
68
+ /* Dark Mode: Antique Study by Candlelight */
69
+ [data-theme="dark"],
70
+ html[data-theme="dark"],
66
71
  [data-mode="dark"] {
67
- --gl-bg: #10161c;
68
- --gl-surface: #171f26;
69
- --gl-surface-raised: #1f2932;
70
- --gl-text: #e8eef2;
71
- --gl-text-muted: #8c8c8c;
72
- --gl-border: #2a3540;
73
- --gl-accent: #3d97ff;
74
- --gl-accent-bright: #6cb2ff;
75
- --gl-accent-deep: #007aff;
76
- --gl-on-accent: #0b1218;
77
- --gl-tint-deep: #142430;
78
- --gl-tint: #2b4356;
79
- --gl-tint-bright: #4d7391;
80
- --gl-tint-pale: #93b7c9;
81
- --gl-on-tint: #0d1720;
72
+ --gl-bg: #141210;
73
+ --gl-surface: #1c1916;
74
+ --gl-surface-raised: #24201c;
75
+ --gl-text: #eae4d5;
76
+ --gl-text-muted: #9e9585;
77
+ --gl-border: #2e2924;
78
+ --gl-accent: #6ea3cc;
79
+ --gl-accent-bright: #8bb9dc;
80
+ --gl-accent-deep: #4a7e9f;
81
+ --gl-on-accent: #141210;
82
+ --gl-tint-deep: #0f181f;
83
+ --gl-tint: #243642;
84
+ --gl-tint-bright: #3d596b;
85
+ --gl-tint-pale: #7a9cb3;
86
+ --gl-on-tint: #eae4d5;
82
87
  }
package/web/i18n.js CHANGED
@@ -63,6 +63,25 @@ export const STRINGS = {
63
63
  menu: "Menu",
64
64
  commands: "Commands",
65
65
  error: "Error",
66
+ theme: "Theme",
67
+ themeAuto: "System",
68
+ themeLight: "Light",
69
+ themeDark: "Dark",
70
+ themeToggle: "Toggle theme",
71
+ layoutWide: "Wide",
72
+ layoutFocus: "Focus",
73
+ toggleWidth: "Toggle wide layout",
74
+ copy: "Copy",
75
+ copied: "Copied!",
76
+ copyNote: "Copy text",
77
+ searchConversations: "Search conversations...",
78
+ export: "Export",
79
+ exportTitle: "Export conversation",
80
+ confirmShortcutHint: "Y to approve, N to decline",
81
+ starbuck: "Reference checks (Starbuck)",
82
+ starbuckHint: "Checks that cited works exist, match their citation and were not retracted. Sub-Sub restarts when you change this.",
83
+ starbuckOn: "Reference checks are on. Use /verify, or ask the researcher to check a manuscript's references.",
84
+ starbuckOff: "Reference checks are off.",
66
85
  },
67
86
  pt: {
68
87
  researcher: "Investigador",
@@ -125,6 +144,25 @@ export const STRINGS = {
125
144
  menu: "Menu",
126
145
  commands: "Comandos",
127
146
  error: "Erro",
147
+ theme: "Tema",
148
+ themeAuto: "Sistema",
149
+ themeLight: "Claro",
150
+ themeDark: "Escuro",
151
+ themeToggle: "Mudar tema",
152
+ layoutWide: "Largo",
153
+ layoutFocus: "Foco",
154
+ toggleWidth: "Mudar largura do ecrã",
155
+ copy: "Copiar",
156
+ copied: "Copiado!",
157
+ copyNote: "Copiar texto",
158
+ searchConversations: "Procurar conversas...",
159
+ export: "Exportar",
160
+ exportTitle: "Exportar conversa",
161
+ confirmShortcutHint: "Y para aprovar, N para recusar",
162
+ starbuck: "Verificação de referências (Starbuck)",
163
+ starbuckHint: "Verifica se as obras citadas existem, correspondem à citação e não foram retratadas. O Sub-Sub reinicia quando muda esta opção.",
164
+ starbuckOn: "A verificação de referências está ligada. Use /verify, ou peça ao investigador que verifique as referências de um manuscrito.",
165
+ starbuckOff: "A verificação de referências está desligada.",
128
166
  },
129
167
  };
130
168
 
@@ -132,11 +170,17 @@ export const STRINGS = {
132
170
  export const ACTIONS = {
133
171
  researcher: [
134
172
  { cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
173
+ { cmd: "lit", fill: true, profiles: ["scholar", "author", "editor"], en: "Literature review", pt: "Revisão da literatura" },
174
+ { cmd: "compare", fill: true, profiles: ["scholar", "author", "editor"], en: "Compare sources", pt: "Comparar fontes" },
135
175
  { cmd: "lit-note", fill: true, en: "Literature note", pt: "Nota de leitura" },
136
176
  { cmd: "synthesis", fill: true, en: "Synthesis", pt: "Síntese" },
137
177
  { cmd: "gaps", fill: true, en: "What is missing", pt: "O que falta" },
138
178
  { cmd: "manuscript", fill: true, en: "Check a manuscript", pt: "Verificar um manuscrito" },
179
+ { cmd: "verify", fill: true, needs: "starbuck", en: "Check references", pt: "Verificar referências" },
180
+ { cmd: "review", fill: true, profiles: ["author", "editor"], en: "Review a manuscript", pt: "Rever um manuscrito" },
139
181
  { cmd: "alert", send: true, en: "New publications", pt: "Novas publicações" },
182
+ { cmd: "request-copy", fill: true, en: "Ask an author for a copy", pt: "Pedir uma cópia ao autor" },
183
+ { cmd: "digest", send: true, profiles: ["scholar", "author", "editor"], en: "Digest of the week", pt: "Resumo da semana" },
140
184
  ],
141
185
  librarian: [
142
186
  { cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
@@ -204,6 +248,14 @@ export const TOOLS = {
204
248
  queue_imports: "queue imports",
205
249
  attach_note: "add a linked note",
206
250
  check_manuscript: "check a manuscript",
251
+ verify_check_manuscript: "check the references",
252
+ search_europepmc: "search Europe PMC",
253
+ search_multi: "search PubMed, Europe PMC and OpenAlex",
254
+ read_oa_fulltext: "read the open-access full text",
255
+ find_contact: "find the author's address",
256
+ verify_check_references: "check references",
257
+ verify_prepare_claims: "prepare the claim check",
258
+ verify_record_claims: "record the claim check",
207
259
  export_bibliography: "export a bibliography",
208
260
  read: "read a file",
209
261
  write: "write a file",
@@ -252,6 +304,14 @@ export const TOOLS = {
252
304
  queue_imports: "pôr importações na fila",
253
305
  attach_note: "acrescentar uma nota ligada",
254
306
  check_manuscript: "verificar um manuscrito",
307
+ verify_check_manuscript: "verificar as referências",
308
+ search_europepmc: "pesquisar na Europe PMC",
309
+ search_multi: "pesquisar na PubMed, Europe PMC e OpenAlex",
310
+ read_oa_fulltext: "ler o texto integral em acesso aberto",
311
+ find_contact: "procurar o endereço do autor",
312
+ verify_check_references: "verificar referências",
313
+ verify_prepare_claims: "preparar a verificação das afirmações",
314
+ verify_record_claims: "registar a verificação das afirmações",
255
315
  export_bibliography: "exportar uma bibliografia",
256
316
  read: "ler um ficheiro",
257
317
  write: "escrever um ficheiro",