@tiagojct/subsub 0.22.0 → 0.24.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 +20 -0
- package/README.md +18 -4
- package/dist/config.js +2 -1
- package/dist/cost.js +17 -0
- package/dist/doctor.js +13 -2
- package/dist/init.js +38 -2
- package/dist/look.js +2 -2
- package/dist/prompt.js +6 -1
- package/dist/providers.js +78 -0
- package/dist/roles.js +11 -0
- package/dist/screens.js +8 -2
- package/dist/subsub.js +67 -13
- package/dist/tui-view.js +1 -1
- package/dist/tui.js +76 -4
- package/dist/web.js +8 -4
- package/package.json +1 -1
- package/prompts/check-library.md +12 -0
- package/prompts/compare.md +1 -1
- package/prompts/lit.md +9 -4
- package/roles/librarian.md +2 -1
- package/roles/researcher.md +3 -2
- package/src/config.ts +4 -1
- package/src/cost.ts +22 -0
- package/src/doctor.ts +12 -2
- package/src/init.ts +39 -2
- package/src/look.ts +2 -2
- package/src/prompt.ts +14 -1
- package/src/providers.ts +82 -0
- package/src/roles.ts +11 -0
- package/src/screens.ts +14 -2
- package/src/subsub.ts +67 -12
- package/src/tui-view.ts +1 -1
- package/src/tui.ts +79 -4
- package/src/web.ts +8 -3
- package/web/app.css +106 -53
- package/web/app.js +67 -16
- package/web/i18n.js +15 -2
- package/web/index.html +13 -7
package/web/app.css
CHANGED
|
@@ -115,9 +115,13 @@ body {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
button, input, select, textarea { font: inherit; color: inherit; }
|
|
118
|
-
a { color: var(--accent); text-underline-offset: 0.
|
|
118
|
+
a { color: var(--accent); text-underline-offset: 0.2em; text-decoration-thickness: 1px; }
|
|
119
119
|
a:hover { text-decoration-thickness: 2px; }
|
|
120
120
|
svg { flex: none; }
|
|
121
|
+
/* Scrollbars as fine rules, not as grey bars. */
|
|
122
|
+
* { scrollbar-width: thin; }
|
|
123
|
+
/* The name breaks nowhere, least of all at its hyphen. */
|
|
124
|
+
.nobr { white-space: nowrap; }
|
|
121
125
|
|
|
122
126
|
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
123
127
|
[hidden] { display: none !important; }
|
|
@@ -164,20 +168,20 @@ button:disabled { opacity: 0.45; cursor: default; }
|
|
|
164
168
|
.icon-btn:hover { color: var(--text); background: var(--surface); }
|
|
165
169
|
.icon-btn[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); }
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
/* Top bar text buttons, as the website's navigation: muted, inked and underlined on hover or when open. */
|
|
168
172
|
.chip-btn {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
white-space: nowrap;
|
|
173
|
-
font-family: var(--mono);
|
|
174
|
-
font-size: 0.78rem;
|
|
173
|
+
flex: none;
|
|
174
|
+
padding: 0.35rem 0.5rem;
|
|
175
|
+
font-size: 0.9rem;
|
|
175
176
|
color: var(--muted);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
text-underline-offset: 0.35em;
|
|
179
|
+
text-decoration-thickness: 1px;
|
|
179
180
|
}
|
|
180
|
-
.chip-btn:hover { color: var(--text);
|
|
181
|
+
.chip-btn:hover { color: var(--text); text-decoration-line: underline; }
|
|
182
|
+
.chip-btn.model-btn { flex: 0 1 auto; max-width: 16rem; overflow: hidden; text-overflow: ellipsis; font-family: var(--mono); font-size: 0.8rem; }
|
|
183
|
+
/* No model yet: pending, so gamboge and dashed. */
|
|
184
|
+
.model-btn.needs { color: var(--warn); border-color: var(--warn); border-style: dashed; font-family: var(--sans); }
|
|
181
185
|
|
|
182
186
|
select {
|
|
183
187
|
appearance: none;
|
|
@@ -319,7 +323,7 @@ select:hover { border-color: var(--rule-strong); }
|
|
|
319
323
|
.new-btn:hover { border-color: var(--text); }
|
|
320
324
|
.new-btn svg { width: 16px; height: 16px; color: var(--accent); }
|
|
321
325
|
|
|
322
|
-
.actions { display: flex; flex-direction: column; }
|
|
326
|
+
.actions, .actions > div { display: flex; flex-direction: column; }
|
|
323
327
|
.actions button {
|
|
324
328
|
text-align: left;
|
|
325
329
|
padding: 0.32rem 0.5rem;
|
|
@@ -329,9 +333,35 @@ select:hover { border-color: var(--rule-strong); }
|
|
|
329
333
|
}
|
|
330
334
|
.actions button:hover { background: var(--bg); }
|
|
331
335
|
.actions .group { margin: 0.6rem 0 0.15rem; padding: 0 0.5rem; font-family: var(--serif); font-style: italic; font-size: 0.86rem; color: var(--muted); }
|
|
332
|
-
.actions
|
|
336
|
+
.actions > :first-child .group { margin-top: 0; }
|
|
337
|
+
|
|
338
|
+
/* Sections and action groups fold at their heading; a fine chevron points down when open, right when folded. */
|
|
339
|
+
.side .fold {
|
|
340
|
+
display: inline-flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
gap: 0.55em;
|
|
343
|
+
padding: 0;
|
|
344
|
+
border: 0;
|
|
345
|
+
border-radius: 0;
|
|
346
|
+
font-size: inherit;
|
|
347
|
+
letter-spacing: inherit;
|
|
348
|
+
color: inherit;
|
|
349
|
+
text-align: left;
|
|
350
|
+
}
|
|
351
|
+
.side .fold:hover { color: var(--text); background: none; }
|
|
352
|
+
.side .fold::after {
|
|
353
|
+
content: "";
|
|
354
|
+
width: 0.34em;
|
|
355
|
+
height: 0.34em;
|
|
356
|
+
border-right: 1px solid currentColor;
|
|
357
|
+
border-bottom: 1px solid currentColor;
|
|
358
|
+
transform: translateY(-0.15em) rotate(45deg);
|
|
359
|
+
}
|
|
360
|
+
.side .fold[aria-expanded="false"]::after { transform: rotate(-45deg); }
|
|
361
|
+
[data-fold].folded > :not(h2, .side-subhead, .group) { display: none; }
|
|
333
362
|
|
|
334
363
|
.side .grow { flex: 1; display: flex; flex-direction: column; min-height: 8rem; }
|
|
364
|
+
.side .grow.folded { flex: none; min-height: 0; }
|
|
335
365
|
.side-subhead { display: flex; justify-content: space-between; align-items: baseline; padding-right: 0.3rem; }
|
|
336
366
|
|
|
337
367
|
.search {
|
|
@@ -423,7 +453,6 @@ body.layout-full { --measure: 72rem; }
|
|
|
423
453
|
padding: 0.6rem 0.6rem 0.6rem 0.9rem;
|
|
424
454
|
border: 1px solid var(--rule);
|
|
425
455
|
border-top: 2px solid var(--warn);
|
|
426
|
-
border-radius: var(--radius);
|
|
427
456
|
background: var(--raised);
|
|
428
457
|
display: flex;
|
|
429
458
|
gap: 0.75rem;
|
|
@@ -454,14 +483,16 @@ body.layout-full { --measure: 72rem; }
|
|
|
454
483
|
overflow-wrap: anywhere;
|
|
455
484
|
font-size: 0.95rem;
|
|
456
485
|
}
|
|
486
|
+
/* A command is code: it is set in the mono, as the website's plate draws it. */
|
|
487
|
+
.msg.user .bubble.cmd { font-family: var(--mono); font-size: 0.86rem; }
|
|
457
488
|
.msg.user details.bubble summary { cursor: pointer; color: var(--muted); }
|
|
458
489
|
.msg.user details.bubble[open] summary { margin-bottom: 0.4rem; }
|
|
459
490
|
|
|
460
491
|
/* Replies read as text, in the serif. */
|
|
461
492
|
.md {
|
|
462
493
|
font-family: var(--serif);
|
|
463
|
-
font-size: 1.
|
|
464
|
-
line-height: 1.
|
|
494
|
+
font-size: 1.0625rem;
|
|
495
|
+
line-height: 1.65;
|
|
465
496
|
font-variant-numeric: oldstyle-nums proportional-nums;
|
|
466
497
|
overflow-wrap: anywhere;
|
|
467
498
|
}
|
|
@@ -470,16 +501,20 @@ body.layout-full { --measure: 72rem; }
|
|
|
470
501
|
.md p, .md ul, .md ol { margin: 0.7rem 0; }
|
|
471
502
|
.md li { margin: 0.2rem 0; }
|
|
472
503
|
.md li::marker { color: var(--faint); }
|
|
504
|
+
/* Numbered steps as on the website: the number in the accent, lining figures. */
|
|
505
|
+
.md ol > li::marker { color: var(--accent); font-weight: 600; font-variant-numeric: lining-nums tabular-nums; }
|
|
506
|
+
/* Headings as on the website: roman, weight 500 for the large ones, a fine rule above a section. */
|
|
473
507
|
.md h1, .md h2, .md h3, .md h4 {
|
|
474
508
|
font-family: var(--serif);
|
|
475
|
-
line-height: 1.
|
|
476
|
-
margin: 1.
|
|
509
|
+
line-height: 1.25;
|
|
510
|
+
margin: 1.8rem 0 0.6rem;
|
|
477
511
|
font-weight: 600;
|
|
512
|
+
text-wrap: balance;
|
|
478
513
|
}
|
|
479
|
-
.md h1 { font-size: 1.
|
|
480
|
-
.md h2 { font-size: 1.
|
|
481
|
-
.md h3 { font-size:
|
|
482
|
-
.md h4 { font-size: 0.
|
|
514
|
+
.md h1 { font-size: 1.5rem; font-weight: 500; line-height: 1.15; letter-spacing: -0.012em; }
|
|
515
|
+
.md h2 { font-size: 1.25rem; font-weight: 500; letter-spacing: -0.008em; padding-top: 0.5rem; border-top: 1px solid var(--rule-strong); }
|
|
516
|
+
.md h3 { font-size: 1.04rem; }
|
|
517
|
+
.md h4 { font-size: 0.95rem; color: var(--muted); }
|
|
483
518
|
.md strong { font-weight: 600; }
|
|
484
519
|
.md hr { border: 0; border-top: 1px solid var(--rule); margin: 1.5rem 0; }
|
|
485
520
|
.msg.assistant + .msg.assistant { margin-top: 0.9rem; }
|
|
@@ -568,7 +603,7 @@ body.layout-full { --measure: 72rem; }
|
|
|
568
603
|
|
|
569
604
|
.msg-actions { display: flex; gap: 0.25rem; margin-top: 0.4rem; opacity: 0; transition: opacity 0.12s ease; }
|
|
570
605
|
.msg.assistant:hover .msg-actions, .msg-actions:focus-within { opacity: 1; }
|
|
571
|
-
.msg-action-btn { font-size: 0.75rem; padding: 0.15rem 0.45rem; color: var(--faint); }
|
|
606
|
+
.msg-action-btn { font-family: var(--sans); font-size: 0.75rem; padding: 0.15rem 0.45rem; color: var(--faint); }
|
|
572
607
|
.msg-action-btn:hover { color: var(--text); background: var(--surface); }
|
|
573
608
|
.msg-action-btn.copied { color: var(--add); }
|
|
574
609
|
.table-actions { display: flex; justify-content: flex-end; margin: -0.35rem 0 0.9rem; }
|
|
@@ -633,7 +668,6 @@ body.layout-full { --measure: 72rem; }
|
|
|
633
668
|
.note {
|
|
634
669
|
margin-top: 1rem;
|
|
635
670
|
padding: 0.6rem 0.9rem;
|
|
636
|
-
border-radius: var(--radius);
|
|
637
671
|
background: var(--surface);
|
|
638
672
|
border: 1px solid var(--rule);
|
|
639
673
|
font-size: 0.88rem;
|
|
@@ -659,44 +693,48 @@ body.layout-full { --measure: 72rem; }
|
|
|
659
693
|
gap: 0.55rem;
|
|
660
694
|
align-items: center;
|
|
661
695
|
}
|
|
696
|
+
/* Working is drawn by line, as in the terminal: dotted, dashed, solid. Without motion it stays dashed: pending. */
|
|
662
697
|
.working::before {
|
|
663
698
|
content: "";
|
|
664
699
|
width: 28px;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
background: linear-gradient(90deg, var(--accent) 0 40%, var(--rule) 40% 100%);
|
|
668
|
-
background-size: 250% 100%;
|
|
669
|
-
animation: sweep 1.3s ease-in-out infinite alternate;
|
|
700
|
+
border-top: 2px dashed var(--accent);
|
|
701
|
+
animation: lines 1.5s steps(1, end) infinite;
|
|
670
702
|
}
|
|
671
|
-
@keyframes
|
|
703
|
+
@keyframes lines { 0% { border-top-style: dotted; } 33% { border-top-style: dashed; } 67% { border-top-style: solid; } }
|
|
672
704
|
@keyframes pulse { 50% { opacity: 0.3; } }
|
|
673
705
|
|
|
674
706
|
/* Empty conversation. */
|
|
675
707
|
.empty { padding-top: 11vh; }
|
|
676
|
-
|
|
708
|
+
/* The running head of the website's plates: the name, an italic middle, the mode in its colour. */
|
|
709
|
+
.empty .runhead {
|
|
677
710
|
display: flex;
|
|
678
711
|
justify-content: space-between;
|
|
679
|
-
|
|
680
|
-
|
|
712
|
+
align-items: baseline;
|
|
713
|
+
gap: 0.5rem 1.5rem;
|
|
714
|
+
margin: 0 0 1.75rem;
|
|
715
|
+
padding-bottom: 0.35rem;
|
|
681
716
|
border-bottom: 1px solid var(--text);
|
|
682
717
|
font-family: var(--serif);
|
|
683
|
-
font-size:
|
|
718
|
+
font-size: 1.02rem;
|
|
719
|
+
line-height: 1.3;
|
|
684
720
|
font-variant-caps: all-small-caps;
|
|
685
721
|
letter-spacing: 0.08em;
|
|
686
|
-
color: var(--
|
|
722
|
+
color: var(--muted);
|
|
687
723
|
}
|
|
688
|
-
.empty .
|
|
724
|
+
.empty .runhead .mid { font-style: italic; font-variant-caps: normal; letter-spacing: 0; text-align: center; }
|
|
725
|
+
.empty .runhead .mode { color: var(--accent); }
|
|
689
726
|
.empty h1 {
|
|
690
727
|
font-family: var(--serif);
|
|
691
728
|
font-weight: 500;
|
|
692
|
-
font-size: clamp(
|
|
693
|
-
line-height: 1.
|
|
694
|
-
letter-spacing: -0.
|
|
695
|
-
margin: 0 0 0.
|
|
729
|
+
font-size: clamp(2rem, 3.6vw, 2.75rem);
|
|
730
|
+
line-height: 1.06;
|
|
731
|
+
letter-spacing: -0.018em;
|
|
732
|
+
margin: 0 0 0.9rem;
|
|
733
|
+
text-wrap: balance;
|
|
696
734
|
}
|
|
697
|
-
.empty .lede { color: var(--muted); max-width:
|
|
735
|
+
.empty .lede { font-family: var(--serif); font-size: 1.08rem; line-height: 1.55; color: var(--muted); max-width: 36rem; margin: 0 0 2rem; text-wrap: pretty; }
|
|
698
736
|
/* Suggestions: a short list of lines to start from, not a row of cards. */
|
|
699
|
-
.chips { display: grid; max-width:
|
|
737
|
+
.chips { display: grid; max-width: 36rem; border-bottom: 1px solid var(--rule-strong); }
|
|
700
738
|
.chips button {
|
|
701
739
|
text-align: left;
|
|
702
740
|
border: 0;
|
|
@@ -792,7 +830,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
792
830
|
}
|
|
793
831
|
.modal-kicker::after { content: "Sub-Sub"; color: var(--muted); }
|
|
794
832
|
.modal-kicker:empty { display: none; }
|
|
795
|
-
.modal h2 { font-family: var(--serif); font-size: 1.
|
|
833
|
+
.modal h2 { font-family: var(--serif); font-size: 1.4rem; font-weight: 500; line-height: 1.2; letter-spacing: -0.01em; margin: 0 0 0.35rem; overflow-wrap: anywhere; }
|
|
796
834
|
.modal h3 { font-family: var(--serif); font-size: 0.98rem; font-weight: 400; font-variant-caps: all-small-caps; letter-spacing: 0.08em; color: var(--muted); margin: 1.4rem 0 0.5rem; }
|
|
797
835
|
.modal .hint { color: var(--muted); margin: 0 0 1rem; font-size: 0.88rem; }
|
|
798
836
|
|
|
@@ -888,8 +926,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
888
926
|
|
|
889
927
|
/* ---------------------------------------------------------------- library search and notes */
|
|
890
928
|
|
|
891
|
-
#notes-btn {
|
|
892
|
-
#notes-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }
|
|
929
|
+
#notes-btn[aria-expanded="true"] { color: var(--text); text-decoration-line: underline; }
|
|
893
930
|
|
|
894
931
|
.items { list-style: none; margin: 0; padding: 0; max-height: 18rem; overflow-y: auto; }
|
|
895
932
|
.items:empty { display: none; }
|
|
@@ -910,11 +947,12 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
910
947
|
.session-tools button { width: auto; padding: 0.1rem 0.35rem; font-size: 0.7rem; color: var(--muted); }
|
|
911
948
|
.session-tools button:hover { color: var(--text); background: var(--bg); }
|
|
912
949
|
|
|
950
|
+
/* A note is a page: the lighter plate field, as on the website. */
|
|
913
951
|
.note-pane {
|
|
914
952
|
grid-row: 2;
|
|
915
953
|
grid-column: 3;
|
|
916
|
-
border-left: 1px solid var(--rule);
|
|
917
|
-
background: var(--
|
|
954
|
+
border-left: 1px solid var(--rule-strong);
|
|
955
|
+
background: var(--raised);
|
|
918
956
|
display: flex;
|
|
919
957
|
flex-direction: column;
|
|
920
958
|
min-height: 0;
|
|
@@ -923,7 +961,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
923
961
|
.note-pane[hidden] { display: none; }
|
|
924
962
|
.note-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--rule); }
|
|
925
963
|
.note-name { flex: 1; min-width: 0; font-family: var(--mono); font-size: 0.8rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
926
|
-
.note-pane > .search { margin: 0.6rem 0.9rem 0; width: auto; }
|
|
964
|
+
.note-pane > .search { margin: 0.6rem 0.9rem 0; width: auto; background: var(--bg); }
|
|
927
965
|
.note-body { flex: 1; overflow-y: auto; padding: 0.9rem 1.1rem 1.5rem; }
|
|
928
966
|
.note-body .front { font-size: 0.74rem; color: var(--muted); margin: 0 0 1rem; }
|
|
929
967
|
/* What a note was written from, drawn by line: solid for the full text, dashed for the abstract,
|
|
@@ -940,11 +978,25 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
940
978
|
#note-read[aria-pressed="true"] { color: var(--accent); text-decoration: underline; }
|
|
941
979
|
.note-body .front summary { cursor: pointer; }
|
|
942
980
|
.note-body .front pre { background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius); padding: 0.5rem 0.7rem; margin: 0.4rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
943
|
-
|
|
944
|
-
.note-list
|
|
981
|
+
/* The notes, one folder per section under a small-capitals head, rows ruled as in a table. */
|
|
982
|
+
.note-list section + section { margin-top: 1.4rem; }
|
|
983
|
+
.note-list h3 {
|
|
984
|
+
margin: 0;
|
|
985
|
+
padding: 0 0.5rem 0.25rem;
|
|
986
|
+
border-bottom: 1px solid var(--text);
|
|
987
|
+
font-family: var(--serif);
|
|
988
|
+
font-size: 1rem;
|
|
989
|
+
font-weight: 400;
|
|
990
|
+
font-variant-caps: all-small-caps;
|
|
991
|
+
letter-spacing: 0.08em;
|
|
992
|
+
color: var(--muted);
|
|
993
|
+
}
|
|
994
|
+
.note-list ul { list-style: none; margin: 0; padding: 0; }
|
|
995
|
+
.note-list li + li { border-top: 1px solid var(--rule); }
|
|
996
|
+
.note-list button { width: 100%; text-align: left; padding: 0.5rem; border-radius: 0; display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
|
|
945
997
|
.note-list button:hover { background: var(--bg); }
|
|
946
|
-
.note-list .rel { font-size: 0.
|
|
947
|
-
.note-list .when { font-size: 0.
|
|
998
|
+
.note-list .rel { min-width: 0; font-size: 0.9rem; overflow-wrap: anywhere; }
|
|
999
|
+
.note-list .when { flex: none; font-size: 0.74rem; color: var(--faint); font-variant-numeric: tabular-nums; }
|
|
948
1000
|
.note-body img.note-img { display: block; max-width: 100%; height: auto; margin: 0.8rem 0; border: 1px solid var(--rule); border-radius: var(--radius); background: #fff; }
|
|
949
1001
|
a[data-note] { color: var(--accent); text-decoration: underline; text-decoration-color: var(--accent-soft); text-underline-offset: 0.18em; cursor: pointer; }
|
|
950
1002
|
|
|
@@ -980,6 +1032,7 @@ a[data-note] { color: var(--accent); text-decoration: underline; text-decoration
|
|
|
980
1032
|
@media (max-width: 30rem) {
|
|
981
1033
|
.brand > span { display: none; }
|
|
982
1034
|
.modes button { padding: 0.28rem 0.6rem; }
|
|
1035
|
+
.empty .runhead .mid { display: none; }
|
|
983
1036
|
}
|
|
984
1037
|
|
|
985
1038
|
@media (prefers-reduced-motion: reduce) {
|
package/web/app.js
CHANGED
|
@@ -12,6 +12,8 @@ let session = null; // pi's session state
|
|
|
12
12
|
let busy = false;
|
|
13
13
|
let commands = [];
|
|
14
14
|
let providers = [];
|
|
15
|
+
/** The login form's choice for a provider that pi does not include (src/providers.ts CUSTOM_DEFAULT_NAME). */
|
|
16
|
+
const OTHER_PROVIDER = "custom";
|
|
15
17
|
const dialogs = []; // waiting dialog requests, oldest first
|
|
16
18
|
let shownDialog = null;
|
|
17
19
|
let currentMsg = null; // assistant message being streamed
|
|
@@ -255,7 +257,10 @@ function renderHeader() {
|
|
|
255
257
|
$("starbuck-label").title = t("starbuckHint");
|
|
256
258
|
const m = state?.model ?? session?.model;
|
|
257
259
|
const none = !m || !m.id || m.id === "unknown";
|
|
258
|
-
|
|
260
|
+
// With a cost limit in the settings, the conversation's cost and the limit follow the model.
|
|
261
|
+
const c = state?.cost;
|
|
262
|
+
const cost = c && typeof c.spent === "number" ? ` · $${c.spent.toFixed(2)}/$${c.limit.toFixed(2)}` : "";
|
|
263
|
+
$("model-btn").textContent = $("model-btn-side").textContent = none ? t("noModel") : m.id + cost;
|
|
259
264
|
for (const b of [$("model-btn"), $("model-btn-side")]) b.classList.toggle("needs", none);
|
|
260
265
|
const lib = $("library");
|
|
261
266
|
const txt = lib.querySelector(".library-text") ?? lib;
|
|
@@ -296,6 +301,16 @@ function hasCommand(name) {
|
|
|
296
301
|
return commands.length === 0 || commands.some((c) => c.name === base);
|
|
297
302
|
}
|
|
298
303
|
|
|
304
|
+
// Side panel sections and action groups fold at their heading; the folded ones are remembered on this computer.
|
|
305
|
+
const folded = new Set((localStorage.getItem("subsub-folded") || "").split(" ").filter(Boolean));
|
|
306
|
+
function setFold(box, on) {
|
|
307
|
+
box.classList.toggle("folded", on);
|
|
308
|
+
box.querySelector(".fold").setAttribute("aria-expanded", String(!on));
|
|
309
|
+
if (on) folded.add(box.dataset.fold);
|
|
310
|
+
else folded.delete(box.dataset.fold);
|
|
311
|
+
localStorage.setItem("subsub-folded", [...folded].join(" "));
|
|
312
|
+
}
|
|
313
|
+
|
|
299
314
|
function renderActions() {
|
|
300
315
|
const box = $("actions");
|
|
301
316
|
box.replaceChildren();
|
|
@@ -305,8 +320,9 @@ function renderActions() {
|
|
|
305
320
|
for (const [group, label] of groups) {
|
|
306
321
|
const list = ACTIONS[group].filter(shown);
|
|
307
322
|
if (!list.length) continue;
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
const g = el("div", { "data-fold": `actions-${group}` }, el("p", { class: "group" }, el("button", { type: "button", class: "fold", text: t(label) })), list.map(actionButton));
|
|
324
|
+
setFold(g, folded.has(g.dataset.fold));
|
|
325
|
+
box.append(g);
|
|
310
326
|
}
|
|
311
327
|
}
|
|
312
328
|
|
|
@@ -315,7 +331,10 @@ function actionButton(a) {
|
|
|
315
331
|
type: "button",
|
|
316
332
|
text: a[lang] ?? a.en,
|
|
317
333
|
onclick: () => {
|
|
318
|
-
if (a.item)
|
|
334
|
+
if (a.item) {
|
|
335
|
+
setFold($("library-search").closest("[data-fold]"), false);
|
|
336
|
+
return $("library-search").focus();
|
|
337
|
+
}
|
|
319
338
|
closeSide();
|
|
320
339
|
if (a.send) sendMessage(`/${a.cmd}`);
|
|
321
340
|
else {
|
|
@@ -349,7 +368,9 @@ function renderItems(items, message) {
|
|
|
349
368
|
if (message) return ul.append(el("li", { class: "muted", text: message }));
|
|
350
369
|
const researcher = mode() === "researcher";
|
|
351
370
|
for (const it of items) {
|
|
352
|
-
|
|
371
|
+
// A group library's item says its library (keep in step with itemRef in src/screens.ts).
|
|
372
|
+
const ref = it.library ? `${it.citekey || it.key} in ${it.library}` : it.citekey || it.key;
|
|
373
|
+
const scope = it.library ? `groups/${it.library.split(":")[1]}` : "library";
|
|
353
374
|
const run = (cmd) => () => {
|
|
354
375
|
closeSide();
|
|
355
376
|
sendMessage(`/${cmd} ${ref}`);
|
|
@@ -359,13 +380,13 @@ function renderItems(items, message) {
|
|
|
359
380
|
"li",
|
|
360
381
|
{},
|
|
361
382
|
el("div", { class: "item-title", text: it.title || it.key }),
|
|
362
|
-
el("div", { class: "item-meta", text: [it.authors, it.year, it.citekey].filter(Boolean).join(" · ") }),
|
|
383
|
+
el("div", { class: "item-meta", text: [it.authors, it.year, it.citekey, it.group].filter(Boolean).join(" · ") }),
|
|
363
384
|
el(
|
|
364
385
|
"div",
|
|
365
386
|
{ class: "item-actions" },
|
|
366
387
|
researcher ? el("button", { type: "button", text: t("readItem"), onclick: run("read") }) : null,
|
|
367
388
|
researcher ? el("button", { type: "button", text: t("noteItem"), onclick: run("lit-note") }) : null,
|
|
368
|
-
el("a", { href: `zotero://select/
|
|
389
|
+
el("a", { href: `zotero://select/${scope}/items/${encodeURIComponent(it.key)}`, text: "Zotero" }),
|
|
369
390
|
),
|
|
370
391
|
),
|
|
371
392
|
);
|
|
@@ -407,16 +428,34 @@ function renderNoteList() {
|
|
|
407
428
|
const q = $("note-filter").value.trim().toLowerCase();
|
|
408
429
|
const list = notesList.filter((n) => !q || n.rel.toLowerCase().includes(q));
|
|
409
430
|
const fmt = new Intl.DateTimeFormat(lang === "pt" ? "pt-PT" : "en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
431
|
+
// One group per folder (Inbox, Literature, ...), newest first inside each, as the server sorts them.
|
|
432
|
+
const groups = new Map();
|
|
433
|
+
for (const n of list) {
|
|
434
|
+
const [folder, ...rest] = n.rel.split(/[\\/]/);
|
|
435
|
+
if (!groups.has(folder)) groups.set(folder, []);
|
|
436
|
+
groups.get(folder).push({ ...n, name: rest.join("/").replace(/\.md$/i, "") || folder });
|
|
437
|
+
}
|
|
410
438
|
body.replaceChildren(
|
|
411
439
|
list.length
|
|
412
440
|
? el(
|
|
413
|
-
"
|
|
441
|
+
"div",
|
|
414
442
|
{ class: "note-list" },
|
|
415
|
-
|
|
443
|
+
[...groups].map(([folder, notes]) =>
|
|
416
444
|
el(
|
|
417
|
-
"
|
|
445
|
+
"section",
|
|
418
446
|
{},
|
|
419
|
-
el("
|
|
447
|
+
el("h3", { text: folder }),
|
|
448
|
+
el(
|
|
449
|
+
"ul",
|
|
450
|
+
{},
|
|
451
|
+
notes.map((n) =>
|
|
452
|
+
el(
|
|
453
|
+
"li",
|
|
454
|
+
{},
|
|
455
|
+
el("button", { type: "button", title: n.rel, onclick: () => openNote(n.path) }, el("span", { class: "rel", text: n.name }), el("span", { class: "when", text: fmt.format(new Date(n.modified)) })),
|
|
456
|
+
),
|
|
457
|
+
),
|
|
458
|
+
),
|
|
420
459
|
),
|
|
421
460
|
),
|
|
422
461
|
)
|
|
@@ -692,7 +731,7 @@ function renderEmpty() {
|
|
|
692
731
|
el(
|
|
693
732
|
"div",
|
|
694
733
|
{ class: "empty" },
|
|
695
|
-
el("p", { class: "
|
|
734
|
+
el("p", { class: "runhead" }, el("span", { class: "nobr", text: "Sub-Sub" }), el("span", { class: "mid", text: t("tagline") }), el("span", { class: "mode", text: t(mode()) })),
|
|
696
735
|
el("h1", { text: t("emptyTitle") }),
|
|
697
736
|
el("p", { class: "lede", text: t(mode() === "librarian" ? "emptyLibrarian" : "emptyResearcher") }),
|
|
698
737
|
setupList(),
|
|
@@ -732,7 +771,7 @@ function userMessage(text) {
|
|
|
732
771
|
if (text.length > 700) {
|
|
733
772
|
const first = text.split("\n").find((l) => l.trim()) ?? "";
|
|
734
773
|
bubble = el("details", { class: "bubble" }, el("summary", { text: first.length > 120 ? `${first.slice(0, 117)}...` : first }), el("div", { text }));
|
|
735
|
-
} else bubble = el("div", { class: "bubble", text });
|
|
774
|
+
} else bubble = el("div", { class: /^\/\S/.test(text) ? "bubble cmd" : "bubble", text });
|
|
736
775
|
log.append(el("div", { class: "msg user" }, bubble));
|
|
737
776
|
scrollDown(true);
|
|
738
777
|
}
|
|
@@ -1228,15 +1267,22 @@ async function openModels() {
|
|
|
1228
1267
|
const ul = $("models");
|
|
1229
1268
|
ul.replaceChildren(el("li", { class: "empty-models", text: "…" }));
|
|
1230
1269
|
const sel = $("login-provider");
|
|
1231
|
-
sel.replaceChildren(...providers.map((p) => el("option", { value: p.id, text: p.label })));
|
|
1270
|
+
sel.replaceChildren(...providers.map((p) => el("option", { value: p.id, text: p.label })), el("option", { value: OTHER_PROVIDER, text: t("otherProvider") }));
|
|
1232
1271
|
const link = $("login-link");
|
|
1233
1272
|
const setLink = () => {
|
|
1234
1273
|
const p = providers.find((x) => x.id === sel.value);
|
|
1274
|
+
const other = sel.value === OTHER_PROVIDER;
|
|
1275
|
+
link.hidden = other;
|
|
1235
1276
|
link.href = p?.url ?? "#";
|
|
1277
|
+
$("login-url-row").hidden = !other;
|
|
1278
|
+
$("login-model-row").hidden = !other;
|
|
1279
|
+
$("login-url").required = other;
|
|
1280
|
+
$("login-model").required = other;
|
|
1281
|
+
$("login-key").required = !other;
|
|
1236
1282
|
const free = { google: "freeGoogle", openrouter: "freeOpenRouter" }[sel.value];
|
|
1237
1283
|
$("login-free").hidden = !free;
|
|
1238
1284
|
$("login-free").textContent = free ? t(free) : "";
|
|
1239
|
-
$("login-where").textContent = p?.where ? t(`where_${p.where}`) : "";
|
|
1285
|
+
$("login-where").textContent = other ? t("otherNote") : p?.where ? t(`where_${p.where}`) : "";
|
|
1240
1286
|
};
|
|
1241
1287
|
sel.onchange = setLink;
|
|
1242
1288
|
setLink();
|
|
@@ -1329,7 +1375,7 @@ $("login-form")?.addEventListener("submit", async (e) => {
|
|
|
1329
1375
|
btn.disabled = true;
|
|
1330
1376
|
btn.textContent = t("saving");
|
|
1331
1377
|
try {
|
|
1332
|
-
await api("/api/login", { provider: $("login-provider").value, key: $("login-key").value });
|
|
1378
|
+
await api("/api/login", { provider: $("login-provider").value, key: $("login-key").value, baseUrl: $("login-url").value, model: $("login-model").value });
|
|
1333
1379
|
$("login-key").value = "";
|
|
1334
1380
|
note(t("saved"));
|
|
1335
1381
|
await openModels();
|
|
@@ -1517,6 +1563,11 @@ $("theme-btn-side")?.addEventListener("click", () => {
|
|
|
1517
1563
|
closeSide();
|
|
1518
1564
|
toggleTheme();
|
|
1519
1565
|
});
|
|
1566
|
+
$("side").addEventListener("click", (e) => {
|
|
1567
|
+
const box = e.target.closest(".fold")?.closest("[data-fold]");
|
|
1568
|
+
if (box) setFold(box, !box.classList.contains("folded"));
|
|
1569
|
+
});
|
|
1570
|
+
for (const box of document.querySelectorAll(".side section[data-fold]")) setFold(box, folded.has(box.dataset.fold));
|
|
1520
1571
|
$("session-search")?.addEventListener("input", (e) => renderSessionsList(e.target.value));
|
|
1521
1572
|
$("library-search").addEventListener("input", searchLibrary);
|
|
1522
1573
|
$("notes-btn").addEventListener("click", () => ($("note-pane").hidden ? showNoteList() : showPane(false)));
|
package/web/i18n.js
CHANGED
|
@@ -24,7 +24,7 @@ export const STRINGS = {
|
|
|
24
24
|
starbuckReport: "Write the Starbuck report to {x}?",
|
|
25
25
|
approveHint: "Nothing changes until you select Yes.",
|
|
26
26
|
zoteroChecking: "Zotero: checking",
|
|
27
|
-
zoteroDown: "Zotero is
|
|
27
|
+
zoteroDown: "Zotero is closed: search only",
|
|
28
28
|
items: "{n} items",
|
|
29
29
|
toReview: "{n} to check",
|
|
30
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.",
|
|
@@ -59,6 +59,10 @@ export const STRINGS = {
|
|
|
59
59
|
save: "Save",
|
|
60
60
|
getKey: "Get a key",
|
|
61
61
|
keyNote: "The key is saved on this computer, in Sub-Sub's settings folder, and is sent only to that provider.",
|
|
62
|
+
otherProvider: "Another provider (OpenAI API)",
|
|
63
|
+
serverUrl: "Server address",
|
|
64
|
+
modelId: "Model id",
|
|
65
|
+
otherNote: "A server that speaks the OpenAI API: your institution's, or a local one (Ollama, LM Studio). The key can stay empty for a local server. Your messages go to that server.",
|
|
62
66
|
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.",
|
|
63
67
|
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.",
|
|
64
68
|
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.",
|
|
@@ -84,6 +88,7 @@ export const STRINGS = {
|
|
|
84
88
|
saved: "Saved.",
|
|
85
89
|
busy: "Wait until Sub-Sub finishes.",
|
|
86
90
|
emptyTitle: "What would you like to do?",
|
|
91
|
+
tagline: "A research assistant for Zotero",
|
|
87
92
|
quit: "Close Sub-Sub",
|
|
88
93
|
quitDone: "Sub-Sub is closed. You can close this tab.",
|
|
89
94
|
earlier: "Earlier messages were summarised.",
|
|
@@ -166,7 +171,7 @@ export const STRINGS = {
|
|
|
166
171
|
starbuckReport: "Escrever o relatório do Starbuck em {x}?",
|
|
167
172
|
approveHint: "Nada muda até selecionar Sim.",
|
|
168
173
|
zoteroChecking: "Zotero: a verificar",
|
|
169
|
-
zoteroDown: "O Zotero
|
|
174
|
+
zoteroDown: "O Zotero está fechado: só pesquisa",
|
|
170
175
|
items: "{n} itens",
|
|
171
176
|
toReview: "{n} por verificar",
|
|
172
177
|
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.",
|
|
@@ -201,6 +206,10 @@ export const STRINGS = {
|
|
|
201
206
|
save: "Guardar",
|
|
202
207
|
getKey: "Obter uma chave",
|
|
203
208
|
keyNote: "A chave fica guardada neste computador, na pasta de definições do Sub-Sub, e só é enviada a esse fornecedor.",
|
|
209
|
+
otherProvider: "Outro fornecedor (API da OpenAI)",
|
|
210
|
+
serverUrl: "Endereço do servidor",
|
|
211
|
+
modelId: "Identificador do modelo",
|
|
212
|
+
otherNote: "Um servidor que fala a API da OpenAI: o da sua instituição, ou um servidor local (Ollama, LM Studio). Num servidor local, a chave pode ficar vazia. As suas mensagens vão para esse servidor.",
|
|
204
213
|
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.",
|
|
205
214
|
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.",
|
|
206
215
|
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.",
|
|
@@ -226,6 +235,7 @@ export const STRINGS = {
|
|
|
226
235
|
saved: "Guardado.",
|
|
227
236
|
busy: "Espere que o Sub-Sub termine.",
|
|
228
237
|
emptyTitle: "O que quer fazer?",
|
|
238
|
+
tagline: "Um assistente de investigação para o Zotero",
|
|
229
239
|
quit: "Fechar o Sub-Sub",
|
|
230
240
|
quitDone: "O Sub-Sub está fechado. Pode fechar este separador.",
|
|
231
241
|
earlier: "As mensagens anteriores foram resumidas.",
|
|
@@ -314,6 +324,7 @@ export const ACTIONS = {
|
|
|
314
324
|
{ cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
|
|
315
325
|
{ cmd: "tag-batch 10", send: true, en: "Tag 10 items", pt: "Etiquetar 10 itens" },
|
|
316
326
|
{ cmd: "clean-tags", send: true, en: "Clean up tags", pt: "Limpar etiquetas" },
|
|
327
|
+
{ cmd: "check-library", send: true, en: "Check the library", pt: "Verificar a biblioteca" },
|
|
317
328
|
{ cmd: "import-queue", send: true, en: "Import the queue", pt: "Importar a fila" },
|
|
318
329
|
{ cmd: "history", send: true, en: "Recent changes", pt: "Alterações recentes" },
|
|
319
330
|
{ cmd: "undo", send: true, en: "Undo the last change", pt: "Desfazer a última alteração" },
|
|
@@ -339,6 +350,7 @@ export const SUGGESTIONS = {
|
|
|
339
350
|
export const TOOLS = {
|
|
340
351
|
en: {
|
|
341
352
|
find_items: "search the library",
|
|
353
|
+
list_libraries: "list the libraries",
|
|
342
354
|
get_figure: "cut a figure out of the PDF",
|
|
343
355
|
standalone_items: "list files without a parent item",
|
|
344
356
|
set_parent_items: "give files a parent item",
|
|
@@ -400,6 +412,7 @@ export const TOOLS = {
|
|
|
400
412
|
},
|
|
401
413
|
pt: {
|
|
402
414
|
find_items: "pesquisar na biblioteca",
|
|
415
|
+
list_libraries: "listar as bibliotecas",
|
|
403
416
|
get_figure: "recortar uma figura do PDF",
|
|
404
417
|
standalone_items: "listar ficheiros sem item principal",
|
|
405
418
|
set_parent_items: "dar um item principal aos ficheiros",
|
package/web/index.html
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
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>
|
|
25
25
|
<button type="button" class="chip-btn model-btn wide-only" id="model-btn"></button>
|
|
26
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>
|
|
27
|
-
<button type="button" class="icon-btn" id="theme-btn"></button>
|
|
27
|
+
<button type="button" class="icon-btn wide-only" id="theme-btn"></button>
|
|
28
28
|
</div>
|
|
29
29
|
</header>
|
|
30
30
|
|
|
@@ -38,18 +38,18 @@
|
|
|
38
38
|
<button type="button" class="secondary" id="theme-btn-side"></button>
|
|
39
39
|
</label>
|
|
40
40
|
</section>
|
|
41
|
-
<section>
|
|
42
|
-
<h2 data-i18n="groupLibrary">Library</h2>
|
|
41
|
+
<section data-fold="library">
|
|
42
|
+
<h2><button type="button" class="fold" aria-expanded="true" data-i18n="groupLibrary">Library</button></h2>
|
|
43
43
|
<input type="search" id="library-search" class="search" data-i18n-placeholder="librarySearch" placeholder="Search your library" autocomplete="off">
|
|
44
44
|
<ul class="items" id="items" aria-live="polite"></ul>
|
|
45
45
|
</section>
|
|
46
|
-
<section>
|
|
47
|
-
<h2 data-i18n="actions">Actions</h2>
|
|
46
|
+
<section data-fold="actions">
|
|
47
|
+
<h2><button type="button" class="fold" aria-expanded="true" data-i18n="actions">Actions</button></h2>
|
|
48
48
|
<div class="actions" id="actions"></div>
|
|
49
49
|
</section>
|
|
50
|
-
<section class="grow">
|
|
50
|
+
<section class="grow" data-fold="conversations">
|
|
51
51
|
<div class="side-subhead">
|
|
52
|
-
<h2 data-i18n="conversations">Conversations</h2>
|
|
52
|
+
<h2><button type="button" class="fold" aria-expanded="true" data-i18n="conversations">Conversations</button></h2>
|
|
53
53
|
<button type="button" class="link-btn" id="export-btn" data-i18n="export" data-i18n-title="exportTitle">Export</button>
|
|
54
54
|
</div>
|
|
55
55
|
<input type="search" id="session-search" class="search" data-i18n-placeholder="searchConversations" placeholder="Search conversations" autocomplete="off">
|
|
@@ -120,6 +120,12 @@
|
|
|
120
120
|
<label><span data-i18n="provider">Provider</span>
|
|
121
121
|
<select id="login-provider"></select>
|
|
122
122
|
</label>
|
|
123
|
+
<label id="login-url-row" hidden><span data-i18n="serverUrl">Server address</span>
|
|
124
|
+
<input type="url" id="login-url" autocomplete="off" spellcheck="false" placeholder="https://llm.example.org/v1">
|
|
125
|
+
</label>
|
|
126
|
+
<label id="login-model-row" hidden><span data-i18n="modelId">Model id</span>
|
|
127
|
+
<input type="text" id="login-model" autocomplete="off" spellcheck="false">
|
|
128
|
+
</label>
|
|
123
129
|
<label><span data-i18n="apiKey">API key</span>
|
|
124
130
|
<input type="password" id="login-key" autocomplete="off" spellcheck="false" required>
|
|
125
131
|
</label>
|