@tiagojct/subsub 0.15.0 → 0.17.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/CHANGELOG.md +22 -0
- package/README.md +15 -12
- package/dist/bench.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/config.js +0 -6
- package/dist/doctor.js +1 -2
- package/dist/init.js +7 -20
- package/dist/look.js +2 -2
- package/dist/prompt.js +4 -6
- package/dist/read.js +88 -0
- package/dist/roles.js +4 -10
- package/dist/statement.js +35 -0
- package/dist/subsub.js +102 -47
- package/dist/web.js +122 -9
- package/package.json +1 -1
- package/prompts/lit.md +6 -2
- package/roles/researcher.md +3 -1
- package/src/bench.ts +1 -1
- package/src/cli.ts +2 -2
- package/src/config.ts +1 -11
- package/src/doctor.ts +1 -2
- package/src/init.ts +6 -31
- package/src/look.ts +2 -4
- package/src/prompt.ts +6 -8
- package/src/read.ts +107 -0
- package/src/roles.ts +5 -12
- package/src/statement.ts +45 -0
- package/src/subsub.ts +97 -48
- package/src/usage.ts +1 -1
- package/src/web.ts +114 -9
- package/templates/Zotero agent.md +1 -1
- package/web/app.css +61 -2
- package/web/app.js +215 -18
- package/web/i18n.js +56 -24
- package/web/index.html +17 -16
- package/dist/profiles.js +0 -96
- package/src/profiles.ts +0 -118
package/web/app.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ACTIONS, STRINGS, SUGGESTIONS, TOOLS } from "/i18n.js";
|
|
|
7
7
|
// ---------------------------------------------------------------- state
|
|
8
8
|
|
|
9
9
|
let lang = "en";
|
|
10
|
-
let state = null; // Sub-Sub's own state: mode,
|
|
10
|
+
let state = null; // Sub-Sub's own state: mode, model, library, down
|
|
11
11
|
let session = null; // pi's session state
|
|
12
12
|
let busy = false;
|
|
13
13
|
let commands = [];
|
|
@@ -49,9 +49,27 @@ async function api(path, body) {
|
|
|
49
49
|
// ---------------------------------------------------------------- markdown (no raw HTML, safe links)
|
|
50
50
|
|
|
51
51
|
const SAFE_LINK = /^(https?:|mailto:|zotero:|obsidian:)/i;
|
|
52
|
+
/** A path or name that the notes pane can open: a .md file without a scheme. */
|
|
53
|
+
const NOTE_PATH = /^(?![a-z][\w+.-]*:)[^\n]*\.md$/i;
|
|
54
|
+
const noteLink = (target, inner) => `<a href="#" data-note="${escapeHtml(target)}">${inner}</a>`;
|
|
52
55
|
function escapeHtml(s) {
|
|
53
56
|
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
54
57
|
}
|
|
58
|
+
// [[citekey]] and [[name|label]], as in Obsidian: a link to the note in the notes pane.
|
|
59
|
+
marked.use({
|
|
60
|
+
extensions: [
|
|
61
|
+
{
|
|
62
|
+
name: "wikilink",
|
|
63
|
+
level: "inline",
|
|
64
|
+
start: (src) => src.indexOf("[["),
|
|
65
|
+
tokenizer(src) {
|
|
66
|
+
const m = /^\[\[([^\]|\n#]+)(?:#[^\]|\n]*)?(?:\|([^\]\n]+))?\]\]/.exec(src);
|
|
67
|
+
if (m) return { type: "wikilink", raw: m[0], target: m[1].trim(), label: (m[2] ?? m[1]).trim() };
|
|
68
|
+
},
|
|
69
|
+
renderer: (tok) => noteLink(tok.target, escapeHtml(tok.label)),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
55
73
|
marked.use({
|
|
56
74
|
gfm: true,
|
|
57
75
|
breaks: false,
|
|
@@ -61,9 +79,14 @@ marked.use({
|
|
|
61
79
|
},
|
|
62
80
|
link(token) {
|
|
63
81
|
const text = this.parser.parseInline(token.tokens);
|
|
82
|
+
if (NOTE_PATH.test(decodeURI(token.href ?? ""))) return noteLink(decodeURI(token.href), text);
|
|
64
83
|
if (!SAFE_LINK.test(token.href ?? "")) return text;
|
|
65
84
|
return `<a href="${escapeHtml(token.href)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
66
85
|
},
|
|
86
|
+
codespan(token) {
|
|
87
|
+
const code = `<code>${escapeHtml(token.text)}</code>`;
|
|
88
|
+
return NOTE_PATH.test(token.text) ? noteLink(token.text, code) : code;
|
|
89
|
+
},
|
|
67
90
|
image(token) {
|
|
68
91
|
return escapeHtml(token.text || token.href || "");
|
|
69
92
|
},
|
|
@@ -179,7 +202,6 @@ function renderHeader() {
|
|
|
179
202
|
b.setAttribute("aria-pressed", String(b.dataset.mode === mode()));
|
|
180
203
|
b.title = t(b.dataset.mode === "librarian" ? "librarianHint" : "researcherHint");
|
|
181
204
|
}
|
|
182
|
-
if (state?.profile) $("profile").value = $("profile-side").value = state.profile;
|
|
183
205
|
$("starbuck-toggle").checked = Boolean(state?.starbuck);
|
|
184
206
|
$("starbuck-label").title = t("starbuckHint");
|
|
185
207
|
const m = state?.model ?? session?.model;
|
|
@@ -229,10 +251,7 @@ function renderActions() {
|
|
|
229
251
|
const box = $("actions");
|
|
230
252
|
box.replaceChildren();
|
|
231
253
|
const shown = (a) =>
|
|
232
|
-
hasCommand(a.cmd) &&
|
|
233
|
-
|
|
234
|
-
(!a.needs || state?.[a.needs]) &&
|
|
235
|
-
(!a.profiles || a.profiles.includes(state?.profile ?? "scholar"));
|
|
254
|
+
hasCommand(a.cmd) && (!a.needs || state?.[a.needs]);
|
|
236
255
|
const groups = mode() === "librarian" ? [["library", "groupLibrary"]] : [["research", "groupResearch"], ["library", "groupLibrary"]];
|
|
237
256
|
for (const [group, label] of groups) {
|
|
238
257
|
const list = ACTIONS[group].filter(shown);
|
|
@@ -247,6 +266,7 @@ function actionButton(a) {
|
|
|
247
266
|
type: "button",
|
|
248
267
|
text: a[lang] ?? a.en,
|
|
249
268
|
onclick: () => {
|
|
269
|
+
if (a.item) return $("library-search").focus();
|
|
250
270
|
closeSide();
|
|
251
271
|
if (a.send) sendMessage(`/${a.cmd}`);
|
|
252
272
|
else {
|
|
@@ -269,6 +289,130 @@ function renderBanner() {
|
|
|
269
289
|
} else b.hidden = true;
|
|
270
290
|
}
|
|
271
291
|
|
|
292
|
+
// ---------------------------------------------------------------- library search
|
|
293
|
+
|
|
294
|
+
let libraryTimer = 0;
|
|
295
|
+
let libraryQuery = "";
|
|
296
|
+
|
|
297
|
+
function renderItems(items, message) {
|
|
298
|
+
const ul = $("items");
|
|
299
|
+
ul.replaceChildren();
|
|
300
|
+
if (message) return ul.append(el("li", { class: "muted", text: message }));
|
|
301
|
+
const researcher = mode() === "researcher";
|
|
302
|
+
for (const it of items) {
|
|
303
|
+
const ref = it.citekey || it.key;
|
|
304
|
+
const run = (cmd) => () => {
|
|
305
|
+
closeSide();
|
|
306
|
+
sendMessage(`/${cmd} ${ref}`);
|
|
307
|
+
};
|
|
308
|
+
ul.append(
|
|
309
|
+
el(
|
|
310
|
+
"li",
|
|
311
|
+
{},
|
|
312
|
+
el("div", { class: "item-title", text: it.title || it.key }),
|
|
313
|
+
el("div", { class: "item-meta", text: [it.authors, it.year, it.citekey].filter(Boolean).join(" · ") }),
|
|
314
|
+
el(
|
|
315
|
+
"div",
|
|
316
|
+
{ class: "item-actions" },
|
|
317
|
+
researcher ? el("button", { type: "button", text: t("readItem"), onclick: run("read") }) : null,
|
|
318
|
+
researcher ? el("button", { type: "button", text: t("noteItem"), onclick: run("lit-note") }) : null,
|
|
319
|
+
el("a", { href: `zotero://select/library/items/${encodeURIComponent(it.key)}`, text: "Zotero" }),
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function searchLibrary() {
|
|
327
|
+
const q = $("library-search").value.replace(/\s+/g, " ").trim();
|
|
328
|
+
libraryQuery = q;
|
|
329
|
+
clearTimeout(libraryTimer);
|
|
330
|
+
if (!q) return renderItems([]);
|
|
331
|
+
libraryTimer = setTimeout(async () => {
|
|
332
|
+
renderItems([], t("searching"));
|
|
333
|
+
try {
|
|
334
|
+
const data = await api(`/api/library?q=${encodeURIComponent(q)}`);
|
|
335
|
+
if (data.q !== libraryQuery) return; // an older search answered late
|
|
336
|
+
if (data.error) renderItems([], data.error);
|
|
337
|
+
else renderItems(data.items ?? [], data.items?.length ? "" : t("noItems"));
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (q === libraryQuery) renderItems([], err.message);
|
|
340
|
+
}
|
|
341
|
+
}, 350);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------- notes pane
|
|
345
|
+
|
|
346
|
+
let noteShown = null; // the note on screen, or null for the list
|
|
347
|
+
let notesList = [];
|
|
348
|
+
|
|
349
|
+
function showPane(open) {
|
|
350
|
+
$("note-pane").hidden = !open;
|
|
351
|
+
document.querySelector(".app").classList.toggle("note-open", open);
|
|
352
|
+
$("notes-btn").setAttribute("aria-expanded", String(open));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function renderNoteList() {
|
|
356
|
+
const body = $("note-body");
|
|
357
|
+
const q = $("note-filter").value.trim().toLowerCase();
|
|
358
|
+
const list = notesList.filter((n) => !q || n.rel.toLowerCase().includes(q));
|
|
359
|
+
const fmt = new Intl.DateTimeFormat(lang === "pt" ? "pt-PT" : "en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
360
|
+
body.replaceChildren(
|
|
361
|
+
list.length
|
|
362
|
+
? el(
|
|
363
|
+
"ul",
|
|
364
|
+
{ class: "note-list" },
|
|
365
|
+
list.map((n) =>
|
|
366
|
+
el(
|
|
367
|
+
"li",
|
|
368
|
+
{},
|
|
369
|
+
el("button", { type: "button", onclick: () => openNote(n.path) }, el("span", { class: "rel", text: n.rel }), el("span", { class: "when", text: fmt.format(new Date(n.modified)) })),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
: el("p", { class: "muted small", text: t("noNotes") }),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function showNoteList() {
|
|
378
|
+
noteShown = null;
|
|
379
|
+
$("note-name").textContent = t("notes");
|
|
380
|
+
$("note-back").hidden = true;
|
|
381
|
+
$("note-app").hidden = true;
|
|
382
|
+
$("note-filter").hidden = false;
|
|
383
|
+
showPane(true);
|
|
384
|
+
try {
|
|
385
|
+
notesList = (await api("/api/notes")).notes ?? [];
|
|
386
|
+
} catch (err) {
|
|
387
|
+
notesList = [];
|
|
388
|
+
note(err.message, "warning");
|
|
389
|
+
}
|
|
390
|
+
renderNoteList();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Show a note in the pane; anything that is not a note in the Sub-Sub folder goes to the system, as before. */
|
|
394
|
+
async function openNote(path) {
|
|
395
|
+
let data;
|
|
396
|
+
try {
|
|
397
|
+
data = await api(`/api/note?path=${encodeURIComponent(path)}`);
|
|
398
|
+
} catch {
|
|
399
|
+
return api("/api/open", { path }).catch((err) => note(err.message, "warning"));
|
|
400
|
+
}
|
|
401
|
+
noteShown = data;
|
|
402
|
+
$("note-name").textContent = data.name;
|
|
403
|
+
$("note-name").title = data.path;
|
|
404
|
+
$("note-back").hidden = false;
|
|
405
|
+
$("note-app").hidden = false;
|
|
406
|
+
$("note-filter").hidden = true;
|
|
407
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(data.text);
|
|
408
|
+
const content = el("div", { class: "md" });
|
|
409
|
+
content.innerHTML = md(front ? data.text.slice(front[0].length) : data.text);
|
|
410
|
+
attachCopyButtons(content);
|
|
411
|
+
$("note-body").replaceChildren(front ? el("details", { class: "front" }, el("summary", { text: t("properties") }), el("pre", { text: front[1] })) : "", content);
|
|
412
|
+
$("note-body").scrollTop = 0;
|
|
413
|
+
showPane(true);
|
|
414
|
+
}
|
|
415
|
+
|
|
272
416
|
let loadedSessions = [];
|
|
273
417
|
|
|
274
418
|
function renderSessionsList(filterText = "") {
|
|
@@ -283,12 +427,30 @@ function renderSessionsList(filterText = "") {
|
|
|
283
427
|
return;
|
|
284
428
|
}
|
|
285
429
|
const fmt = new Intl.DateTimeFormat(lang === "pt" ? "pt-PT" : "en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
430
|
+
// shortcut: the browser's own prompt and confirm; move to the page's dialog if they look out of place.
|
|
431
|
+
const rename = (s) => async () => {
|
|
432
|
+
const name = window.prompt(t("renamePrompt"), s.name || s.first || "");
|
|
433
|
+
if (!name?.trim()) return;
|
|
434
|
+
await api("/api/session/name", { path: s.path, name }).catch((err) => note(err.message, "warning"));
|
|
435
|
+
loadSessions();
|
|
436
|
+
};
|
|
437
|
+
const trash = (s) => async () => {
|
|
438
|
+
if (!window.confirm(t("trashConfirm"))) return;
|
|
439
|
+
await api("/api/session/trash", { path: s.path }).catch((err) => note(err.message, "warning"));
|
|
440
|
+
loadSessions();
|
|
441
|
+
};
|
|
286
442
|
for (const s of filtered) {
|
|
287
443
|
const current = session?.sessionFile === s.path;
|
|
288
444
|
ul.append(
|
|
289
445
|
el(
|
|
290
446
|
"li",
|
|
291
447
|
{ class: current ? "current" : "" },
|
|
448
|
+
el(
|
|
449
|
+
"span",
|
|
450
|
+
{ class: "session-tools" },
|
|
451
|
+
el("button", { type: "button", text: t("rename"), onclick: rename(s) }),
|
|
452
|
+
current ? null : el("button", { type: "button", text: t("trash"), onclick: trash(s) }),
|
|
453
|
+
),
|
|
292
454
|
el(
|
|
293
455
|
"button",
|
|
294
456
|
{
|
|
@@ -333,18 +495,30 @@ function exportConversation() {
|
|
|
333
495
|
note(t("noConversations"), "warning");
|
|
334
496
|
return;
|
|
335
497
|
}
|
|
498
|
+
const fence = (x) => `\`\`\`text\n${x.length > 3000 ? `${x.slice(0, 3000)}\n...` : x}\n\`\`\``;
|
|
336
499
|
for (const m of items) {
|
|
337
500
|
if (m.classList.contains("user")) {
|
|
338
|
-
const
|
|
339
|
-
|
|
501
|
+
const b = m.querySelector(".bubble");
|
|
502
|
+
// A long message is folded: export its full text, not the summary line.
|
|
503
|
+
const text = (b?.tagName === "DETAILS" ? b.querySelector("div")?.innerText : b?.innerText) ?? "";
|
|
504
|
+
if (text && text !== "/subsub-refresh") msgs.push(`### ${t("exportUser")}\n\n${text}\n`);
|
|
340
505
|
} else if (m.classList.contains("assistant")) {
|
|
341
|
-
const
|
|
342
|
-
|
|
506
|
+
const parts = [];
|
|
507
|
+
for (const c of m.children) {
|
|
508
|
+
if (c.classList.contains("md")) parts.push(c.innerText);
|
|
509
|
+
else if (c.classList.contains("tool")) {
|
|
510
|
+
const label = c.querySelector(".label")?.textContent ?? "";
|
|
511
|
+
const args = c.querySelector(".args")?.textContent ?? "";
|
|
512
|
+
const out = c.querySelector("pre")?.innerText;
|
|
513
|
+
parts.push(`> ${label}${args ? `: ${args}` : ""}${out ? `\n\n${fence(out)}` : ""}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (parts.length) msgs.push(`### Sub-Sub\n\n${parts.join("\n\n")}\n`);
|
|
343
517
|
}
|
|
344
518
|
}
|
|
345
519
|
if (!msgs.length) return;
|
|
346
520
|
const date = new Date().toISOString().slice(0, 10);
|
|
347
|
-
const content = `#
|
|
521
|
+
const content = `# ${t("exportHeading")} (${date})\n\n${t("exportMode")}: ${t(mode())}\n\n---\n\n${msgs.join("\n---\n\n")}`;
|
|
348
522
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
|
|
349
523
|
const url = URL.createObjectURL(blob);
|
|
350
524
|
const a = document.createElement("a");
|
|
@@ -521,7 +695,7 @@ function updateTool(row, name, args) {
|
|
|
521
695
|
text: t("open"),
|
|
522
696
|
onclick: (e) => {
|
|
523
697
|
e.preventDefault();
|
|
524
|
-
|
|
698
|
+
openNote(args.path);
|
|
525
699
|
},
|
|
526
700
|
}),
|
|
527
701
|
);
|
|
@@ -847,7 +1021,7 @@ function dialogTitle(d) {
|
|
|
847
1021
|
// The program sends the title in the user's language; both forms get the same friendly title.
|
|
848
1022
|
if ((m = raw.match(/^(?:apply|aplicar) (\S+)\?$/))) return t("apply", { x: toolLabel(m[1]) });
|
|
849
1023
|
if ((m = raw.match(/^(?:run|executar) (\S+)\?$/))) return t("run", { x: toolLabel(m[1]) });
|
|
850
|
-
if (raw === "undo?" || raw === "anular?") return t("
|
|
1024
|
+
if (raw === "undo?" || raw === "anular?") return t("undoAsk");
|
|
851
1025
|
if ((m = raw.match(/^(?:write the Starbuck report to|escrever o relatório do Starbuck em) (.+)\?$/))) return t("starbuckReport", { x: m[1] });
|
|
852
1026
|
if ((m = raw.match(/^(write|edit) (.+)\?$/))) return t("write", { x: m[2] });
|
|
853
1027
|
return raw;
|
|
@@ -895,6 +1069,7 @@ function nextDialog() {
|
|
|
895
1069
|
body.replaceChildren();
|
|
896
1070
|
const buttons = $("dialog-buttons");
|
|
897
1071
|
buttons.replaceChildren();
|
|
1072
|
+
buttons.classList.toggle("choices", d.method === "select" && (d.options ?? []).some((o) => o.length > 24));
|
|
898
1073
|
const answer = async (reply) => {
|
|
899
1074
|
try {
|
|
900
1075
|
await api("/api/dialog", { id: d.id, ...reply });
|
|
@@ -1120,12 +1295,20 @@ let suggestIndex = 0;
|
|
|
1120
1295
|
function suggest() {
|
|
1121
1296
|
const box = $("suggest");
|
|
1122
1297
|
const v = input.value;
|
|
1298
|
+
// "/lit-note " (a command and a space): show what it needs.
|
|
1299
|
+
const chosen = /^\/(\S+) $/.exec(v) && commands.find((c) => c.name === v.slice(1, -1));
|
|
1300
|
+
if (chosen?.hint) {
|
|
1301
|
+
box.replaceChildren(el("div", { class: "hint-row" }, el("span", { class: "name", text: `/${chosen.name}` }), el("span", { class: "desc", text: chosen.hint })));
|
|
1302
|
+
box.hidden = false;
|
|
1303
|
+
box.list = null;
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1123
1306
|
if (!/^\/\S*$/.test(v)) {
|
|
1124
1307
|
box.hidden = true;
|
|
1125
1308
|
return;
|
|
1126
1309
|
}
|
|
1127
1310
|
const q = v.slice(1).toLowerCase();
|
|
1128
|
-
const list = commands.filter((c) => c.name
|
|
1311
|
+
const list = commands.filter((c) => !c.name.startsWith("subsub-") && c.name.toLowerCase().startsWith(q)).slice(0, 12);
|
|
1129
1312
|
if (!list.length) {
|
|
1130
1313
|
box.hidden = true;
|
|
1131
1314
|
return;
|
|
@@ -1141,7 +1324,7 @@ function suggest() {
|
|
|
1141
1324
|
"aria-selected": String(i === suggestIndex),
|
|
1142
1325
|
onclick: () => pick(c),
|
|
1143
1326
|
},
|
|
1144
|
-
el("span", { class: "name", text: `/${c.name}` }),
|
|
1327
|
+
el("span", { class: "name", text: c.hint ? `/${c.name} ${c.hint}` : `/${c.name}` }),
|
|
1145
1328
|
el("span", { class: "desc", text: (c.description ?? "").replace(/^Sub-Sub:\s*/, "") }),
|
|
1146
1329
|
),
|
|
1147
1330
|
),
|
|
@@ -1151,8 +1334,8 @@ function suggest() {
|
|
|
1151
1334
|
}
|
|
1152
1335
|
function pick(c) {
|
|
1153
1336
|
input.value = `/${c.name} `;
|
|
1154
|
-
$("suggest").hidden = true;
|
|
1155
1337
|
input.focus();
|
|
1338
|
+
suggest();
|
|
1156
1339
|
}
|
|
1157
1340
|
|
|
1158
1341
|
input.addEventListener("input", () => {
|
|
@@ -1206,7 +1389,6 @@ for (const b of document.querySelectorAll(".modes button")) {
|
|
|
1206
1389
|
if (b.dataset.mode !== mode()) sendMessage(`/${b.dataset.mode}`);
|
|
1207
1390
|
});
|
|
1208
1391
|
}
|
|
1209
|
-
$("profile").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
|
|
1210
1392
|
$("starbuck-toggle").addEventListener("change", async (e) => {
|
|
1211
1393
|
const on = e.target.checked;
|
|
1212
1394
|
e.target.disabled = true;
|
|
@@ -1220,7 +1402,6 @@ $("starbuck-toggle").addEventListener("change", async (e) => {
|
|
|
1220
1402
|
e.target.disabled = false;
|
|
1221
1403
|
}
|
|
1222
1404
|
});
|
|
1223
|
-
$("profile-side").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
|
|
1224
1405
|
$("model-btn").addEventListener("click", showModels);
|
|
1225
1406
|
$("model-btn-side").addEventListener("click", () => {
|
|
1226
1407
|
closeSide();
|
|
@@ -1251,6 +1432,22 @@ $("theme-btn-side")?.addEventListener("click", () => {
|
|
|
1251
1432
|
toggleTheme();
|
|
1252
1433
|
});
|
|
1253
1434
|
$("session-search")?.addEventListener("input", (e) => renderSessionsList(e.target.value));
|
|
1435
|
+
$("library-search").addEventListener("input", searchLibrary);
|
|
1436
|
+
$("notes-btn").addEventListener("click", () => ($("note-pane").hidden ? showNoteList() : showPane(false)));
|
|
1437
|
+
$("note-back").addEventListener("click", showNoteList);
|
|
1438
|
+
$("note-close").addEventListener("click", () => showPane(false));
|
|
1439
|
+
$("note-app").addEventListener("click", () => noteShown && api("/api/open", { path: noteShown.path }).catch((err) => note(err.message, "warning")));
|
|
1440
|
+
$("note-filter").addEventListener("input", renderNoteList);
|
|
1441
|
+
$("note-pane").addEventListener("keydown", (e) => {
|
|
1442
|
+
if (e.key === "Escape") showPane(false);
|
|
1443
|
+
});
|
|
1444
|
+
// Links to notes, in replies and in notes: [[wikilinks]], .md paths in code, relative .md links.
|
|
1445
|
+
document.addEventListener("click", (e) => {
|
|
1446
|
+
const a = e.target.closest?.("a[data-note]");
|
|
1447
|
+
if (!a) return;
|
|
1448
|
+
e.preventDefault();
|
|
1449
|
+
openNote(a.dataset.note);
|
|
1450
|
+
});
|
|
1254
1451
|
$("export-btn")?.addEventListener("click", exportConversation);
|
|
1255
1452
|
$("quit-btn").addEventListener("click", async () => {
|
|
1256
1453
|
try {
|
package/web/i18n.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
export const STRINGS = {
|
|
5
5
|
en: {
|
|
6
|
-
profile: "Profile",
|
|
7
6
|
model: "Model",
|
|
8
7
|
noModel: "Connect a model",
|
|
9
8
|
noModelSend: "No model is connected yet. Choose a provider and save its key; your message is still in the box.",
|
|
@@ -20,7 +19,7 @@ export const STRINGS = {
|
|
|
20
19
|
close: "Close",
|
|
21
20
|
apply: "Apply {x}?",
|
|
22
21
|
run: "Run {x}?",
|
|
23
|
-
|
|
22
|
+
undoAsk: "Undo this change?",
|
|
24
23
|
write: "Allow writing to {x}?",
|
|
25
24
|
starbuckReport: "Write the Starbuck report to {x}?",
|
|
26
25
|
approveHint: "Nothing changes until you select Yes.",
|
|
@@ -31,9 +30,7 @@ export const STRINGS = {
|
|
|
31
30
|
toReviewHint: "Items that Sub-Sub tagged and that wait for your check (they carry the _agent tag). Select Library overview, or ask Sub-Sub to list them.",
|
|
32
31
|
serversDown: "Not running: {x}",
|
|
33
32
|
working: "Working",
|
|
34
|
-
using: "Using: {x}",
|
|
35
33
|
failed: "failed",
|
|
36
|
-
result: "Result",
|
|
37
34
|
open: "Open",
|
|
38
35
|
stopped: "Stopped.",
|
|
39
36
|
agentExit: "Sub-Sub stopped.",
|
|
@@ -91,7 +88,6 @@ export const STRINGS = {
|
|
|
91
88
|
quitDone: "Sub-Sub is closed. You can close this tab.",
|
|
92
89
|
earlier: "Earlier messages were summarised.",
|
|
93
90
|
menu: "Menu",
|
|
94
|
-
commands: "Commands",
|
|
95
91
|
error: "Error",
|
|
96
92
|
theme: "Theme",
|
|
97
93
|
themeAuto: "System",
|
|
@@ -99,7 +95,6 @@ export const STRINGS = {
|
|
|
99
95
|
themeDark: "Dark",
|
|
100
96
|
themeToggle: "Toggle theme",
|
|
101
97
|
layoutWide: "Wide",
|
|
102
|
-
layoutFocus: "Focus",
|
|
103
98
|
toggleWidth: "Toggle wide layout",
|
|
104
99
|
copy: "Copy",
|
|
105
100
|
copied: "Copied",
|
|
@@ -107,6 +102,25 @@ export const STRINGS = {
|
|
|
107
102
|
searchConversations: "Search conversations",
|
|
108
103
|
export: "Export",
|
|
109
104
|
exportTitle: "Export conversation",
|
|
105
|
+
exportHeading: "Sub-Sub conversation",
|
|
106
|
+
exportUser: "You",
|
|
107
|
+
exportMode: "Mode",
|
|
108
|
+
notes: "Notes",
|
|
109
|
+
notesHint: "The notes in your Sub-Sub folder",
|
|
110
|
+
allNotes: "All notes",
|
|
111
|
+
properties: "Properties",
|
|
112
|
+
filterNotes: "Filter notes",
|
|
113
|
+
noNotes: "No notes yet.",
|
|
114
|
+
openInApp: "Open in app",
|
|
115
|
+
librarySearch: "Search your library",
|
|
116
|
+
searching: "Searching...",
|
|
117
|
+
noItems: "Nothing found.",
|
|
118
|
+
readItem: "Read",
|
|
119
|
+
noteItem: "Note",
|
|
120
|
+
rename: "Rename",
|
|
121
|
+
renamePrompt: "A new name for this conversation",
|
|
122
|
+
trash: "Delete",
|
|
123
|
+
trashConfirm: "Move this conversation to the trash folder (~/.subsub/trash)? You can move it back from there.",
|
|
110
124
|
confirmShortcutHint: "Y to approve, N to decline",
|
|
111
125
|
starbuck: "Reference checks (Starbuck)",
|
|
112
126
|
starbuckHint: "Checks that cited works exist, match their citation and were not retracted. Sub-Sub restarts when you change this.",
|
|
@@ -128,7 +142,6 @@ export const STRINGS = {
|
|
|
128
142
|
groupLibrary: "Library",
|
|
129
143
|
},
|
|
130
144
|
pt: {
|
|
131
|
-
profile: "Perfil",
|
|
132
145
|
model: "Modelo",
|
|
133
146
|
noModel: "Ligar um modelo",
|
|
134
147
|
noModelSend: "Ainda não há um modelo ligado. Escolha um fornecedor e guarde a chave; a sua mensagem continua na caixa.",
|
|
@@ -145,7 +158,7 @@ export const STRINGS = {
|
|
|
145
158
|
close: "Fechar",
|
|
146
159
|
apply: "Confirmar: {x}?",
|
|
147
160
|
run: "Executar {x}?",
|
|
148
|
-
|
|
161
|
+
undoAsk: "Desfazer esta alteração?",
|
|
149
162
|
write: "Permitir escrever em {x}?",
|
|
150
163
|
starbuckReport: "Escrever o relatório do Starbuck em {x}?",
|
|
151
164
|
approveHint: "Nada muda até selecionar Sim.",
|
|
@@ -156,9 +169,7 @@ export const STRINGS = {
|
|
|
156
169
|
toReviewHint: "Itens que o Sub-Sub etiquetou e que esperam a sua verificação (têm a etiqueta _agent). Selecione Resumo da biblioteca, ou peça ao Sub-Sub para os listar.",
|
|
157
170
|
serversDown: "Parados: {x}",
|
|
158
171
|
working: "A trabalhar",
|
|
159
|
-
using: "A usar: {x}",
|
|
160
172
|
failed: "falhou",
|
|
161
|
-
result: "Resultado",
|
|
162
173
|
open: "Abrir",
|
|
163
174
|
stopped: "Parado.",
|
|
164
175
|
agentExit: "O Sub-Sub parou.",
|
|
@@ -216,7 +227,6 @@ export const STRINGS = {
|
|
|
216
227
|
quitDone: "O Sub-Sub está fechado. Pode fechar este separador.",
|
|
217
228
|
earlier: "As mensagens anteriores foram resumidas.",
|
|
218
229
|
menu: "Menu",
|
|
219
|
-
commands: "Comandos",
|
|
220
230
|
error: "Erro",
|
|
221
231
|
theme: "Tema",
|
|
222
232
|
themeAuto: "Sistema",
|
|
@@ -224,7 +234,6 @@ export const STRINGS = {
|
|
|
224
234
|
themeDark: "Escuro",
|
|
225
235
|
themeToggle: "Mudar tema",
|
|
226
236
|
layoutWide: "Largo",
|
|
227
|
-
layoutFocus: "Foco",
|
|
228
237
|
toggleWidth: "Mudar largura do ecrã",
|
|
229
238
|
copy: "Copiar",
|
|
230
239
|
copied: "Copiado",
|
|
@@ -232,6 +241,25 @@ export const STRINGS = {
|
|
|
232
241
|
searchConversations: "Procurar conversas",
|
|
233
242
|
export: "Exportar",
|
|
234
243
|
exportTitle: "Exportar conversa",
|
|
244
|
+
exportHeading: "Conversa do Sub-Sub",
|
|
245
|
+
exportUser: "Utilizador",
|
|
246
|
+
exportMode: "Modo",
|
|
247
|
+
notes: "Notas",
|
|
248
|
+
notesHint: "As notas da sua pasta do Sub-Sub",
|
|
249
|
+
allNotes: "Todas as notas",
|
|
250
|
+
properties: "Propriedades",
|
|
251
|
+
filterNotes: "Filtrar notas",
|
|
252
|
+
noNotes: "Ainda não há notas.",
|
|
253
|
+
openInApp: "Abrir na aplicação",
|
|
254
|
+
librarySearch: "Pesquisar na biblioteca",
|
|
255
|
+
searching: "A pesquisar...",
|
|
256
|
+
noItems: "Nada encontrado.",
|
|
257
|
+
readItem: "Ler",
|
|
258
|
+
noteItem: "Nota",
|
|
259
|
+
rename: "Mudar o nome",
|
|
260
|
+
renamePrompt: "Um novo nome para esta conversa",
|
|
261
|
+
trash: "Apagar",
|
|
262
|
+
trashConfirm: "Mover esta conversa para a pasta do lixo (~/.subsub/trash)? Pode trazê-la de volta a partir daí.",
|
|
235
263
|
confirmShortcutHint: "S para aprovar, N para recusar",
|
|
236
264
|
starbuck: "Verificação de referências (Starbuck)",
|
|
237
265
|
starbuckHint: "Verifica se as obras citadas existem, correspondem à citação e não foram retratadas. O Sub-Sub reinicia quando muda esta opção.",
|
|
@@ -255,22 +283,26 @@ export const STRINGS = {
|
|
|
255
283
|
};
|
|
256
284
|
|
|
257
285
|
/**
|
|
258
|
-
* Buttons in the side panel, in two groups.
|
|
259
|
-
*
|
|
286
|
+
* Buttons in the side panel, in two groups. A button puts the command in the message box; `send` runs it at once;
|
|
287
|
+
* `item` opens the library search, whose results have their own buttons.
|
|
288
|
+
* The Librarian sees the Library group only. `needs` limits a button to an add-on.
|
|
260
289
|
*/
|
|
261
290
|
export const ACTIONS = {
|
|
262
291
|
research: [
|
|
263
|
-
{ cmd: "
|
|
264
|
-
{ cmd: "
|
|
265
|
-
{ cmd: "lit
|
|
266
|
-
{ cmd: "
|
|
267
|
-
{ cmd: "
|
|
268
|
-
{ cmd: "
|
|
269
|
-
{ cmd: "
|
|
270
|
-
{ cmd: "
|
|
292
|
+
{ cmd: "read", send: true, en: "Read a paper", pt: "Ler um artigo" },
|
|
293
|
+
{ cmd: "lit", en: "Literature review", pt: "Revisão da literatura" },
|
|
294
|
+
{ cmd: "lit reading list:", en: "Reading list", pt: "Lista de leitura" },
|
|
295
|
+
{ cmd: "compare", en: "Compare sources", pt: "Comparar fontes" },
|
|
296
|
+
{ cmd: "lit-note", item: true, en: "Literature note", pt: "Nota de leitura" },
|
|
297
|
+
{ cmd: "synthesis", en: "Synthesis", pt: "Síntese" },
|
|
298
|
+
{ cmd: "gaps", en: "What is missing", pt: "O que falta" },
|
|
299
|
+
{ cmd: "manuscript", en: "Check a manuscript", pt: "Verificar um manuscrito" },
|
|
300
|
+
{ cmd: "verify", needs: "starbuck", en: "Check references", pt: "Verificar referências" },
|
|
301
|
+
{ cmd: "review", en: "Review a manuscript", pt: "Rever um manuscrito" },
|
|
271
302
|
{ cmd: "alert", send: true, en: "New publications", pt: "Novas publicações" },
|
|
272
|
-
{ cmd: "request-copy",
|
|
273
|
-
{ cmd: "
|
|
303
|
+
{ cmd: "request-copy", en: "Ask an author for a copy", pt: "Pedir uma cópia ao autor" },
|
|
304
|
+
{ cmd: "ai-statement", en: "Statement on AI use", pt: "Declaração de uso de IA" },
|
|
305
|
+
{ cmd: "digest", send: true, en: "Digest of the week", pt: "Resumo da semana" },
|
|
274
306
|
],
|
|
275
307
|
library: [
|
|
276
308
|
{ cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
|
package/web/index.html
CHANGED
|
@@ -21,14 +21,7 @@
|
|
|
21
21
|
</div>
|
|
22
22
|
<div class="status">
|
|
23
23
|
<span class="library" id="library"><span class="library-dot" aria-hidden="true"></span><span class="library-text"></span></span>
|
|
24
|
-
<
|
|
25
|
-
<select id="profile">
|
|
26
|
-
<option value="reader">Reader</option>
|
|
27
|
-
<option value="scholar">Scholar</option>
|
|
28
|
-
<option value="author">Author</option>
|
|
29
|
-
<option value="editor">Editor</option>
|
|
30
|
-
</select>
|
|
31
|
-
</label>
|
|
24
|
+
<button type="button" class="chip-btn" id="notes-btn" aria-controls="note-pane" aria-expanded="false" data-i18n="notes" data-i18n-title="notesHint">Notes</button>
|
|
32
25
|
<button type="button" class="chip-btn model-btn wide-only" id="model-btn"></button>
|
|
33
26
|
<button type="button" class="icon-btn wide-only" id="width-btn" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h16M4 12l3.5-3.5M4 12l3.5 3.5M20 12l-3.5-3.5M20 12l-3.5 3.5"/></svg><span class="visually-hidden" id="width-btn-text">Wide</span></button>
|
|
34
27
|
<button type="button" class="icon-btn" id="theme-btn"></button>
|
|
@@ -38,14 +31,6 @@
|
|
|
38
31
|
<aside class="side" id="side">
|
|
39
32
|
<button type="button" class="new-btn" id="new-btn"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span data-i18n="newConversation">New conversation</span></button>
|
|
40
33
|
<section class="phone-only settings">
|
|
41
|
-
<label><span data-i18n="profile">Profile</span>
|
|
42
|
-
<select id="profile-side">
|
|
43
|
-
<option value="reader">Reader</option>
|
|
44
|
-
<option value="scholar">Scholar</option>
|
|
45
|
-
<option value="author">Author</option>
|
|
46
|
-
<option value="editor">Editor</option>
|
|
47
|
-
</select>
|
|
48
|
-
</label>
|
|
49
34
|
<label><span data-i18n="model">Model</span>
|
|
50
35
|
<button type="button" class="secondary model-btn" id="model-btn-side"></button>
|
|
51
36
|
</label>
|
|
@@ -53,6 +38,11 @@
|
|
|
53
38
|
<button type="button" class="secondary" id="theme-btn-side"></button>
|
|
54
39
|
</label>
|
|
55
40
|
</section>
|
|
41
|
+
<section>
|
|
42
|
+
<h2 data-i18n="groupLibrary">Library</h2>
|
|
43
|
+
<input type="search" id="library-search" class="search" data-i18n-placeholder="librarySearch" placeholder="Search your library" autocomplete="off">
|
|
44
|
+
<ul class="items" id="items" aria-live="polite"></ul>
|
|
45
|
+
</section>
|
|
56
46
|
<section>
|
|
57
47
|
<h2 data-i18n="actions">Actions</h2>
|
|
58
48
|
<div class="actions" id="actions"></div>
|
|
@@ -91,6 +81,17 @@
|
|
|
91
81
|
</div>
|
|
92
82
|
</form>
|
|
93
83
|
</main>
|
|
84
|
+
|
|
85
|
+
<aside class="note-pane" id="note-pane" hidden>
|
|
86
|
+
<div class="note-head">
|
|
87
|
+
<button type="button" class="link-btn" id="note-back" data-i18n="allNotes">All notes</button>
|
|
88
|
+
<span class="note-name" id="note-name"></span>
|
|
89
|
+
<button type="button" class="link-btn" id="note-app" data-i18n="openInApp" hidden>Open in app</button>
|
|
90
|
+
<button type="button" class="link-btn" id="note-close" data-i18n="close">Close</button>
|
|
91
|
+
</div>
|
|
92
|
+
<input type="search" id="note-filter" class="search" data-i18n-placeholder="filterNotes" placeholder="Filter notes" autocomplete="off">
|
|
93
|
+
<div class="note-body" id="note-body"></div>
|
|
94
|
+
</aside>
|
|
94
95
|
</div>
|
|
95
96
|
|
|
96
97
|
<dialog id="dialog" class="modal">
|