@tiagojct/subsub 0.10.0 → 0.11.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/README.md +11 -10
- package/dist/bench.js +5 -4
- package/dist/config.js +19 -13
- package/dist/doctor.js +1 -1
- package/dist/init.js +15 -20
- package/dist/look.js +2 -2
- package/dist/prompt.js +4 -7
- package/dist/roles.js +12 -12
- package/dist/subsub.js +41 -65
- package/dist/web.js +9 -6
- package/docs/Subsub.md +13 -13
- package/package.json +1 -1
- package/roles/librarian.md +46 -0
- package/roles/{assistant.md → researcher.md} +4 -5
- package/src/bench.ts +5 -8
- package/src/config.ts +22 -17
- package/src/doctor.ts +1 -1
- package/src/init.ts +18 -28
- package/src/look.ts +3 -4
- package/src/prompt.ts +5 -10
- package/src/roles.ts +14 -15
- package/src/subsub.ts +40 -66
- package/src/web.ts +7 -5
- package/templates/Zotero agent.md +2 -2
- package/web/app.css +21 -14
- package/web/app.js +23 -22
- package/web/i18n.js +36 -28
- package/web/index.html +6 -2
package/src/subsub.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sub-Sub: a Zotero librarian and research assistant for pi.
|
|
3
3
|
*
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* - Two modes: the Librarian manages the library (Zotero tools only); the
|
|
5
|
+
* Researcher does the same and also searches PubMed, Europe PMC and OpenAlex
|
|
6
|
+
* and writes notes. Each mode can have its own model.
|
|
7
7
|
* - An approval gate: every library change is previewed by the server (dry run)
|
|
8
8
|
* and shown to the user; nothing is applied without a yes.
|
|
9
9
|
* - Profiles (reader, scholar, author, editor) set what Sub-Sub writes and which
|
|
10
10
|
* tools it offers; /profile changes it.
|
|
11
|
-
* - Commands: /
|
|
12
|
-
* /researcher (the two modes before 0.10) turn library changes on and off.
|
|
11
|
+
* - Commands: /librarian, /researcher, /profile, /subsub, /history, /undo.
|
|
13
12
|
* - Add-on: with "addons": ["starbuck"], Starbuck runs as a third server
|
|
14
13
|
* ("verify") for reference checks.
|
|
15
14
|
*
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
loadConfig,
|
|
30
29
|
parseEnvFile,
|
|
31
30
|
type Profile,
|
|
31
|
+
type Mode,
|
|
32
32
|
saveConfig,
|
|
33
33
|
serverCommand,
|
|
34
34
|
starbuckCommand,
|
|
@@ -45,6 +45,8 @@ import { headerLines, type LibraryState, paintPreview, QUOTES, type Scheme, sche
|
|
|
45
45
|
import { systemAddition } from "./prompt.ts";
|
|
46
46
|
import { gateKind, SERVER_PREFIXES, toolsFor, unavailableReason } from "./roles.ts";
|
|
47
47
|
|
|
48
|
+
const MODE_ENTRY = "subsub-mode";
|
|
49
|
+
|
|
48
50
|
export interface SubsubDeps {
|
|
49
51
|
config?: SubsubConfig;
|
|
50
52
|
/** A ready bridge (tests). When given, Sub-Sub does not start or stop servers. */
|
|
@@ -127,12 +129,11 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
127
129
|
|
|
128
130
|
if (deps.bridge) registerTools(deps.bridge);
|
|
129
131
|
|
|
130
|
-
pi.registerFlag("
|
|
131
|
-
pi.registerFlag("librarian", { description: "Start Sub-Sub with library changes on (this run only)", type: "boolean", default: false });
|
|
132
|
+
pi.registerFlag("librarian", { description: "Start Sub-Sub in the Librarian mode", type: "boolean", default: false });
|
|
132
133
|
|
|
133
|
-
let
|
|
134
|
+
let mode: Mode = cfg.defaultMode;
|
|
134
135
|
const benchRun = Boolean(process.env.SUBSUB_BENCH_OUT);
|
|
135
|
-
const toolOpts = () => ({
|
|
136
|
+
const toolOpts = () => ({ profile, bench: benchRun });
|
|
136
137
|
let profile: Profile = cfg.profile;
|
|
137
138
|
const user = who(cfg);
|
|
138
139
|
/** True when started with the `subsub` command (not as a package inside plain pi). */
|
|
@@ -154,7 +155,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
154
155
|
if (!webView || !ctx || ctx.mode !== "rpc") return;
|
|
155
156
|
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
156
157
|
const state = {
|
|
157
|
-
|
|
158
|
+
mode,
|
|
158
159
|
profile,
|
|
159
160
|
profileLabel: profileSpec(profile).label,
|
|
160
161
|
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : null,
|
|
@@ -221,7 +222,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
221
222
|
const th = ctx.ui.theme;
|
|
222
223
|
return headerLines({
|
|
223
224
|
version,
|
|
224
|
-
|
|
225
|
+
mode,
|
|
225
226
|
profile: profileSpec(profile).label,
|
|
226
227
|
model: modelId,
|
|
227
228
|
library,
|
|
@@ -244,37 +245,27 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
244
245
|
function status(ctx: ExtensionContext): void {
|
|
245
246
|
modelId = ctx.model ? `${ctx.model.id}` : "no model";
|
|
246
247
|
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
247
|
-
|
|
248
|
-
ctx.ui.
|
|
249
|
-
if (standalone) ctx.ui.setTitle(libraryChanges ? "Sub-Sub" : "Sub-Sub (read only)");
|
|
248
|
+
ctx.ui.setStatus("subsub", `Sub-Sub: ${mode} | ${profileSpec(profile).label} | ${modelId}${down.length ? ` | not running: ${down.join(", ")}` : ""}`);
|
|
249
|
+
if (standalone) ctx.ui.setTitle(`Sub-Sub: ${mode}`);
|
|
250
250
|
headerTui?.requestRender();
|
|
251
251
|
webCtx = ctx;
|
|
252
252
|
emitWeb();
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
255
|
+
async function applyMode(ctx: ExtensionContext, next: Mode, remember: boolean): Promise<void> {
|
|
256
|
+
mode = next;
|
|
257
|
+
pi.setActiveTools(toolsFor(mode, registered(), toolOpts()));
|
|
258
|
+
const spec = cfg.models[mode];
|
|
259
|
+
if (spec) {
|
|
260
|
+
const i = spec.indexOf("/");
|
|
261
|
+
const model = i > 0 ? ctx.modelRegistry.find(spec.slice(0, i), spec.slice(i + 1)) : undefined;
|
|
262
|
+
const ok = model ? await pi.setModel(model) : false;
|
|
263
|
+
if (!ok) ctx.ui.notify(`Sub-Sub: cannot use ${spec} (unknown model or no key); keeping the current model.`, "warning");
|
|
265
264
|
}
|
|
265
|
+
if (remember) pi.appendEntry(MODE_ENTRY, { mode });
|
|
266
266
|
status(ctx);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
async function applyModel(ctx: ExtensionContext): Promise<void> {
|
|
270
|
-
const spec = cfg.model;
|
|
271
|
-
if (!spec) return;
|
|
272
|
-
const i = spec.indexOf("/");
|
|
273
|
-
const model = i > 0 ? ctx.modelRegistry.find(spec.slice(0, i), spec.slice(i + 1)) : undefined;
|
|
274
|
-
const ok = model ? await pi.setModel(model) : false;
|
|
275
|
-
if (!ok) ctx.ui.notify(`Sub-Sub: cannot use ${spec} (unknown model or no key); keeping the current model.`, "warning");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
269
|
pi.on("session_start", async (_event, ctx) => {
|
|
279
270
|
if (!bridge) {
|
|
280
271
|
const b = new Bridge();
|
|
@@ -282,12 +273,15 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
282
273
|
bridge = b;
|
|
283
274
|
registerTools(b);
|
|
284
275
|
}
|
|
276
|
+
let restored: Mode | undefined;
|
|
277
|
+
for (const e of ctx.sessionManager.getBranch() as Array<{ customType?: string; data?: { mode?: unknown } }>) {
|
|
278
|
+
const m = e.customType === MODE_ENTRY ? e.data?.mode : undefined;
|
|
279
|
+
if (m === "librarian" || m === "researcher") restored = m;
|
|
280
|
+
}
|
|
285
281
|
// Judge light or dark before Sub-Sub changes the theme.
|
|
286
282
|
if (lookOn(ctx)) scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
287
|
-
await applyModel(ctx);
|
|
288
283
|
applyTheme(ctx);
|
|
289
|
-
|
|
290
|
-
applyLibrary(ctx, pi.getFlag("read-only") ? false : pi.getFlag("librarian") ? true : cfg.libraryChanges, false);
|
|
284
|
+
await applyMode(ctx, restored ?? (pi.getFlag("librarian") ? "librarian" : cfg.defaultMode), false);
|
|
291
285
|
if (lookOn(ctx)) {
|
|
292
286
|
setHeader(ctx);
|
|
293
287
|
void refreshLibrary();
|
|
@@ -318,7 +312,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
318
312
|
});
|
|
319
313
|
|
|
320
314
|
pi.on("before_agent_start", async (event, ctx) => ({
|
|
321
|
-
systemPrompt: `${event.systemPrompt}\n\n${systemAddition({ ...cfg, profile }, ctx.cwd
|
|
315
|
+
systemPrompt: `${event.systemPrompt}\n\n${systemAddition(mode, { ...cfg, profile }, ctx.cwd)}`,
|
|
322
316
|
}));
|
|
323
317
|
|
|
324
318
|
/** Model test (subsub-bench): record every change instead of asking, and apply none. */
|
|
@@ -333,7 +327,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
333
327
|
if (schema) coerceArgs(schema, input);
|
|
334
328
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
335
329
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
336
|
-
const why = unavailableReason(name, registered(), toolOpts());
|
|
330
|
+
const why = unavailableReason(name, mode, registered(), toolOpts());
|
|
337
331
|
if (why) {
|
|
338
332
|
if (benchOut) record({ kind: "wrong_mode", tool: name, input });
|
|
339
333
|
return { block: true, reason: why };
|
|
@@ -447,39 +441,19 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
447
441
|
};
|
|
448
442
|
}
|
|
449
443
|
|
|
450
|
-
const libraryNote = (on: boolean) =>
|
|
451
|
-
on
|
|
452
|
-
? "Sub-Sub: library changes on. Every change shows a preview first."
|
|
453
|
-
: "Sub-Sub: library changes off. Sub-Sub reads, searches and writes notes; it does not change the library.";
|
|
454
|
-
|
|
455
|
-
pi.registerCommand("library", {
|
|
456
|
-
description: "Sub-Sub: turn library changes on or off (/library on, /library off)",
|
|
457
|
-
handler: async (args, ctx) => {
|
|
458
|
-
const a = args.trim().toLowerCase();
|
|
459
|
-
const on = a === "on" ? true : a === "off" ? false : a === "" ? !libraryChanges : undefined;
|
|
460
|
-
if (on === undefined) {
|
|
461
|
-
ctx.ui.notify("Type /library on or /library off.", "warning");
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
applyLibrary(ctx, on, true);
|
|
465
|
-
ctx.ui.notify(libraryNote(on), "info");
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// The two modes before 0.10, kept so that old habits and notes still work.
|
|
470
444
|
pi.registerCommand("librarian", {
|
|
471
|
-
description: "Sub-Sub:
|
|
445
|
+
description: "Sub-Sub: switch to the Librarian (manages the library: tags, imports, metadata, PDFs)",
|
|
472
446
|
handler: async (_args, ctx) => {
|
|
473
|
-
|
|
474
|
-
ctx.ui.notify(
|
|
447
|
+
await applyMode(ctx, "librarian", true);
|
|
448
|
+
ctx.ui.notify("Sub-Sub: Librarian mode. For searches and notes, switch to /researcher.", "info");
|
|
475
449
|
},
|
|
476
450
|
});
|
|
477
451
|
|
|
478
452
|
pi.registerCommand("researcher", {
|
|
479
|
-
description: "Sub-Sub:
|
|
453
|
+
description: "Sub-Sub: switch to the Researcher (searches, notes, reviews, and everything the Librarian does)",
|
|
480
454
|
handler: async (_args, ctx) => {
|
|
481
|
-
|
|
482
|
-
ctx.ui.notify(
|
|
455
|
+
await applyMode(ctx, "researcher", true);
|
|
456
|
+
ctx.ui.notify("Sub-Sub: Researcher mode. It searches, writes notes and also changes the library, with a preview.", "info");
|
|
483
457
|
},
|
|
484
458
|
});
|
|
485
459
|
|
|
@@ -502,7 +476,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
502
476
|
} catch (err) {
|
|
503
477
|
saved = ` Not saved: ${(err as Error).message}`;
|
|
504
478
|
}
|
|
505
|
-
|
|
479
|
+
await applyMode(ctx, mode, false);
|
|
506
480
|
ctx.ui.notify(`Sub-Sub: ${profileSpec(profile).label} profile (${profileSpec(profile).summary}).${saved}`, "info");
|
|
507
481
|
},
|
|
508
482
|
});
|
|
@@ -510,7 +484,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
510
484
|
pi.registerCommand("subsub", {
|
|
511
485
|
description: "Sub-Sub: Zotero connection and library overview",
|
|
512
486
|
handler: async (_args, ctx) => {
|
|
513
|
-
const lines = [`
|
|
487
|
+
const lines = [`Mode: ${mode}. Profile: ${profileSpec(profile).label}. Model: ${ctx.model?.id ?? "none"}.`];
|
|
514
488
|
if (!bridge) {
|
|
515
489
|
ctx.ui.notify(`${lines[0]}\nThe Zotero servers are not running. Try /reload.`, "warning");
|
|
516
490
|
return;
|
package/src/web.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { extname, join, normalize, resolve, sep } from "node:path";
|
|
|
23
23
|
import { StringDecoder } from "node:string_decoder";
|
|
24
24
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
25
25
|
import { expand, loadConfig, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
26
|
-
import {
|
|
26
|
+
import { TESTED_MODELS } from "./init.ts";
|
|
27
27
|
import { obsidianRoot } from "./layout.ts";
|
|
28
28
|
import { realish, within } from "./paths.ts";
|
|
29
29
|
|
|
@@ -568,9 +568,11 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
568
568
|
if (busy) return send(res, 409, { error: "busy" });
|
|
569
569
|
const body = await readJson(req);
|
|
570
570
|
const spec = `${String(body.provider)}/${String(body.id)}`;
|
|
571
|
-
// Remember the model
|
|
572
|
-
|
|
573
|
-
|
|
571
|
+
// Remember the model for the current mode (or both), then restart pi so the extension reads it.
|
|
572
|
+
const mode = (subsubState as { mode?: string } | null)?.mode === "librarian" ? "librarian" : "researcher";
|
|
573
|
+
const models: Record<string, string> = { ...(loadConfig(env).models ?? {}), [mode]: spec };
|
|
574
|
+
if (body.both) models[mode === "librarian" ? "researcher" : "librarian"] = spec;
|
|
575
|
+
saveConfig({ models, model: undefined }, env);
|
|
574
576
|
await agent.request({ type: "set_model", provider: String(body.provider), modelId: String(body.id) });
|
|
575
577
|
await restartAgent();
|
|
576
578
|
return send(res, 200, { ok: true });
|
|
@@ -594,7 +596,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
594
596
|
const body = await readJson(req);
|
|
595
597
|
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
596
598
|
const current = loadConfig(env);
|
|
597
|
-
if (body.provider === "opencode-go" &&
|
|
599
|
+
if (body.provider === "opencode-go" && Object.keys(current.models).length === 0) saveConfig({ models: TESTED_MODELS, model: undefined }, env);
|
|
598
600
|
await restartAgent();
|
|
599
601
|
return send(res, 200, { ok: true });
|
|
600
602
|
}
|
|
@@ -2,8 +2,8 @@ Shared rules for Sub-Sub. Sub-Sub reads this file at the start of every session.
|
|
|
2
2
|
|
|
3
3
|
## What Sub-Sub does
|
|
4
4
|
|
|
5
|
-
- Sub-Sub reads the library, searches PubMed, Europe PMC and OpenAlex,
|
|
6
|
-
-
|
|
5
|
+
- Sub-Sub has two modes. The Librarian manages the library: tags, imports, metadata, PDFs, collections, notes in Zotero. The Researcher reads the library, searches PubMed, Europe PMC and OpenAlex, writes notes in this folder, and also does everything the Librarian does.
|
|
6
|
+
- Every library change shows a preview and needs the user's yes.
|
|
7
7
|
- Works found in a search are imported directly when the user chooses them. For a list to choose from later, Sub-Sub writes Inbox/Zotero import queue.md; the user ticks the lines and Sub-Sub imports them.
|
|
8
8
|
|
|
9
9
|
## Safety
|
package/web/app.css
CHANGED
|
@@ -87,8 +87,9 @@
|
|
|
87
87
|
--warn: #d6a84e;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/*
|
|
90
|
+
/* The mode sets the accent: blue for the Researcher, rust for the Librarian. */
|
|
91
91
|
body { --accent: var(--researcher); --accent-soft: var(--researcher-soft); }
|
|
92
|
+
body[data-mode="librarian"] { --accent: var(--librarian); --accent-soft: var(--librarian-soft); }
|
|
92
93
|
|
|
93
94
|
/* ---------------------------------------------------------------- base */
|
|
94
95
|
|
|
@@ -219,20 +220,27 @@ select:hover { border-color: var(--rule-strong); }
|
|
|
219
220
|
.brand .mark { color: var(--accent); }
|
|
220
221
|
.brand .hyphen { color: var(--accent); }
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
padding:
|
|
225
|
-
border:
|
|
226
|
-
border-radius: 999px;
|
|
223
|
+
.modes {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
padding: 3px;
|
|
226
|
+
border-radius: 8px;
|
|
227
227
|
background: var(--surface);
|
|
228
|
-
|
|
228
|
+
border: 1px solid var(--rule);
|
|
229
|
+
}
|
|
230
|
+
.modes button {
|
|
231
|
+
padding: 0.28rem 0.8rem;
|
|
232
|
+
border-radius: 5px;
|
|
233
|
+
font-size: 0.86rem;
|
|
229
234
|
color: var(--muted);
|
|
230
|
-
|
|
235
|
+
border: 0;
|
|
236
|
+
}
|
|
237
|
+
.modes button:hover { color: var(--text); }
|
|
238
|
+
.modes button[aria-pressed="true"] {
|
|
239
|
+
background: var(--raised);
|
|
240
|
+
color: var(--accent);
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.08), 0 0 0 1px var(--rule);
|
|
231
243
|
}
|
|
232
|
-
.lib-switch:hover { border-color: var(--rule-strong); color: var(--text); }
|
|
233
|
-
.top .lib-switch input:checked + .track { background: var(--librarian); }
|
|
234
|
-
body[data-library="on"] .lib-switch { color: var(--text); }
|
|
235
|
-
body[data-library="off"] .top { border-top-color: var(--rule-strong); }
|
|
236
244
|
|
|
237
245
|
.status {
|
|
238
246
|
margin-left: auto;
|
|
@@ -855,8 +863,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
855
863
|
|
|
856
864
|
@media (max-width: 30rem) {
|
|
857
865
|
.brand > span { display: none; }
|
|
858
|
-
.
|
|
859
|
-
.lib-switch { padding: 0.35rem 0.5rem; }
|
|
866
|
+
.modes button { padding: 0.28rem 0.6rem; }
|
|
860
867
|
}
|
|
861
868
|
|
|
862
869
|
@media (prefers-reduced-motion: reduce) {
|
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:
|
|
10
|
+
let state = null; // Sub-Sub's own state: mode, profile, model, library, down
|
|
11
11
|
let session = null; // pi's session state
|
|
12
12
|
let busy = false;
|
|
13
13
|
let commands = [];
|
|
@@ -169,15 +169,16 @@ function applyLang() {
|
|
|
169
169
|
updateLayoutUI();
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return state?.libraryChanges !== false;
|
|
172
|
+
function mode() {
|
|
173
|
+
return state?.mode === "librarian" ? "librarian" : "researcher";
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
function renderHeader() {
|
|
178
|
-
document.body.dataset.
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
document.body.dataset.mode = mode();
|
|
178
|
+
for (const b of document.querySelectorAll(".modes button")) {
|
|
179
|
+
b.setAttribute("aria-pressed", String(b.dataset.mode === mode()));
|
|
180
|
+
b.title = t(b.dataset.mode === "librarian" ? "librarianHint" : "researcherHint");
|
|
181
|
+
}
|
|
181
182
|
if (state?.profile) $("profile").value = $("profile-side").value = state.profile;
|
|
182
183
|
$("starbuck-toggle").checked = Boolean(state?.starbuck);
|
|
183
184
|
$("starbuck-label").title = t("starbuckHint");
|
|
@@ -205,7 +206,7 @@ function renderHeader() {
|
|
|
205
206
|
lib.classList.add("down");
|
|
206
207
|
}
|
|
207
208
|
lib.title = txt.textContent;
|
|
208
|
-
$("input").placeholder =
|
|
209
|
+
$("input").placeholder = mode() === "librarian" ? t("placeholderLibrarian") : t("placeholderResearcher");
|
|
209
210
|
if (log.querySelector(".empty")) {
|
|
210
211
|
clearEmpty();
|
|
211
212
|
renderEmpty();
|
|
@@ -225,10 +226,11 @@ function renderActions() {
|
|
|
225
226
|
box.replaceChildren();
|
|
226
227
|
const shown = (a) =>
|
|
227
228
|
hasCommand(a.cmd) &&
|
|
228
|
-
|
|
229
|
+
|
|
229
230
|
(!a.needs || state?.[a.needs]) &&
|
|
230
231
|
(!a.profiles || a.profiles.includes(state?.profile ?? "scholar"));
|
|
231
|
-
|
|
232
|
+
const groups = mode() === "librarian" ? [["library", "groupLibrary"]] : [["research", "groupResearch"], ["library", "groupLibrary"]];
|
|
233
|
+
for (const [group, label] of groups) {
|
|
232
234
|
const list = ACTIONS[group].filter(shown);
|
|
233
235
|
if (!list.length) continue;
|
|
234
236
|
box.append(el("p", { class: "group", text: t(label) }));
|
|
@@ -376,14 +378,14 @@ function clearEmpty() {
|
|
|
376
378
|
|
|
377
379
|
function renderEmpty() {
|
|
378
380
|
if (log.children.length) return;
|
|
379
|
-
const s = SUGGESTIONS
|
|
381
|
+
const s = SUGGESTIONS[mode()].map((x) => x[lang] ?? x.en);
|
|
380
382
|
log.append(
|
|
381
383
|
el(
|
|
382
384
|
"div",
|
|
383
385
|
{ class: "empty" },
|
|
384
|
-
el("p", { class: "kicker", text:
|
|
386
|
+
el("p", { class: "kicker", text: t(mode()) }),
|
|
385
387
|
el("h1", { text: t("emptyTitle") }),
|
|
386
|
-
el("p", { class: "lede", text: t(
|
|
388
|
+
el("p", { class: "lede", text: t(mode() === "librarian" ? "emptyLibrarian" : "emptyResearcher") }),
|
|
387
389
|
el(
|
|
388
390
|
"div",
|
|
389
391
|
{ class: "chips" },
|
|
@@ -715,8 +717,8 @@ function onEvent(ev) {
|
|
|
715
717
|
break;
|
|
716
718
|
case "extension_ui_request":
|
|
717
719
|
if (ev.method === "notify") {
|
|
718
|
-
// The
|
|
719
|
-
if (/^Sub-Sub:
|
|
720
|
+
// The mode buttons show the mode; these confirmations would only repeat it.
|
|
721
|
+
if (/^Sub-Sub: (Librarian|Researcher) mode\./.test(ev.message)) break;
|
|
720
722
|
note(ev.message, ev.notifyType === "error" ? "error" : ev.notifyType === "warning" ? "warning" : "info", /\n/.test(ev.message));
|
|
721
723
|
} else {
|
|
722
724
|
dialogs.push(ev);
|
|
@@ -901,7 +903,7 @@ async function openModels() {
|
|
|
901
903
|
onclick: async () => {
|
|
902
904
|
if (busy) return note(t("busy"), "warning");
|
|
903
905
|
try {
|
|
904
|
-
await api("/api/model", { provider: m.provider, id: m.id });
|
|
906
|
+
await api("/api/model", { provider: m.provider, id: m.id, both: $("model-both").checked });
|
|
905
907
|
dlg.close();
|
|
906
908
|
} catch (err) {
|
|
907
909
|
note(err.message, "error");
|
|
@@ -1042,12 +1044,11 @@ $("new-btn").addEventListener("click", async () => {
|
|
|
1042
1044
|
note(err.message, "error");
|
|
1043
1045
|
}
|
|
1044
1046
|
});
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
});
|
|
1047
|
+
for (const b of document.querySelectorAll(".modes button")) {
|
|
1048
|
+
b.addEventListener("click", () => {
|
|
1049
|
+
if (b.dataset.mode !== mode()) sendMessage(`/${b.dataset.mode}`);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1051
1052
|
$("profile").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
|
|
1052
1053
|
$("starbuck-toggle").addEventListener("change", async (e) => {
|
|
1053
1054
|
const on = e.target.checked;
|
package/web/i18n.js
CHANGED
|
@@ -79,14 +79,15 @@ export const STRINGS = {
|
|
|
79
79
|
starbuckOff: "Reference checks are off.",
|
|
80
80
|
composerHint: "Enter sends. Shift+Enter starts a new line. / shows the commands.",
|
|
81
81
|
previewKicker: "Preview",
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
researcher: "Researcher",
|
|
83
|
+
librarian: "Librarian",
|
|
84
|
+
researcherHint: "Searches, reads and writes notes, and also changes the library (every change is previewed).",
|
|
85
|
+
librarianHint: "Manages the library only: tags, imports, metadata, PDFs. No literature search.",
|
|
86
|
+
placeholderResearcher: "Ask about your library or the literature, or type / for commands",
|
|
87
|
+
placeholderLibrarian: "Ask for a change to your library, or type / for commands",
|
|
88
|
+
emptyResearcher: "The Researcher reads your library, searches PubMed, Europe PMC and OpenAlex and writes notes. It also imports, tags and repairs items: every change shows a preview, and nothing changes until you approve it.",
|
|
89
|
+
emptyLibrarian: "The Librarian manages your library: tags, imports, metadata and PDFs. Every change shows a preview, and nothing changes until you approve it. For searches and notes, switch to the Researcher.",
|
|
90
|
+
forBoth: "Use it in both modes",
|
|
90
91
|
groupResearch: "Research",
|
|
91
92
|
groupLibrary: "Library",
|
|
92
93
|
},
|
|
@@ -167,14 +168,15 @@ export const STRINGS = {
|
|
|
167
168
|
starbuckOff: "A verificação de referências está desligada.",
|
|
168
169
|
composerHint: "Enter envia. Shift+Enter muda de linha. / mostra os comandos.",
|
|
169
170
|
previewKicker: "Pré-visualização",
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
researcher: "Investigador",
|
|
172
|
+
librarian: "Bibliotecário",
|
|
173
|
+
researcherHint: "Pesquisa, lê e escreve notas, e também altera a biblioteca (cada alteração é pré-visualizada).",
|
|
174
|
+
librarianHint: "Só gere a biblioteca: etiquetas, importações, metadados, PDF. Não pesquisa a literatura.",
|
|
175
|
+
placeholderResearcher: "Pergunte sobre a sua biblioteca ou a literatura, ou escreva / para ver os comandos",
|
|
176
|
+
placeholderLibrarian: "Peça uma alteração à biblioteca, ou escreva / para ver os comandos",
|
|
177
|
+
emptyResearcher: "O Investigador lê a sua biblioteca, pesquisa na PubMed, Europe PMC e OpenAlex e escreve notas. Também importa, etiqueta e corrige itens: cada alteração mostra uma pré-visualização, e nada muda até a aprovar.",
|
|
178
|
+
emptyLibrarian: "O Bibliotecário gere a sua biblioteca: etiquetas, importações, metadados e PDF. Cada alteração mostra uma pré-visualização, e nada muda até a aprovar. Para pesquisar e escrever notas, mude para o Investigador.",
|
|
179
|
+
forBoth: "Usar nos dois modos",
|
|
178
180
|
groupResearch: "Investigação",
|
|
179
181
|
groupLibrary: "Biblioteca",
|
|
180
182
|
},
|
|
@@ -182,7 +184,7 @@ export const STRINGS = {
|
|
|
182
184
|
|
|
183
185
|
/**
|
|
184
186
|
* Buttons in the side panel, in two groups. `fill` puts the command in the message box; `send` runs it at once.
|
|
185
|
-
*
|
|
187
|
+
* The Librarian sees the Library group only. `profiles` and `needs` limit a button to some profiles or add-ons.
|
|
186
188
|
*/
|
|
187
189
|
export const ACTIONS = {
|
|
188
190
|
research: [
|
|
@@ -200,21 +202,27 @@ export const ACTIONS = {
|
|
|
200
202
|
],
|
|
201
203
|
library: [
|
|
202
204
|
{ cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
|
|
203
|
-
{ cmd: "tag-batch 10", send: true,
|
|
204
|
-
{ cmd: "clean-tags", send: true,
|
|
205
|
-
{ cmd: "import-queue", send: true,
|
|
205
|
+
{ cmd: "tag-batch 10", send: true, en: "Tag 10 items", pt: "Etiquetar 10 itens" },
|
|
206
|
+
{ cmd: "clean-tags", send: true, en: "Clean up tags", pt: "Limpar etiquetas" },
|
|
207
|
+
{ cmd: "import-queue", send: true, en: "Import the queue", pt: "Importar a fila" },
|
|
206
208
|
{ cmd: "history", send: true, en: "Recent changes", pt: "Alterações recentes" },
|
|
207
|
-
{ cmd: "undo", send: true,
|
|
209
|
+
{ cmd: "undo", send: true, en: "Undo the last change", pt: "Desfazer a última alteração" },
|
|
208
210
|
],
|
|
209
211
|
};
|
|
210
212
|
|
|
211
|
-
/** Suggestions on an empty conversation
|
|
212
|
-
export const SUGGESTIONS =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
]
|
|
213
|
+
/** Suggestions on an empty conversation, per mode. */
|
|
214
|
+
export const SUGGESTIONS = {
|
|
215
|
+
researcher: [
|
|
216
|
+
{ en: "What do I have on ", pt: "O que tenho sobre " },
|
|
217
|
+
{ en: "Find recent systematic reviews on ", pt: "Procure revisões sistemáticas recentes sobre " },
|
|
218
|
+
{ en: "Which items have no PDF?", pt: "Que itens não têm PDF?" },
|
|
219
|
+
],
|
|
220
|
+
librarian: [
|
|
221
|
+
{ en: "Tag the next 10 items without a topic", pt: "Etiquete os próximos 10 itens sem tópico" },
|
|
222
|
+
{ en: "Import 10.1136/bmj.n71", pt: "Importe 10.1136/bmj.n71" },
|
|
223
|
+
{ en: "Find duplicates", pt: "Procure duplicados" },
|
|
224
|
+
],
|
|
225
|
+
};
|
|
218
226
|
|
|
219
227
|
/** Plain names for the tools, shown while they run. Unknown tools keep their name. */
|
|
220
228
|
export const TOOLS = {
|
package/web/index.html
CHANGED
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
<link rel="stylesheet" href="/app.css">
|
|
11
11
|
<script type="module" src="/app.js"></script>
|
|
12
12
|
</head>
|
|
13
|
-
<body data-
|
|
13
|
+
<body data-mode="researcher">
|
|
14
14
|
<div class="app">
|
|
15
15
|
<header class="top">
|
|
16
16
|
<button type="button" class="icon-btn" id="menu-btn" aria-controls="side" aria-expanded="true" data-i18n-title="menu" title="Menu"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M9.5 4.5v15"/></svg><span class="visually-hidden" data-i18n="menu">Menu</span></button>
|
|
17
17
|
<div class="brand"><svg class="mark" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="1.5"/><path d="M6.5 9h7M6.5 12h11"/><circle cx="12" cy="16" r="1.1"/></svg><span>Sub<span class="hyphen">-</span>Sub</span></div>
|
|
18
|
-
<
|
|
18
|
+
<div class="modes" role="group" aria-label="Mode">
|
|
19
|
+
<button type="button" data-mode="researcher" aria-pressed="true" data-i18n="researcher">Researcher</button>
|
|
20
|
+
<button type="button" data-mode="librarian" aria-pressed="false" data-i18n="librarian">Librarian</button>
|
|
21
|
+
</div>
|
|
19
22
|
<div class="status">
|
|
20
23
|
<span class="library" id="library"><span class="library-dot" aria-hidden="true"></span><span class="library-text"></span></span>
|
|
21
24
|
<label class="profile wide-only"><span class="visually-hidden" data-i18n="profile">Profile</span>
|
|
@@ -107,6 +110,7 @@
|
|
|
107
110
|
<button type="button" class="link-btn" id="model-close" data-i18n="close">Close</button>
|
|
108
111
|
</div>
|
|
109
112
|
<h3 data-i18n="chooseModel">Choose a model</h3>
|
|
113
|
+
<label class="check"><input type="checkbox" id="model-both"> <span data-i18n="forBoth">Use it in both modes</span></label>
|
|
110
114
|
<ul class="models" id="models"></ul>
|
|
111
115
|
<h3 data-i18n="connectProvider">Connect a provider</h3>
|
|
112
116
|
<form id="login-form" class="login">
|