@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/LICENSE +21 -0
- package/README.md +200 -61
- package/THIRD_PARTY_NOTICES +55 -0
- package/dist/bench.js +60 -5
- package/dist/config.js +50 -18
- package/dist/doctor.js +23 -2
- package/dist/init.js +26 -11
- package/dist/notes.js +42 -0
- package/dist/profiles.js +15 -0
- package/dist/prompt.js +3 -0
- package/dist/roles.js +7 -1
- package/dist/subsub.js +94 -9
- package/dist/web.js +17 -3
- package/docs/Subsub.md +4 -2
- package/package.json +7 -3
- package/prompts/compare.md +23 -0
- package/prompts/digest.md +23 -0
- package/prompts/lit.md +87 -0
- package/prompts/request-copy.md +35 -0
- package/prompts/review.md +31 -0
- package/prompts/verify.md +18 -0
- package/roles/researcher.md +18 -5
- package/src/bench.ts +63 -4
- package/src/config.ts +61 -18
- package/src/doctor.ts +32 -2
- package/src/init.ts +42 -11
- package/src/notes.ts +46 -0
- package/src/profiles.ts +17 -0
- package/src/prompt.ts +6 -0
- package/src/roles.ts +8 -1
- package/src/subsub.ts +100 -9
- package/src/web.ts +15 -3
- package/templates/Zotero agent.md +2 -0
- package/themes/subsub-glauca-dark.json +34 -34
- package/themes/subsub-glauca-light.json +32 -32
- package/themes/subsub-try-works-dark.json +34 -34
- package/themes/subsub-try-works-light.json +34 -34
- package/web/app.css +821 -131
- package/web/app.js +270 -26
- package/web/glauca.css +54 -49
- package/web/i18n.js +60 -0
- package/web/icon.svg +12 -1
- package/web/index.html +21 -4
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
|
-
// ----------------------------------------------------------------
|
|
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")
|
|
101
|
-
|
|
102
|
-
lib.
|
|
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
|
-
|
|
184
|
+
txt.textContent = `Zotero: ${parts.join(", ")}`;
|
|
185
|
+
lib.classList.add("reachable");
|
|
108
186
|
}
|
|
109
187
|
if (state?.down?.length) {
|
|
110
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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")
|
|
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")
|
|
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",
|
|
1033
|
+
$("model-btn").addEventListener("click", showModels);
|
|
812
1034
|
$("model-btn-side").addEventListener("click", () => {
|
|
813
1035
|
closeSide();
|
|
814
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
/*
|
|
1
|
+
/* Sub-Sub Glauca tokens: Vintage & Minimal Bookmaking System */
|
|
2
2
|
:root {
|
|
3
|
-
--gl-font-serif: "
|
|
4
|
-
--gl-font-sans: "
|
|
5
|
-
--gl-font-mono: "
|
|
6
|
-
--gl-font-reading: "
|
|
7
|
-
--gl-text-xs: 0.
|
|
8
|
-
--gl-text-sm: 0.
|
|
9
|
-
--gl-text-base:
|
|
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.
|
|
12
|
-
--gl-text-2xl: 1.
|
|
13
|
-
--gl-text-3xl: 2.
|
|
14
|
-
--gl-text-4xl: 3.
|
|
15
|
-
--gl-text-display: clamp(2.
|
|
16
|
-
--gl-text-poster: clamp(3rem,
|
|
17
|
-
--gl-leading-tight: 1.
|
|
18
|
-
--gl-leading-snug: 1.
|
|
19
|
-
--gl-leading-normal: 1.
|
|
20
|
-
--gl-leading-relaxed: 1.
|
|
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
|
-
/*
|
|
46
|
+
/* Light Default: Warm Archival Paper & Printer's Inks */
|
|
47
47
|
:root,
|
|
48
|
-
[data-mode="light"]
|
|
49
|
-
|
|
50
|
-
|
|
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: #
|
|
53
|
-
--gl-text-muted: #
|
|
54
|
-
--gl-border: #
|
|
55
|
-
--gl-accent: #
|
|
56
|
-
--gl-accent-bright: #
|
|
57
|
-
--gl-accent-deep: #
|
|
58
|
-
--gl-on-accent: #
|
|
59
|
-
--gl-tint-deep: #
|
|
60
|
-
--gl-tint: #
|
|
61
|
-
--gl-tint-bright: #
|
|
62
|
-
--gl-tint-pale: #
|
|
63
|
-
--gl-on-tint: #
|
|
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: #
|
|
68
|
-
--gl-surface: #
|
|
69
|
-
--gl-surface-raised: #
|
|
70
|
-
--gl-text: #
|
|
71
|
-
--gl-text-muted: #
|
|
72
|
-
--gl-border: #
|
|
73
|
-
--gl-accent: #
|
|
74
|
-
--gl-accent-bright: #
|
|
75
|
-
--gl-accent-deep: #
|
|
76
|
-
--gl-on-accent: #
|
|
77
|
-
--gl-tint-deep: #
|
|
78
|
-
--gl-tint: #
|
|
79
|
-
--gl-tint-bright: #
|
|
80
|
-
--gl-tint-pale: #
|
|
81
|
-
--gl-on-tint: #
|
|
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",
|