@tiagojct/subsub 0.11.3 → 0.12.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 +64 -0
- package/README.md +10 -5
- package/dist/bench.js +10 -4
- package/dist/cli.js +6 -0
- package/dist/config.js +28 -1
- package/dist/init.js +49 -3
- package/dist/keys.js +81 -0
- package/dist/preview.js +101 -26
- package/dist/subsub.js +74 -14
- package/dist/usage.js +69 -0
- package/dist/web.js +5 -37
- package/package.json +4 -3
- package/src/bench.ts +10 -4
- package/src/cli.ts +6 -0
- package/src/config.ts +30 -1
- package/src/init.ts +60 -2
- package/src/keys.ts +79 -0
- package/src/preview.ts +100 -25
- package/src/subsub.ts +72 -13
- package/src/usage.ts +75 -0
- package/src/web.ts +5 -35
- package/web/app.css +24 -0
- package/web/app.js +135 -22
- package/web/i18n.js +58 -0
- package/web/index.html +3 -0
package/web/app.css
CHANGED
|
@@ -631,6 +631,11 @@ body.layout-full { --measure: 72rem; }
|
|
|
631
631
|
.note.mono { font-family: var(--mono); font-size: 0.76rem; line-height: 1.55; }
|
|
632
632
|
.note pre { font-family: var(--mono); font-size: 0.74rem; white-space: pre-wrap; }
|
|
633
633
|
.note button { margin-top: 0.5rem; }
|
|
634
|
+
.note p { margin: 0; }
|
|
635
|
+
.note details { margin-top: 0.4rem; color: var(--muted); }
|
|
636
|
+
.note details summary { cursor: pointer; font-size: 0.78rem; }
|
|
637
|
+
.note details pre { margin: 0.3rem 0 0; overflow-wrap: anywhere; }
|
|
638
|
+
.free-note { border-left: 3px solid var(--accent); padding-left: 0.6rem; }
|
|
634
639
|
|
|
635
640
|
.working {
|
|
636
641
|
margin-top: 1rem;
|
|
@@ -826,6 +831,14 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
826
831
|
.models button:hover { background: var(--bg); }
|
|
827
832
|
.models .current { color: var(--accent); font-weight: 600; background: var(--accent-soft); }
|
|
828
833
|
.models .empty-models { padding: 0.65rem 0.8rem; color: var(--muted); font-size: 0.88rem; }
|
|
834
|
+
.model-search { width: 100%; margin-bottom: 0.5rem; }
|
|
835
|
+
.models { max-height: 20rem; }
|
|
836
|
+
.models .models-group { padding: 0.45rem 0.8rem 0.3rem; font-size: 0.78rem; font-weight: 600; color: var(--muted); background: var(--surface); }
|
|
837
|
+
.models button { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.6rem; }
|
|
838
|
+
.models .needs-key { opacity: 0.65; }
|
|
839
|
+
.models .model-name { min-width: 0; overflow-wrap: anywhere; }
|
|
840
|
+
.models .model-tag { font-family: var(--sans); font-size: 0.72rem; font-weight: 600; color: var(--accent); border: 1px solid currentColor; border-radius: 999px; padding: 0 0.45rem; }
|
|
841
|
+
.models .model-why { font-family: var(--sans); font-size: 0.75rem; color: var(--muted); }
|
|
829
842
|
|
|
830
843
|
/* ---------------------------------------------------------------- narrow screens */
|
|
831
844
|
|
|
@@ -870,3 +883,14 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
870
883
|
@media (prefers-reduced-motion: reduce) {
|
|
871
884
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
872
885
|
}
|
|
886
|
+
|
|
887
|
+
/* First run: what is still missing */
|
|
888
|
+
.setup { margin-block: 1.4rem 1rem; padding: 0.9rem 1rem; border: 1px solid var(--rule); border-left: 3px solid var(--accent); border-radius: 6px; background: var(--raised); text-align: left; max-width: 40rem; }
|
|
889
|
+
.setup h2 { margin: 0 0 0.5rem; font-size: 0.95rem; }
|
|
890
|
+
.setup ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
|
|
891
|
+
.setup li { display: grid; grid-template-columns: 4.5rem 1fr auto; gap: 0.6rem; align-items: start; font-size: 0.88rem; line-height: 1.45; }
|
|
892
|
+
.setup li > * { min-width: 0; }
|
|
893
|
+
.setup .mark { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; padding-top: 0.15rem; color: var(--muted); }
|
|
894
|
+
.setup li.todo .mark { color: var(--rem); }
|
|
895
|
+
.setup li.ok .step-text { color: var(--muted); }
|
|
896
|
+
@media (max-width: 560px) { .setup li { grid-template-columns: 1fr; gap: 0.2rem; } }
|
package/web/app.js
CHANGED
|
@@ -368,6 +368,21 @@ function scrollDown(force = false) {
|
|
|
368
368
|
if (force || near) log.scrollTop = log.scrollHeight;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// A provider error in plain words (keep in step with providerTrouble in src/keys.ts), with the raw text below.
|
|
372
|
+
function troubleOf(text) {
|
|
373
|
+
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|usage limit/i.test(text)) return "quota";
|
|
374
|
+
if (/\b(503|529)\b|UNAVAILABLE|high demand|overloaded/i.test(text)) return "busy";
|
|
375
|
+
if (/RECITATION|SAFETY|PROHIBITED_CONTENT/.test(text)) return "blocked";
|
|
376
|
+
if (/\b(401|403)\b|API key not valid|invalid api key|PERMISSION_DENIED|UNAUTHENTICATED/i.test(text)) return "key";
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function errorNote(text) {
|
|
381
|
+
const kind = troubleOf(text);
|
|
382
|
+
if (!kind) return el("div", { class: "note error", text });
|
|
383
|
+
return el("div", { class: "note error" }, el("p", { text: t(`trouble_${kind}`) }), el("details", {}, el("summary", { text: t("details") }), el("pre", { text })));
|
|
384
|
+
}
|
|
385
|
+
|
|
371
386
|
function note(text, kind = "info", mono = false) {
|
|
372
387
|
clearEmpty();
|
|
373
388
|
const n = el("div", { class: `note ${kind}${mono ? " mono" : ""}`, text });
|
|
@@ -380,6 +395,36 @@ function clearEmpty() {
|
|
|
380
395
|
log.querySelector(".empty")?.remove();
|
|
381
396
|
}
|
|
382
397
|
|
|
398
|
+
/** First run: what is still missing (Zotero, a model, the servers), each with its fix. Hidden when all is ready. */
|
|
399
|
+
function setupList() {
|
|
400
|
+
const L = state?.library;
|
|
401
|
+
const m = state?.model ?? session?.model;
|
|
402
|
+
const steps = [
|
|
403
|
+
{ ok: L?.zotero === "reachable", wait: !L || L.zotero === "checking", text: t("stepZotero") },
|
|
404
|
+
{ ok: Boolean(m && m.id && m.id !== "unknown"), text: t("stepModel"), action: { label: t("connect"), run: openModels } },
|
|
405
|
+
{ ok: !state?.down?.length, text: t("stepServers", { x: (state?.down ?? []).join(", ") }) },
|
|
406
|
+
];
|
|
407
|
+
if (steps.every((x) => x.ok || x.wait)) return null;
|
|
408
|
+
return el(
|
|
409
|
+
"div",
|
|
410
|
+
{ class: "setup" },
|
|
411
|
+
el("h2", { text: t("setupTitle") }),
|
|
412
|
+
el(
|
|
413
|
+
"ol",
|
|
414
|
+
{},
|
|
415
|
+
steps.map((x) =>
|
|
416
|
+
el(
|
|
417
|
+
"li",
|
|
418
|
+
{ class: x.ok ? "ok" : x.wait ? "wait" : "todo" },
|
|
419
|
+
el("span", { class: "mark", text: x.ok ? t("stepDone") : x.wait ? "…" : t("stepTodo") }),
|
|
420
|
+
el("span", { class: "step-text", text: x.ok ? x.text.split(". ")[0] : x.text }),
|
|
421
|
+
!x.ok && x.action ? el("button", { type: "button", class: "primary small-btn", text: x.action.label, onclick: x.action.run }) : null,
|
|
422
|
+
),
|
|
423
|
+
),
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
383
428
|
function renderEmpty() {
|
|
384
429
|
if (log.children.length) return;
|
|
385
430
|
const s = SUGGESTIONS[mode()].map((x) => x[lang] ?? x.en);
|
|
@@ -390,6 +435,7 @@ function renderEmpty() {
|
|
|
390
435
|
el("p", { class: "kicker", text: t(mode()) }),
|
|
391
436
|
el("h1", { text: t("emptyTitle") }),
|
|
392
437
|
el("p", { class: "lede", text: t(mode() === "librarian" ? "emptyLibrarian" : "emptyResearcher") }),
|
|
438
|
+
setupList(),
|
|
393
439
|
el(
|
|
394
440
|
"div",
|
|
395
441
|
{ class: "chips" },
|
|
@@ -490,6 +536,23 @@ function finishTool(id, result, isError) {
|
|
|
490
536
|
row.querySelector("pre")?.remove();
|
|
491
537
|
if (text) row.append(el("pre", { text: text.length > 6000 ? `${text.slice(0, 6000)}\n...` : text }));
|
|
492
538
|
if (isError) row.querySelector(".label").textContent = `${toolLabel(row.dataset.name)} (${t("failed")})`;
|
|
539
|
+
// An applied library change gets an Undo button; it runs /undo with its id, which previews first.
|
|
540
|
+
const jid = !isError && !/undo/.test(row.dataset.name) && /"journal_id":\s*"(\d{8}-\d{6}-\d{2}-[a-z_]+)"/.exec(text ?? "")?.[1];
|
|
541
|
+
if (jid && !row.querySelector(".undo")) {
|
|
542
|
+
row.querySelector("summary").append(
|
|
543
|
+
el("button", {
|
|
544
|
+
type: "button",
|
|
545
|
+
class: "open undo",
|
|
546
|
+
text: t("undo"),
|
|
547
|
+
title: t("undoTitle"),
|
|
548
|
+
onclick: (e) => {
|
|
549
|
+
e.preventDefault();
|
|
550
|
+
if (busy) return note(t("busy"), "warning");
|
|
551
|
+
sendMessage(`/undo ${jid}`);
|
|
552
|
+
},
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
}
|
|
493
556
|
}
|
|
494
557
|
|
|
495
558
|
function attachCopyButtons(root) {
|
|
@@ -596,7 +659,7 @@ function renderAssistantFinal(message, msg = startAssistant()) {
|
|
|
596
659
|
);
|
|
597
660
|
msg.box.append(actions);
|
|
598
661
|
}
|
|
599
|
-
if (message.stopReason === "error" && message.errorMessage) msg.box.append(
|
|
662
|
+
if (message.stopReason === "error" && message.errorMessage) msg.box.append(errorNote(message.errorMessage));
|
|
600
663
|
if (message.stopReason === "aborted") msg.box.append(el("div", { class: "note", text: t("stopped") }));
|
|
601
664
|
if (!msg.box.children.length) msg.box.remove();
|
|
602
665
|
scrollDown();
|
|
@@ -907,44 +970,94 @@ async function openModels() {
|
|
|
907
970
|
const setLink = () => {
|
|
908
971
|
const p = providers.find((x) => x.id === sel.value);
|
|
909
972
|
link.href = p?.url ?? "#";
|
|
973
|
+
const free = { google: "freeGoogle", openrouter: "freeOpenRouter" }[sel.value];
|
|
974
|
+
$("login-free").hidden = !free;
|
|
975
|
+
$("login-free").textContent = free ? t(free) : "";
|
|
976
|
+
$("login-where").textContent = p?.where ? t(`where_${p.where}`) : "";
|
|
910
977
|
};
|
|
911
978
|
sel.onchange = setLink;
|
|
912
979
|
setLink();
|
|
913
980
|
if (!dlg.open) dlg.showModal();
|
|
914
981
|
let models = [];
|
|
982
|
+
let recommended = [];
|
|
915
983
|
try {
|
|
916
|
-
|
|
984
|
+
const data = await api("/api/models");
|
|
985
|
+
models = data.models ?? [];
|
|
986
|
+
recommended = data.recommended ?? [];
|
|
917
987
|
} catch (err) {
|
|
918
988
|
note(err.message, "error");
|
|
919
989
|
}
|
|
920
|
-
ul.replaceChildren();
|
|
921
|
-
if (!models.length) ul.append(el("li", { class: "empty-models", text: t("noModels") }));
|
|
922
990
|
const cur = state?.model ?? session?.model;
|
|
991
|
+
const specOf = (m) => `${m.provider}/${m.id}`;
|
|
923
992
|
const isCurrent = (m) => cur && cur.provider === m.provider && cur.id === m.id;
|
|
924
|
-
|
|
925
|
-
for (
|
|
926
|
-
|
|
927
|
-
|
|
993
|
+
const available = new Set(models.map(specOf));
|
|
994
|
+
// Recommended for this mode first (also when the provider has no key yet), then the rest.
|
|
995
|
+
const mine = recommended.filter((r) => r.modes.includes(mode()));
|
|
996
|
+
const recSpecs = new Set(mine.map((r) => r.spec));
|
|
997
|
+
const others = models
|
|
998
|
+
.filter((m) => !recSpecs.has(specOf(m)))
|
|
999
|
+
.sort((a, b) => Number(isCurrent(b)) - Number(isCurrent(a)) || specOf(a).localeCompare(specOf(b)));
|
|
1000
|
+
const choose = async (m) => {
|
|
1001
|
+
if (busy) return note(t("busy"), "warning");
|
|
1002
|
+
try {
|
|
1003
|
+
await api("/api/model", { provider: m.provider, id: m.id, both: $("model-both").checked });
|
|
1004
|
+
dlg.close();
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
note(err.message, "error");
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
const row = (m, rec) => {
|
|
1010
|
+
const spec = specOf(m);
|
|
1011
|
+
const can = available.has(spec);
|
|
1012
|
+
const label = providers.find((p) => p.id === m.provider)?.label ?? m.provider;
|
|
1013
|
+
return el(
|
|
1014
|
+
"li",
|
|
1015
|
+
{ "data-spec": spec.toLowerCase() },
|
|
928
1016
|
el(
|
|
929
|
-
"
|
|
930
|
-
{
|
|
931
|
-
el("button", {
|
|
1017
|
+
"button",
|
|
1018
|
+
{
|
|
932
1019
|
type: "button",
|
|
933
|
-
class:
|
|
934
|
-
|
|
935
|
-
onclick:
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
} catch (err) {
|
|
941
|
-
note(err.message, "error");
|
|
1020
|
+
class: [isCurrent(m) ? "current" : "", rec ? "rec" : "", can ? "" : "needs-key"].join(" ").trim(),
|
|
1021
|
+
// Without a key, the click goes to the form below: that provider, and the key field.
|
|
1022
|
+
onclick: () => {
|
|
1023
|
+
if (can) return choose(m);
|
|
1024
|
+
if (providers.some((p) => p.id === m.provider)) {
|
|
1025
|
+
sel.value = m.provider;
|
|
1026
|
+
setLink();
|
|
942
1027
|
}
|
|
1028
|
+
$("login-key").focus();
|
|
943
1029
|
},
|
|
944
|
-
}
|
|
1030
|
+
},
|
|
1031
|
+
el("span", { class: "model-name", text: spec }),
|
|
1032
|
+
rec ? el("span", { class: "model-tag", text: t(`rec_${rec.why}`) }) : null,
|
|
1033
|
+
rec && !can ? el("span", { class: "model-why", text: t("needsKey", { x: label }) }) : null,
|
|
945
1034
|
),
|
|
946
1035
|
);
|
|
947
|
-
}
|
|
1036
|
+
};
|
|
1037
|
+
const search = $("model-search");
|
|
1038
|
+
const render = () => {
|
|
1039
|
+
const q = search.value.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
1040
|
+
const hit = (spec) => q.every((w) => spec.toLowerCase().includes(w));
|
|
1041
|
+
const recRows = mine.filter((r) => hit(r.spec)).map((r) => {
|
|
1042
|
+
const i = r.spec.indexOf("/");
|
|
1043
|
+
return row({ provider: r.spec.slice(0, i), id: r.spec.slice(i + 1) }, r);
|
|
1044
|
+
});
|
|
1045
|
+
const rest = others.filter((m) => hit(specOf(m))).map((m) => row(m, null));
|
|
1046
|
+
ul.replaceChildren();
|
|
1047
|
+
if (recRows.length) ul.append(el("li", { class: "models-group", text: t("recommended") }), ...recRows);
|
|
1048
|
+
if (rest.length) ul.append(el("li", { class: "models-group", text: t(recRows.length ? "otherModels" : "allModels") }), ...rest);
|
|
1049
|
+
if (!recRows.length && !rest.length) ul.append(el("li", { class: "empty-models", text: models.length ? t("noMatch") : t("noModels") }));
|
|
1050
|
+
};
|
|
1051
|
+
search.value = "";
|
|
1052
|
+
search.oninput = render;
|
|
1053
|
+
search.onkeydown = (e) => {
|
|
1054
|
+
if (e.key === "Enter") {
|
|
1055
|
+
e.preventDefault();
|
|
1056
|
+
ul.querySelector("button:not(.needs-key)")?.click();
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
render();
|
|
1060
|
+
search.focus();
|
|
948
1061
|
}
|
|
949
1062
|
|
|
950
1063
|
$("login-form")?.addEventListener("submit", async (e) => {
|
package/web/i18n.js
CHANGED
|
@@ -45,6 +45,16 @@ export const STRINGS = {
|
|
|
45
45
|
modelTitleResearcher: "Model for the Researcher",
|
|
46
46
|
modelTitleLibrarian: "Model for the Librarian",
|
|
47
47
|
chooseModel: "Choose a model",
|
|
48
|
+
searchModels: "Search, e.g. flash or openrouter free",
|
|
49
|
+
recommended: "Recommended for this mode",
|
|
50
|
+
otherModels: "Other models",
|
|
51
|
+
allModels: "All models",
|
|
52
|
+
noMatch: "No model matches.",
|
|
53
|
+
rec_tested: "tested default",
|
|
54
|
+
rec_cheaper: "tested, cheaper",
|
|
55
|
+
rec_free: "tested, free",
|
|
56
|
+
rec_eu: "tested, EU",
|
|
57
|
+
needsKey: "needs a key from {x}",
|
|
48
58
|
noModels: "No models yet. Connect a provider below.",
|
|
49
59
|
connectProvider: "Connect a provider",
|
|
50
60
|
provider: "Provider",
|
|
@@ -52,6 +62,25 @@ export const STRINGS = {
|
|
|
52
62
|
save: "Save",
|
|
53
63
|
getKey: "Get a key",
|
|
54
64
|
keyNote: "The key is saved on this computer, in Sub-Sub's settings folder, and is sent only to that provider.",
|
|
65
|
+
freeGoogle: "Free to try: a Google AI Studio key needs no card. The free tier limits the text sent each minute and the requests each day, enough to try Sub-Sub, and gemini-3.1-flash-lite is the tested free model. You must be 18 or older. In the EU, the UK and Switzerland, Google does not use your prompts to improve its products; elsewhere, on the free tier, it may.",
|
|
66
|
+
freeOpenRouter: "Free to try: models whose names end in :free cost nothing, with 50 requests a day (1000 after you buy 10 dollars of credit). The providers of some free models may keep your prompts.",
|
|
67
|
+
trouble_quota: "The model provider refused the request: a limit of your account was reached. Free tiers limit the text sent each minute and the requests each day. Wait a minute and ask Sub-Sub to continue; if it happens again, the daily limit is used up (it resets the next day). Or choose another model with the model button.",
|
|
68
|
+
trouble_busy: "The model provider is busy at the moment. Try again in a few minutes, or choose another model with the model button.",
|
|
69
|
+
trouble_blocked: "The model provider stopped the reply (for example, because it repeated a source word for word). Ask again in other words, or choose another model.",
|
|
70
|
+
trouble_key: "The model provider did not accept the API key. Enter it again with the model button.",
|
|
71
|
+
details: "Details",
|
|
72
|
+
undo: "Undo",
|
|
73
|
+
where_eu: "Where your data goes: servers in the European Union. Covered by the GDPR, the choice for material you would not send outside the EU.",
|
|
74
|
+
where_us: "Where your data goes: servers outside the EU, mostly in the United States, under the provider's terms for the EU. Read them before you send unpublished or sensitive material.",
|
|
75
|
+
where_cn: "Where your data goes: servers in China. Do not send unpublished or sensitive material.",
|
|
76
|
+
where_varies: "Where your data goes: it depends on the model. Each model runs at another company, in the United States, China or elsewhere. For material you would not send outside the EU, choose Mistral.",
|
|
77
|
+
setupTitle: "Before you start",
|
|
78
|
+
stepZotero: "Zotero is open and lets Sub-Sub in. If not: start Zotero, then in Settings > Advanced turn on \"Allow other applications on this computer to communicate with Zotero\".",
|
|
79
|
+
stepModel: "A model is connected. If not: select Connect and paste an API key. To try Sub-Sub for free, choose Google Gemini and get a free key there.",
|
|
80
|
+
stepServers: "The Zotero servers are running. If not ({x}): close Sub-Sub, open a terminal and type subsub doctor. Each line marked FIX says what to do.",
|
|
81
|
+
stepDone: "done",
|
|
82
|
+
stepTodo: "to do",
|
|
83
|
+
undoTitle: "Undo this change. Sub-Sub shows what it would restore before it changes anything.",
|
|
55
84
|
oauthNote: "To sign in through a provider's website (for example with a subscription), open a terminal, type subsub, then /login.",
|
|
56
85
|
saving: "Saving",
|
|
57
86
|
saved: "Saved.",
|
|
@@ -140,6 +169,16 @@ export const STRINGS = {
|
|
|
140
169
|
modelTitleResearcher: "Modelo do Investigador",
|
|
141
170
|
modelTitleLibrarian: "Modelo do Bibliotecário",
|
|
142
171
|
chooseModel: "Escolha um modelo",
|
|
172
|
+
searchModels: "Pesquisar, por exemplo flash ou openrouter free",
|
|
173
|
+
recommended: "Recomendados para este modo",
|
|
174
|
+
otherModels: "Outros modelos",
|
|
175
|
+
allModels: "Todos os modelos",
|
|
176
|
+
noMatch: "Nenhum modelo corresponde.",
|
|
177
|
+
rec_tested: "testado, por omissão",
|
|
178
|
+
rec_cheaper: "testado, mais barato",
|
|
179
|
+
rec_free: "testado, gratuito",
|
|
180
|
+
rec_eu: "testado, UE",
|
|
181
|
+
needsKey: "precisa de uma chave de {x}",
|
|
143
182
|
noModels: "Ainda não há modelos. Ligue um fornecedor abaixo.",
|
|
144
183
|
connectProvider: "Ligar um fornecedor",
|
|
145
184
|
provider: "Fornecedor",
|
|
@@ -147,6 +186,25 @@ export const STRINGS = {
|
|
|
147
186
|
save: "Guardar",
|
|
148
187
|
getKey: "Obter uma chave",
|
|
149
188
|
keyNote: "A chave fica guardada neste computador, na pasta de definições do Sub-Sub, e só é enviada a esse fornecedor.",
|
|
189
|
+
freeGoogle: "Para experimentar sem pagar: uma chave do Google AI Studio não pede cartão. O nível gratuito limita o texto enviado por minuto e os pedidos por dia, o que chega para experimentar o Sub-Sub; o modelo gratuito testado é o gemini-3.1-flash-lite. Tem de ter 18 anos ou mais. Na UE, no Reino Unido e na Suíça, a Google não usa os seus pedidos para melhorar os produtos; noutros países, no nível gratuito, pode usar.",
|
|
190
|
+
freeOpenRouter: "Para experimentar sem pagar: os modelos cujo nome termina em :free não custam nada, com 50 pedidos por dia (1000 depois de comprar 10 dólares de crédito). Os fornecedores de alguns modelos gratuitos podem guardar os seus pedidos.",
|
|
191
|
+
trouble_quota: "O fornecedor do modelo recusou o pedido: atingiu um limite da sua conta. Os níveis gratuitos limitam o texto enviado por minuto e os pedidos por dia. Espere um minuto e peça ao Sub-Sub para continuar; se voltar a acontecer, esgotou o limite diário (renova-se no dia seguinte). Ou escolha outro modelo no botão do modelo.",
|
|
192
|
+
trouble_busy: "O fornecedor do modelo está sobrecarregado neste momento. Tente outra vez dentro de alguns minutos ou escolha outro modelo no botão do modelo.",
|
|
193
|
+
trouble_blocked: "O fornecedor do modelo interrompeu a resposta (por exemplo, por repetir uma fonte palavra por palavra). Peça outra vez por outras palavras ou escolha outro modelo.",
|
|
194
|
+
trouble_key: "O fornecedor do modelo não aceitou a chave da API. Volte a introduzi-la no botão do modelo.",
|
|
195
|
+
details: "Detalhes",
|
|
196
|
+
undo: "Anular",
|
|
197
|
+
where_eu: "Para onde vão os seus dados: servidores na União Europeia. Abrangidos pelo RGPD; a escolha para material que não enviaria para fora da UE.",
|
|
198
|
+
where_us: "Para onde vão os seus dados: servidores fora da UE, sobretudo nos Estados Unidos, com os termos do fornecedor para a UE. Leia-os antes de enviar material inédito ou sensível.",
|
|
199
|
+
where_cn: "Para onde vão os seus dados: servidores na China. Não envie material inédito ou sensível.",
|
|
200
|
+
where_varies: "Para onde vão os seus dados: depende do modelo. Cada modelo funciona noutra empresa, nos Estados Unidos, na China ou noutro país. Para material que não enviaria para fora da UE, escolha a Mistral.",
|
|
201
|
+
setupTitle: "Antes de começar",
|
|
202
|
+
stepZotero: "O Zotero está aberto e deixa o Sub-Sub entrar. Se não: abra o Zotero e, em Definições > Avançado, ative \"Allow other applications on this computer to communicate with Zotero\".",
|
|
203
|
+
stepModel: "Há um modelo ligado. Se não: selecione Ligar e cole uma chave de API. Para experimentar sem pagar, escolha Google Gemini e obtenha lá uma chave gratuita.",
|
|
204
|
+
stepServers: "Os servidores do Zotero estão a funcionar. Se não ({x}): feche o Sub-Sub, abra um terminal e escreva subsub doctor. Cada linha marcada FIX diz o que fazer.",
|
|
205
|
+
stepDone: "feito",
|
|
206
|
+
stepTodo: "por fazer",
|
|
207
|
+
undoTitle: "Anular esta alteração. O Sub-Sub mostra o que repõe antes de alterar alguma coisa.",
|
|
150
208
|
oauthNote: "Para entrar através do site de um fornecedor (por exemplo, com uma subscrição), abra um terminal, escreva subsub e depois /login.",
|
|
151
209
|
saving: "A guardar",
|
|
152
210
|
saved: "Guardado.",
|
package/web/index.html
CHANGED
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
</div>
|
|
112
112
|
<h3 data-i18n="chooseModel">Choose a model</h3>
|
|
113
113
|
<label class="check"><input type="checkbox" id="model-both"> <span data-i18n="forBoth">Use it in both modes</span></label>
|
|
114
|
+
<input type="search" id="model-search" class="model-search" data-i18n-placeholder="searchModels" placeholder="Search models" autocomplete="off" spellcheck="false">
|
|
114
115
|
<ul class="models" id="models"></ul>
|
|
115
116
|
<h3 data-i18n="connectProvider">Connect a provider</h3>
|
|
116
117
|
<form id="login-form" class="login">
|
|
@@ -121,6 +122,8 @@
|
|
|
121
122
|
<input type="password" id="login-key" autocomplete="off" spellcheck="false" required>
|
|
122
123
|
</label>
|
|
123
124
|
<p class="small"><a id="login-link" href="#" target="_blank" rel="noopener noreferrer" data-i18n="getKey">Get a key</a></p>
|
|
125
|
+
<p class="small muted" id="login-where"></p>
|
|
126
|
+
<p class="small free-note" id="login-free" hidden></p>
|
|
124
127
|
<p class="small muted" data-i18n="keyNote"></p>
|
|
125
128
|
<div class="modal-buttons"><button type="submit" class="primary" id="login-save" data-i18n="save">Save</button></div>
|
|
126
129
|
<p class="small muted" data-i18n="oauthNote"></p>
|