@tiagojct/subsub 0.17.0 → 0.19.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 +25 -0
- package/README.md +4 -3
- package/THIRD_PARTY_NOTICES +98 -96
- package/dist/config.js +1 -1
- package/dist/feedback.js +55 -0
- package/dist/notes.js +41 -11
- package/dist/read.js +2 -1
- package/dist/subsub.js +80 -6
- package/dist/web.js +22 -5
- package/package.json +1 -1
- package/prompts/compare.md +1 -1
- package/prompts/gaps.md +1 -1
- package/prompts/lit.md +26 -7
- package/roles/researcher.md +5 -2
- package/src/config.ts +1 -1
- package/src/feedback.ts +70 -0
- package/src/notes.ts +44 -10
- package/src/read.ts +2 -1
- package/src/subsub.ts +83 -9
- package/src/web.ts +20 -5
- package/templates/Zotero agent.md +1 -1
- package/web/app.css +211 -168
- package/web/app.js +100 -6
- package/web/fonts/AtkinsonHyperlegibleMono.woff2 +0 -0
- package/web/fonts/AtkinsonHyperlegibleNext.woff2 +0 -0
- package/web/fonts/Literata-Italic.woff2 +0 -0
- package/web/fonts/Literata.woff2 +0 -0
- package/web/i18n.js +10 -1
- package/web/icon.svg +4 -4
- package/web/index.html +2 -1
- package/web/fonts/IBMPlexMono.woff2 +0 -0
- package/web/fonts/IBMPlexSans.woff2 +0 -0
- package/web/fonts/IBMPlexSerif.woff2 +0 -0
package/web/app.js
CHANGED
|
@@ -83,11 +83,21 @@ marked.use({
|
|
|
83
83
|
if (!SAFE_LINK.test(token.href ?? "")) return text;
|
|
84
84
|
return `<a href="${escapeHtml(token.href)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
85
85
|
},
|
|
86
|
+
code(token) {
|
|
87
|
+
return (token.lang === "mermaid" && flowHtml(token.text)) || false;
|
|
88
|
+
},
|
|
86
89
|
codespan(token) {
|
|
87
90
|
const code = `<code>${escapeHtml(token.text)}</code>`;
|
|
88
91
|
return NOTE_PATH.test(token.text) ? noteLink(token.text, code) : code;
|
|
89
92
|
},
|
|
90
93
|
image(token) {
|
|
94
|
+
// In the notes pane, an image next to the note (a figure cut out of the PDF) is shown;
|
|
95
|
+
// anywhere else, and for any other address, only its description.
|
|
96
|
+
const href = decodeURI(token.href ?? "");
|
|
97
|
+
if (noteDir && /^(?![a-z][\w+.-]*:|[\\/])[^?#]+\.(png|jpe?g|gif|webp)$/i.test(href)) {
|
|
98
|
+
const src = `/api/note-file?path=${encodeURIComponent(`${noteDir}/${href}`)}`;
|
|
99
|
+
return `<img class="note-img" src="${escapeHtml(src)}" alt="${escapeHtml(token.text ?? "")}" loading="lazy">`;
|
|
100
|
+
}
|
|
91
101
|
return escapeHtml(token.text || token.href || "");
|
|
92
102
|
},
|
|
93
103
|
table(token) {
|
|
@@ -110,6 +120,45 @@ marked.use({
|
|
|
110
120
|
},
|
|
111
121
|
},
|
|
112
122
|
});
|
|
123
|
+
/**
|
|
124
|
+
* The flow of works that /lit writes as Mermaid (a chain of boxes, with dotted branches to the side
|
|
125
|
+
* for what was left out), drawn as boxes. Any other diagram stays as code; Obsidian draws it.
|
|
126
|
+
*/
|
|
127
|
+
function flowHtml(src) {
|
|
128
|
+
const lines = String(src).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
129
|
+
if (!/^(flowchart|graph)\s+(TD|TB)\b/.test(lines.shift() ?? "")) return null;
|
|
130
|
+
const label = {};
|
|
131
|
+
const next = {};
|
|
132
|
+
const side = {};
|
|
133
|
+
const into = new Set();
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const n = /^(\w+)\["([^"]*)"\]$/.exec(line);
|
|
136
|
+
if (n) {
|
|
137
|
+
label[n[1]] = n[2];
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const m = /^(\w+)(?:\["([^"]*)"\])?\s*(-->|-\.->)\s*(\w+)(?:\["([^"]*)"\])?$/.exec(line);
|
|
141
|
+
if (!m) return null;
|
|
142
|
+
const [, a, la, arrow, b, lb] = m;
|
|
143
|
+
if (la !== undefined) label[a] = la;
|
|
144
|
+
if (lb !== undefined) label[b] = lb;
|
|
145
|
+
if (arrow === "-->") {
|
|
146
|
+
if (next[a]) return null; // a branch of the main chain: not this kind of diagram
|
|
147
|
+
next[a] = b;
|
|
148
|
+
into.add(b);
|
|
149
|
+
} else (side[a] ??= []).push(b);
|
|
150
|
+
}
|
|
151
|
+
const rows = [];
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
for (let id = Object.keys(next).find((k) => !into.has(k)); id && !seen.has(id); id = next[id]) {
|
|
154
|
+
seen.add(id);
|
|
155
|
+
const sides = (side[id] ?? []).map((s) => `<div class="flow-side">${escapeHtml(label[s] ?? s)}</div>`).join("");
|
|
156
|
+
rows.push(`<div class="flow-row"><div class="flow-box">${escapeHtml(label[id] ?? id)}</div>${sides}</div>`);
|
|
157
|
+
}
|
|
158
|
+
if (rows.length < 2) return null;
|
|
159
|
+
return `<div class="flow">${rows.join("")}</div>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
113
162
|
function md(text) {
|
|
114
163
|
try {
|
|
115
164
|
return marked.parse(String(text ?? ""));
|
|
@@ -344,6 +393,7 @@ function searchLibrary() {
|
|
|
344
393
|
// ---------------------------------------------------------------- notes pane
|
|
345
394
|
|
|
346
395
|
let noteShown = null; // the note on screen, or null for the list
|
|
396
|
+
let noteDir = ""; // while a note is rendered: its folder, for the images next to it
|
|
347
397
|
let notesList = [];
|
|
348
398
|
|
|
349
399
|
function showPane(open) {
|
|
@@ -379,6 +429,7 @@ async function showNoteList() {
|
|
|
379
429
|
$("note-name").textContent = t("notes");
|
|
380
430
|
$("note-back").hidden = true;
|
|
381
431
|
$("note-app").hidden = true;
|
|
432
|
+
$("note-read").hidden = true;
|
|
382
433
|
$("note-filter").hidden = false;
|
|
383
434
|
showPane(true);
|
|
384
435
|
try {
|
|
@@ -406,9 +457,20 @@ async function openNote(path) {
|
|
|
406
457
|
$("note-filter").hidden = true;
|
|
407
458
|
const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(data.text);
|
|
408
459
|
const content = el("div", { class: "md" });
|
|
460
|
+
noteDir = data.path.replace(/[\\/][^\\/]*$/, "");
|
|
409
461
|
content.innerHTML = md(front ? data.text.slice(front[0].length) : data.text);
|
|
462
|
+
noteDir = "";
|
|
410
463
|
attachCopyButtons(content);
|
|
411
|
-
|
|
464
|
+
attachTableButtons(content, data.name.replace(/\.md$/i, ""));
|
|
465
|
+
// The evidence line: what the note was written from, drawn solid, dashed or dotted.
|
|
466
|
+
const evidence = front && /^evidence:\s*(.+?)\s*$/m.exec(front[1])?.[1];
|
|
467
|
+
const kind = evidence && (/full/i.test(evidence) ? "ev-full" : /abstract/i.test(evidence) ? "ev-abstract" : /metadata/i.test(evidence) ? "ev-meta" : "");
|
|
468
|
+
$("note-body").replaceChildren(
|
|
469
|
+
evidence ? el("p", { class: `evidence ${kind}` }, "evidence: ", el("span", { text: evidence })) : "",
|
|
470
|
+
front ? el("details", { class: "front" }, el("summary", { text: t("properties") }), el("pre", { text: front[1] })) : "",
|
|
471
|
+
content,
|
|
472
|
+
);
|
|
473
|
+
$("note-read").hidden = false;
|
|
412
474
|
$("note-body").scrollTop = 0;
|
|
413
475
|
showPane(true);
|
|
414
476
|
}
|
|
@@ -519,17 +581,40 @@ function exportConversation() {
|
|
|
519
581
|
if (!msgs.length) return;
|
|
520
582
|
const date = new Date().toISOString().slice(0, 10);
|
|
521
583
|
const content = `# ${t("exportHeading")} (${date})\n\n${t("exportMode")}: ${t(mode())}\n\n---\n\n${msgs.join("\n---\n\n")}`;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
584
|
+
download(content, `Sub-Sub-${date}.md`, "text/markdown;charset=utf-8");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Save text as a file in the downloads folder. */
|
|
588
|
+
function download(content, name, type) {
|
|
589
|
+
const url = URL.createObjectURL(new Blob([content], { type }));
|
|
590
|
+
const a = el("a", { href: url, download: name });
|
|
527
591
|
document.body.append(a);
|
|
528
592
|
a.click();
|
|
529
593
|
a.remove();
|
|
530
594
|
URL.revokeObjectURL(url);
|
|
531
595
|
}
|
|
532
596
|
|
|
597
|
+
/** A table as CSV for Excel, Rayyan or Covidence. Excel in Portuguese expects semicolons; the BOM keeps the accents. */
|
|
598
|
+
function tableCsv(table) {
|
|
599
|
+
const sep = lang === "pt" ? ";" : ",";
|
|
600
|
+
const cell = (c) => {
|
|
601
|
+
const v = c.textContent.replace(/\s+/g, " ").trim(); // not innerText: the header is upper case only in CSS
|
|
602
|
+
return /[",;\n]/.test(v) ? `"${v.replaceAll('"', '""')}"` : v;
|
|
603
|
+
};
|
|
604
|
+
return `\ufeff${[...table.rows].map((r) => [...r.cells].map(cell).join(sep)).join("\r\n")}\r\n`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** A "Download CSV" button under each table (evidence tables, extraction sheets, matrices). */
|
|
608
|
+
function attachTableButtons(root, name) {
|
|
609
|
+
const wraps = [...root.querySelectorAll(".md-table-wrap")];
|
|
610
|
+
wraps.forEach((wrap, i) => {
|
|
611
|
+
if (wrap.nextElementSibling?.classList.contains("table-actions")) return;
|
|
612
|
+
const file = `${name}${wraps.length > 1 ? `-${i + 1}` : ""}.csv`;
|
|
613
|
+
const btn = el("button", { type: "button", class: "msg-action-btn", text: t("downloadCsv"), onclick: () => download(tableCsv(wrap.querySelector("table")), file, "text/csv;charset=utf-8") });
|
|
614
|
+
wrap.after(el("div", { class: "table-actions" }, btn));
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
533
618
|
function closeSide() {
|
|
534
619
|
$("side").classList.remove("open");
|
|
535
620
|
$("menu-btn").setAttribute("aria-expanded", "false");
|
|
@@ -801,6 +886,7 @@ function renderAssistantFinal(message, msg = startAssistant()) {
|
|
|
801
886
|
const node = el("div", { class: "md" });
|
|
802
887
|
node.innerHTML = md(c.text);
|
|
803
888
|
attachCopyButtons(node);
|
|
889
|
+
attachTableButtons(node, `Sub-Sub-${new Date().toISOString().slice(0, 10)}`);
|
|
804
890
|
msg.box.append(node);
|
|
805
891
|
} else if (c.type === "toolCall") {
|
|
806
892
|
const row = toolRow(c.id, c.name, c.arguments);
|
|
@@ -1436,6 +1522,14 @@ $("library-search").addEventListener("input", searchLibrary);
|
|
|
1436
1522
|
$("notes-btn").addEventListener("click", () => ($("note-pane").hidden ? showNoteList() : showPane(false)));
|
|
1437
1523
|
$("note-back").addEventListener("click", showNoteList);
|
|
1438
1524
|
$("note-close").addEventListener("click", () => showPane(false));
|
|
1525
|
+
// Reading mode for notes: larger type and wider spacing, remembered on this computer.
|
|
1526
|
+
function setReading(on) {
|
|
1527
|
+
$("note-pane").classList.toggle("reading", on);
|
|
1528
|
+
$("note-read").setAttribute("aria-pressed", String(on));
|
|
1529
|
+
localStorage.setItem("subsub-reading", String(on));
|
|
1530
|
+
}
|
|
1531
|
+
$("note-read").addEventListener("click", () => setReading(!$("note-pane").classList.contains("reading")));
|
|
1532
|
+
setReading(localStorage.getItem("subsub-reading") === "true");
|
|
1439
1533
|
$("note-app").addEventListener("click", () => noteShown && api("/api/open", { path: noteShown.path }).catch((err) => note(err.message, "warning")));
|
|
1440
1534
|
$("note-filter").addEventListener("input", renderNoteList);
|
|
1441
1535
|
$("note-pane").addEventListener("keydown", (e) => {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/web/i18n.js
CHANGED
|
@@ -109,6 +109,9 @@ export const STRINGS = {
|
|
|
109
109
|
notesHint: "The notes in your Sub-Sub folder",
|
|
110
110
|
allNotes: "All notes",
|
|
111
111
|
properties: "Properties",
|
|
112
|
+
readingMode: "Reading mode",
|
|
113
|
+
readingModeHint: "Larger type and wider line spacing for the note",
|
|
114
|
+
downloadCsv: "Download CSV",
|
|
112
115
|
filterNotes: "Filter notes",
|
|
113
116
|
noNotes: "No notes yet.",
|
|
114
117
|
openInApp: "Open in app",
|
|
@@ -248,6 +251,9 @@ export const STRINGS = {
|
|
|
248
251
|
notesHint: "As notas da sua pasta do Sub-Sub",
|
|
249
252
|
allNotes: "Todas as notas",
|
|
250
253
|
properties: "Propriedades",
|
|
254
|
+
readingMode: "Modo de leitura",
|
|
255
|
+
readingModeHint: "Letra maior e mais espaço entre linhas na nota",
|
|
256
|
+
downloadCsv: "Transferir CSV",
|
|
251
257
|
filterNotes: "Filtrar notas",
|
|
252
258
|
noNotes: "Ainda não há notas.",
|
|
253
259
|
openInApp: "Abrir na aplicação",
|
|
@@ -293,7 +299,7 @@ export const ACTIONS = {
|
|
|
293
299
|
{ cmd: "lit", en: "Literature review", pt: "Revisão da literatura" },
|
|
294
300
|
{ cmd: "lit reading list:", en: "Reading list", pt: "Lista de leitura" },
|
|
295
301
|
{ cmd: "compare", en: "Compare sources", pt: "Comparar fontes" },
|
|
296
|
-
{ cmd: "lit-note", item: true, en: "Literature note", pt: "Nota de
|
|
302
|
+
{ cmd: "lit-note", item: true, en: "Literature note", pt: "Nota de literatura" },
|
|
297
303
|
{ cmd: "synthesis", en: "Synthesis", pt: "Síntese" },
|
|
298
304
|
{ cmd: "gaps", en: "What is missing", pt: "O que falta" },
|
|
299
305
|
{ cmd: "manuscript", en: "Check a manuscript", pt: "Verificar um manuscrito" },
|
|
@@ -311,6 +317,7 @@ export const ACTIONS = {
|
|
|
311
317
|
{ cmd: "import-queue", send: true, en: "Import the queue", pt: "Importar a fila" },
|
|
312
318
|
{ cmd: "history", send: true, en: "Recent changes", pt: "Alterações recentes" },
|
|
313
319
|
{ cmd: "undo", send: true, en: "Undo the last change", pt: "Desfazer a última alteração" },
|
|
320
|
+
{ cmd: "feedback", en: "Report a problem", pt: "Comunicar um problema" },
|
|
314
321
|
],
|
|
315
322
|
};
|
|
316
323
|
|
|
@@ -332,6 +339,7 @@ export const SUGGESTIONS = {
|
|
|
332
339
|
export const TOOLS = {
|
|
333
340
|
en: {
|
|
334
341
|
find_items: "search the library",
|
|
342
|
+
get_figure: "cut a figure out of the PDF",
|
|
335
343
|
standalone_items: "list files without a parent item",
|
|
336
344
|
set_parent_items: "give files a parent item",
|
|
337
345
|
find_reference: "look up a reference",
|
|
@@ -392,6 +400,7 @@ export const TOOLS = {
|
|
|
392
400
|
},
|
|
393
401
|
pt: {
|
|
394
402
|
find_items: "pesquisar na biblioteca",
|
|
403
|
+
get_figure: "recortar uma figura do PDF",
|
|
395
404
|
standalone_items: "listar ficheiros sem item principal",
|
|
396
405
|
set_parent_items: "dar um item principal aos ficheiros",
|
|
397
406
|
find_reference: "procurar uma referência",
|
package/web/icon.svg
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
-
<rect width="32" height="32" rx="7" fill="#
|
|
3
|
-
<rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#
|
|
4
|
-
<path d="M9 12.5h8M9 16h14" stroke="#
|
|
5
|
-
<circle cx="16" cy="20.3" r="1.5" fill="#
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#2c5d87"/>
|
|
3
|
+
<rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#f4f5f1"/>
|
|
4
|
+
<path d="M9 12.5h8M9 16h14" stroke="#2c5d87" stroke-width="1.6" stroke-linecap="round"/>
|
|
5
|
+
<circle cx="16" cy="20.3" r="1.5" fill="#2c5d87"/>
|
|
6
6
|
</svg>
|
package/web/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
7
|
<title>Sub-Sub</title>
|
|
8
8
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
|
9
|
-
<link rel="preload" href="/fonts/
|
|
9
|
+
<link rel="preload" href="/fonts/AtkinsonHyperlegibleNext.woff2" as="font" type="font/woff2" crossorigin>
|
|
10
10
|
<link rel="stylesheet" href="/app.css">
|
|
11
11
|
<script type="module" src="/app.js"></script>
|
|
12
12
|
</head>
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
<div class="note-head">
|
|
87
87
|
<button type="button" class="link-btn" id="note-back" data-i18n="allNotes">All notes</button>
|
|
88
88
|
<span class="note-name" id="note-name"></span>
|
|
89
|
+
<button type="button" class="link-btn" id="note-read" aria-pressed="false" data-i18n="readingMode" data-i18n-title="readingModeHint" hidden>Reading mode</button>
|
|
89
90
|
<button type="button" class="link-btn" id="note-app" data-i18n="openInApp" hidden>Open in app</button>
|
|
90
91
|
<button type="button" class="link-btn" id="note-close" data-i18n="close">Close</button>
|
|
91
92
|
</div>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|