@alisio/plugin-semantic 0.1.0 → 0.1.1
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.es.md +16 -0
- package/README.md +16 -0
- package/dist/apps.d.ts +2 -2
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +6 -0
- package/dist/apps.js.map +1 -1
- package/dist/embedded.html +903 -54
- package/dist/engine/store.d.ts +2 -2
- package/dist/engine/store.d.ts.map +1 -1
- package/dist/engine/store.js +21 -13
- package/dist/engine/store.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -32
- package/dist/index.js.map +1 -1
- package/dist/messages.js +2 -2
- package/dist/messages.js.map +1 -1
- package/dist/notebook/answer.d.ts +2 -0
- package/dist/notebook/answer.d.ts.map +1 -1
- package/dist/notebook/answer.js +15 -4
- package/dist/notebook/answer.js.map +1 -1
- package/dist/notebook/db.d.ts +10 -1
- package/dist/notebook/db.d.ts.map +1 -1
- package/dist/notebook/db.js +28 -0
- package/dist/notebook/db.js.map +1 -1
- package/dist/notebook/service.d.ts +21 -1
- package/dist/notebook/service.d.ts.map +1 -1
- package/dist/notebook/service.js +144 -12
- package/dist/notebook/service.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/embedded.html
CHANGED
|
@@ -351,13 +351,24 @@ textarea {
|
|
|
351
351
|
.nb-card:focus-within {
|
|
352
352
|
border-color: var(--border-strong);
|
|
353
353
|
}
|
|
354
|
+
/* "New notebook": icon and label centered, same height as the notebook cards. */
|
|
354
355
|
.nb-card.new {
|
|
356
|
+
display: flex;
|
|
355
357
|
flex-direction: column;
|
|
358
|
+
align-items: center;
|
|
359
|
+
justify-content: center;
|
|
356
360
|
gap: 12px;
|
|
357
361
|
color: var(--accent);
|
|
358
362
|
border-style: dashed;
|
|
359
363
|
font-weight: 650;
|
|
360
364
|
width: 100%;
|
|
365
|
+
height: 100%;
|
|
366
|
+
min-height: 170px;
|
|
367
|
+
padding: 18px;
|
|
368
|
+
text-align: center;
|
|
369
|
+
}
|
|
370
|
+
.nb-card.new .nb-glyph {
|
|
371
|
+
margin-bottom: 0;
|
|
361
372
|
}
|
|
362
373
|
.nb-open {
|
|
363
374
|
display: flex;
|
|
@@ -600,19 +611,167 @@ textarea {
|
|
|
600
611
|
gap: 10px;
|
|
601
612
|
color: var(--muted);
|
|
602
613
|
}
|
|
614
|
+
/* Citation chip: clearly a control, not a bare number. */
|
|
603
615
|
.cite {
|
|
604
616
|
min-height: 0;
|
|
605
617
|
min-width: 22px;
|
|
606
|
-
height:
|
|
618
|
+
height: 20px;
|
|
607
619
|
padding: 0 6px;
|
|
608
|
-
margin: 0
|
|
609
|
-
border-radius:
|
|
610
|
-
font-size:
|
|
611
|
-
font-weight:
|
|
612
|
-
|
|
620
|
+
margin: 0 1px 0 3px;
|
|
621
|
+
border-radius: 999px;
|
|
622
|
+
font-size: 11.5px;
|
|
623
|
+
font-weight: 750;
|
|
624
|
+
line-height: 1;
|
|
625
|
+
vertical-align: 2px;
|
|
613
626
|
background: var(--amber-bg);
|
|
614
627
|
color: var(--amber);
|
|
628
|
+
border: 1px solid var(--amber);
|
|
629
|
+
}
|
|
630
|
+
.cite:hover {
|
|
631
|
+
background: var(--amber);
|
|
632
|
+
color: var(--bg);
|
|
633
|
+
}
|
|
634
|
+
/* Rendered Markdown in answers, the guide and summaries. */
|
|
635
|
+
.md > :first-child {
|
|
636
|
+
margin-top: 0;
|
|
637
|
+
}
|
|
638
|
+
.md > :last-child {
|
|
639
|
+
margin-bottom: 0;
|
|
640
|
+
}
|
|
641
|
+
.md p,
|
|
642
|
+
.md ul,
|
|
643
|
+
.md ol,
|
|
644
|
+
.md blockquote,
|
|
645
|
+
.md pre,
|
|
646
|
+
.md .table-wrap {
|
|
647
|
+
margin: 0 0 10px;
|
|
648
|
+
}
|
|
649
|
+
.md ul,
|
|
650
|
+
.md ol {
|
|
651
|
+
padding-left: 22px;
|
|
652
|
+
}
|
|
653
|
+
.md li + li {
|
|
654
|
+
margin-top: 4px;
|
|
655
|
+
}
|
|
656
|
+
.md h3,
|
|
657
|
+
.md h4,
|
|
658
|
+
.md h5 {
|
|
659
|
+
margin: 14px 0 6px;
|
|
660
|
+
font-size: 15px;
|
|
661
|
+
text-transform: none;
|
|
662
|
+
letter-spacing: 0;
|
|
663
|
+
color: var(--text);
|
|
664
|
+
}
|
|
665
|
+
.md h3 {
|
|
666
|
+
font-size: 17px;
|
|
667
|
+
}
|
|
668
|
+
.md code {
|
|
669
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
670
|
+
font-size: 12.5px;
|
|
671
|
+
background: var(--raised);
|
|
672
|
+
border-radius: 5px;
|
|
673
|
+
padding: 1px 5px;
|
|
674
|
+
}
|
|
675
|
+
.md pre {
|
|
676
|
+
background: var(--raised);
|
|
677
|
+
border-radius: var(--radius-sm);
|
|
678
|
+
padding: 10px 12px;
|
|
679
|
+
overflow: auto;
|
|
680
|
+
}
|
|
681
|
+
.md pre code {
|
|
682
|
+
background: none;
|
|
683
|
+
padding: 0;
|
|
684
|
+
}
|
|
685
|
+
.md blockquote {
|
|
686
|
+
border-left: 3px solid var(--border-strong);
|
|
687
|
+
padding: 2px 0 2px 12px;
|
|
688
|
+
background: none;
|
|
689
|
+
border-radius: 0;
|
|
690
|
+
white-space: normal;
|
|
691
|
+
}
|
|
692
|
+
.md hr {
|
|
693
|
+
border: none;
|
|
694
|
+
border-top: 1px solid var(--border-strong);
|
|
695
|
+
margin: 12px 0;
|
|
696
|
+
}
|
|
697
|
+
.md .table-wrap {
|
|
698
|
+
overflow-x: auto;
|
|
699
|
+
}
|
|
700
|
+
.md table {
|
|
701
|
+
border-collapse: collapse;
|
|
702
|
+
font-size: 13px;
|
|
703
|
+
}
|
|
704
|
+
.md th,
|
|
705
|
+
.md td {
|
|
706
|
+
border: 1px solid var(--border-strong);
|
|
707
|
+
padding: 5px 9px;
|
|
708
|
+
text-align: left;
|
|
709
|
+
vertical-align: top;
|
|
710
|
+
}
|
|
711
|
+
.md th {
|
|
712
|
+
background: var(--raised);
|
|
713
|
+
}
|
|
714
|
+
.msg .who {
|
|
715
|
+
display: flex;
|
|
716
|
+
align-items: center;
|
|
717
|
+
justify-content: space-between;
|
|
718
|
+
gap: 8px;
|
|
719
|
+
}
|
|
720
|
+
.copy-controls {
|
|
721
|
+
display: inline-flex;
|
|
722
|
+
gap: 2px;
|
|
723
|
+
margin-left: auto;
|
|
724
|
+
}
|
|
725
|
+
.copy-controls .icon-btn {
|
|
726
|
+
width: 28px;
|
|
727
|
+
min-width: 28px;
|
|
728
|
+
height: 28px;
|
|
729
|
+
min-height: 28px;
|
|
730
|
+
color: var(--muted);
|
|
731
|
+
}
|
|
732
|
+
.block h3 {
|
|
733
|
+
display: flex;
|
|
734
|
+
align-items: center;
|
|
735
|
+
gap: 6px;
|
|
736
|
+
}
|
|
737
|
+
.chat-head:empty {
|
|
738
|
+
display: none;
|
|
739
|
+
}
|
|
740
|
+
.chat-head {
|
|
741
|
+
padding: 12px 18px 0;
|
|
742
|
+
}
|
|
743
|
+
.model-picker {
|
|
744
|
+
display: inline-flex;
|
|
745
|
+
align-items: center;
|
|
746
|
+
gap: 8px;
|
|
747
|
+
color: var(--muted);
|
|
748
|
+
font-size: 13px;
|
|
749
|
+
max-width: 100%;
|
|
750
|
+
}
|
|
751
|
+
.model-picker select {
|
|
752
|
+
min-height: 34px;
|
|
753
|
+
max-width: 340px;
|
|
754
|
+
border: 1px solid var(--border-strong);
|
|
755
|
+
border-radius: var(--radius-sm);
|
|
756
|
+
background: var(--surface);
|
|
757
|
+
color: var(--text);
|
|
758
|
+
padding: 4px 8px;
|
|
759
|
+
}
|
|
760
|
+
.block-state {
|
|
761
|
+
background: var(--amber-bg);
|
|
615
762
|
border-color: transparent;
|
|
763
|
+
margin: 0;
|
|
764
|
+
}
|
|
765
|
+
textarea:disabled {
|
|
766
|
+
opacity: 0.55;
|
|
767
|
+
cursor: not-allowed;
|
|
768
|
+
}
|
|
769
|
+
.copy-area {
|
|
770
|
+
max-height: 50vh;
|
|
771
|
+
overflow: auto;
|
|
772
|
+
border: 1px solid var(--border-strong);
|
|
773
|
+
border-radius: var(--radius-sm);
|
|
774
|
+
padding: 10px;
|
|
616
775
|
}
|
|
617
776
|
.warn {
|
|
618
777
|
display: flex;
|
|
@@ -649,16 +808,113 @@ textarea {
|
|
|
649
808
|
justify-content: flex-start;
|
|
650
809
|
border-radius: var(--radius-sm);
|
|
651
810
|
}
|
|
811
|
+
/* Composer: one rounded container with an auto-growing textarea and a round send button inside. */
|
|
652
812
|
.composer {
|
|
813
|
+
padding: 12px 18px 10px;
|
|
814
|
+
border-top: 1px solid var(--border);
|
|
815
|
+
}
|
|
816
|
+
.composer-box {
|
|
653
817
|
display: flex;
|
|
654
|
-
gap: 10px;
|
|
655
818
|
align-items: flex-end;
|
|
656
|
-
|
|
657
|
-
border
|
|
819
|
+
gap: 8px;
|
|
820
|
+
border: 1px solid var(--border-strong);
|
|
821
|
+
border-radius: var(--radius);
|
|
822
|
+
background: var(--surface);
|
|
823
|
+
padding: 6px 6px 6px 14px;
|
|
824
|
+
transition:
|
|
825
|
+
border-color 0.15s,
|
|
826
|
+
box-shadow 0.15s;
|
|
827
|
+
}
|
|
828
|
+
.composer-box:focus-within {
|
|
829
|
+
border-color: var(--accent);
|
|
830
|
+
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
831
|
+
}
|
|
832
|
+
.composer-box textarea {
|
|
833
|
+
flex: 1;
|
|
834
|
+
min-width: 0;
|
|
835
|
+
border: none;
|
|
836
|
+
background: transparent;
|
|
837
|
+
padding: 7px 0;
|
|
838
|
+
min-height: 22px;
|
|
839
|
+
line-height: 22px;
|
|
840
|
+
resize: none;
|
|
841
|
+
overflow-y: hidden;
|
|
842
|
+
}
|
|
843
|
+
.composer-box textarea:focus,
|
|
844
|
+
.composer-box textarea:focus-visible {
|
|
845
|
+
outline: none;
|
|
846
|
+
box-shadow: none;
|
|
658
847
|
}
|
|
659
|
-
|
|
848
|
+
.composer-actions {
|
|
660
849
|
display: flex;
|
|
661
|
-
|
|
850
|
+
flex: none;
|
|
851
|
+
}
|
|
852
|
+
.send-btn {
|
|
853
|
+
width: 36px;
|
|
854
|
+
min-width: 36px;
|
|
855
|
+
height: 36px;
|
|
856
|
+
min-height: 36px;
|
|
857
|
+
padding: 0;
|
|
858
|
+
border-radius: 999px;
|
|
859
|
+
background: var(--accent);
|
|
860
|
+
border-color: var(--accent);
|
|
861
|
+
color: var(--on-accent);
|
|
862
|
+
}
|
|
863
|
+
.send-btn:hover {
|
|
864
|
+
background: var(--accent-hover);
|
|
865
|
+
border-color: var(--accent-hover);
|
|
866
|
+
}
|
|
867
|
+
.send-btn:disabled {
|
|
868
|
+
background: var(--surface-2);
|
|
869
|
+
border-color: var(--border-strong);
|
|
870
|
+
color: var(--faint);
|
|
871
|
+
opacity: 1;
|
|
872
|
+
}
|
|
873
|
+
.send-btn.stop {
|
|
874
|
+
background: var(--text);
|
|
875
|
+
border-color: var(--text);
|
|
876
|
+
color: var(--bg);
|
|
877
|
+
}
|
|
878
|
+
.send-btn .icon {
|
|
879
|
+
width: 18px;
|
|
880
|
+
height: 18px;
|
|
881
|
+
}
|
|
882
|
+
.composer-hint,
|
|
883
|
+
.disclaimer {
|
|
884
|
+
margin: 6px 4px 0;
|
|
885
|
+
font-size: 12px;
|
|
886
|
+
line-height: 1.4;
|
|
887
|
+
color: var(--faint);
|
|
888
|
+
text-align: center;
|
|
889
|
+
}
|
|
890
|
+
.disclaimer {
|
|
891
|
+
margin-top: 2px;
|
|
892
|
+
}
|
|
893
|
+
/* Copy controls: top right of each generated block; shown on hover and keyboard focus, always
|
|
894
|
+
on touch screens and narrow layouts. */
|
|
895
|
+
.copyable .copy-controls {
|
|
896
|
+
opacity: 0;
|
|
897
|
+
transition: opacity 0.15s;
|
|
898
|
+
}
|
|
899
|
+
.copyable:hover .copy-controls,
|
|
900
|
+
.copyable:focus-within .copy-controls {
|
|
901
|
+
opacity: 1;
|
|
902
|
+
}
|
|
903
|
+
@media (hover: none), (max-width: 899px) {
|
|
904
|
+
.copyable .copy-controls {
|
|
905
|
+
opacity: 1;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
.chat-head {
|
|
909
|
+
display: flex;
|
|
910
|
+
align-items: center;
|
|
911
|
+
gap: 10px;
|
|
912
|
+
}
|
|
913
|
+
.chat-head > .banner {
|
|
914
|
+
flex: 1;
|
|
915
|
+
}
|
|
916
|
+
.chat-head > .icon-btn {
|
|
917
|
+
margin-left: auto;
|
|
662
918
|
}
|
|
663
919
|
.spinner {
|
|
664
920
|
width: 16px;
|
|
@@ -748,6 +1004,9 @@ dialog::backdrop {
|
|
|
748
1004
|
margin: 15vh auto;
|
|
749
1005
|
padding: 0 24px;
|
|
750
1006
|
text-align: center;
|
|
1007
|
+
display: grid;
|
|
1008
|
+
justify-items: center;
|
|
1009
|
+
gap: 12px;
|
|
751
1010
|
}
|
|
752
1011
|
|
|
753
1012
|
/* ---------- Narrow layout: tabs under 900 px ---------- */
|
|
@@ -951,6 +1210,8 @@ const en = {
|
|
|
951
1210
|
keywordAnswer: "Found with keyword search",
|
|
952
1211
|
clearChat: "Clear chat",
|
|
953
1212
|
clearChatConfirm: "Clear the whole chat of this notebook?",
|
|
1213
|
+
composerHint: "Enter to send, Shift+Enter for a new line",
|
|
1214
|
+
disclaimer: "The assistant can make mistakes, so check its answers against the cited sources.",
|
|
954
1215
|
noticeTitle: "Before your first question",
|
|
955
1216
|
noticeText:
|
|
956
1217
|
"To answer, the passages that match your question are sent to the AI model selected in Alisio, which may run outside this computer. Reading and indexing your files stays on this computer.",
|
|
@@ -969,6 +1230,21 @@ const en = {
|
|
|
969
1230
|
showGuide: "Show guide panel",
|
|
970
1231
|
hideGuide: "Hide guide panel",
|
|
971
1232
|
awaiting_approval: "Waiting for your approval in Alisio…",
|
|
1233
|
+
model: "Model",
|
|
1234
|
+
modelDefault: (name) => (name ? `Alisio's current model (${name})` : "Alisio's current model"),
|
|
1235
|
+
modelFallback:
|
|
1236
|
+
"The model chosen for this notebook is no longer available; Alisio's current model answered.",
|
|
1237
|
+
noModelTitle: "Connect a model to chat",
|
|
1238
|
+
noModel:
|
|
1239
|
+
"To chat with your sources, first connect a model in Alisio (/connect). Indexing and keyword search still work.",
|
|
1240
|
+
outdated: "Outdated: sources changed since it was written.",
|
|
1241
|
+
regenerate: "Regenerate",
|
|
1242
|
+
copyText: "Copy text",
|
|
1243
|
+
copyFormatted: "Copy formatted",
|
|
1244
|
+
copied: "Copied",
|
|
1245
|
+
copyManual: "Press Ctrl+C / ⌘C to copy",
|
|
1246
|
+
retry: "Try again",
|
|
1247
|
+
checking: "Checking…",
|
|
972
1248
|
unavailableTitle: "Notebook is unavailable",
|
|
973
1249
|
unavailable:
|
|
974
1250
|
"This app is no longer available. The plugin may have been turned off or updated; reopen Notebook from the Apps list.",
|
|
@@ -990,6 +1266,7 @@ const en = {
|
|
|
990
1266
|
download_failed: "The download failed. Check your connection and try again.",
|
|
991
1267
|
cancelled: "Cancelled.",
|
|
992
1268
|
answer_failed: "The AI model did not answer. Check the model selected in Alisio.",
|
|
1269
|
+
no_model: "Connect a model in Alisio (/connect) to chat with your sources.",
|
|
993
1270
|
operation_failed: "Something went wrong. Please try again.",
|
|
994
1271
|
not_permitted_write:
|
|
995
1272
|
'Alisio did not allow Notebook to save changes. Set this session to Auto (or Full access), or approve the action in the chat with "Allow for session".',
|
|
@@ -1100,8 +1377,11 @@ const es = {
|
|
|
1100
1377
|
assistant: "Cuaderno",
|
|
1101
1378
|
notGrounded: "Esta respuesta no cita tus fuentes. Revísala antes de confiar en ella.",
|
|
1102
1379
|
keywordAnswer: "Encontrado con palabras clave",
|
|
1103
|
-
clearChat: "
|
|
1104
|
-
clearChatConfirm: "¿
|
|
1380
|
+
clearChat: "Limpiar chat",
|
|
1381
|
+
clearChatConfirm: "¿Limpiar todo el chat de este cuaderno?",
|
|
1382
|
+
composerHint: "Enter para enviar, Shift+Enter para nueva línea",
|
|
1383
|
+
disclaimer:
|
|
1384
|
+
"El asistente puede cometer errores; comprueba sus respuestas con las fuentes citadas.",
|
|
1105
1385
|
noticeTitle: "Antes de tu primera pregunta",
|
|
1106
1386
|
noticeText:
|
|
1107
1387
|
"Para responder, los pasajes que coinciden con tu pregunta se envían al modelo de IA elegido en Alisio, que puede funcionar fuera de este equipo. La lectura y el indexado de tus archivos se quedan en este equipo.",
|
|
@@ -1119,6 +1399,21 @@ const es = {
|
|
|
1119
1399
|
showGuide: "Mostrar panel de guía",
|
|
1120
1400
|
hideGuide: "Ocultar panel de guía",
|
|
1121
1401
|
awaiting_approval: "Esperando tu aprobación en Alisio…",
|
|
1402
|
+
model: "Modelo",
|
|
1403
|
+
modelDefault: (name) => (name ? `Modelo actual de Alisio (${name})` : "Modelo actual de Alisio"),
|
|
1404
|
+
modelFallback:
|
|
1405
|
+
"El modelo elegido para este cuaderno ya no está disponible; respondió el modelo actual de Alisio.",
|
|
1406
|
+
noModelTitle: "Conecta un modelo para chatear",
|
|
1407
|
+
noModel:
|
|
1408
|
+
"Para chatear con tus fuentes, primero conecta un modelo en Alisio (/connect). El indexado y la búsqueda por palabras clave siguen funcionando.",
|
|
1409
|
+
outdated: "Desactualizado: las fuentes cambiaron desde que se escribió.",
|
|
1410
|
+
regenerate: "Regenerar",
|
|
1411
|
+
copyText: "Copiar texto",
|
|
1412
|
+
copyFormatted: "Copiar con formato",
|
|
1413
|
+
copied: "Copiado",
|
|
1414
|
+
copyManual: "Pulsa Ctrl+C / ⌘C para copiar",
|
|
1415
|
+
retry: "Reintentar",
|
|
1416
|
+
checking: "Comprobando…",
|
|
1122
1417
|
unavailableTitle: "Cuaderno no está disponible",
|
|
1123
1418
|
unavailable:
|
|
1124
1419
|
"Esta app ya no está disponible. Puede que el plugin se haya desactivado o actualizado; vuelve a abrir Cuaderno desde la lista de Aplicaciones.",
|
|
@@ -1141,6 +1436,7 @@ const es = {
|
|
|
1141
1436
|
download_failed: "La descarga falló. Revisa tu conexión e inténtalo de nuevo.",
|
|
1142
1437
|
cancelled: "Cancelado.",
|
|
1143
1438
|
answer_failed: "El modelo de IA no respondió. Revisa el modelo elegido en Alisio.",
|
|
1439
|
+
no_model: "Conecta un modelo en Alisio (/connect) para chatear con tus fuentes.",
|
|
1144
1440
|
operation_failed: "Algo salió mal. Inténtalo de nuevo.",
|
|
1145
1441
|
not_permitted_write:
|
|
1146
1442
|
'Alisio no permitió que Cuaderno guarde cambios. Pon esta sesión en Auto (o Acceso total), o aprueba la acción en el chat con "Permitir en la sesión".',
|
|
@@ -1332,7 +1628,7 @@ const PATHS = {
|
|
|
1332
1628
|
folder: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
|
1333
1629
|
file: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8zM14 3v5h5",
|
|
1334
1630
|
send: "M4 12l16-8-6 16-2-6z",
|
|
1335
|
-
stop: "
|
|
1631
|
+
stop: "M8 8h8v8H8z",
|
|
1336
1632
|
trash: "M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3",
|
|
1337
1633
|
edit: "M4 20h4L19 9l-4-4L4 16zM14 6l4 4",
|
|
1338
1634
|
sparkle:
|
|
@@ -1344,7 +1640,10 @@ const PATHS = {
|
|
|
1344
1640
|
book: "M4 5a2 2 0 0 1 2-2h5v17H6a2 2 0 0 0-2 2zM20 5a2 2 0 0 0-2-2h-5v17h5a2 2 0 0 1 2 2z",
|
|
1345
1641
|
check: "M5 12l5 5 9-10",
|
|
1346
1642
|
up: "M12 19V5m0 0l-6 6m6-6l6 6",
|
|
1643
|
+
arrowUp: "M12 19V6m0 0l-6 6m6-6l6 6",
|
|
1347
1644
|
close: "M6 6l12 12M18 6L6 18",
|
|
1645
|
+
copy: "M9 9h10v10H9zM5 15V5h10",
|
|
1646
|
+
copyRich: "M9 9h10v10H9zM5 15V5h10M12 13h4M12 16h3",
|
|
1348
1647
|
quote: "M7 7h4v4c0 3-2 5-4 6M15 7h4v4c0 3-2 5-4 6",
|
|
1349
1648
|
};
|
|
1350
1649
|
|
|
@@ -1352,6 +1651,324 @@ function icon(name, cls = "icon") {
|
|
|
1352
1651
|
return `<svg class="${cls}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${PATHS[name] ?? ""}"/></svg>`;
|
|
1353
1652
|
}
|
|
1354
1653
|
|
|
1654
|
+
// Small, safe Markdown renderer for answers (0.1.1). Every piece of model text is HTML-escaped
|
|
1655
|
+
// before any formatting is applied, so the only tags in the output are the ones built here.
|
|
1656
|
+
// Supports paragraphs, line breaks, # to ### headings, bold, italic, inline code, fenced code,
|
|
1657
|
+
// nested ordered/unordered lists, blockquotes, simple GFM tables and horizontal rules. Links are
|
|
1658
|
+
// shown as plain text (the frame cannot navigate). `[n]` citations that match `citations` become
|
|
1659
|
+
// chips; other numbers stay text.
|
|
1660
|
+
|
|
1661
|
+
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
1662
|
+
const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (c) => ESCAPES[c]);
|
|
1663
|
+
|
|
1664
|
+
function chip(citation) {
|
|
1665
|
+
const label = `${citation.n}: ${citation.sourceName ?? ""}`;
|
|
1666
|
+
return `<button type="button" class="cite" data-action="cite" data-source="${escapeHtml(citation.sourceId)}" data-ordinal="${escapeHtml(citation.ordinal)}" aria-label="${escapeHtml(label)}" title="${escapeHtml(citation.sourceName ?? "")}">${escapeHtml(citation.n)}</button>`;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Inline formatting of one line of raw text. `options.chips === false` keeps citations as plain
|
|
1671
|
+
* `[n]` text (copied HTML must not contain buttons).
|
|
1672
|
+
*/
|
|
1673
|
+
function renderInline(raw, citations = [], options = {}) {
|
|
1674
|
+
const byNumber = new Map(citations.map((c) => [Number(c.n), c]));
|
|
1675
|
+
const slots = [];
|
|
1676
|
+
const hold = (html) => `\uE000${slots.push(html) - 1}\uE000`;
|
|
1677
|
+
let s = escapeHtml(String(raw ?? "").replace(/\uE000/g, ""));
|
|
1678
|
+
// Code spans first: nothing inside them is formatted.
|
|
1679
|
+
s = s.replace(/`([^`]+)`/g, (_, code) => hold(`<code>${code}</code>`));
|
|
1680
|
+
// Links become their text plus the address, as plain text.
|
|
1681
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text, url) =>
|
|
1682
|
+
text === url ? text : `${text} (${url})`,
|
|
1683
|
+
);
|
|
1684
|
+
s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "<strong>$1</strong>");
|
|
1685
|
+
s = s.replace(/__(?=\S)([\s\S]*?\S)__/g, "<strong>$1</strong>");
|
|
1686
|
+
s = s.replace(/(^|[^\w*])\*(?=[^\s*])([^*]*?[^\s*])\*(?!\*)/g, "$1<em>$2</em>");
|
|
1687
|
+
s = s.replace(/(^|[^\w])_(?=[^\s_])([^_]*?[^\s_])_(?![\w])/g, "$1<em>$2</em>");
|
|
1688
|
+
s = s.replace(/\[(\d{1,3}(?:\s*,\s*\d{1,3})*)\]/g, (match, list) => {
|
|
1689
|
+
const numbers = list.split(",").map((n) => Number(n.trim()));
|
|
1690
|
+
if (!numbers.every((n) => byNumber.has(n))) return match;
|
|
1691
|
+
if (options.chips === false) return `[${numbers.join(", ")}]`;
|
|
1692
|
+
return numbers.map((n) => hold(chip(byNumber.get(n)))).join("");
|
|
1693
|
+
});
|
|
1694
|
+
return s.replace(/\uE000(\d+)\uE000/g, (_, i) => slots[Number(i)]);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const LIST = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
|
|
1698
|
+
const RULE = /^\s{0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
1699
|
+
const FENCE = /^\s{0,3}(```|~~~)/;
|
|
1700
|
+
const HEADING = /^\s{0,3}(#{1,3})\s+(.*?)\s*#*\s*$/;
|
|
1701
|
+
const QUOTE = /^\s{0,3}>\s?(.*)$/;
|
|
1702
|
+
const TABLE_RULE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
|
|
1703
|
+
const isTableRow = (line) => line.includes("|");
|
|
1704
|
+
const cells = (line) =>
|
|
1705
|
+
line
|
|
1706
|
+
.trim()
|
|
1707
|
+
.replace(/^\|/, "")
|
|
1708
|
+
.replace(/\|$/, "")
|
|
1709
|
+
.split("|")
|
|
1710
|
+
.map((c) => c.trim());
|
|
1711
|
+
const indentOf = (s) => s.replace(/\t/g, " ").length;
|
|
1712
|
+
|
|
1713
|
+
function renderList(lines, citations, options) {
|
|
1714
|
+
// Builds nested lists from indentation: deeper markers open a child list in the last item.
|
|
1715
|
+
const root = { indent: -1, items: [] };
|
|
1716
|
+
const stack = [];
|
|
1717
|
+
for (const line of lines) {
|
|
1718
|
+
const m = LIST.exec(line);
|
|
1719
|
+
if (!m) {
|
|
1720
|
+
const top = stack.at(-1);
|
|
1721
|
+
const item = top?.list.items.at(-1);
|
|
1722
|
+
if (item) item.text += `\n${line.trim()}`;
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const indent = indentOf(m[1]);
|
|
1726
|
+
const ordered = /\d/.test(m[2]);
|
|
1727
|
+
while (stack.length && indent < stack.at(-1).indent) stack.pop();
|
|
1728
|
+
let top = stack.at(-1);
|
|
1729
|
+
if (!top || indent > top.indent) {
|
|
1730
|
+
const parentItem = top?.list.items.at(-1);
|
|
1731
|
+
const list = { ordered, items: [] };
|
|
1732
|
+
if (parentItem) parentItem.children.push(list);
|
|
1733
|
+
else root.items.push(list);
|
|
1734
|
+
top = { indent, list };
|
|
1735
|
+
stack.push(top);
|
|
1736
|
+
}
|
|
1737
|
+
top.list.items.push({ text: m[3], children: [] });
|
|
1738
|
+
}
|
|
1739
|
+
const html = (list) =>
|
|
1740
|
+
`<${list.ordered ? "ol" : "ul"}>${list.items
|
|
1741
|
+
.map(
|
|
1742
|
+
(item) =>
|
|
1743
|
+
`<li>${item.text
|
|
1744
|
+
.split("\n")
|
|
1745
|
+
.map((t) => renderInline(t, citations, options))
|
|
1746
|
+
.join("<br>")}${item.children.map(html).join("")}</li>`,
|
|
1747
|
+
)
|
|
1748
|
+
.join("")}</${list.ordered ? "ol" : "ul"}>`;
|
|
1749
|
+
return root.items.map(html).join("");
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function renderTable(rows, citations, options) {
|
|
1753
|
+
const [head, , ...body] = rows.map(cells);
|
|
1754
|
+
const th = (head ?? []).map((c) => `<th>${renderInline(c, citations, options)}</th>`).join("");
|
|
1755
|
+
const tr = body
|
|
1756
|
+
.map(
|
|
1757
|
+
(r) => `<tr>${r.map((c) => `<td>${renderInline(c, citations, options)}</td>`).join("")}</tr>`,
|
|
1758
|
+
)
|
|
1759
|
+
.join("");
|
|
1760
|
+
return `<div class="table-wrap"><table><thead><tr>${th}</tr></thead><tbody>${tr}</tbody></table></div>`;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/** Block-level rendering; returns HTML built only from escaped text and fixed tags. */
|
|
1764
|
+
function renderMarkdown(text, citations = [], options = {}) {
|
|
1765
|
+
const lines = String(text ?? "")
|
|
1766
|
+
.replace(/\r\n?/g, "\n")
|
|
1767
|
+
.split("\n");
|
|
1768
|
+
const out = [];
|
|
1769
|
+
let i = 0;
|
|
1770
|
+
while (i < lines.length) {
|
|
1771
|
+
const line = lines[i];
|
|
1772
|
+
if (!line.trim()) {
|
|
1773
|
+
i++;
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const fence = FENCE.exec(line);
|
|
1777
|
+
if (fence) {
|
|
1778
|
+
const code = [];
|
|
1779
|
+
i++;
|
|
1780
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(fence[1])) code.push(lines[i++]);
|
|
1781
|
+
i++;
|
|
1782
|
+
out.push(`<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
const heading = HEADING.exec(line);
|
|
1786
|
+
if (heading) {
|
|
1787
|
+
const level = heading[1].length + 2; // # -> h3: answers live inside the chat
|
|
1788
|
+
out.push(`<h${level}>${renderInline(heading[2], citations, options)}</h${level}>`);
|
|
1789
|
+
i++;
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
if (RULE.test(line)) {
|
|
1793
|
+
out.push("<hr>");
|
|
1794
|
+
i++;
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (QUOTE.test(line)) {
|
|
1798
|
+
const inner = [];
|
|
1799
|
+
while (i < lines.length && QUOTE.test(lines[i])) inner.push(QUOTE.exec(lines[i++])[1]);
|
|
1800
|
+
out.push(`<blockquote>${renderMarkdown(inner.join("\n"), citations, options)}</blockquote>`);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (isTableRow(line) && i + 1 < lines.length && TABLE_RULE.test(lines[i + 1])) {
|
|
1804
|
+
const rows = [];
|
|
1805
|
+
while (i < lines.length && lines[i].trim() && isTableRow(lines[i])) rows.push(lines[i++]);
|
|
1806
|
+
out.push(renderTable(rows, citations, options));
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (LIST.test(line)) {
|
|
1810
|
+
const block = [];
|
|
1811
|
+
while (
|
|
1812
|
+
i < lines.length &&
|
|
1813
|
+
lines[i].trim() &&
|
|
1814
|
+
(LIST.test(lines[i]) || /^\s+\S/.test(lines[i]))
|
|
1815
|
+
)
|
|
1816
|
+
block.push(lines[i++]);
|
|
1817
|
+
out.push(renderList(block, citations, options));
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
const paragraph = [];
|
|
1821
|
+
while (
|
|
1822
|
+
i < lines.length &&
|
|
1823
|
+
lines[i].trim() &&
|
|
1824
|
+
!FENCE.test(lines[i]) &&
|
|
1825
|
+
!HEADING.test(lines[i]) &&
|
|
1826
|
+
!QUOTE.test(lines[i]) &&
|
|
1827
|
+
!LIST.test(lines[i]) &&
|
|
1828
|
+
!RULE.test(lines[i])
|
|
1829
|
+
)
|
|
1830
|
+
paragraph.push(lines[i++]);
|
|
1831
|
+
out.push(`<p>${paragraph.map((l) => renderInline(l, citations, options)).join("<br>")}</p>`);
|
|
1832
|
+
}
|
|
1833
|
+
return out.join("");
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Copy an answer as text or with formatting (0.1.1, O14). The formatted HTML comes from the same
|
|
1837
|
+
// safe renderer (model text is always escaped), with citations as plain [n] and inline styles only.
|
|
1838
|
+
|
|
1839
|
+
/** Plain text to copy: the answer's Markdown source, citations kept as [n]. */
|
|
1840
|
+
const copyText = (markdown) => String(markdown ?? "");
|
|
1841
|
+
|
|
1842
|
+
const STYLES = [
|
|
1843
|
+
[/<table>/g, '<table style="border-collapse:collapse">'],
|
|
1844
|
+
[/<th>/g, '<th style="border:1px solid #999;padding:4px 8px;text-align:left">'],
|
|
1845
|
+
[/<td>/g, '<td style="border:1px solid #999;padding:4px 8px">'],
|
|
1846
|
+
[/<pre>/g, '<pre style="background:#f4f4f4;padding:8px;white-space:pre-wrap">'],
|
|
1847
|
+
[
|
|
1848
|
+
/<blockquote>/g,
|
|
1849
|
+
'<blockquote style="border-left:3px solid #999;margin:0 0 8px;padding-left:10px">',
|
|
1850
|
+
],
|
|
1851
|
+
[/ class="table-wrap"/g, ""],
|
|
1852
|
+
];
|
|
1853
|
+
|
|
1854
|
+
/** Rich HTML to copy: rendered Markdown without chips, buttons or classes. */
|
|
1855
|
+
function copyHtml(markdown, citations, render) {
|
|
1856
|
+
let html = render(String(markdown ?? ""), citations ?? [], { chips: false });
|
|
1857
|
+
for (const [pattern, replacement] of STYLES) html = html.replace(pattern, replacement);
|
|
1858
|
+
return `<div>${html}</div>`;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* Tries the async Clipboard API, then a `copy` event fired by `execCommand` on a hidden
|
|
1863
|
+
* selection (sandboxed frames often lack the clipboard permission). Returns the path that worked:
|
|
1864
|
+
* "clipboard", "execCommand", or "manual" when the caller must show the content to copy by hand.
|
|
1865
|
+
*/
|
|
1866
|
+
async function writeClipboard({ text, html }, env = globalThis) {
|
|
1867
|
+
const nav = env.navigator;
|
|
1868
|
+
try {
|
|
1869
|
+
if (html && nav?.clipboard?.write && typeof env.ClipboardItem === "function") {
|
|
1870
|
+
await nav.clipboard.write([
|
|
1871
|
+
new env.ClipboardItem({
|
|
1872
|
+
"text/html": new env.Blob([html], { type: "text/html" }),
|
|
1873
|
+
"text/plain": new env.Blob([text], { type: "text/plain" }),
|
|
1874
|
+
}),
|
|
1875
|
+
]);
|
|
1876
|
+
return "clipboard";
|
|
1877
|
+
}
|
|
1878
|
+
if (!html && nav?.clipboard?.writeText) {
|
|
1879
|
+
await nav.clipboard.writeText(text);
|
|
1880
|
+
return "clipboard";
|
|
1881
|
+
}
|
|
1882
|
+
} catch {
|
|
1883
|
+
// Permission denied in the sandbox: fall back below.
|
|
1884
|
+
}
|
|
1885
|
+
const doc = env.document;
|
|
1886
|
+
try {
|
|
1887
|
+
const area = doc.createElement("textarea");
|
|
1888
|
+
area.value = text;
|
|
1889
|
+
area.setAttribute("readonly", "");
|
|
1890
|
+
area.style.cssText = "position:fixed;left:-9999px;top:0;opacity:0";
|
|
1891
|
+
doc.body.append(area);
|
|
1892
|
+
area.select();
|
|
1893
|
+
let filled = false;
|
|
1894
|
+
const onCopy = (event) => {
|
|
1895
|
+
event.clipboardData?.setData("text/plain", text);
|
|
1896
|
+
if (html) event.clipboardData?.setData("text/html", html);
|
|
1897
|
+
if (event.clipboardData) {
|
|
1898
|
+
event.preventDefault();
|
|
1899
|
+
filled = true;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
doc.addEventListener("copy", onCopy);
|
|
1903
|
+
let ok = false;
|
|
1904
|
+
try {
|
|
1905
|
+
ok = !!doc.execCommand?.("copy");
|
|
1906
|
+
} finally {
|
|
1907
|
+
doc.removeEventListener("copy", onCopy);
|
|
1908
|
+
area.remove();
|
|
1909
|
+
}
|
|
1910
|
+
if (ok && (filled || !html)) return "execCommand";
|
|
1911
|
+
} catch {
|
|
1912
|
+
// execCommand is unavailable: manual copy.
|
|
1913
|
+
}
|
|
1914
|
+
return "manual";
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* What a copy control copies (one shared component for every generated block, O14):
|
|
1919
|
+
* `chat:<i>` an answer, `guide` the notebook summary, `questions` its suggested questions,
|
|
1920
|
+
* `summary` a source summary, `passage` the passage text plus a plain reference line.
|
|
1921
|
+
* `place` formats the passage's location ("Page 3", "Slide 2", "Sheet: Sales").
|
|
1922
|
+
*/
|
|
1923
|
+
function copySourceFor(ref, state) {
|
|
1924
|
+
const { chat = [], guide, summary, passage, place } = state;
|
|
1925
|
+
if (ref === "guide")
|
|
1926
|
+
return guide?.summary
|
|
1927
|
+
? { text: guide.summary, citations: guide.citations ?? [], markdown: true }
|
|
1928
|
+
: undefined;
|
|
1929
|
+
if (ref === "questions") {
|
|
1930
|
+
const questions = (guide?.questions ?? []).filter(Boolean);
|
|
1931
|
+
return questions.length
|
|
1932
|
+
? { text: questions.map((q) => `- ${q}`).join("\n"), citations: [], markdown: true }
|
|
1933
|
+
: undefined;
|
|
1934
|
+
}
|
|
1935
|
+
if (ref === "summary")
|
|
1936
|
+
return summary?.summary
|
|
1937
|
+
? { text: summary.summary, citations: summary.citations ?? [], markdown: true }
|
|
1938
|
+
: undefined;
|
|
1939
|
+
if (ref === "passage") {
|
|
1940
|
+
if (!passage) return undefined;
|
|
1941
|
+
const where = [passage.sourceName, place?.(passage)].filter(Boolean).join(", ");
|
|
1942
|
+
return { text: passage.text, reference: where, citations: [], markdown: false };
|
|
1943
|
+
}
|
|
1944
|
+
const message = chat[Number(String(ref).split(":")[1])];
|
|
1945
|
+
return message?.text
|
|
1946
|
+
? { text: message.text, citations: message.citations ?? [], markdown: true }
|
|
1947
|
+
: undefined;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const escapeText = (value) =>
|
|
1951
|
+
String(value ?? "").replace(
|
|
1952
|
+
/[&<>"']/g,
|
|
1953
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
/** Plain text and (when `formatted`) rich HTML for a copy source. */
|
|
1957
|
+
function copyPayload(source, render, formatted) {
|
|
1958
|
+
const text = source.reference
|
|
1959
|
+
? `${copyText(source.text)}\n\n— ${source.reference}`
|
|
1960
|
+
: copyText(source.text);
|
|
1961
|
+
if (!formatted) return { text };
|
|
1962
|
+
if (source.markdown) return { text, html: copyHtml(source.text, source.citations, render) };
|
|
1963
|
+
// Source text is not Markdown: quote it as is, with its reference line.
|
|
1964
|
+
const quoted = escapeText(source.text).split(/\n/).join("<br>");
|
|
1965
|
+
const reference = source.reference ? `<p>— ${escapeText(source.reference)}</p>` : "";
|
|
1966
|
+
return {
|
|
1967
|
+
text,
|
|
1968
|
+
html: `<div><blockquote style="border-left:3px solid #999;margin:0 0 8px;padding-left:10px">${quoted}</blockquote>${reference}</div>`,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1355
1972
|
|
|
1356
1973
|
const root = document.querySelector("#app");
|
|
1357
1974
|
const dialog = document.querySelector("#dialog");
|
|
@@ -1370,6 +1987,8 @@ const WRITE_ACTIONS = new Set([
|
|
|
1370
1987
|
"jobs.cancel",
|
|
1371
1988
|
"notice.ack",
|
|
1372
1989
|
"engine.setup",
|
|
1990
|
+
"notebooks.setModel",
|
|
1991
|
+
"notebooks.setScope",
|
|
1373
1992
|
]);
|
|
1374
1993
|
|
|
1375
1994
|
// ---------- Transport ----------
|
|
@@ -1394,7 +2013,8 @@ const connected = new Promise((resolve) => {
|
|
|
1394
2013
|
if (message?.type === "locale") changeLocale(message.locale);
|
|
1395
2014
|
if (message?.type === "visibility") {
|
|
1396
2015
|
visible = message.visible !== false;
|
|
1397
|
-
|
|
2016
|
+
// Coming back: re-check the app (it may have been reopened by Alisio) and the models.
|
|
2017
|
+
if (visible) void (unavailable ? verifyAvailable() : refreshOnShow());
|
|
1398
2018
|
}
|
|
1399
2019
|
if (message?.type === "response") settle(message);
|
|
1400
2020
|
};
|
|
@@ -1410,12 +2030,37 @@ function settle(message) {
|
|
|
1410
2030
|
clearTimeout(request.timer);
|
|
1411
2031
|
clearTimeout(request.approvalTimer);
|
|
1412
2032
|
settleApprovalNotice();
|
|
1413
|
-
if (message.error === "not_found") showUnavailable();
|
|
1414
2033
|
// `message.error` is a host transport code; `result.error` a Notebook code. For not_permitted,
|
|
1415
2034
|
// prefer the host's `effect`; older hosts omit it, so fall back to the action's own effect.
|
|
1416
2035
|
const error = responseError(message, WRITE_ACTIONS.has(request.action) ? "write" : "read");
|
|
1417
|
-
|
|
2036
|
+
if (!error) return request.resolve(message.result?.value);
|
|
2037
|
+
const failure = new Error(error);
|
|
2038
|
+
// Only the HOST's not_found can mean "app gone"; a plugin's not_found is an unknown item.
|
|
2039
|
+
failure.transport = !!message.error;
|
|
2040
|
+
if (message.error === "not_found") hostNotFound();
|
|
2041
|
+
request.reject(failure);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// O13: one host not_found never latches. It is confirmed with a `status` request after a short
|
|
2045
|
+
// delay; only a second host not_found shows the unavailable screen, which keeps retrying.
|
|
2046
|
+
let verifying;
|
|
2047
|
+
function hostNotFound() {
|
|
2048
|
+
if (unavailable || verifying) return;
|
|
2049
|
+
verifying = new Promise((r) => setTimeout(r, VERIFY_DELAY_MS)).then(verifyAvailable);
|
|
2050
|
+
}
|
|
2051
|
+
async function verifyAvailable() {
|
|
2052
|
+
try {
|
|
2053
|
+
await send("status", {});
|
|
2054
|
+
const wasUnavailable = unavailable;
|
|
2055
|
+
unavailable = false;
|
|
2056
|
+
if (wasUnavailable) await reloadAll();
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
if (error?.transport && error.message === "not_found") showUnavailable();
|
|
2059
|
+
} finally {
|
|
2060
|
+
verifying = undefined;
|
|
2061
|
+
}
|
|
1418
2062
|
}
|
|
2063
|
+
const VERIFY_DELAY_MS = 1500;
|
|
1419
2064
|
|
|
1420
2065
|
// The host runs at most PLUGIN_APP_LIMITS.inFlight (8) requests per frame; stay below it.
|
|
1421
2066
|
const MAX_IN_FLIGHT = 6;
|
|
@@ -1483,6 +2128,8 @@ const fail = (error) => {
|
|
|
1483
2128
|
};
|
|
1484
2129
|
|
|
1485
2130
|
// ---------- State ----------
|
|
2131
|
+
/** Configured models (O12): an empty list blocks asking, not indexing or keyword search. */
|
|
2132
|
+
let models = { models: [], active: null, available: true };
|
|
1486
2133
|
let status = {};
|
|
1487
2134
|
let notebooks = [];
|
|
1488
2135
|
let current; // { ...notebook, sources }
|
|
@@ -1504,11 +2151,6 @@ let tab = "chat";
|
|
|
1504
2151
|
let pollBusy = false;
|
|
1505
2152
|
let draft = "";
|
|
1506
2153
|
|
|
1507
|
-
const escapeHtml = (value) =>
|
|
1508
|
-
String(value ?? "").replace(
|
|
1509
|
-
/[&<>"']/g,
|
|
1510
|
-
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
|
|
1511
|
-
);
|
|
1512
2154
|
const tf = (key, ...args) => {
|
|
1513
2155
|
const value = t(key);
|
|
1514
2156
|
return typeof value === "function" ? value(...args) : value;
|
|
@@ -1524,7 +2166,7 @@ const announce = (text) => {
|
|
|
1524
2166
|
|
|
1525
2167
|
function showUnavailable() {
|
|
1526
2168
|
unavailable = true;
|
|
1527
|
-
root.innerHTML = `<main class="unavailable"><span class="mark" aria-hidden="true"></span><h1>${escapeHtml(t("unavailableTitle"))}</h1><p>${escapeHtml(t("unavailable"))}</p
|
|
2169
|
+
root.innerHTML = `<main class="unavailable"><span class="mark" aria-hidden="true"></span><h1>${escapeHtml(t("unavailableTitle"))}</h1><p>${escapeHtml(t("unavailable"))}</p>${button("retry", escapeHtml(t("retry")), "primary")}</main>`;
|
|
1528
2170
|
}
|
|
1529
2171
|
|
|
1530
2172
|
// ---------- Rendering ----------
|
|
@@ -1640,12 +2282,13 @@ function renderNotebook() {
|
|
|
1640
2282
|
<div class="stack">${button("add-computer", `${icon("upload")}<span>${escapeHtml(t("addComputer"))}</span>`, "full")}${button("add-workspace", `${icon("folder")}<span>${escapeHtml(t("addWorkspace"))}</span>`, "full outline")}</div>
|
|
1641
2283
|
<input id="file-input" type="file" multiple hidden accept="${SOURCE_EXTENSIONS.join(",")}">
|
|
1642
2284
|
<div id="sources-list"></div></aside>
|
|
1643
|
-
<section class="panel chat" aria-label="${escapeHtml(t("chatPanel"))}"><div id="banner"></div><div id="messages" class="messages"></div>
|
|
1644
|
-
<div class="composer"><label class="sr-only" for="question">${escapeHtml(t("placeholder"))}</label><textarea id="question" rows="
|
|
2285
|
+
<section class="panel chat" aria-label="${escapeHtml(t("chatPanel"))}"><div id="chat-head" class="chat-head"></div><div id="banner"></div><div id="messages" class="messages"></div>
|
|
2286
|
+
<div class="composer"><div class="composer-box"><label class="sr-only" for="question">${escapeHtml(t("placeholder"))}</label><textarea id="question" rows="1" maxlength="4000" placeholder="${escapeHtml(t("placeholder"))}" aria-describedby="composer-hint">${escapeHtml(draft)}</textarea><div id="composer-actions" class="composer-actions"></div></div><p id="composer-hint" class="composer-hint">${escapeHtml(t("composerHint"))}</p><p class="disclaimer">${escapeHtml(t("disclaimer"))}</p></div>
|
|
1645
2287
|
<p id="live" class="sr-only" aria-live="polite"></p></section>
|
|
1646
2288
|
<aside class="panel guide" aria-label="${escapeHtml(t("guidePanel"))}" id="guide"></aside>
|
|
1647
2289
|
</main>`;
|
|
1648
2290
|
renderSources();
|
|
2291
|
+
renderChatHead();
|
|
1649
2292
|
renderMessages();
|
|
1650
2293
|
renderGuide();
|
|
1651
2294
|
renderComposer();
|
|
@@ -1676,34 +2319,58 @@ function renderSources() {
|
|
|
1676
2319
|
: t(`status_${view.key}`);
|
|
1677
2320
|
return `<li class="source ${view.key}"><label class="check"><input type="checkbox" data-source="${s.id}" ${checked.has(s.id) ? "checked" : ""} ${usable ? "" : "disabled"} aria-label="${escapeHtml(tf("includeSource", s.name))}"></label>
|
|
1678
2321
|
<span class="source-main">${icon("file")}<span class="source-name" title="${escapeHtml(s.name)}">${escapeHtml(s.name)}</span><span class="source-state small">${escapeHtml(label)}</span></span>
|
|
1679
|
-
<span class="source-actions">${usable ? iconButton("summarize", "sparkle", t("summarize"), `data-id="${s.id}"`) : ""}${iconButton("delete-source", "trash", t("removeSource"), `data-id="${s.id}"`)}</span></li>`;
|
|
2322
|
+
<span class="source-actions">${usable ? iconButton("summarize", "sparkle", t("summarize"), `data-id="${s.id}" ${canAsk() || s.summary ? "" : "disabled"}`) : ""}${iconButton("delete-source", "trash", t("removeSource"), `data-id="${s.id}"`)}</span></li>`;
|
|
1680
2323
|
})
|
|
1681
2324
|
.join("")}</ul>`;
|
|
1682
2325
|
const selectAll = document.querySelector("#select-all");
|
|
1683
2326
|
if (selectAll) selectAll.indeterminate = all === "mixed";
|
|
1684
2327
|
}
|
|
1685
2328
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2329
|
+
const canAsk = () => models.available !== false;
|
|
2330
|
+
|
|
2331
|
+
/** Copy controls for an answer, the guide or a summary (O14). */
|
|
2332
|
+
const copyControls = (ref) =>
|
|
2333
|
+
`<span class="copy-controls">${iconButton("copy-text", "copy", t("copyText"), `data-copy="${ref}"`)}${iconButton("copy-formatted", "copyRich", t("copyFormatted"), `data-copy="${ref}"`)}</span>`;
|
|
2334
|
+
|
|
2335
|
+
/** Model selector and the no-model state above the chat (O12). */
|
|
2336
|
+
function renderChatHead() {
|
|
2337
|
+
const el = document.querySelector("#chat-head");
|
|
2338
|
+
if (!el || !current) return;
|
|
2339
|
+
if (!canAsk()) {
|
|
2340
|
+
el.innerHTML = `<div class="banner block-state" role="alert">${icon("warning")}<span><strong>${escapeHtml(t("noModelTitle"))}</strong> ${escapeHtml(t("noModel"))}</span></div>${clearChatButton()}`;
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
const chosen = current.model ?? "";
|
|
2344
|
+
const groups = new Map();
|
|
2345
|
+
for (const m of models.models) {
|
|
2346
|
+
if (!groups.has(m.provider)) groups.set(m.provider, []);
|
|
2347
|
+
groups.get(m.provider).push(m);
|
|
2348
|
+
}
|
|
2349
|
+
const options = [...groups.entries()]
|
|
2350
|
+
.map(
|
|
2351
|
+
([provider, list]) =>
|
|
2352
|
+
`<optgroup label="${escapeHtml(provider)}">${list.map((m) => `<option value="${escapeHtml(m.reference)}" ${m.reference === chosen ? "selected" : ""}>${escapeHtml(m.name)}</option>`).join("")}</optgroup>`,
|
|
1692
2353
|
)
|
|
1693
2354
|
.join("");
|
|
2355
|
+
el.innerHTML = `<label class="model-picker"><span>${escapeHtml(t("model"))}</span><select id="model-select"><option value="" ${chosen ? "" : "selected"}>${escapeHtml(tf("modelDefault", models.active ?? ""))}</option>${options}</select></label>${clearChatButton()}`;
|
|
1694
2356
|
}
|
|
2357
|
+
const clearChatButton = () =>
|
|
2358
|
+
chat.length && !askJob ? iconButton("clear-chat", "trash", t("clearChat")) : "";
|
|
1695
2359
|
|
|
1696
|
-
function messageHtml(m) {
|
|
2360
|
+
function messageHtml(m, index) {
|
|
1697
2361
|
if (m.role === "user")
|
|
1698
2362
|
return `<div class="msg user"><span class="who">${escapeHtml(t("you"))}</span><p>${escapeHtml(m.text)}</p></div>`;
|
|
1699
|
-
const body =
|
|
2363
|
+
const body = renderMarkdown(m.text, m.citations);
|
|
1700
2364
|
const warnings = [
|
|
2365
|
+
m.modelFallback
|
|
2366
|
+
? `<p class="warn small">${icon("warning")}${escapeHtml(t("modelFallback"))}</p>`
|
|
2367
|
+
: "",
|
|
1701
2368
|
!m.grounded && !m.notFound
|
|
1702
2369
|
? `<p class="warn small">${icon("warning")}${escapeHtml(t("notGrounded"))}</p>`
|
|
1703
2370
|
: "",
|
|
1704
2371
|
m.keywordOnly ? `<span class="badge small">${escapeHtml(t("keywordAnswer"))}</span>` : "",
|
|
1705
2372
|
].join("");
|
|
1706
|
-
return `<div class="msg assistant"><span class="who">${escapeHtml(t("assistant"))}
|
|
2373
|
+
return `<div class="msg assistant copyable"><span class="who">${escapeHtml(t("assistant"))}${m.notFound ? "" : copyControls(`chat:${index}`)}</span><div class="md">${body}</div>${warnings}</div>`;
|
|
1707
2374
|
}
|
|
1708
2375
|
|
|
1709
2376
|
function renderMessages() {
|
|
@@ -1712,13 +2379,13 @@ function renderMessages() {
|
|
|
1712
2379
|
if (chat.length === 0 && !askJob) {
|
|
1713
2380
|
const questions = suggestions(guide);
|
|
1714
2381
|
el.innerHTML = `<div class="empty-chat">${icon("quote", "icon big")}<h2>${escapeHtml(t("emptyChatTitle"))}</h2><p class="muted">${escapeHtml(t("emptyChatText"))}</p>
|
|
1715
|
-
${guide?.summary ? `<
|
|
1716
|
-
${questions.length ? `<div class="chips">${questions.map((q, i) => button("suggest", escapeHtml(q), "chip", `data-index="${i}"`)).join("")}</div>` : current?.sources.length ? button("guide", `${icon("sparkle")}<span>${escapeHtml(guideJob ? t("guideWorking") : t("makeGuide"))}</span>`, "outline", guideJob ? "disabled" : "") : ""}
|
|
2382
|
+
${guide?.summary ? `<div class="guide-summary md">${renderMarkdown(guide.summary, guide.citations)}</div>` : ""}
|
|
2383
|
+
${questions.length ? `<div class="chips">${questions.map((q, i) => button("suggest", escapeHtml(q), "chip", `data-index="${i}" ${canAsk() ? "" : "disabled"}`)).join("")}</div>` : current?.sources.length ? button("guide", `${icon("sparkle")}<span>${escapeHtml(guideJob ? t("guideWorking") : t("makeGuide"))}</span>`, "outline", guideJob || !canAsk() ? "disabled" : "") : ""}
|
|
1717
2384
|
</div>`;
|
|
1718
2385
|
return;
|
|
1719
2386
|
}
|
|
1720
2387
|
el.innerHTML =
|
|
1721
|
-
chat.map(messageHtml).join("") +
|
|
2388
|
+
chat.map((m, i) => messageHtml(m, i)).join("") +
|
|
1722
2389
|
(askJob
|
|
1723
2390
|
? `<div class="msg assistant pending"><span class="spinner" aria-hidden="true"></span>${escapeHtml(t("thinking"))}</div>`
|
|
1724
2391
|
: "");
|
|
@@ -1728,9 +2395,23 @@ function renderMessages() {
|
|
|
1728
2395
|
function renderComposer() {
|
|
1729
2396
|
const el = document.querySelector("#composer-actions");
|
|
1730
2397
|
if (!el) return;
|
|
2398
|
+
// One compact round button inside the composer: Send, or Stop while an answer is running.
|
|
2399
|
+
const empty = !draft.trim();
|
|
1731
2400
|
el.innerHTML = askJob
|
|
1732
|
-
? button
|
|
1733
|
-
:
|
|
2401
|
+
? `<button type="button" class="send-btn stop" data-action="stop" aria-label="${escapeHtml(t("stop"))}" title="${escapeHtml(t("stop"))}">${icon("stop")}</button>`
|
|
2402
|
+
: `<button type="button" class="send-btn" data-action="send" aria-label="${escapeHtml(t("send"))}" title="${escapeHtml(t("send"))}" ${canAsk() && !empty ? "" : "disabled"}>${icon("arrowUp")}</button>`;
|
|
2403
|
+
const box = document.querySelector("#question");
|
|
2404
|
+
if (box) {
|
|
2405
|
+
box.disabled = !canAsk();
|
|
2406
|
+
autoGrow(box);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
/** 1 line minimum, about 6 lines maximum, then the textarea scrolls. */
|
|
2410
|
+
function autoGrow(box) {
|
|
2411
|
+
box.style.height = "auto";
|
|
2412
|
+
const max = 6 * 22 + 16;
|
|
2413
|
+
box.style.height = `${Math.min(box.scrollHeight || 0, max) || ""}px`;
|
|
2414
|
+
box.style.overflowY = box.scrollHeight > max ? "auto" : "hidden";
|
|
1734
2415
|
}
|
|
1735
2416
|
|
|
1736
2417
|
function renderBanner() {
|
|
@@ -1747,10 +2428,14 @@ function renderGuide() {
|
|
|
1747
2428
|
if (!el) return;
|
|
1748
2429
|
const questions = suggestions(guide);
|
|
1749
2430
|
el.innerHTML = `<h2>${escapeHtml(t("guidePanel"))}</h2>
|
|
1750
|
-
<section class="block"><h3>${escapeHtml(t("summaryTitle"))}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2431
|
+
<section class="block copyable"><h3>${escapeHtml(t("summaryTitle"))}${guide?.summary ? copyControls("guide") : ""}</h3>${
|
|
2432
|
+
guide?.summary
|
|
2433
|
+
? `${guide.outdated ? `<p class="warn small">${icon("warning")}${escapeHtml(t("outdated"))} ${button("guide", escapeHtml(guideJob ? t("guideWorking") : t("regenerate")), "small", guideJob || !canAsk() ? "disabled" : "")}</p>` : ""}<div class="md">${renderMarkdown(guide.summary, guide.citations)}</div>`
|
|
2434
|
+
: `<p class="muted small">${escapeHtml(t("noGuide"))}</p>${current?.sources.length ? button("guide", `${icon("sparkle")}<span>${escapeHtml(guideJob ? t("guideWorking") : t("makeGuide"))}</span>`, "small", guideJob || !canAsk() ? "disabled" : "") : ""}`
|
|
2435
|
+
}</section>
|
|
2436
|
+
${questions.length ? `<section class="block copyable"><h3>${escapeHtml(t("questionsTitle"))}${copyControls("questions")}</h3><div class="chips column">${questions.map((q, i) => button("suggest", escapeHtml(q), "chip", `data-index="${i}" ${canAsk() ? "" : "disabled"}`)).join("")}</div></section>` : ""}
|
|
2437
|
+
${summary ? `<section class="block copyable"><h3>${escapeHtml(summary.name)}${copyControls("summary")}</h3><div class="md">${renderMarkdown(summary.summary, summary.citations)}</div><p>${button("summarize-again", escapeHtml(t("regenerate")), "small", `data-id="${summary.sourceId}" ${canAsk() ? "" : "disabled"}`)}</p></section>` : ""}
|
|
2438
|
+
<section class="block passage copyable" aria-live="polite"><h3>${escapeHtml(t("passageTitle"))}${passage ? copyControls("passage") : ""}</h3>${
|
|
1754
2439
|
passage
|
|
1755
2440
|
? `<p class="muted small">${escapeHtml(passage.sourceName)}${placeLabel(passage) ? ` · ${escapeHtml(tf(...placeLabel(passage)))}` : ""}</p><blockquote>${escapeHtml(passage.text)}</blockquote>`
|
|
1756
2441
|
: `<p class="muted small">${escapeHtml(t("passageEmpty"))}</p>`
|
|
@@ -1776,17 +2461,68 @@ async function refreshNotebook() {
|
|
|
1776
2461
|
checked.add(s.id);
|
|
1777
2462
|
current = fresh;
|
|
1778
2463
|
}
|
|
2464
|
+
async function loadModels() {
|
|
2465
|
+
try {
|
|
2466
|
+
models = (await api("models.list")) ?? models;
|
|
2467
|
+
} catch {
|
|
2468
|
+
// Keep the last known list; a transport problem is handled by the not_found check.
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
1779
2471
|
async function openNotebook(id) {
|
|
1780
2472
|
current = await api("notebooks.get", { id });
|
|
2473
|
+
// The ticked scope, the guide and the chat persist in the notebook (O15).
|
|
1781
2474
|
checked.clear();
|
|
1782
|
-
|
|
2475
|
+
const excluded = new Set(current.excluded ?? []);
|
|
2476
|
+
for (const s of current.sources)
|
|
2477
|
+
if (s.status !== "failed" && !excluded.has(s.id)) checked.add(s.id);
|
|
1783
2478
|
chat = await api("chat.history", { notebookId: id });
|
|
1784
|
-
guide = undefined;
|
|
2479
|
+
guide = current.guide ?? undefined;
|
|
1785
2480
|
passage = undefined;
|
|
1786
2481
|
summary = undefined;
|
|
1787
2482
|
tab = "chat";
|
|
2483
|
+
await loadModels();
|
|
2484
|
+
render();
|
|
2485
|
+
}
|
|
2486
|
+
/** Visible again: refresh what may have changed while hidden (models, notebook, status). */
|
|
2487
|
+
async function refreshOnShow() {
|
|
2488
|
+
try {
|
|
2489
|
+
await loadStatus();
|
|
2490
|
+
if (current) {
|
|
2491
|
+
await loadModels();
|
|
2492
|
+
await refreshNotebook();
|
|
2493
|
+
render();
|
|
2494
|
+
} else {
|
|
2495
|
+
await loadHome();
|
|
2496
|
+
render();
|
|
2497
|
+
}
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
fail(error);
|
|
2500
|
+
}
|
|
2501
|
+
void poll();
|
|
2502
|
+
}
|
|
2503
|
+
/** After the app comes back from the unavailable state. */
|
|
2504
|
+
async function reloadAll() {
|
|
2505
|
+
await loadStatus();
|
|
2506
|
+
await loadHome();
|
|
2507
|
+
if (current) {
|
|
2508
|
+
try {
|
|
2509
|
+
await openNotebook(current.id);
|
|
2510
|
+
return;
|
|
2511
|
+
} catch {
|
|
2512
|
+
current = undefined;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
1788
2515
|
render();
|
|
1789
2516
|
}
|
|
2517
|
+
function saveScope() {
|
|
2518
|
+
if (!current) return;
|
|
2519
|
+
const usable = current.sources.filter(
|
|
2520
|
+
(s) => s.status === "ready" || s.status === "ready_keyword",
|
|
2521
|
+
);
|
|
2522
|
+
const excluded = usable.filter((s) => !checked.has(s.id)).map((s) => s.id);
|
|
2523
|
+
current.excluded = excluded;
|
|
2524
|
+
api("notebooks.setScope", { id: current.id, excluded }).catch(fail);
|
|
2525
|
+
}
|
|
1790
2526
|
|
|
1791
2527
|
function track(jobId, info) {
|
|
1792
2528
|
if (!jobId) return;
|
|
@@ -1799,7 +2535,18 @@ async function poll() {
|
|
|
1799
2535
|
pollBusy = true;
|
|
1800
2536
|
try {
|
|
1801
2537
|
if (setupJob && jobView(setupJob).running) {
|
|
1802
|
-
|
|
2538
|
+
try {
|
|
2539
|
+
setupJob = await api("jobs.get", { jobId: setupJob.id });
|
|
2540
|
+
} catch (error) {
|
|
2541
|
+
// The backend no longer knows this job (e.g. it was restarted): stop polling it.
|
|
2542
|
+
if (error?.message !== "not_found" || error.transport) throw error;
|
|
2543
|
+
setupJob = undefined;
|
|
2544
|
+
await loadStatus();
|
|
2545
|
+
}
|
|
2546
|
+
if (!setupJob) {
|
|
2547
|
+
render();
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
1803
2550
|
const slot = document.querySelector("#setup");
|
|
1804
2551
|
if (slot) slot.innerHTML = setupCard();
|
|
1805
2552
|
if (!jobView(setupJob).running) {
|
|
@@ -1810,7 +2557,13 @@ async function poll() {
|
|
|
1810
2557
|
}
|
|
1811
2558
|
let changed = false;
|
|
1812
2559
|
for (const [jobId, info] of jobs) {
|
|
1813
|
-
|
|
2560
|
+
let job;
|
|
2561
|
+
try {
|
|
2562
|
+
job = await api("jobs.get", { jobId });
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
if (error?.message !== "not_found" || error.transport) throw error;
|
|
2565
|
+
job = { state: "failed", error: "not_found" };
|
|
2566
|
+
}
|
|
1814
2567
|
jobState.set(jobId, job);
|
|
1815
2568
|
if (jobView(job).running) {
|
|
1816
2569
|
if (info.kind === "index") renderSources();
|
|
@@ -1838,7 +2591,8 @@ async function poll() {
|
|
|
1838
2591
|
|
|
1839
2592
|
async function finished(info, job) {
|
|
1840
2593
|
const view = jobView(job);
|
|
1841
|
-
if (view.failed && info.kind !== "index"
|
|
2594
|
+
if (view.failed && info.kind !== "index" && view.error !== "not_found")
|
|
2595
|
+
fail(new Error(view.error));
|
|
1842
2596
|
if (!current || info.notebookId !== current.id) return;
|
|
1843
2597
|
if (info.kind === "index" || info.kind === "rebuild") {
|
|
1844
2598
|
if (info.kind === "rebuild") rebuildJob = undefined;
|
|
@@ -1851,17 +2605,29 @@ async function finished(info, job) {
|
|
|
1851
2605
|
chat = await api("chat.history", { notebookId: current.id });
|
|
1852
2606
|
renderMessages();
|
|
1853
2607
|
renderComposer();
|
|
2608
|
+
renderChatHead();
|
|
1854
2609
|
announce(chat.at(-1)?.text ?? "");
|
|
1855
2610
|
} else if (info.kind === "guide") {
|
|
1856
2611
|
guideJob = undefined;
|
|
1857
2612
|
if (view.done) guide = job.result;
|
|
1858
2613
|
renderGuide();
|
|
1859
2614
|
renderMessages();
|
|
1860
|
-
} else if (info.kind === "summary"
|
|
1861
|
-
|
|
1862
|
-
|
|
2615
|
+
} else if (info.kind === "summary") {
|
|
2616
|
+
if (view.done) {
|
|
2617
|
+
const source = current.sources.find((s) => s.id === info.sourceId);
|
|
2618
|
+
summary = { name: source?.name ?? "", ...job.result };
|
|
2619
|
+
await refreshNotebook();
|
|
2620
|
+
}
|
|
1863
2621
|
renderGuide();
|
|
1864
2622
|
}
|
|
2623
|
+
if (view.failed && view.error === "not_found") {
|
|
2624
|
+
// A job the backend no longer knows: just refresh what it was about.
|
|
2625
|
+
if (info.kind === "ask") askJob = undefined;
|
|
2626
|
+
if (info.kind === "guide") guideJob = undefined;
|
|
2627
|
+
if (info.kind === "rebuild") rebuildJob = undefined;
|
|
2628
|
+
await refreshNotebook();
|
|
2629
|
+
render();
|
|
2630
|
+
}
|
|
1865
2631
|
}
|
|
1866
2632
|
|
|
1867
2633
|
// ---------- Actions ----------
|
|
@@ -2008,7 +2774,7 @@ async function workspaceBrowser(dir = "") {
|
|
|
2008
2774
|
|
|
2009
2775
|
async function ask(question) {
|
|
2010
2776
|
const text = question.trim();
|
|
2011
|
-
if (!text || askJob || !current) return;
|
|
2777
|
+
if (!text || askJob || !current || !canAsk()) return;
|
|
2012
2778
|
if (needsRemoteNotice(current)) {
|
|
2013
2779
|
const ok = await confirmDialog("noticeText", "noticeOk", "noticeTitle");
|
|
2014
2780
|
if (!ok) return;
|
|
@@ -2092,6 +2858,17 @@ async function handle(action, target) {
|
|
|
2092
2858
|
await refreshNotebook();
|
|
2093
2859
|
return renderSources();
|
|
2094
2860
|
case "summarize": {
|
|
2861
|
+
// A stored summary is shown as is; it is regenerated only on request (O15).
|
|
2862
|
+
const source = current.sources.find((s) => s.id === target.dataset.id);
|
|
2863
|
+
if (source?.summary) {
|
|
2864
|
+
summary = { name: source.name, sourceId: source.id, ...source.summary };
|
|
2865
|
+
if (!panels.guide) panels.guide = true;
|
|
2866
|
+
tab = "guide";
|
|
2867
|
+
return renderNotebook();
|
|
2868
|
+
}
|
|
2869
|
+
return handle("summarize-again", target);
|
|
2870
|
+
}
|
|
2871
|
+
case "summarize-again": {
|
|
2095
2872
|
const { jobId } = await api("sources.summary", { sourceId: target.dataset.id, locale });
|
|
2096
2873
|
track(jobId, { kind: "summary", notebookId: current.id, sourceId: target.dataset.id });
|
|
2097
2874
|
return;
|
|
@@ -2103,6 +2880,20 @@ async function handle(action, target) {
|
|
|
2103
2880
|
renderGuide();
|
|
2104
2881
|
return renderMessages();
|
|
2105
2882
|
}
|
|
2883
|
+
case "retry":
|
|
2884
|
+
root.querySelector('[data-action="retry"]')?.setAttribute("disabled", "");
|
|
2885
|
+
return verifyAvailable();
|
|
2886
|
+
case "copy-text":
|
|
2887
|
+
case "copy-formatted": {
|
|
2888
|
+
const source = copySource(target.dataset.copy);
|
|
2889
|
+
if (!source) return;
|
|
2890
|
+
const { text, html } = copyPayload(source, renderMarkdown, action === "copy-formatted");
|
|
2891
|
+
const path = await writeClipboard({ text, ...(html ? { html } : {}) });
|
|
2892
|
+
lastCopyPath = path;
|
|
2893
|
+
if (path === "manual") return manualCopy(text, html);
|
|
2894
|
+
toast(t("copied"));
|
|
2895
|
+
return announce(t("copied"));
|
|
2896
|
+
}
|
|
2106
2897
|
case "suggest":
|
|
2107
2898
|
return ask(suggestions(guide)[Number(target.dataset.index)] ?? "");
|
|
2108
2899
|
case "send":
|
|
@@ -2160,13 +2951,30 @@ root.addEventListener("change", (event) => {
|
|
|
2160
2951
|
if (s.status === "ready" || s.status === "ready_keyword")
|
|
2161
2952
|
target.checked ? checked.add(s.id) : checked.delete(s.id);
|
|
2162
2953
|
renderSources();
|
|
2954
|
+
saveScope();
|
|
2163
2955
|
} else if (target.dataset.source) {
|
|
2164
2956
|
target.checked ? checked.add(target.dataset.source) : checked.delete(target.dataset.source);
|
|
2165
2957
|
renderSources();
|
|
2958
|
+
saveScope();
|
|
2959
|
+
} else if (target.id === "model-select") {
|
|
2960
|
+
const model = target.value || null;
|
|
2961
|
+
api("notebooks.setModel", { id: current.id, model })
|
|
2962
|
+
.then(() => {
|
|
2963
|
+
current.model = model;
|
|
2964
|
+
})
|
|
2965
|
+
.catch((error) => {
|
|
2966
|
+
fail(error);
|
|
2967
|
+
renderChatHead();
|
|
2968
|
+
});
|
|
2166
2969
|
}
|
|
2167
2970
|
});
|
|
2168
2971
|
root.addEventListener("input", (event) => {
|
|
2169
|
-
if (event.target.id === "question")
|
|
2972
|
+
if (event.target.id === "question") {
|
|
2973
|
+
const wasEmpty = !draft.trim();
|
|
2974
|
+
draft = event.target.value;
|
|
2975
|
+
autoGrow(event.target);
|
|
2976
|
+
if (wasEmpty !== !draft.trim()) renderComposer();
|
|
2977
|
+
}
|
|
2170
2978
|
});
|
|
2171
2979
|
root.addEventListener("keydown", (event) => {
|
|
2172
2980
|
if (
|
|
@@ -2180,6 +2988,47 @@ root.addEventListener("keydown", (event) => {
|
|
|
2180
2988
|
}
|
|
2181
2989
|
});
|
|
2182
2990
|
|
|
2991
|
+
/** Text and citations behind a copy button (one shared component for every generated block). */
|
|
2992
|
+
const copySource = (ref) =>
|
|
2993
|
+
copySourceFor(ref, {
|
|
2994
|
+
chat,
|
|
2995
|
+
guide,
|
|
2996
|
+
summary,
|
|
2997
|
+
passage,
|
|
2998
|
+
place: (p) => (placeLabel(p) ? tf(...placeLabel(p)) : ""),
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
/** Last resort when the frame may not write the clipboard: show the content preselected. */
|
|
3002
|
+
function manualCopy(text, html) {
|
|
3003
|
+
openDialog(
|
|
3004
|
+
() =>
|
|
3005
|
+
`<div class="dialog-body wide"><p class="lead">${escapeHtml(t("copyManual"))}</p>${
|
|
3006
|
+
html
|
|
3007
|
+
? `<div id="copy-area" class="copy-area md" contenteditable="true">${html}</div>`
|
|
3008
|
+
: `<textarea id="copy-area" class="copy-area" readonly rows="10">${escapeHtml(text)}</textarea>`
|
|
3009
|
+
}<div class="row end">${button("dialog-cancel", escapeHtml(t("close")))}</div></div>`,
|
|
3010
|
+
() => {
|
|
3011
|
+
const area = dialog.querySelector("#copy-area");
|
|
3012
|
+
if (!area) return;
|
|
3013
|
+
if (area.select) area.select();
|
|
3014
|
+
else {
|
|
3015
|
+
const range = document.createRange();
|
|
3016
|
+
range.selectNodeContents(area);
|
|
3017
|
+
const selection = window.getSelection();
|
|
3018
|
+
selection?.removeAllRanges();
|
|
3019
|
+
selection?.addRange(range);
|
|
3020
|
+
}
|
|
3021
|
+
area.focus();
|
|
3022
|
+
},
|
|
3023
|
+
);
|
|
3024
|
+
dialog.onclick = (event) => {
|
|
3025
|
+
if (event.target.closest("[data-action]")?.dataset.action === "dialog-cancel") closeDialog();
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
/** Which copy path worked last ("clipboard", "execCommand", "manual"); read by the visual check. */
|
|
3029
|
+
let lastCopyPath;
|
|
3030
|
+
globalThis.semanticLastCopyPath = () => lastCopyPath;
|
|
3031
|
+
|
|
2183
3032
|
// ---------- Start ----------
|
|
2184
3033
|
await connected;
|
|
2185
3034
|
try {
|