dsh-select-quote 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/lib/client.js +800 -571
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
8
|
let react_dom = require("react-dom");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
10
|
//#region src/client/dom/composer-host.ts
|
|
11
11
|
/**
|
|
12
12
|
* Host DOM contract for the resident composer.
|
|
@@ -138,6 +138,28 @@ ${atomRules}
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/* Same entrance for the below-anchor variant (only the content top uses it). */
|
|
142
|
+
@keyframes dsq-toolbar-in-below {
|
|
143
|
+
from {
|
|
144
|
+
transform: translate(-50%, 0) translateY(-4px) scale(0.96);
|
|
145
|
+
}
|
|
146
|
+
to {
|
|
147
|
+
transform: translate(-50%, 0) translateY(0) scale(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Comment phase: the hint sits above the popover's top-left corner (explicit
|
|
152
|
+
top-left placement comes from placeCommentPop), so the entrance no longer
|
|
153
|
+
owns the centering shift. */
|
|
154
|
+
@keyframes dsq-toolbar-in-popover-top {
|
|
155
|
+
from {
|
|
156
|
+
transform: translateY(4px) scale(0.96);
|
|
157
|
+
}
|
|
158
|
+
to {
|
|
159
|
+
transform: translateY(0) scale(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/* 内容层的 rise:悬停面板的列表在用;工具条改用容器 scale 后这条在这里待命。 */
|
|
142
164
|
@keyframes dsq-rise-in {
|
|
143
165
|
from {
|
|
@@ -184,10 +206,35 @@ ${atomRules}
|
|
|
184
206
|
--dsq-shadow: var(--dsw-elevation-prominent, 0 0 0 .5px rgba(0, 0, 0, 0.04), 0 3px 8px rgba(0, 0, 0, 0.04), 0 0 20px rgba(0, 0, 0, 0.05));
|
|
185
207
|
}
|
|
186
208
|
|
|
209
|
+
/* 浮层挂载与层带(2026-10-20 定,改前先读):
|
|
210
|
+
工具条/评论泡/状态不再 portal 到 body,而是由 dom/floating-layer.ts 挂进对话
|
|
211
|
+
真正的滚动容器(桌面抽屉布局是 ST7X_W_scrollBody,普通布局是 .icaHSq_scroll)
|
|
212
|
+
里的一个 absolute host,坐标用**内容坐标**。
|
|
213
|
+
|
|
214
|
+
为什么不是 body:header(对话标题那一排)是滚动容器之外的非定位元素,任何
|
|
215
|
+
body 级 positioned + z-index 的浮层都天然压过它(这就是 z-index 降到 0 也仍在
|
|
216
|
+
header 之上的原因);composer 座位是 sticky + z-index 7,只有低于它才不会被
|
|
217
|
+
输入框穿透。
|
|
218
|
+
|
|
219
|
+
现在把两件事交回宿主布局:
|
|
220
|
+
- 滚动容器负责裁剪:浮层随内容上移,到容器上边缘就被裁掉,自然滑进 header
|
|
221
|
+
下面,和正文一致;
|
|
222
|
+
- host 的 z-index 固定 6(见 floating-layer.ts):低于 composer 的 7,高于
|
|
223
|
+
内容里吸顶元素的 6(同 z 时 host 追加在滚动容器末尾,DOM 次序压过它们)。
|
|
224
|
+
host 内部次序:status 3 > toolbar 2 > commentPop 1。
|
|
225
|
+
|
|
226
|
+
摘要块(composer 与历史消息)在内容普通流里,不参与这套层带;悬停面板仍是
|
|
227
|
+
body 级 fixed(原因见 AnnotationCardView 里 contain:layout 的说明)。 */
|
|
228
|
+
|
|
187
229
|
.dsq_toolbar {
|
|
188
|
-
position:
|
|
189
|
-
z-index:
|
|
190
|
-
|
|
230
|
+
position: absolute;
|
|
231
|
+
z-index: 2;
|
|
232
|
+
/* The layer host is 0×0, so an auto-width absolute child would shrink to
|
|
233
|
+
min-content (Chinese hint text then wraps one character per line).
|
|
234
|
+
max-content keeps the bubble sized by its row regardless of the host. */
|
|
235
|
+
width: max-content;
|
|
236
|
+
max-width: calc(100vw - 24px);
|
|
237
|
+
transform: translate(-50%, var(--dsq-anchor-y, -100%));
|
|
191
238
|
gap: 2px;
|
|
192
239
|
padding: 4px;
|
|
193
240
|
/* 无边框:--dsq-shadow(宿主的 elevation-prominent)自带 0 0 0 .5px 的描边环,
|
|
@@ -202,6 +249,18 @@ ${atomRules}
|
|
|
202
249
|
animation: dsq-toolbar-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
203
250
|
}
|
|
204
251
|
|
|
252
|
+
/* Hangs below the selection: no room above at the very top of the transcript. */
|
|
253
|
+
.dsq_toolbar[data-anchor='below'] {
|
|
254
|
+
--dsq-anchor-y: 0%;
|
|
255
|
+
animation-name: dsq-toolbar-in-below;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Comment phase: placed at an explicit top-left, 6px above the comment popover. */
|
|
259
|
+
.dsq_toolbar[data-anchor='popover-top'] {
|
|
260
|
+
transform: none;
|
|
261
|
+
animation-name: dsq-toolbar-in-popover-top;
|
|
262
|
+
}
|
|
263
|
+
|
|
205
264
|
/* 按钮行:只负责布局;入场动画在容器上(见 .dsq_toolbar 的 A/B 说明)。 */
|
|
206
265
|
.dsq_toolbarInner {
|
|
207
266
|
display: inline-flex;
|
|
@@ -248,8 +307,8 @@ ${atomRules}
|
|
|
248
307
|
}
|
|
249
308
|
|
|
250
309
|
.dsq_status {
|
|
251
|
-
position:
|
|
252
|
-
z-index:
|
|
310
|
+
position: absolute;
|
|
311
|
+
z-index: 3;
|
|
253
312
|
transform: translate(-50%, 8px);
|
|
254
313
|
padding: 4px 10px;
|
|
255
314
|
border-radius: 999px;
|
|
@@ -266,7 +325,7 @@ ${atomRules}
|
|
|
266
325
|
position: absolute;
|
|
267
326
|
top: 8px;
|
|
268
327
|
left: 8px;
|
|
269
|
-
z-index:
|
|
328
|
+
z-index: 12;
|
|
270
329
|
width: 20%;
|
|
271
330
|
min-width: 148px;
|
|
272
331
|
max-width: 220px;
|
|
@@ -275,78 +334,8 @@ ${atomRules}
|
|
|
275
334
|
pointer-events: auto;
|
|
276
335
|
}
|
|
277
336
|
|
|
278
|
-
/*
|
|
279
|
-
|
|
280
|
-
numbers where the transcript really differs.
|
|
281
|
-
Geometry mirrors the product's own file card (\`.nyYjTG_file\` in
|
|
282
|
-
ui-deliverables): hairline border, neutral fill, 18px radius, fixed 72px row. */
|
|
283
|
-
.dsq_card {
|
|
284
|
-
--dsq-card-fill: var(--dsq-neutral-50);
|
|
285
|
-
--dsq-card-hover: var(--dsq-neutral-100);
|
|
286
|
-
flex: 0 0 auto;
|
|
287
|
-
gap: 10px;
|
|
288
|
-
width: fit-content;
|
|
289
|
-
max-width: calc(25% - 5px);
|
|
290
|
-
min-width: 0;
|
|
291
|
-
height: 64px;
|
|
292
|
-
margin: 0;
|
|
293
|
-
padding: 10px;
|
|
294
|
-
border: 0.5px solid var(--dsq-line);
|
|
295
|
-
border-radius: 18px;
|
|
296
|
-
background: var(--dsq-card-fill);
|
|
297
|
-
color: var(--dsq-label);
|
|
298
|
-
transition: background-color 0.12s;
|
|
299
|
-
position: relative;
|
|
300
|
-
overflow: hidden;
|
|
301
|
-
user-select: none;
|
|
302
|
-
pointer-events: auto;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
.dsq_card:hover {
|
|
306
|
-
background: var(--dsq-card-hover);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/* Scaled down from the reference 48px: the composer card is width-capped at a
|
|
310
|
-
quarter of the input, so 48px would leave no room for the quote text. */
|
|
311
|
-
.dsq_cardIcon {
|
|
312
|
-
z-index: 2;
|
|
313
|
-
width: 36px;
|
|
314
|
-
height: 36px;
|
|
315
|
-
border: 0.5px solid var(--dsq-line);
|
|
316
|
-
border-radius: 12px;
|
|
317
|
-
background: var(--dsq-card-fill);
|
|
318
|
-
color: var(--dsq-link);
|
|
319
|
-
font-size: 12px;
|
|
320
|
-
font-weight: 600;
|
|
321
|
-
letter-spacing: -0.02em;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
.dsq_cardBody {
|
|
325
|
-
z-index: 2;
|
|
326
|
-
justify-content: center;
|
|
327
|
-
gap: 2px;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/* Keeps the ellipsised title clear of the corner remove button, which only the
|
|
331
|
-
composer stack renders. */
|
|
332
|
-
.dsq_cardStack .dsq_cardBody {
|
|
333
|
-
padding-right: 14px;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.dsq_cardTitle {
|
|
337
|
-
font-size: 14px;
|
|
338
|
-
font-weight: 500;
|
|
339
|
-
line-height: 22px;
|
|
340
|
-
color: var(--dsq-label);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
.dsq_cardSubtitle {
|
|
344
|
-
font-size: 12px;
|
|
345
|
-
line-height: 18px;
|
|
346
|
-
color: var(--dsq-label-3);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/* Corner remove button: hidden until the card is hovered or focused. */
|
|
337
|
+
/* Corner remove-all button on the summary block: hidden until the block is
|
|
338
|
+
hovered or focused. */
|
|
350
339
|
.dsq_cardClose {
|
|
351
340
|
position: absolute;
|
|
352
341
|
top: 4px;
|
|
@@ -363,8 +352,6 @@ ${atomRules}
|
|
|
363
352
|
transition: opacity 0.12s, background-color 0.12s;
|
|
364
353
|
}
|
|
365
354
|
|
|
366
|
-
.dsq_card:hover .dsq_cardClose,
|
|
367
|
-
.dsq_card:focus-within .dsq_cardClose,
|
|
368
355
|
.dsq_summary:hover .dsq_cardClose,
|
|
369
356
|
.dsq_summary:focus-within .dsq_cardClose {
|
|
370
357
|
opacity: 1;
|
|
@@ -376,37 +363,10 @@ ${atomRules}
|
|
|
376
363
|
color: var(--dsq-label);
|
|
377
364
|
}
|
|
378
365
|
|
|
379
|
-
/* Secondary corner action (edit comment). */
|
|
380
|
-
.dsq_cardAction {
|
|
381
|
-
position: absolute;
|
|
382
|
-
top: 4px;
|
|
383
|
-
right: 28px;
|
|
384
|
-
z-index: 3;
|
|
385
|
-
width: 20px;
|
|
386
|
-
height: 20px;
|
|
387
|
-
border-radius: 6px;
|
|
388
|
-
color: var(--dsq-label-3);
|
|
389
|
-
font-size: 12px;
|
|
390
|
-
line-height: 1;
|
|
391
|
-
opacity: 0;
|
|
392
|
-
pointer-events: none;
|
|
393
|
-
transition: opacity 0.12s, background-color 0.12s;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
.dsq_card:hover .dsq_cardAction,
|
|
397
|
-
.dsq_card:focus-within .dsq_cardAction {
|
|
398
|
-
opacity: 1;
|
|
399
|
-
pointer-events: auto;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
.dsq_cardAction:hover {
|
|
403
|
-
background: var(--dsq-fill-hover);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
366
|
/* Qoder-style annotation summary (“N 条批注”) inside the composer. */
|
|
407
367
|
.dsq_summary {
|
|
408
368
|
position: relative;
|
|
409
|
-
z-index:
|
|
369
|
+
z-index: 13;
|
|
410
370
|
gap: 10px;
|
|
411
371
|
width: 100%;
|
|
412
372
|
max-width: 100%;
|
|
@@ -450,7 +410,7 @@ ${atomRules}
|
|
|
450
410
|
flex atoms — the open class below turns display back on. */
|
|
451
411
|
display: none;
|
|
452
412
|
position: fixed;
|
|
453
|
-
z-index:
|
|
413
|
+
z-index: 14;
|
|
454
414
|
width: min(320px, calc(100vw - 16px));
|
|
455
415
|
flex-direction: column;
|
|
456
416
|
padding: 12px;
|
|
@@ -671,15 +631,19 @@ ${atomRules}
|
|
|
671
631
|
|
|
672
632
|
/* Inline comment composer under the floating selection toolbar. */
|
|
673
633
|
.dsq_commentPop {
|
|
674
|
-
position:
|
|
675
|
-
z-index:
|
|
676
|
-
|
|
634
|
+
position: absolute;
|
|
635
|
+
z-index: 1;
|
|
636
|
+
/* Vertical offset comes from placeCommentPop (below the selection); only
|
|
637
|
+
the horizontal centering belongs to the transform. */
|
|
638
|
+
transform: translateX(-50%);
|
|
677
639
|
width: min(320px, calc(100vw - 24px));
|
|
678
640
|
gap: 8px;
|
|
679
641
|
padding: 8px;
|
|
680
642
|
border-radius: 16px;
|
|
681
643
|
/* 底色与模糊都在材质图层里 */
|
|
682
644
|
box-shadow: var(--dsq-shadow);
|
|
645
|
+
/* The layer host is pointer-events:none; interactive surfaces opt back in. */
|
|
646
|
+
pointer-events: auto;
|
|
683
647
|
}
|
|
684
648
|
|
|
685
649
|
.dsq_commentInput {
|
|
@@ -707,98 +671,19 @@ ${atomRules}
|
|
|
707
671
|
gap: 6px;
|
|
708
672
|
}
|
|
709
673
|
|
|
710
|
-
/* Transcript
|
|
711
|
-
.
|
|
674
|
+
/* Transcript summary — the same block as the composer's, right-aligned under
|
|
675
|
+
the user bubble. One block per message; the hover list carries every
|
|
676
|
+
annotation with its selected text and comment. */
|
|
677
|
+
.dsq_tSummary {
|
|
712
678
|
display: flex;
|
|
713
|
-
flex-direction: row;
|
|
714
|
-
flex-wrap: wrap;
|
|
715
679
|
justify-content: flex-end;
|
|
716
|
-
align-items: stretch;
|
|
717
|
-
gap: 6px;
|
|
718
680
|
margin: 4px 0 8px;
|
|
719
|
-
/* Cards lay out horizontally and wrap onto the next row when they run out of
|
|
720
|
-
width. Each card carries the selected text plus the user's comment, so they
|
|
721
|
-
stay compact and stay on the user-bubble side (right). */
|
|
722
|
-
max-width: 100%;
|
|
723
|
-
margin-left: auto;
|
|
724
|
-
margin-right: 0;
|
|
725
681
|
}
|
|
726
682
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
.dsq_tCard {
|
|
683
|
+
.dsq_tSummary .dsq_summary {
|
|
684
|
+
width: auto;
|
|
685
|
+
min-width: 220px;
|
|
731
686
|
max-width: min(calc(var(--dsq-chat-width) * 0.72), 520px);
|
|
732
|
-
margin-left: auto;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
.dsq_tCardIcon {
|
|
736
|
-
width: 40px;
|
|
737
|
-
height: 40px;
|
|
738
|
-
font-size: 14px;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
.dsq_tCardButton {
|
|
742
|
-
/* Compact cards: several fit in one row before wrapping. */
|
|
743
|
-
display: block;
|
|
744
|
-
flex: 0 1 auto;
|
|
745
|
-
width: 230px;
|
|
746
|
-
max-width: 100%;
|
|
747
|
-
padding: 0;
|
|
748
|
-
font: inherit;
|
|
749
|
-
text-align: left;
|
|
750
|
-
border-radius: 14px;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
.dsq_tCardButton:focus-visible {
|
|
754
|
-
outline: 2px solid var(--dsq-accent);
|
|
755
|
-
outline-offset: 2px;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
.dsq_tCardButton .dsq_card {
|
|
759
|
-
height: auto;
|
|
760
|
-
min-height: 0;
|
|
761
|
-
align-items: flex-start;
|
|
762
|
-
width: 100%;
|
|
763
|
-
gap: 8px;
|
|
764
|
-
padding: 8px;
|
|
765
|
-
border-radius: 14px;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
.dsq_tCardButton .dsq_cardIcon {
|
|
769
|
-
width: 22px;
|
|
770
|
-
height: 22px;
|
|
771
|
-
border-radius: 8px;
|
|
772
|
-
font-size: 11px;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
.dsq_tCardButton .dsq_cardTitle {
|
|
776
|
-
display: -webkit-box;
|
|
777
|
-
-webkit-line-clamp: 2;
|
|
778
|
-
-webkit-box-orient: vertical;
|
|
779
|
-
overflow: hidden;
|
|
780
|
-
white-space: normal;
|
|
781
|
-
overflow-wrap: anywhere;
|
|
782
|
-
font-size: 13px;
|
|
783
|
-
line-height: 18px;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
.dsq_tCardButton .dsq_cardSubtitle {
|
|
787
|
-
display: -webkit-box;
|
|
788
|
-
-webkit-line-clamp: 3;
|
|
789
|
-
-webkit-box-orient: vertical;
|
|
790
|
-
overflow: hidden;
|
|
791
|
-
white-space: pre-wrap;
|
|
792
|
-
overflow-wrap: anywhere;
|
|
793
|
-
margin-top: 2px;
|
|
794
|
-
font-size: 12px;
|
|
795
|
-
line-height: 16px;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/* A comment is the user's own words — give it more presence than the neutral
|
|
799
|
-
"no comment" placeholder. */
|
|
800
|
-
.dsq_cardSubtitle[data-dsq-has-comment='true'] {
|
|
801
|
-
color: var(--dsq-label-2);
|
|
802
687
|
}
|
|
803
688
|
|
|
804
689
|
/* Replacement user bubble (quote block stripped from display). */
|
|
@@ -829,6 +714,12 @@ ${atomRules}
|
|
|
829
714
|
max-width: 100%;
|
|
830
715
|
}
|
|
831
716
|
|
|
717
|
+
.dsq_userImages img {
|
|
718
|
+
display: block;
|
|
719
|
+
max-width: 100%;
|
|
720
|
+
border-radius: 12px;
|
|
721
|
+
}
|
|
722
|
+
|
|
832
723
|
/* Model-written :dsh-annotation{index="N"} chips. */
|
|
833
724
|
.dsq_annDirective {
|
|
834
725
|
appearance: none;
|
|
@@ -849,6 +740,7 @@ ${atomRules}
|
|
|
849
740
|
.dsq_annMark {
|
|
850
741
|
--dsq-mark-color: var(--dsq-accent);
|
|
851
742
|
--dsq-mark-tint: var(--dsq-accent-tint);
|
|
743
|
+
position: relative;
|
|
852
744
|
background: var(--dsq-mark-tint);
|
|
853
745
|
text-decoration: underline;
|
|
854
746
|
text-decoration-style: dashed;
|
|
@@ -862,10 +754,17 @@ ${atomRules}
|
|
|
862
754
|
|
|
863
755
|
/* Ordinal as a superscript corner badge at the END of the marked text, so it
|
|
864
756
|
never sits between the reader and the sentence. Only the last fragment of a
|
|
865
|
-
multi-fragment mark carries the index, so the number appears exactly once.
|
|
757
|
+
multi-fragment mark carries the index, so the number appears exactly once.
|
|
758
|
+
Absolute so the badge adds no inline advance width: annotating never reflows
|
|
759
|
+
the paragraph or moves following words. Bottom (not top) anchors to the last
|
|
760
|
+
fragment: for a mark spanning several lines the containing block is the union
|
|
761
|
+
of the first and last fragments, so its bottom edge is the last line. */
|
|
866
762
|
.dsq_annMark[data-dsq-ann-index]::after {
|
|
867
763
|
content: attr(data-dsq-ann-index);
|
|
868
|
-
|
|
764
|
+
position: absolute;
|
|
765
|
+
bottom: 8px;
|
|
766
|
+
left: 100%;
|
|
767
|
+
display: grid;
|
|
869
768
|
place-items: center;
|
|
870
769
|
min-width: 15px;
|
|
871
770
|
height: 15px;
|
|
@@ -877,7 +776,7 @@ ${atomRules}
|
|
|
877
776
|
font-size: 10px;
|
|
878
777
|
font-weight: 600;
|
|
879
778
|
line-height: 1;
|
|
880
|
-
|
|
779
|
+
pointer-events: none;
|
|
881
780
|
}
|
|
882
781
|
|
|
883
782
|
.dsq_annMark:hover {
|
|
@@ -903,13 +802,7 @@ ${atomRules}
|
|
|
903
802
|
}
|
|
904
803
|
|
|
905
804
|
/* The product flips its palette on body[data-ds-dark-theme], and every
|
|
906
|
-
--dsq-* alias follows it. Only values we own ourselves need restating
|
|
907
|
-
transcript card carries .dsq_card too, so one selector covers both. */
|
|
908
|
-
body[data-ds-dark-theme] .dsq_card {
|
|
909
|
-
--dsq-card-fill: var(--dsq-neutral-850);
|
|
910
|
-
--dsq-card-hover: var(--dsq-neutral-800);
|
|
911
|
-
}
|
|
912
|
-
|
|
805
|
+
--dsq-* alias follows it. Only values we own ourselves need restating. */
|
|
913
806
|
body[data-ds-dark-theme] .dsq_annMark {
|
|
914
807
|
--dsq-mark-tint: var(--dsq-accent-tint-dark);
|
|
915
808
|
}
|
|
@@ -938,14 +831,8 @@ const styles = {
|
|
|
938
831
|
divider: "dsq_divider",
|
|
939
832
|
status: "dsq_status",
|
|
940
833
|
cardStack: "dsq_cardStack",
|
|
941
|
-
card: "dsq_card",
|
|
942
|
-
cardIcon: "dsq_cardIcon",
|
|
943
|
-
cardBody: "dsq_cardBody",
|
|
944
|
-
cardTitle: "dsq_cardTitle",
|
|
945
|
-
cardSubtitle: "dsq_cardSubtitle",
|
|
946
834
|
cardClose: "dsq_cardClose",
|
|
947
835
|
cardPad: "dsq_cardPad",
|
|
948
|
-
cardAction: "dsq_cardAction",
|
|
949
836
|
summary: "dsq_summary",
|
|
950
837
|
summaryIcon: "dsq_summaryIcon",
|
|
951
838
|
summaryBody: "dsq_summaryBody",
|
|
@@ -977,10 +864,7 @@ const styles = {
|
|
|
977
864
|
commentPop: "dsq_commentPop",
|
|
978
865
|
commentInput: "dsq_commentInput",
|
|
979
866
|
commentActions: "dsq_commentActions",
|
|
980
|
-
|
|
981
|
-
tCardButton: "dsq_tCardButton",
|
|
982
|
-
tCard: "dsq_tCard",
|
|
983
|
-
tCardIcon: "dsq_tCardIcon",
|
|
867
|
+
tSummary: "dsq_tSummary",
|
|
984
868
|
userRow: "dsq_userRow",
|
|
985
869
|
userStack: "dsq_userStack",
|
|
986
870
|
userImages: "dsq_userImages",
|
|
@@ -1163,6 +1047,12 @@ function createMark(job, withIndex) {
|
|
|
1163
1047
|
* selections that cross inline elements or paragraph boundaries never reorder
|
|
1164
1048
|
* the DOM; fragments of one annotation share the mark id, and the ordinal
|
|
1165
1049
|
* badge rides on the final fragment (end of the annotation).
|
|
1050
|
+
*
|
|
1051
|
+
* `surroundContents` is load-bearing: `extractContents()` leaves the text
|
|
1052
|
+
* before **and after** the range in the same node, so inserting the mark at
|
|
1053
|
+
* `node.nextSibling` (the old implementation) appended every partial selection
|
|
1054
|
+
* to the end of its parent — a selection starting at offset 0, such as the
|
|
1055
|
+
* sentence after a colon, visibly jumped to the paragraph's end.
|
|
1166
1056
|
*/
|
|
1167
1057
|
function wrapRange(item, job, from, to) {
|
|
1168
1058
|
const located = cleanTextRange(item, from, to);
|
|
@@ -1172,15 +1062,12 @@ function wrapRange(item, job, from, to) {
|
|
|
1172
1062
|
let wrapped = false;
|
|
1173
1063
|
pieces.forEach((piece, i) => {
|
|
1174
1064
|
const node = piece.entry.node;
|
|
1175
|
-
|
|
1176
|
-
if (!parent) return;
|
|
1065
|
+
if (!node.parentElement) return;
|
|
1177
1066
|
const range = document.createRange();
|
|
1178
1067
|
range.setStart(node, piece.start - piece.entry.start);
|
|
1179
1068
|
range.setEnd(node, piece.end - piece.entry.start);
|
|
1180
1069
|
const mark = createMark(job, i === pieces.length - 1);
|
|
1181
|
-
|
|
1182
|
-
parent.insertBefore(mark, node.nextSibling);
|
|
1183
|
-
if (node.nodeValue === "") node.remove();
|
|
1070
|
+
range.surroundContents(mark);
|
|
1184
1071
|
wrapped = true;
|
|
1185
1072
|
});
|
|
1186
1073
|
return wrapped;
|
|
@@ -1517,47 +1404,80 @@ function stripProtocolHeader(prefix) {
|
|
|
1517
1404
|
if (prefix.indexOf("# Response annotations:") >= 0) return "";
|
|
1518
1405
|
return prefix.replace(/\s+$/, "");
|
|
1519
1406
|
}
|
|
1407
|
+
function blockParts(text) {
|
|
1408
|
+
const open = text.indexOf(ANNOTATIONS_BLOCK_OPEN);
|
|
1409
|
+
if (open < 0) return null;
|
|
1410
|
+
const close = text.indexOf(ANNOTATIONS_BLOCK_CLOSE, open + 22);
|
|
1411
|
+
if (close < 0) return null;
|
|
1412
|
+
return {
|
|
1413
|
+
before: stripProtocolHeader(text.slice(0, open)),
|
|
1414
|
+
after: text.slice(close + 23).replace(/^\s+/, ""),
|
|
1415
|
+
json: text.slice(open + 22, close).trim()
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function joinedRest(parts) {
|
|
1419
|
+
return [parts.before, parts.after].filter((part) => part.length > 0).join("\n\n").trim();
|
|
1420
|
+
}
|
|
1421
|
+
/** Wire payload as an array; null when the block is not valid JSON. */
|
|
1422
|
+
function parseWire(parts) {
|
|
1423
|
+
let parsed;
|
|
1424
|
+
try {
|
|
1425
|
+
parsed = JSON.parse(parts.json);
|
|
1426
|
+
} catch {
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
1430
|
+
}
|
|
1431
|
+
/** Every source an item carries; falls back to the selected text itself. */
|
|
1432
|
+
function sourcesOf(record, body) {
|
|
1433
|
+
const single = record.source;
|
|
1434
|
+
if (single !== null && typeof single === "object" && typeof single.text === "string") return [single];
|
|
1435
|
+
if (Array.isArray(record.sources)) {
|
|
1436
|
+
const list = record.sources.filter((item) => item !== null && typeof item === "object" && typeof item.text === "string");
|
|
1437
|
+
if (list.length > 0) return list;
|
|
1438
|
+
}
|
|
1439
|
+
return [{ text: body }];
|
|
1440
|
+
}
|
|
1441
|
+
/** One usable wire item; null for entries without a selected text. */
|
|
1442
|
+
function wireBody(entry) {
|
|
1443
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
1444
|
+
const record = entry;
|
|
1445
|
+
const body = typeof record.text === "string" ? record.text.trim() : "";
|
|
1446
|
+
if (!body) return null;
|
|
1447
|
+
return {
|
|
1448
|
+
body,
|
|
1449
|
+
comment: typeof record.annotation === "string" ? record.annotation : "",
|
|
1450
|
+
sources: sourcesOf(record, body)
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1520
1453
|
/**
|
|
1521
1454
|
* Read plugin annotations out of a durable message. Tolerates the JSON block
|
|
1522
1455
|
* and strips it from `rest` so the bubble can show only the user's question.
|
|
1523
1456
|
*/
|
|
1524
1457
|
function scanAnnotatedMessage(text) {
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
if (open < 0 || close < 0 || close < open) return {
|
|
1458
|
+
const parts = blockParts(text);
|
|
1459
|
+
if (parts === null) return {
|
|
1528
1460
|
annotations: [],
|
|
1529
1461
|
rest: text
|
|
1530
1462
|
};
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
const rest = [stripProtocolHeader(text.slice(0, open)), text.slice(close + 23).replace(/^\s+/, "")].filter((part) => part.length > 0).join("\n\n").trim();
|
|
1534
|
-
let parsed;
|
|
1535
|
-
try {
|
|
1536
|
-
parsed = JSON.parse(jsonText);
|
|
1537
|
-
} catch {
|
|
1538
|
-
return {
|
|
1539
|
-
annotations: [],
|
|
1540
|
-
rest: text.replace(/^\s+/, "")
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
if (!Array.isArray(parsed)) return {
|
|
1463
|
+
const parsed = parseWire(parts);
|
|
1464
|
+
if (parsed === null) return {
|
|
1544
1465
|
annotations: [],
|
|
1545
|
-
rest
|
|
1466
|
+
rest: text.replace(/^\s+/, "")
|
|
1546
1467
|
};
|
|
1468
|
+
const rest = joinedRest(parts);
|
|
1547
1469
|
const annotations = [];
|
|
1548
1470
|
for (const entry of parsed) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
const
|
|
1552
|
-
if (!body) continue;
|
|
1553
|
-
const source = record.source && typeof record.source === "object" ? record.source : Array.isArray(record.sources) ? record.sources.find((item) => item && typeof item === "object") : void 0;
|
|
1471
|
+
const item = wireBody(entry);
|
|
1472
|
+
if (item === null) continue;
|
|
1473
|
+
const source = item.sources[0];
|
|
1554
1474
|
const messageId = typeof source?.messageId === "string" ? source.messageId : void 0;
|
|
1555
1475
|
const startOffset = typeof source?.startOffset === "number" ? source.startOffset : void 0;
|
|
1556
1476
|
const endOffset = typeof source?.endOffset === "number" ? source.endOffset : void 0;
|
|
1557
1477
|
annotations.push({
|
|
1558
|
-
title: previewTitle(body),
|
|
1559
|
-
text: body,
|
|
1560
|
-
comment:
|
|
1478
|
+
title: previewTitle(item.body),
|
|
1479
|
+
text: item.body,
|
|
1480
|
+
comment: item.comment,
|
|
1561
1481
|
...messageId ? { messageId } : {},
|
|
1562
1482
|
...startOffset !== void 0 ? { startOffset } : {},
|
|
1563
1483
|
...endOffset !== void 0 ? { endOffset } : {}
|
|
@@ -1568,6 +1488,33 @@ function scanAnnotatedMessage(text) {
|
|
|
1568
1488
|
rest
|
|
1569
1489
|
};
|
|
1570
1490
|
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Inverse of `composeAnnotatedMessage` for text that is about to live in the
|
|
1493
|
+
* composer: dsh-rewind refills a withdrawn message verbatim, protocol block
|
|
1494
|
+
* included. Returns the pending annotations and the user's own text, or null
|
|
1495
|
+
* when `text` carries no complete, valid block — a draft that merely mentions
|
|
1496
|
+
* the markers must never be edited away.
|
|
1497
|
+
*/
|
|
1498
|
+
function readComposedDraft(text) {
|
|
1499
|
+
const parts = blockParts(text);
|
|
1500
|
+
if (parts === null) return null;
|
|
1501
|
+
const parsed = parseWire(parts);
|
|
1502
|
+
if (parsed === null || parsed.length === 0) return null;
|
|
1503
|
+
const annotations = [];
|
|
1504
|
+
for (const entry of parsed) {
|
|
1505
|
+
const item = wireBody(entry);
|
|
1506
|
+
if (item !== null) annotations.push({
|
|
1507
|
+
text: item.body,
|
|
1508
|
+
comment: item.comment,
|
|
1509
|
+
sources: item.sources
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
if (annotations.length === 0) return null;
|
|
1513
|
+
return {
|
|
1514
|
+
annotations,
|
|
1515
|
+
rest: joinedRest(parts)
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1571
1518
|
//#endregion
|
|
1572
1519
|
//#region src/client/runtime.ts
|
|
1573
1520
|
/**
|
|
@@ -1615,6 +1562,9 @@ function persist(partitionKey) {
|
|
|
1615
1562
|
if (!persistence || partitionKey === MEMORY_KEY) return;
|
|
1616
1563
|
persistence.save(partitionKey, bySession.get(partitionKey) ?? EMPTY$1);
|
|
1617
1564
|
}
|
|
1565
|
+
function newAnnotationId() {
|
|
1566
|
+
return `ann_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1567
|
+
}
|
|
1618
1568
|
/** Append one selection; an identical text+comment pair is reused. */
|
|
1619
1569
|
function saveAnnotation(sessionId, input) {
|
|
1620
1570
|
const k = partition(sessionId);
|
|
@@ -1623,7 +1573,7 @@ function saveAnnotation(sessionId, input) {
|
|
|
1623
1573
|
const duplicate = current.find((item) => item.text === input.text && (item.comment ?? "") === comment);
|
|
1624
1574
|
if (duplicate) return duplicate;
|
|
1625
1575
|
const annotation = {
|
|
1626
|
-
id:
|
|
1576
|
+
id: newAnnotationId(),
|
|
1627
1577
|
text: input.text,
|
|
1628
1578
|
...comment ? { comment } : {},
|
|
1629
1579
|
sources: input.sources?.length ? input.sources : [{ text: input.text }]
|
|
@@ -1633,6 +1583,36 @@ function saveAnnotation(sessionId, input) {
|
|
|
1633
1583
|
notify();
|
|
1634
1584
|
return annotation;
|
|
1635
1585
|
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Re-adopt annotations lifted out of a composed message.
|
|
1588
|
+
*
|
|
1589
|
+
* dsh-rewind puts a withdrawn message back into the composer verbatim, so the
|
|
1590
|
+
* protocol block arrives there too; the composer half parses it and hands the
|
|
1591
|
+
* items here. Same text+comment pairs already pending win, so a repeated
|
|
1592
|
+
* restore never duplicates a card. Returns how many items were added.
|
|
1593
|
+
*/
|
|
1594
|
+
function restoreAnnotations(sessionId, items) {
|
|
1595
|
+
const k = partition(sessionId);
|
|
1596
|
+
const current = bySession.get(k) ?? EMPTY$1;
|
|
1597
|
+
const next = [...current];
|
|
1598
|
+
for (const item of items) {
|
|
1599
|
+
const text = item.text.trim();
|
|
1600
|
+
if (!text) continue;
|
|
1601
|
+
const comment = item.comment?.trim() ?? "";
|
|
1602
|
+
if (next.some((entry) => entry.text === text && (entry.comment ?? "") === comment)) continue;
|
|
1603
|
+
next.push({
|
|
1604
|
+
id: newAnnotationId(),
|
|
1605
|
+
text,
|
|
1606
|
+
...comment ? { comment } : {},
|
|
1607
|
+
sources: item.sources.length > 0 ? item.sources : [{ text }]
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
if (next.length === current.length) return 0;
|
|
1611
|
+
bySession.set(k, next);
|
|
1612
|
+
persist(k);
|
|
1613
|
+
notify();
|
|
1614
|
+
return next.length - current.length;
|
|
1615
|
+
}
|
|
1636
1616
|
function getAnnotations(sessionId) {
|
|
1637
1617
|
return bySession.get(partition(sessionId)) ?? EMPTY$1;
|
|
1638
1618
|
}
|
|
@@ -1715,20 +1695,24 @@ const GAP = 8;
|
|
|
1715
1695
|
const MARGIN = 8;
|
|
1716
1696
|
/**
|
|
1717
1697
|
* Place `panel` near `anchor`, flipping vertical side when the preferred side
|
|
1718
|
-
* lacks room. Keeps the panel inside the viewport
|
|
1698
|
+
* lacks room. Keeps the panel inside `bounds` (the viewport by default).
|
|
1719
1699
|
*/
|
|
1720
|
-
function placePopover(anchor, panel, preferred = "bottom", gap = GAP) {
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1700
|
+
function placePopover(anchor, panel, preferred = "bottom", gap = GAP, bounds) {
|
|
1701
|
+
const box = bounds ?? {
|
|
1702
|
+
top: 0,
|
|
1703
|
+
left: 0,
|
|
1704
|
+
right: window.innerWidth,
|
|
1705
|
+
bottom: window.innerHeight
|
|
1706
|
+
};
|
|
1707
|
+
const spaceBelow = box.bottom - anchor.bottom;
|
|
1708
|
+
const spaceAbove = anchor.top - box.top;
|
|
1725
1709
|
let side = preferred;
|
|
1726
1710
|
const need = panel.height + gap;
|
|
1727
1711
|
if (preferred === "bottom" && spaceBelow < need && spaceAbove > spaceBelow) side = "top";
|
|
1728
1712
|
else if (preferred === "top" && spaceAbove < need && spaceBelow > spaceAbove) side = "bottom";
|
|
1729
1713
|
return {
|
|
1730
|
-
top: side === "bottom" ? Math.min(anchor.bottom + gap,
|
|
1731
|
-
left: Math.min(Math.max(anchor.left, MARGIN), Math.max(MARGIN,
|
|
1714
|
+
top: side === "bottom" ? Math.min(anchor.bottom + gap, box.bottom - panel.height - MARGIN) : Math.max(anchor.top - panel.height - gap, box.top + MARGIN),
|
|
1715
|
+
left: Math.min(Math.max(anchor.left, box.left + MARGIN), Math.max(box.left + MARGIN, box.right - panel.width - MARGIN)),
|
|
1732
1716
|
side
|
|
1733
1717
|
};
|
|
1734
1718
|
}
|
|
@@ -1746,78 +1730,32 @@ const SVG_ATTRS = {
|
|
|
1746
1730
|
focusable: false
|
|
1747
1731
|
};
|
|
1748
1732
|
const SHAPES = {
|
|
1749
|
-
copy: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("rect", {
|
|
1750
|
-
key: "r",
|
|
1733
|
+
copy: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1751
1734
|
width: 14,
|
|
1752
1735
|
height: 14,
|
|
1753
1736
|
x: 8,
|
|
1754
1737
|
y: 8,
|
|
1755
1738
|
rx: 2,
|
|
1756
1739
|
ry: 2
|
|
1757
|
-
}), (0, react_jsx_runtime.jsx)("path", {
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
key: "p2",
|
|
1766
|
-
d: "m15 5 4 4"
|
|
1767
|
-
})] }),
|
|
1768
|
-
"trash-2": (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
1769
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1770
|
-
key: "p1",
|
|
1771
|
-
d: "M10 11v6"
|
|
1772
|
-
}),
|
|
1773
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1774
|
-
key: "p2",
|
|
1775
|
-
d: "M14 11v6"
|
|
1776
|
-
}),
|
|
1777
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1778
|
-
key: "p3",
|
|
1779
|
-
d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"
|
|
1780
|
-
}),
|
|
1781
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1782
|
-
key: "p4",
|
|
1783
|
-
d: "M3 6h18"
|
|
1784
|
-
}),
|
|
1785
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1786
|
-
key: "p5",
|
|
1787
|
-
d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
|
1788
|
-
})
|
|
1740
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })] }),
|
|
1741
|
+
pencil: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m15 5 4 4" })] }),
|
|
1742
|
+
"trash-2": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
1743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 11v6" }),
|
|
1744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 11v6" }),
|
|
1745
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }),
|
|
1746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
1747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
|
|
1789
1748
|
] }),
|
|
1790
|
-
x: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("path", {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
d: "
|
|
1796
|
-
})] }),
|
|
1797
|
-
plus: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("path", {
|
|
1798
|
-
key: "p1",
|
|
1799
|
-
d: "M5 12h14"
|
|
1800
|
-
}), (0, react_jsx_runtime.jsx)("path", {
|
|
1801
|
-
key: "p2",
|
|
1802
|
-
d: "M12 5v14"
|
|
1803
|
-
})] }),
|
|
1804
|
-
"external-link": (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
1805
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1806
|
-
key: "p1",
|
|
1807
|
-
d: "M15 3h6v6"
|
|
1808
|
-
}),
|
|
1809
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1810
|
-
key: "p2",
|
|
1811
|
-
d: "M10 14 21 3"
|
|
1812
|
-
}),
|
|
1813
|
-
(0, react_jsx_runtime.jsx)("path", {
|
|
1814
|
-
key: "p3",
|
|
1815
|
-
d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
|
|
1816
|
-
})
|
|
1749
|
+
x: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })] }),
|
|
1750
|
+
plus: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12h14" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 5v14" })] }),
|
|
1751
|
+
"external-link": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
1752
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 3h6v6" }),
|
|
1753
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 14 21 3" }),
|
|
1754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
1817
1755
|
] })
|
|
1818
1756
|
};
|
|
1819
1757
|
function LucideIcon({ name, size = 15 }) {
|
|
1820
|
-
return (0, react_jsx_runtime.jsx)("svg", {
|
|
1758
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1821
1759
|
...SVG_ATTRS,
|
|
1822
1760
|
width: size,
|
|
1823
1761
|
height: size,
|
|
@@ -1856,11 +1794,6 @@ const zh = {
|
|
|
1856
1794
|
"toolbar.commentDialogAria": "添加可选评论",
|
|
1857
1795
|
"toolbar.skipComment": "暂不评论",
|
|
1858
1796
|
"toolbar.addComment": "添加评论",
|
|
1859
|
-
"card.commentSubtitle": "评论 · {comment}",
|
|
1860
|
-
"card.ariaIndexed": "批注 {index}",
|
|
1861
|
-
"card.ariaPlain": "划词批注",
|
|
1862
|
-
"card.editComment": "编辑评论",
|
|
1863
|
-
"card.remove": "移除批注",
|
|
1864
1797
|
"summary.hint": "悬停查看批注",
|
|
1865
1798
|
"summary.dialogAria": "{count} 条划词批注详情",
|
|
1866
1799
|
"summary.edit": "编辑批注 {index}",
|
|
@@ -1886,11 +1819,6 @@ const en = {
|
|
|
1886
1819
|
"toolbar.commentDialogAria": "Add an optional comment",
|
|
1887
1820
|
"toolbar.skipComment": "Skip comment",
|
|
1888
1821
|
"toolbar.addComment": "Add comment",
|
|
1889
|
-
"card.commentSubtitle": "Comment · {comment}",
|
|
1890
|
-
"card.ariaIndexed": "Annotation {index}",
|
|
1891
|
-
"card.ariaPlain": "Selection annotation",
|
|
1892
|
-
"card.editComment": "Edit comment",
|
|
1893
|
-
"card.remove": "Remove annotation",
|
|
1894
1822
|
"summary.hint": "Hover to view annotations",
|
|
1895
1823
|
"summary.dialogAria": "Annotation details: {count}",
|
|
1896
1824
|
"summary.edit": "Edit annotation {index}",
|
|
@@ -1923,11 +1851,6 @@ const ru = {
|
|
|
1923
1851
|
"toolbar.commentDialogAria": "Добавить необязательный комментарий",
|
|
1924
1852
|
"toolbar.skipComment": "Без комментария",
|
|
1925
1853
|
"toolbar.addComment": "Добавить комментарий",
|
|
1926
|
-
"card.commentSubtitle": "Комментарий · {comment}",
|
|
1927
|
-
"card.ariaIndexed": "Аннотация {index}",
|
|
1928
|
-
"card.ariaPlain": "Аннотация к выделенному тексту",
|
|
1929
|
-
"card.editComment": "Изменить комментарий",
|
|
1930
|
-
"card.remove": "Удалить аннотацию",
|
|
1931
1854
|
"summary.hint": "Наведите, чтобы увидеть аннотации",
|
|
1932
1855
|
"summary.dialogAria": "Детали аннотаций: {count}",
|
|
1933
1856
|
"summary.edit": "Изменить аннотацию {index}",
|
|
@@ -2025,52 +1948,6 @@ function useLocale() {
|
|
|
2025
1948
|
}
|
|
2026
1949
|
//#endregion
|
|
2027
1950
|
//#region src/client/ui/AnnotationCardView.tsx
|
|
2028
|
-
function AnnotationCardView({ title, text, comment, variant, index, onRemove, onEditComment }) {
|
|
2029
|
-
useLocale();
|
|
2030
|
-
const composer = variant === "composer";
|
|
2031
|
-
const subtitle = comment ? composer ? t("card.commentSubtitle", { comment }) : comment : composer ? t("common.selectedText") : t("summary.noComment");
|
|
2032
|
-
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2033
|
-
className: cx(A.box, A.row, styles.card, !composer && styles.tCard),
|
|
2034
|
-
role: "group",
|
|
2035
|
-
"aria-label": index ? t("card.ariaIndexed", { index }) : t("card.ariaPlain"),
|
|
2036
|
-
children: [
|
|
2037
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2038
|
-
className: cx(A.keep, A.gridMid, styles.cardIcon, !composer && styles.tCardIcon),
|
|
2039
|
-
"aria-hidden": true,
|
|
2040
|
-
children: index ? String(index) : "AI"
|
|
2041
|
-
}),
|
|
2042
|
-
(0, react_jsx_runtime.jsxs)("div", {
|
|
2043
|
-
className: cx(A.grow, A.col, styles.cardBody),
|
|
2044
|
-
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
2045
|
-
className: cx(A.ell, styles.cardTitle),
|
|
2046
|
-
title: text,
|
|
2047
|
-
children: title
|
|
2048
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
2049
|
-
className: cx(A.ell, styles.cardSubtitle),
|
|
2050
|
-
...comment ? {
|
|
2051
|
-
title: comment,
|
|
2052
|
-
"data-dsq-has-comment": "true"
|
|
2053
|
-
} : {},
|
|
2054
|
-
children: subtitle
|
|
2055
|
-
})]
|
|
2056
|
-
}),
|
|
2057
|
-
composer && onEditComment ? (0, react_jsx_runtime.jsx)("button", {
|
|
2058
|
-
type: "button",
|
|
2059
|
-
className: cx(A.plain, A.gridMid, styles.cardAction),
|
|
2060
|
-
"aria-label": t("card.editComment"),
|
|
2061
|
-
onClick: onEditComment,
|
|
2062
|
-
children: "✎"
|
|
2063
|
-
}) : null,
|
|
2064
|
-
composer && onRemove ? (0, react_jsx_runtime.jsx)("button", {
|
|
2065
|
-
type: "button",
|
|
2066
|
-
className: cx(A.plain, A.gridMid, styles.cardClose),
|
|
2067
|
-
"aria-label": t("card.remove"),
|
|
2068
|
-
onClick: onRemove,
|
|
2069
|
-
children: "×"
|
|
2070
|
-
}) : null
|
|
2071
|
-
]
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
1951
|
/** Grace period so pointer transit between the summary and the panel never closes it. */
|
|
2075
1952
|
const CLOSE_DELAY = 320;
|
|
2076
1953
|
/** After a viewport change, wait this long before position-checking the pointer. */
|
|
@@ -2239,62 +2116,62 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2239
2116
|
editDraft,
|
|
2240
2117
|
onItemEdit
|
|
2241
2118
|
]);
|
|
2242
|
-
const panel = open ? (0, react_jsx_runtime.jsx)("div", {
|
|
2119
|
+
const panel = open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2243
2120
|
ref: popRef,
|
|
2244
2121
|
className: `${styles.summaryHover} ${styles.summaryHoverOpen}`,
|
|
2245
2122
|
role: "dialog",
|
|
2246
2123
|
"aria-label": t("summary.dialogAria", { count }),
|
|
2247
2124
|
onPointerEnter: openPop,
|
|
2248
2125
|
onPointerLeave,
|
|
2249
|
-
children: (0, react_jsx_runtime.jsx)("div", {
|
|
2126
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2250
2127
|
className: cx(A.col, styles.summaryHoverInner),
|
|
2251
|
-
children: (0, react_jsx_runtime.jsx)("div", {
|
|
2128
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2252
2129
|
className: cx(A.col, styles.summaryList),
|
|
2253
2130
|
children: items.map((item, i) => {
|
|
2254
2131
|
const isEditing = editing === i;
|
|
2255
|
-
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2132
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2256
2133
|
className: styles.summaryItem,
|
|
2257
|
-
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
2134
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2258
2135
|
className: cx(A.row, styles.summaryItemHead),
|
|
2259
2136
|
children: [
|
|
2260
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2261
2138
|
className: cx(A.keep, A.gridMid, styles.summaryItemBadge),
|
|
2262
|
-
"aria-hidden": true,
|
|
2139
|
+
"aria-hidden": "true",
|
|
2263
2140
|
children: String(i + 1)
|
|
2264
2141
|
}),
|
|
2265
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2142
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2266
2143
|
className: styles.summaryItemLabel,
|
|
2267
2144
|
children: t("common.selectedText")
|
|
2268
2145
|
}),
|
|
2269
|
-
!isEditing && (onItemRemove || onItemEdit) ? (0, react_jsx_runtime.jsxs)("div", {
|
|
2146
|
+
!isEditing && (onItemRemove || onItemEdit) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2270
2147
|
className: cx(A.row, styles.summaryItemActions),
|
|
2271
|
-
children: [onItemEdit ? (0, react_jsx_runtime.jsx)("button", {
|
|
2148
|
+
children: [onItemEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2272
2149
|
type: "button",
|
|
2273
2150
|
className: cx(A.plain, A.gridMid, styles.summaryItemAction),
|
|
2274
2151
|
"aria-label": t("summary.edit", { index: i + 1 }),
|
|
2275
2152
|
onClick: () => startEdit(i),
|
|
2276
|
-
children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
|
|
2277
|
-
}) : null, onItemRemove ? (0, react_jsx_runtime.jsx)("button", {
|
|
2153
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
|
|
2154
|
+
}) : null, onItemRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2278
2155
|
type: "button",
|
|
2279
2156
|
className: cx(A.plain, A.gridMid, styles.summaryItemAction),
|
|
2280
2157
|
"aria-label": t("summary.remove", { index: i + 1 }),
|
|
2281
2158
|
onClick: () => onItemRemove(i, item),
|
|
2282
|
-
children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
|
|
2159
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
|
|
2283
2160
|
}) : null]
|
|
2284
2161
|
}) : null
|
|
2285
2162
|
]
|
|
2286
|
-
}), isEditing ? (0, react_jsx_runtime.jsxs)("div", {
|
|
2163
|
+
}), isEditing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2287
2164
|
className: styles.summaryItemMain,
|
|
2288
2165
|
children: [
|
|
2289
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2290
2167
|
className: styles.annEditText,
|
|
2291
2168
|
children: item.text
|
|
2292
2169
|
}),
|
|
2293
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2294
2171
|
className: styles.summaryItemLabel,
|
|
2295
2172
|
children: t("summary.userComment")
|
|
2296
2173
|
}),
|
|
2297
|
-
(0, react_jsx_runtime.jsx)("textarea", {
|
|
2174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
2298
2175
|
ref: editInputRef,
|
|
2299
2176
|
className: cx(A.box, styles.annEditInput),
|
|
2300
2177
|
placeholder: t("common.commentPlaceholder"),
|
|
@@ -2312,14 +2189,14 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2312
2189
|
}
|
|
2313
2190
|
}
|
|
2314
2191
|
}),
|
|
2315
|
-
(0, react_jsx_runtime.jsxs)("div", {
|
|
2192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2316
2193
|
className: cx(A.row, styles.annEditActions),
|
|
2317
|
-
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
2194
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2318
2195
|
type: "button",
|
|
2319
2196
|
className: cx(A.plain, styles.annEditCancel),
|
|
2320
2197
|
onClick: () => setEditing(null),
|
|
2321
2198
|
children: t("common.cancel")
|
|
2322
|
-
}), (0, react_jsx_runtime.jsx)("button", {
|
|
2199
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2323
2200
|
type: "button",
|
|
2324
2201
|
className: styles.annEditSave,
|
|
2325
2202
|
onClick: commitEdit,
|
|
@@ -2327,9 +2204,9 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2327
2204
|
})]
|
|
2328
2205
|
})
|
|
2329
2206
|
]
|
|
2330
|
-
}) : (0, react_jsx_runtime.jsx)("div", {
|
|
2207
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2331
2208
|
className: styles.summaryItemMain,
|
|
2332
|
-
children: (0, react_jsx_runtime.jsxs)("button", {
|
|
2209
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2333
2210
|
type: "button",
|
|
2334
2211
|
className: cx(A.plain, styles.summaryItemButton),
|
|
2335
2212
|
onClick: () => {
|
|
@@ -2337,15 +2214,15 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2337
2214
|
closePop();
|
|
2338
2215
|
},
|
|
2339
2216
|
children: [
|
|
2340
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2217
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2341
2218
|
className: styles.summaryItemText,
|
|
2342
2219
|
children: item.title
|
|
2343
2220
|
}),
|
|
2344
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2345
2222
|
className: styles.summaryItemLabel,
|
|
2346
2223
|
children: t("summary.userComment")
|
|
2347
2224
|
}),
|
|
2348
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2225
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2349
2226
|
className: item.comment ? styles.summaryItemText : `${styles.summaryItemText} ${styles.summaryItemTextEmpty}`,
|
|
2350
2227
|
children: item.comment || t("summary.noComment")
|
|
2351
2228
|
})
|
|
@@ -2357,7 +2234,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2357
2234
|
})
|
|
2358
2235
|
})
|
|
2359
2236
|
}) : null;
|
|
2360
|
-
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2237
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2361
2238
|
ref: rootRef,
|
|
2362
2239
|
className: cx(A.row, styles.summary),
|
|
2363
2240
|
tabIndex: 0,
|
|
@@ -2370,27 +2247,27 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2370
2247
|
onBlur,
|
|
2371
2248
|
onKeyDown,
|
|
2372
2249
|
children: [
|
|
2373
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
2250
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2374
2251
|
className: cx(A.keep, A.gridMid, styles.summaryIcon),
|
|
2375
|
-
"aria-hidden": true,
|
|
2252
|
+
"aria-hidden": "true",
|
|
2376
2253
|
children: String(count)
|
|
2377
2254
|
}),
|
|
2378
|
-
(0, react_jsx_runtime.jsxs)("div", {
|
|
2255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2379
2256
|
className: cx(A.grow, styles.summaryBody),
|
|
2380
|
-
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
2257
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2381
2258
|
className: styles.summaryTitle,
|
|
2382
2259
|
children: t("summary.title", { count })
|
|
2383
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
2260
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2384
2261
|
className: styles.summaryHint,
|
|
2385
2262
|
children: t("summary.hint")
|
|
2386
2263
|
})]
|
|
2387
2264
|
}),
|
|
2388
|
-
onRemoveAll ? (0, react_jsx_runtime.jsx)("button", {
|
|
2265
|
+
onRemoveAll ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2389
2266
|
type: "button",
|
|
2390
2267
|
className: cx(A.plain, A.gridMid, styles.cardClose),
|
|
2391
2268
|
"aria-label": t("summary.removeAll"),
|
|
2392
2269
|
onClick: onRemoveAll,
|
|
2393
|
-
children: (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2270
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2394
2271
|
name: "x",
|
|
2395
2272
|
size: 13
|
|
2396
2273
|
})
|
|
@@ -2432,12 +2309,13 @@ function clearDraftMarker(draft, setDraft) {
|
|
|
2432
2309
|
* composer, or the primary send button) so the composer's submit path carries
|
|
2433
2310
|
* the JSON protocol block.
|
|
2434
2311
|
*/
|
|
2435
|
-
function useSendIntercept({ itemCount, draftRef, setDraft, onFolded }) {
|
|
2312
|
+
function useSendIntercept({ itemCount, draftRef, setDraft, onFolded, foldingRef }) {
|
|
2436
2313
|
(0, react.useEffect)(() => {
|
|
2437
2314
|
if (itemCount === 0) return;
|
|
2438
2315
|
const inject = () => {
|
|
2439
2316
|
const current = getAnnotations();
|
|
2440
2317
|
if (current.length === 0) return;
|
|
2318
|
+
if (foldingRef !== void 0) foldingRef.current = true;
|
|
2441
2319
|
setDraft(composeAnnotatedMessage(draftRef.current, current));
|
|
2442
2320
|
clearAnnotations();
|
|
2443
2321
|
onFolded();
|
|
@@ -2473,7 +2351,8 @@ function useSendIntercept({ itemCount, draftRef, setDraft, onFolded }) {
|
|
|
2473
2351
|
itemCount,
|
|
2474
2352
|
draftRef,
|
|
2475
2353
|
setDraft,
|
|
2476
|
-
onFolded
|
|
2354
|
+
onFolded,
|
|
2355
|
+
foldingRef
|
|
2477
2356
|
]);
|
|
2478
2357
|
}
|
|
2479
2358
|
//#endregion
|
|
@@ -2518,6 +2397,7 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2518
2397
|
const draftRef = (0, react.useRef)(draft);
|
|
2519
2398
|
const actionsRef = (0, react.useRef)(inputActions);
|
|
2520
2399
|
const sessionRef = (0, react.useRef)(sessionId);
|
|
2400
|
+
const foldingRef = (0, react.useRef)(false);
|
|
2521
2401
|
const composing = useComposing();
|
|
2522
2402
|
draftRef.current = draft;
|
|
2523
2403
|
actionsRef.current = inputActions;
|
|
@@ -2563,8 +2443,26 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2563
2443
|
itemCount: items.length,
|
|
2564
2444
|
draftRef,
|
|
2565
2445
|
setDraft,
|
|
2566
|
-
onFolded: () => setItems(EMPTY)
|
|
2446
|
+
onFolded: () => setItems(EMPTY),
|
|
2447
|
+
foldingRef
|
|
2567
2448
|
});
|
|
2449
|
+
(0, react.useLayoutEffect)(() => {
|
|
2450
|
+
if (!draft.includes("<response-annotations>")) {
|
|
2451
|
+
foldingRef.current = false;
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
if (foldingRef.current || composing.current) return;
|
|
2455
|
+
const restored = readComposedDraft(draft);
|
|
2456
|
+
if (restored === null) return;
|
|
2457
|
+
foldingRef.current = true;
|
|
2458
|
+
restoreAnnotations(sessionId, restored.annotations);
|
|
2459
|
+
setDraft(restored.rest);
|
|
2460
|
+
}, [
|
|
2461
|
+
draft,
|
|
2462
|
+
sessionId,
|
|
2463
|
+
setDraft,
|
|
2464
|
+
composing
|
|
2465
|
+
]);
|
|
2568
2466
|
(0, react.useEffect)(() => {
|
|
2569
2467
|
if (composing.current) return;
|
|
2570
2468
|
if (items.length === 0) {
|
|
@@ -2605,10 +2503,10 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2605
2503
|
sync();
|
|
2606
2504
|
}, [sync]);
|
|
2607
2505
|
if (!sessionId || items.length === 0) return null;
|
|
2608
|
-
return (0, react_jsx_runtime.jsx)("div", {
|
|
2506
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2609
2507
|
ref: stackRef,
|
|
2610
2508
|
className: cx(A.col, styles.cardStack),
|
|
2611
|
-
children: (0, react_jsx_runtime.jsx)(AnnotationSummary, {
|
|
2509
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationSummary, {
|
|
2612
2510
|
count: items.length,
|
|
2613
2511
|
items: items.map((item) => ({
|
|
2614
2512
|
id: item.id,
|
|
@@ -2626,9 +2524,138 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2626
2524
|
/** Alias kept for overlay registration. */
|
|
2627
2525
|
const QuoteCard = AnnotationPanel;
|
|
2628
2526
|
//#endregion
|
|
2527
|
+
//#region src/client/dom/floating-layer.ts
|
|
2528
|
+
/**
|
|
2529
|
+
* Mount point for the selection UI: a zero-size host inside the transcript's
|
|
2530
|
+
* real scroll container.
|
|
2531
|
+
*
|
|
2532
|
+
* The host is `position: absolute` in **content coordinates**, so floating UI
|
|
2533
|
+
* scrolls with the selected text and the scroller clips it at its own edges.
|
|
2534
|
+
* That is what keeps the toolbar out of the app header and the composer:
|
|
2535
|
+
* clipping happens in the scroller's stacking context instead of competing
|
|
2536
|
+
* with the header (which is not positioned and therefore loses to any
|
|
2537
|
+
* body-level positioned portal, whatever its z-index).
|
|
2538
|
+
*
|
|
2539
|
+
* Two host layouts exist in the product:
|
|
2540
|
+
* - desktop / overlay composer: `ST7X_W_scrollBody` scrolls; the chat frame is
|
|
2541
|
+
* content-sized and the chat's own `.icaHSq_scroll` is `overflow: visible`;
|
|
2542
|
+
* - regular layout: `.icaHSq_scroll` is the scroller.
|
|
2543
|
+
* Walking up from the selected flow item finds whichever one is live.
|
|
2544
|
+
*/
|
|
2545
|
+
const HOST_ATTR = "data-dsq-layer";
|
|
2546
|
+
/** Below the composer seat (sticky, z-index 7); above in-content stickies (6). */
|
|
2547
|
+
const HOST_Z = 6;
|
|
2548
|
+
const layers = /* @__PURE__ */ new WeakMap();
|
|
2549
|
+
/** flow item → layer, so scrolling does not re-walk the ancestor chain. */
|
|
2550
|
+
const itemLayers = /* @__PURE__ */ new WeakMap();
|
|
2551
|
+
let fallback = null;
|
|
2552
|
+
/**
|
|
2553
|
+
* Top edge of the composer seat, in viewport coordinates.
|
|
2554
|
+
*
|
|
2555
|
+
* In the desktop (overlay) layout the composer is `position: sticky` over the
|
|
2556
|
+
* scroller and stacks above this layer (z 7 vs 6), so the scroller's own box
|
|
2557
|
+
* extends **behind** it. Placement that stops at the scroller box therefore
|
|
2558
|
+
* parks popovers under the input box — the bottom edge must be the composer.
|
|
2559
|
+
* In the regular layout the seat sits below the scroller, so it changes nothing.
|
|
2560
|
+
*/
|
|
2561
|
+
function composerTop() {
|
|
2562
|
+
if (typeof document === "undefined") return null;
|
|
2563
|
+
const seat = document.querySelector("[data-composer-seat]") ?? document.querySelector("[data-composer-card]");
|
|
2564
|
+
if (seat === null) return null;
|
|
2565
|
+
return seat.getBoundingClientRect().top;
|
|
2566
|
+
}
|
|
2567
|
+
function contentCoords(scroller, left, top) {
|
|
2568
|
+
const rect = scroller.getBoundingClientRect();
|
|
2569
|
+
return {
|
|
2570
|
+
left: left - rect.left - scroller.clientLeft + scroller.scrollLeft,
|
|
2571
|
+
top: top - rect.top - scroller.clientTop + scroller.scrollTop
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
function layerFor(scroller) {
|
|
2575
|
+
const cached = layers.get(scroller);
|
|
2576
|
+
if (cached?.host.isConnected) return cached;
|
|
2577
|
+
if (getComputedStyle(scroller).position === "static") scroller.style.position = "relative";
|
|
2578
|
+
let host = scroller.querySelector(`:scope > [${HOST_ATTR}]`);
|
|
2579
|
+
if (!host) {
|
|
2580
|
+
host = document.createElement("div");
|
|
2581
|
+
host.setAttribute(HOST_ATTR, "");
|
|
2582
|
+
host.style.cssText = `position:absolute;left:0;top:0;width:0;height:0;z-index:${HOST_Z};pointer-events:none;`;
|
|
2583
|
+
scroller.appendChild(host);
|
|
2584
|
+
}
|
|
2585
|
+
const layer = {
|
|
2586
|
+
host,
|
|
2587
|
+
scroller,
|
|
2588
|
+
bounds: () => {
|
|
2589
|
+
const rect = scroller.getBoundingClientRect();
|
|
2590
|
+
const top = rect.top + scroller.clientTop;
|
|
2591
|
+
const rawBottom = top + scroller.clientHeight;
|
|
2592
|
+
const cover = composerTop();
|
|
2593
|
+
const bottom = cover !== null && cover > top && cover < rawBottom ? cover : rawBottom;
|
|
2594
|
+
return new DOMRect(rect.left + scroller.clientLeft, top, scroller.clientWidth, Math.max(0, bottom - top));
|
|
2595
|
+
},
|
|
2596
|
+
toContent: (left, top) => contentCoords(scroller, left, top),
|
|
2597
|
+
fromContent: (left, top) => {
|
|
2598
|
+
const rect = scroller.getBoundingClientRect();
|
|
2599
|
+
return {
|
|
2600
|
+
left: left + rect.left + scroller.clientLeft - scroller.scrollLeft,
|
|
2601
|
+
top: top + rect.top + scroller.clientTop - scroller.scrollTop
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
layers.set(scroller, layer);
|
|
2606
|
+
return layer;
|
|
2607
|
+
}
|
|
2608
|
+
/** Fallback for hosts whose transcript has no scrollable ancestor. */
|
|
2609
|
+
function fallbackLayer() {
|
|
2610
|
+
if (fallback?.host.isConnected) return fallback;
|
|
2611
|
+
const host = document.createElement("div");
|
|
2612
|
+
host.setAttribute(HOST_ATTR, "");
|
|
2613
|
+
host.style.cssText = `position:absolute;left:0;top:0;width:0;height:0;z-index:${HOST_Z};pointer-events:none;`;
|
|
2614
|
+
document.body.appendChild(host);
|
|
2615
|
+
fallback = {
|
|
2616
|
+
host,
|
|
2617
|
+
scroller: document.body,
|
|
2618
|
+
bounds: () => {
|
|
2619
|
+
const bottom = composerTop() ?? window.innerHeight;
|
|
2620
|
+
return new DOMRect(0, 0, window.innerWidth, Math.max(0, bottom));
|
|
2621
|
+
},
|
|
2622
|
+
toContent: (left, top) => ({
|
|
2623
|
+
left,
|
|
2624
|
+
top
|
|
2625
|
+
}),
|
|
2626
|
+
fromContent: (left, top) => ({
|
|
2627
|
+
left,
|
|
2628
|
+
top
|
|
2629
|
+
})
|
|
2630
|
+
};
|
|
2631
|
+
return fallback;
|
|
2632
|
+
}
|
|
2633
|
+
/** Layer for a node inside the transcript (falls back to a body host). */
|
|
2634
|
+
function floatingLayerFor(node) {
|
|
2635
|
+
const item = flowItemOf(node);
|
|
2636
|
+
const cached = item ? itemLayers.get(item) : void 0;
|
|
2637
|
+
if (cached?.host.isConnected) return cached;
|
|
2638
|
+
let el = item;
|
|
2639
|
+
while (el && el !== document.body) {
|
|
2640
|
+
const { overflowY } = getComputedStyle(el);
|
|
2641
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") {
|
|
2642
|
+
const layer = layerFor(el);
|
|
2643
|
+
if (item) itemLayers.set(item, layer);
|
|
2644
|
+
return layer;
|
|
2645
|
+
}
|
|
2646
|
+
el = el.parentElement;
|
|
2647
|
+
}
|
|
2648
|
+
const layer = fallbackLayer();
|
|
2649
|
+
if (item) itemLayers.set(item, layer);
|
|
2650
|
+
return layer;
|
|
2651
|
+
}
|
|
2652
|
+
//#endregion
|
|
2629
2653
|
//#region src/client/dom/selection.ts
|
|
2630
2654
|
/** Selection snapshot used by the floating toolbar. */
|
|
2631
2655
|
const TOOLBAR_OFFSET = 12;
|
|
2656
|
+
const BELOW_GAP = 8;
|
|
2657
|
+
/** Estimated toolbar height; only used to decide above vs below at content top. */
|
|
2658
|
+
const TOOLBAR_HEIGHT = 40;
|
|
2632
2659
|
const VIEWPORT_MARGIN = 12;
|
|
2633
2660
|
const LINE_TOP_TOLERANCE = 3;
|
|
2634
2661
|
/**
|
|
@@ -2669,12 +2696,24 @@ function getRangeFirstLineRect(range) {
|
|
|
2669
2696
|
const firstTop = Math.min(...rects.map((rect) => rect.top));
|
|
2670
2697
|
return mergeRects(rects.filter((rect) => Math.abs(rect.top - firstTop) <= LINE_TOP_TOLERANCE));
|
|
2671
2698
|
}
|
|
2672
|
-
|
|
2699
|
+
/** Keep the centered toolbar inside the visible horizontal band. */
|
|
2700
|
+
function anchorX(rect, bounds) {
|
|
2673
2701
|
const center = rect.left + rect.width / 2;
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2702
|
+
const min = bounds.left + VIEWPORT_MARGIN;
|
|
2703
|
+
const max = Math.max(min, bounds.right - VIEWPORT_MARGIN);
|
|
2704
|
+
return Math.min(Math.max(center, min), max);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* True when the selection's first line still touches the viewport.
|
|
2708
|
+
*
|
|
2709
|
+
* The toolbar anchors to that line, so once it has scrolled fully out of
|
|
2710
|
+
* view there is nothing to anchor to and the snapshot reads as "gone" — the
|
|
2711
|
+
* toolbar hides and comes back when the user scrolls to the selection again
|
|
2712
|
+
* (scroll re-reads the selection). A partially visible line still counts:
|
|
2713
|
+
* `toolbarPosition` parks the toolbar at the viewport edge in that case.
|
|
2714
|
+
*/
|
|
2715
|
+
function isFirstLineOnScreen(rect) {
|
|
2716
|
+
return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
|
|
2678
2717
|
}
|
|
2679
2718
|
/**
|
|
2680
2719
|
* Read the live selection when it is a non-empty range inside a conversation
|
|
@@ -2692,13 +2731,24 @@ function readSelectionSnapshot(toolbarRoot) {
|
|
|
2692
2731
|
if (!text) return null;
|
|
2693
2732
|
const rect = getRangeFirstLineRect(selection.getRangeAt(0));
|
|
2694
2733
|
if (!rect) return null;
|
|
2695
|
-
|
|
2696
|
-
const
|
|
2734
|
+
if (!isFirstLineOnScreen(rect)) return null;
|
|
2735
|
+
const layer = floatingLayerFor(anchorNode);
|
|
2736
|
+
const x = anchorX(rect, layer.bounds());
|
|
2737
|
+
const above = layer.toContent(x, rect.top - TOOLBAR_OFFSET);
|
|
2738
|
+
const below = above.top < TOOLBAR_HEIGHT;
|
|
2739
|
+
const anchor = below ? layer.toContent(x, rect.bottom + BELOW_GAP) : above;
|
|
2740
|
+
const range = selection.getRangeAt(0);
|
|
2741
|
+
const union = range.getBoundingClientRect();
|
|
2742
|
+
const selectionBottom = layer.toContent(0, union.height > 0 ? union.bottom : rect.bottom).top;
|
|
2743
|
+
const source = rangeOffsetsInFlowItem(range);
|
|
2697
2744
|
const anchorHref = (anchorNode instanceof Element ? anchorNode : anchorNode.parentElement)?.closest("a[href]")?.getAttribute("href") ?? void 0;
|
|
2698
2745
|
return {
|
|
2699
2746
|
text,
|
|
2700
|
-
left,
|
|
2701
|
-
top,
|
|
2747
|
+
left: anchor.left,
|
|
2748
|
+
top: anchor.top,
|
|
2749
|
+
below,
|
|
2750
|
+
selectionBottom,
|
|
2751
|
+
layer,
|
|
2702
2752
|
...source ? { source } : {},
|
|
2703
2753
|
...anchorHref ? { anchorHref } : {}
|
|
2704
2754
|
};
|
|
@@ -2775,35 +2825,72 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2775
2825
|
const [status, setStatus] = (0, react.useState)(null);
|
|
2776
2826
|
const activeRef = (0, react.useRef)(null);
|
|
2777
2827
|
activeRef.current = active;
|
|
2828
|
+
/** Annotation saved when the comment step opened; the box only edits it. */
|
|
2829
|
+
const pendingIdRef = (0, react.useRef)(null);
|
|
2778
2830
|
(0, react.useEffect)(() => {
|
|
2779
2831
|
ensureToolbarStyles();
|
|
2780
2832
|
if (sessionId) bindAnnotationSession(sessionId);
|
|
2781
2833
|
}, [sessionId]);
|
|
2782
2834
|
const hide = (0, react.useCallback)(() => {
|
|
2835
|
+
pendingIdRef.current = null;
|
|
2783
2836
|
setActive(null);
|
|
2784
2837
|
setPhase("idle");
|
|
2785
2838
|
setComment("");
|
|
2786
2839
|
setStatus(null);
|
|
2787
2840
|
}, []);
|
|
2841
|
+
/**
|
|
2842
|
+
* Keep what was typed when the comment popover goes away without an explicit
|
|
2843
|
+
* cancel. The annotation itself was saved when the step opened, so dropping
|
|
2844
|
+
* the text would also lose the only optional part of the interaction.
|
|
2845
|
+
*/
|
|
2846
|
+
const flushComment = (0, react.useCallback)(() => {
|
|
2847
|
+
const id = pendingIdRef.current;
|
|
2848
|
+
pendingIdRef.current = null;
|
|
2849
|
+
if (!id) return;
|
|
2850
|
+
const typed = commentRef.current?.value ?? "";
|
|
2851
|
+
if (typed.trim()) updateAnnotationComment(void 0, id, typed);
|
|
2852
|
+
}, []);
|
|
2853
|
+
/** Save the current selection without a comment; returns the store id. */
|
|
2854
|
+
const saveSelection = (0, react.useCallback)(() => {
|
|
2855
|
+
const snapshot = activeRef.current;
|
|
2856
|
+
if (!snapshot) return null;
|
|
2857
|
+
return saveAnnotation(void 0, {
|
|
2858
|
+
text: snapshot.text,
|
|
2859
|
+
sources: snapshot.source ? [{
|
|
2860
|
+
text: snapshot.text,
|
|
2861
|
+
messageId: snapshot.source.messageId,
|
|
2862
|
+
messageKind: snapshot.source.messageKind,
|
|
2863
|
+
startOffset: snapshot.source.startOffset,
|
|
2864
|
+
endOffset: snapshot.source.endOffset
|
|
2865
|
+
}] : void 0
|
|
2866
|
+
}).id;
|
|
2867
|
+
}, []);
|
|
2788
2868
|
const refresh = (0, react.useCallback)(() => {
|
|
2789
2869
|
const next = readSelectionSnapshot(toolbarRef.current);
|
|
2790
2870
|
if (!next) {
|
|
2791
2871
|
if (phaseRef.current === "idle") setActive(null);
|
|
2792
2872
|
return;
|
|
2793
2873
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}, []);
|
|
2874
|
+
const current = activeRef.current;
|
|
2875
|
+
if (phaseRef.current === "comment" && current && current.text !== next.text) {
|
|
2876
|
+
flushComment();
|
|
2877
|
+
setPhase("idle");
|
|
2878
|
+
setComment("");
|
|
2879
|
+
}
|
|
2880
|
+
setActive((prev) => prev && prev.text === next.text && prev.left === next.left && prev.top === next.top && prev.below === next.below && prev.layer === next.layer ? prev : next);
|
|
2881
|
+
}, [flushComment]);
|
|
2803
2882
|
const phaseRef = (0, react.useRef)(phase);
|
|
2804
2883
|
phaseRef.current = phase;
|
|
2805
2884
|
(0, react.useEffect)(() => {
|
|
2806
|
-
|
|
2885
|
+
let scheduled = false;
|
|
2886
|
+
const schedule = () => {
|
|
2887
|
+
if (scheduled) return;
|
|
2888
|
+
scheduled = true;
|
|
2889
|
+
window.setTimeout(() => {
|
|
2890
|
+
scheduled = false;
|
|
2891
|
+
refresh();
|
|
2892
|
+
}, 0);
|
|
2893
|
+
};
|
|
2807
2894
|
let dragging = false;
|
|
2808
2895
|
const onPointerDown = (event) => {
|
|
2809
2896
|
const target = event.target;
|
|
@@ -2813,6 +2900,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2813
2900
|
if (inToolbar || inComment) return;
|
|
2814
2901
|
dragging = true;
|
|
2815
2902
|
if (phaseRef.current === "comment") {
|
|
2903
|
+
flushComment();
|
|
2816
2904
|
hide();
|
|
2817
2905
|
return;
|
|
2818
2906
|
}
|
|
@@ -2834,7 +2922,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2834
2922
|
const onScroll = (event) => {
|
|
2835
2923
|
const target = event.target;
|
|
2836
2924
|
if (target instanceof Node && (toolbarRef.current?.contains(target) === true || commentWrapRef.current?.contains(target) === true)) return;
|
|
2837
|
-
|
|
2925
|
+
schedule();
|
|
2838
2926
|
};
|
|
2839
2927
|
document.addEventListener("pointerdown", onPointerDown, true);
|
|
2840
2928
|
document.addEventListener("pointerup", onPointerUp, true);
|
|
@@ -2850,12 +2938,27 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2850
2938
|
window.removeEventListener("resize", schedule);
|
|
2851
2939
|
document.removeEventListener("scroll", onScroll, true);
|
|
2852
2940
|
};
|
|
2853
|
-
}, [
|
|
2941
|
+
}, [
|
|
2942
|
+
flushComment,
|
|
2943
|
+
hide,
|
|
2944
|
+
refresh
|
|
2945
|
+
]);
|
|
2854
2946
|
const placeCommentPop = (0, react.useCallback)(() => {
|
|
2947
|
+
const snapshot = activeRef.current;
|
|
2948
|
+
const layer = snapshot?.layer;
|
|
2855
2949
|
const toolbar = toolbarRef.current;
|
|
2856
2950
|
const pop = commentWrapRef.current;
|
|
2857
|
-
if (!toolbar || !pop) return;
|
|
2858
|
-
const
|
|
2951
|
+
if (!snapshot || !layer || !toolbar || !pop) return;
|
|
2952
|
+
const bounds = layer.bounds();
|
|
2953
|
+
const toolbarW = toolbar.offsetWidth;
|
|
2954
|
+
const toolbarH = toolbar.offsetHeight;
|
|
2955
|
+
const home = layer.fromContent(snapshot.left, snapshot.top);
|
|
2956
|
+
const anchor = {
|
|
2957
|
+
left: home.left - toolbarW / 2,
|
|
2958
|
+
top: home.top - (snapshot.below ? 0 : toolbarH),
|
|
2959
|
+
width: toolbarW,
|
|
2960
|
+
height: toolbarH
|
|
2961
|
+
};
|
|
2859
2962
|
pop.style.display = "flex";
|
|
2860
2963
|
pop.style.visibility = "hidden";
|
|
2861
2964
|
const panel = {
|
|
@@ -2863,24 +2966,45 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2863
2966
|
height: pop.offsetHeight || 180
|
|
2864
2967
|
};
|
|
2865
2968
|
pop.style.visibility = "";
|
|
2866
|
-
const
|
|
2969
|
+
const gap = 8;
|
|
2970
|
+
const margin = 8;
|
|
2971
|
+
const belowTop = layer.fromContent(0, snapshot.selectionBottom).top + gap;
|
|
2972
|
+
const aboveTop = anchor.top - panel.height - gap;
|
|
2973
|
+
const top = belowTop <= bounds.bottom - margin - panel.height ? belowTop : aboveTop >= bounds.top + margin ? aboveTop : Math.max(bounds.top + margin, Math.min(belowTop, bounds.bottom - margin - panel.height));
|
|
2867
2974
|
const half = panel.width / 2 + 8;
|
|
2868
|
-
const
|
|
2869
|
-
|
|
2870
|
-
|
|
2975
|
+
const min = bounds.left + half;
|
|
2976
|
+
const center = Math.min(Math.max(anchor.left + anchor.width / 2, min), Math.max(min, bounds.right - half));
|
|
2977
|
+
const at = layer.toContent(center, top);
|
|
2978
|
+
pop.style.top = `${at.top}px`;
|
|
2979
|
+
pop.style.left = `${at.left}px`;
|
|
2980
|
+
const sideGap = 6;
|
|
2981
|
+
const popLeftView = layer.fromContent(at.left, at.top).left - panel.width / 2;
|
|
2982
|
+
const popTopView = top;
|
|
2983
|
+
let leftView = popLeftView;
|
|
2984
|
+
let topView = popTopView - sideGap - toolbarH;
|
|
2985
|
+
if (topView < bounds.top + margin) topView = popTopView + panel.height + sideGap;
|
|
2986
|
+
leftView = Math.max(bounds.left + margin, Math.min(leftView, bounds.right - margin - toolbarW));
|
|
2987
|
+
topView = Math.max(bounds.top + margin, Math.min(topView, bounds.bottom - margin - toolbarH));
|
|
2988
|
+
const toolbarAt = layer.toContent(leftView, topView);
|
|
2989
|
+
toolbar.style.left = `${toolbarAt.left}px`;
|
|
2990
|
+
toolbar.style.top = `${toolbarAt.top}px`;
|
|
2871
2991
|
}, []);
|
|
2992
|
+
(0, react.useLayoutEffect)(() => {
|
|
2993
|
+
if (phase === "comment") placeCommentPop();
|
|
2994
|
+
}, [
|
|
2995
|
+
active,
|
|
2996
|
+
phase,
|
|
2997
|
+
placeCommentPop
|
|
2998
|
+
]);
|
|
2872
2999
|
(0, react.useEffect)(() => {
|
|
2873
|
-
if (phase === "comment")
|
|
2874
|
-
|
|
2875
|
-
commentRef.current?.focus();
|
|
2876
|
-
}
|
|
2877
|
-
}, [phase, placeCommentPop]);
|
|
3000
|
+
if (phase === "comment") commentRef.current?.focus();
|
|
3001
|
+
}, [phase]);
|
|
2878
3002
|
(0, react.useEffect)(() => {
|
|
2879
3003
|
const onResize = () => placeCommentPop();
|
|
2880
3004
|
window.addEventListener("resize", onResize);
|
|
2881
3005
|
return () => window.removeEventListener("resize", onResize);
|
|
2882
3006
|
}, [placeCommentPop]);
|
|
2883
|
-
(0, react.
|
|
3007
|
+
(0, react.useLayoutEffect)(() => {
|
|
2884
3008
|
if (phase !== "comment") return;
|
|
2885
3009
|
const area = commentRef.current;
|
|
2886
3010
|
if (!area) return;
|
|
@@ -2907,12 +3031,18 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2907
3031
|
setStatus(t("toolbar.copyFailed"));
|
|
2908
3032
|
}
|
|
2909
3033
|
}, [hide]);
|
|
3034
|
+
/**
|
|
3035
|
+
* "Add to task" is the commit: the quote is saved immediately, with or
|
|
3036
|
+
* without a comment. The popover only edits that saved entry, so dismissing
|
|
3037
|
+
* it (outside click, new selection) can never undo the annotation.
|
|
3038
|
+
*/
|
|
2910
3039
|
const openComment = (0, react.useCallback)(() => {
|
|
2911
3040
|
if (!activeRef.current) return;
|
|
3041
|
+
pendingIdRef.current = saveSelection();
|
|
2912
3042
|
setPhase("comment");
|
|
2913
3043
|
setComment("");
|
|
2914
3044
|
setStatus(null);
|
|
2915
|
-
}, []);
|
|
3045
|
+
}, [saveSelection]);
|
|
2916
3046
|
/**
|
|
2917
3047
|
* Hand the selected link to the system browser.
|
|
2918
3048
|
*
|
|
@@ -2930,89 +3060,75 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2930
3060
|
clearNativeSelection();
|
|
2931
3061
|
hide();
|
|
2932
3062
|
}, [hide]);
|
|
2933
|
-
/**
|
|
2934
|
-
const
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
hide();
|
|
2938
|
-
return;
|
|
2939
|
-
}
|
|
2940
|
-
saveAnnotation(void 0, {
|
|
2941
|
-
text: snapshot.text,
|
|
2942
|
-
...body && body.trim() ? { comment: body.trim() } : {},
|
|
2943
|
-
sources: snapshot.source ? [{
|
|
2944
|
-
text: snapshot.text,
|
|
2945
|
-
messageId: snapshot.source.messageId,
|
|
2946
|
-
messageKind: snapshot.source.messageKind,
|
|
2947
|
-
startOffset: snapshot.source.startOffset,
|
|
2948
|
-
endOffset: snapshot.source.endOffset
|
|
2949
|
-
}] : void 0
|
|
2950
|
-
});
|
|
2951
|
-
getAnnotations().length;
|
|
3063
|
+
/** Close the optional comment step; the annotation stays saved either way. */
|
|
3064
|
+
const finishComment = (0, react.useCallback)((keepComment) => {
|
|
3065
|
+
if (keepComment) flushComment();
|
|
3066
|
+
else pendingIdRef.current = null;
|
|
2952
3067
|
clearNativeSelection();
|
|
2953
3068
|
hide();
|
|
2954
|
-
setStatus(null);
|
|
2955
3069
|
window.setTimeout(focusComposer, 16);
|
|
2956
|
-
}, [hide]);
|
|
3070
|
+
}, [flushComment, hide]);
|
|
2957
3071
|
if (!active) return null;
|
|
2958
3072
|
const toolbarStyle = {
|
|
2959
3073
|
left: active.left,
|
|
2960
3074
|
top: active.top
|
|
2961
3075
|
};
|
|
2962
3076
|
const link = linkTarget(active.text, active.anchorHref);
|
|
2963
|
-
return (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(
|
|
2964
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
3077
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2965
3079
|
ref: toolbarRef,
|
|
2966
3080
|
className: cx(A.row, styles.toolbar),
|
|
2967
|
-
style: toolbarStyle,
|
|
3081
|
+
style: phase === "comment" ? void 0 : toolbarStyle,
|
|
2968
3082
|
role: "toolbar",
|
|
2969
3083
|
"aria-label": t("toolbar.aria"),
|
|
2970
3084
|
"data-dsq-toolbar": "true",
|
|
3085
|
+
"data-anchor": phase === "comment" ? "popover-top" : active.below ? "below" : "above",
|
|
2971
3086
|
onPointerDown: handleToolbarPointerDown,
|
|
2972
|
-
children: phase === "comment" ? (0, react_jsx_runtime.jsx)("div", {
|
|
3087
|
+
children: phase === "comment" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2973
3088
|
className: cx(A.inlineRow, styles.toolbarInner),
|
|
2974
|
-
children: (0, react_jsx_runtime.jsx)("span", {
|
|
3089
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2975
3090
|
style: {
|
|
2976
3091
|
padding: "0 8px",
|
|
2977
3092
|
fontSize: 13,
|
|
2978
|
-
opacity: .8
|
|
3093
|
+
opacity: .8,
|
|
3094
|
+
whiteSpace: "nowrap"
|
|
2979
3095
|
},
|
|
2980
3096
|
children: t("toolbar.commentHint")
|
|
2981
3097
|
})
|
|
2982
|
-
}) : (0, react_jsx_runtime.jsxs)("div", {
|
|
3098
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2983
3099
|
className: cx(A.inlineRow, styles.toolbarInner),
|
|
2984
3100
|
children: [
|
|
2985
|
-
link ? (0, react_jsx_runtime.
|
|
3101
|
+
link ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2986
3102
|
type: "button",
|
|
2987
3103
|
className: cx(A.plain, A.inlineRow, styles.button),
|
|
2988
3104
|
onClick: handleOpenLink,
|
|
2989
|
-
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
3105
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2990
3106
|
name: "external-link",
|
|
2991
3107
|
size: 14
|
|
2992
3108
|
}), t("toolbar.openLink")]
|
|
2993
3109
|
}) : null,
|
|
2994
|
-
link ? (0, react_jsx_runtime.jsx)("span", {
|
|
3110
|
+
link ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2995
3111
|
className: styles.divider,
|
|
2996
|
-
"aria-hidden": true
|
|
3112
|
+
"aria-hidden": "true"
|
|
2997
3113
|
}) : null,
|
|
2998
|
-
(0, react_jsx_runtime.
|
|
3114
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2999
3115
|
type: "button",
|
|
3000
3116
|
className: cx(A.plain, A.inlineRow, styles.button),
|
|
3001
3117
|
onClick: () => void handleCopy(),
|
|
3002
|
-
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
3118
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
3003
3119
|
name: "copy",
|
|
3004
3120
|
size: 14
|
|
3005
3121
|
}), t("toolbar.copy")]
|
|
3006
3122
|
}),
|
|
3007
|
-
(0, react_jsx_runtime.jsx)("span", {
|
|
3123
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3008
3124
|
className: styles.divider,
|
|
3009
|
-
"aria-hidden": true
|
|
3125
|
+
"aria-hidden": "true"
|
|
3010
3126
|
}),
|
|
3011
|
-
(0, react_jsx_runtime.
|
|
3127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3012
3128
|
type: "button",
|
|
3013
3129
|
className: cx(A.plain, A.inlineRow, styles.button),
|
|
3014
3130
|
onClick: openComment,
|
|
3015
|
-
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
3131
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
3016
3132
|
name: "plus",
|
|
3017
3133
|
size: 14
|
|
3018
3134
|
}), t("toolbar.addToTask")]
|
|
@@ -3020,14 +3136,13 @@ function SelectionToolbar({ sessionId }) {
|
|
|
3020
3136
|
]
|
|
3021
3137
|
})
|
|
3022
3138
|
}),
|
|
3023
|
-
phase === "comment" ? (0, react_jsx_runtime.jsxs)("div", {
|
|
3139
|
+
phase === "comment" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3024
3140
|
ref: commentWrapRef,
|
|
3025
3141
|
className: cx(A.col, styles.commentPop),
|
|
3026
|
-
style: void 0,
|
|
3027
3142
|
role: "dialog",
|
|
3028
3143
|
"aria-label": t("toolbar.commentDialogAria"),
|
|
3029
3144
|
onPointerDown: handleToolbarPointerDown,
|
|
3030
|
-
children: [(0, react_jsx_runtime.jsx)("textarea", {
|
|
3145
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
3031
3146
|
ref: commentRef,
|
|
3032
3147
|
className: cx(A.box, styles.commentInput),
|
|
3033
3148
|
placeholder: t("common.commentPlaceholder"),
|
|
@@ -3036,37 +3151,38 @@ function SelectionToolbar({ sessionId }) {
|
|
|
3036
3151
|
onKeyDown: (e) => {
|
|
3037
3152
|
if (e.key === "Escape") {
|
|
3038
3153
|
e.preventDefault();
|
|
3154
|
+
pendingIdRef.current = null;
|
|
3039
3155
|
hide();
|
|
3040
3156
|
return;
|
|
3041
3157
|
}
|
|
3042
3158
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
3043
3159
|
e.preventDefault();
|
|
3044
|
-
|
|
3160
|
+
finishComment(true);
|
|
3045
3161
|
}
|
|
3046
3162
|
}
|
|
3047
|
-
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
3163
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3048
3164
|
className: styles.commentActions,
|
|
3049
|
-
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
3165
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3050
3166
|
type: "button",
|
|
3051
3167
|
className: cx(A.plain, A.inlineRow, styles.button),
|
|
3052
|
-
onClick: () =>
|
|
3168
|
+
onClick: () => finishComment(false),
|
|
3053
3169
|
children: t("toolbar.skipComment")
|
|
3054
|
-
}), (0, react_jsx_runtime.jsx)("button", {
|
|
3170
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3055
3171
|
type: "button",
|
|
3056
3172
|
className: cx(A.plain, A.inlineRow, styles.button, styles.buttonAddComment),
|
|
3057
3173
|
disabled: !comment.trim(),
|
|
3058
|
-
onClick: () =>
|
|
3174
|
+
onClick: () => finishComment(true),
|
|
3059
3175
|
children: t("toolbar.addComment")
|
|
3060
3176
|
})]
|
|
3061
3177
|
})]
|
|
3062
3178
|
}) : null,
|
|
3063
|
-
status ? (0, react_jsx_runtime.jsx)("div", {
|
|
3179
|
+
status ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3064
3180
|
className: styles.status,
|
|
3065
3181
|
style: toolbarStyle,
|
|
3066
3182
|
role: "status",
|
|
3067
3183
|
children: status
|
|
3068
3184
|
}) : null
|
|
3069
|
-
] }),
|
|
3185
|
+
] }), active.layer.host);
|
|
3070
3186
|
}
|
|
3071
3187
|
//#endregion
|
|
3072
3188
|
//#region src/client/dom/decorateAnnotationDirectives.ts
|
|
@@ -3161,16 +3277,82 @@ function watchAnnotationDirectives(root) {
|
|
|
3161
3277
|
return () => observer.disconnect();
|
|
3162
3278
|
}
|
|
3163
3279
|
//#endregion
|
|
3280
|
+
//#region src/client/dom/stripSubmissionEcho.ts
|
|
3281
|
+
/**
|
|
3282
|
+
* Strip the annotation protocol from the host's local submission echo.
|
|
3283
|
+
*
|
|
3284
|
+
* On submit the host paints an optimistic bubble (`[data-submission-echo]`)
|
|
3285
|
+
* from the composer draft — which, at that instant, is the protocol block the
|
|
3286
|
+
* send gesture just folded in. That echo is rendered by the chat flow itself,
|
|
3287
|
+
* not by a `conversation.chat.node` registration, so `UserMessageDisplay`
|
|
3288
|
+
* never sees it; with a slow model the raw JSON sits on screen until the
|
|
3289
|
+
* durable `user/message` event arrives.
|
|
3290
|
+
*
|
|
3291
|
+
* The durable message keeps the block (the model needs it). This is display
|
|
3292
|
+
* only: the echo's plain text run is rewritten to the user's own text. Any
|
|
3293
|
+
* other DOM shape (text split by session chips, a truncated block) is left
|
|
3294
|
+
* untouched rather than rebuilt, so nothing but our own protocol is edited.
|
|
3295
|
+
*/
|
|
3296
|
+
const ECHO_SELECTOR = "[data-submission-echo]";
|
|
3297
|
+
const STRIPPED_ATTR = "data-dsq-echo-stripped";
|
|
3298
|
+
/** The deepest run that carries both markers, or null. */
|
|
3299
|
+
function protocolRun(row) {
|
|
3300
|
+
for (const el of row.querySelectorAll("*")) {
|
|
3301
|
+
if (el.children.length > 0) continue;
|
|
3302
|
+
const text = el.textContent ?? "";
|
|
3303
|
+
if (text.includes("<response-annotations>") && text.includes("</response-annotations>")) return el;
|
|
3304
|
+
}
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
function strip(row) {
|
|
3308
|
+
if (row.hasAttribute(STRIPPED_ATTR)) return;
|
|
3309
|
+
const run = protocolRun(row);
|
|
3310
|
+
if (run === null) return;
|
|
3311
|
+
const scan = scanAnnotatedMessage(run.textContent ?? "");
|
|
3312
|
+
if (scan.annotations.length === 0) return;
|
|
3313
|
+
run.textContent = scan.rest;
|
|
3314
|
+
row.setAttribute(STRIPPED_ATTR, "");
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Watch for local submission echoes and rewrite the protocol block out of
|
|
3318
|
+
* them. Runs once immediately, then on every DOM insertion.
|
|
3319
|
+
*/
|
|
3320
|
+
function watchSubmissionEcho(root) {
|
|
3321
|
+
const scope = root ?? (typeof document === "undefined" ? void 0 : document.body);
|
|
3322
|
+
if (!scope) return () => {};
|
|
3323
|
+
const sweep = () => {
|
|
3324
|
+
for (const row of scope.querySelectorAll(ECHO_SELECTOR)) strip(row);
|
|
3325
|
+
};
|
|
3326
|
+
sweep();
|
|
3327
|
+
if (typeof MutationObserver === "undefined") return () => {};
|
|
3328
|
+
let frame = 0;
|
|
3329
|
+
const observer = new MutationObserver(() => {
|
|
3330
|
+
if (frame !== 0) return;
|
|
3331
|
+
frame = window.requestAnimationFrame(() => {
|
|
3332
|
+
frame = 0;
|
|
3333
|
+
sweep();
|
|
3334
|
+
});
|
|
3335
|
+
});
|
|
3336
|
+
observer.observe(scope, {
|
|
3337
|
+
childList: true,
|
|
3338
|
+
subtree: true
|
|
3339
|
+
});
|
|
3340
|
+
return () => {
|
|
3341
|
+
observer.disconnect();
|
|
3342
|
+
if (frame !== 0) window.cancelAnimationFrame(frame);
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
//#endregion
|
|
3164
3346
|
//#region src/client/transcript/TranscriptQuoteCard.tsx
|
|
3165
3347
|
/**
|
|
3166
|
-
* History-side
|
|
3348
|
+
* History-side summary for one user message's annotations.
|
|
3167
3349
|
*
|
|
3168
|
-
* One
|
|
3169
|
-
*
|
|
3170
|
-
*
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
* refers to.
|
|
3350
|
+
* One block per message, not one card per annotation: the block carries the
|
|
3351
|
+
* count and the hover list shows every annotation with its selected text and
|
|
3352
|
+
* comment. That is the same shape the composer uses for pending selections, so
|
|
3353
|
+
* a message reads the same before and after it is sent. Clicking an item
|
|
3354
|
+
* scrolls back to the marked text in the source message; the ordinal badge
|
|
3355
|
+
* matches the `:dsh-annotation{index="N"}` the model refers to.
|
|
3174
3356
|
*/
|
|
3175
3357
|
function TranscriptQuoteCard({ node }) {
|
|
3176
3358
|
ensureToolbarStyles();
|
|
@@ -3192,94 +3374,126 @@ function TranscriptQuoteCard({ node }) {
|
|
|
3192
3374
|
const ownerKey = stackRef.current?.closest("[data-chat-flow-key]")?.getAttribute("data-chat-flow-key");
|
|
3193
3375
|
return ownerKey ? `dqsm_${ownerKey}_${index}` : void 0;
|
|
3194
3376
|
}, []);
|
|
3195
|
-
const onSelect = (0, react.useCallback)((index,
|
|
3196
|
-
|
|
3197
|
-
|
|
3377
|
+
const onSelect = (0, react.useCallback)((index, _item) => {
|
|
3378
|
+
const quote = quotes[index];
|
|
3379
|
+
if (quote === void 0) return;
|
|
3380
|
+
scrollToAnnotation(index + 1, quote.text, markIdOf(index));
|
|
3381
|
+
}, [markIdOf, quotes]);
|
|
3198
3382
|
if (quotes.length === 0) return null;
|
|
3199
|
-
return (0, react_jsx_runtime.jsx)("div", {
|
|
3383
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3200
3384
|
ref: stackRef,
|
|
3201
|
-
className: styles.
|
|
3385
|
+
className: styles.tSummary,
|
|
3202
3386
|
"data-dsq-deco": "true",
|
|
3203
|
-
children:
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
"aria-label": quote.title,
|
|
3208
|
-
onClick: () => onSelect(index, quote.text),
|
|
3209
|
-
onKeyDown: (event) => {
|
|
3210
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
3211
|
-
event.preventDefault();
|
|
3212
|
-
onSelect(index, quote.text);
|
|
3213
|
-
},
|
|
3214
|
-
children: (0, react_jsx_runtime.jsx)(AnnotationCardView, {
|
|
3215
|
-
variant: "transcript",
|
|
3387
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationSummary, {
|
|
3388
|
+
count: quotes.length,
|
|
3389
|
+
items: quotes.map((quote, index) => ({
|
|
3390
|
+
id: `dqsm-${index}`,
|
|
3216
3391
|
title: quote.title,
|
|
3217
3392
|
text: quote.text,
|
|
3218
|
-
comment: quote.comment
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
}
|
|
3393
|
+
...quote.comment ? { comment: quote.comment } : {}
|
|
3394
|
+
})),
|
|
3395
|
+
onItemSelect: onSelect
|
|
3396
|
+
})
|
|
3222
3397
|
});
|
|
3223
3398
|
}
|
|
3224
3399
|
//#endregion
|
|
3225
3400
|
//#region src/client/transcript/UserMessageDisplay.tsx
|
|
3226
|
-
function
|
|
3227
|
-
if (
|
|
3401
|
+
function isTextBlock(block) {
|
|
3402
|
+
if (block === null || typeof block !== "object") return false;
|
|
3403
|
+
const candidate = block;
|
|
3404
|
+
if ((candidate.type === "image" || candidate.type === "file") && candidate.attachment !== void 0) return false;
|
|
3405
|
+
return candidate.type === "text" || typeof candidate.text === "string";
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Replace all text blocks with one stripped text block, keeping attachments and
|
|
3409
|
+
* unknown blocks in place. The host's `contentParts` joins text blocks and
|
|
3410
|
+
* lifts attachments out anyway, so one text block is the same message content
|
|
3411
|
+
* with the protocol block gone.
|
|
3412
|
+
*/
|
|
3413
|
+
function strippedContent(raw, text) {
|
|
3414
|
+
const rest = Array.isArray(raw) ? raw.filter((block) => !isTextBlock(block)) : [];
|
|
3415
|
+
if (text) rest.unshift({
|
|
3416
|
+
type: "text",
|
|
3417
|
+
text
|
|
3418
|
+
});
|
|
3419
|
+
return rest;
|
|
3420
|
+
}
|
|
3421
|
+
/** Image attachments for the factory-less fallback bubble. */
|
|
3422
|
+
function imageAttachments(raw) {
|
|
3423
|
+
if (!Array.isArray(raw)) return [];
|
|
3228
3424
|
const images = [];
|
|
3229
|
-
for (const block of
|
|
3425
|
+
for (const block of raw) {
|
|
3230
3426
|
if (block === null || typeof block !== "object") continue;
|
|
3231
3427
|
const candidate = block;
|
|
3232
|
-
if (candidate.type
|
|
3233
|
-
images.push({ attachment: candidate.attachment });
|
|
3428
|
+
if (candidate.type === "image" && candidate.attachment !== void 0) images.push(candidate.attachment);
|
|
3234
3429
|
}
|
|
3235
3430
|
return images;
|
|
3236
3431
|
}
|
|
3237
3432
|
/**
|
|
3238
3433
|
* Replacement for the built-in `user` Chat node view.
|
|
3239
3434
|
*
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3435
|
+
* It does **not** redraw the bubble or its actions row. It resolves the host's
|
|
3436
|
+
* own `UserMessageNodeView` from `ctx.slots.entries('conversation.chat.node')`
|
|
3437
|
+
* and renders that component with a rewritten `node`: the annotation protocol
|
|
3438
|
+
* block is removed from the text blocks, everything else (attachments, file
|
|
3439
|
+
* cards, reference labels, time, copy, and any plugin that portals into the
|
|
3440
|
+
* actions row, e.g. dsh-rewind's ↶) stays host-owned and follows host changes.
|
|
3441
|
+
* The annotation cards are the only extra DOM, prepended above the host row.
|
|
3243
3442
|
*
|
|
3244
|
-
*
|
|
3245
|
-
* `conversation.chat.node` is gated by the shipped
|
|
3246
|
-
* allowlist (system-prompt, user, steering,
|
|
3247
|
-
* turn-error, turn-max-tokens, turn-tail)
|
|
3248
|
-
*
|
|
3249
|
-
* process disclosure of a completed turn — mounted, then invisible. `user` is on
|
|
3250
|
-
* the list, so rendering the cards inside this component is the only placement
|
|
3251
|
-
* that survives turn folding.
|
|
3443
|
+
* Cards must live inside the `user` node view rather than on a
|
|
3444
|
+
* `select-quote` Chat node: `conversation.chat.node` is gated by the shipped
|
|
3445
|
+
* `TURN_PROCESS_INDEPENDENT_KINDS` allowlist (system-prompt, user, steering,
|
|
3446
|
+
* turn-trigger, turn-process, turn-error, turn-max-tokens, turn-tail), and any
|
|
3447
|
+
* other kind is `hidden` when the completed turn's process disclosure folds.
|
|
3252
3448
|
*/
|
|
3253
|
-
function UserMessageDisplay(
|
|
3449
|
+
function UserMessageDisplay(props) {
|
|
3254
3450
|
ensureToolbarStyles();
|
|
3451
|
+
const { node, hostUserOf, ...rest } = props;
|
|
3255
3452
|
const raw = contentBlocksToText(node.data?.content);
|
|
3256
3453
|
const scanned = scanAnnotatedMessage(raw);
|
|
3257
3454
|
const text = scanned.annotations.length > 0 ? scanned.rest : displayTextWithoutQuote(raw).trim();
|
|
3258
3455
|
const quotes = scanned.annotations;
|
|
3259
|
-
const
|
|
3260
|
-
|
|
3261
|
-
|
|
3456
|
+
const rewritten = quotes.length > 0 || text !== raw.trim();
|
|
3457
|
+
const displayNode = (0, react.useMemo)(() => rewritten ? {
|
|
3458
|
+
...node,
|
|
3459
|
+
data: {
|
|
3460
|
+
...node.data,
|
|
3461
|
+
content: strippedContent(node.data?.content, text)
|
|
3462
|
+
}
|
|
3463
|
+
} : node, [
|
|
3464
|
+
node,
|
|
3465
|
+
rewritten,
|
|
3466
|
+
text
|
|
3467
|
+
]);
|
|
3468
|
+
const cards = quotes.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }) : null;
|
|
3469
|
+
const Host = hostUserOf?.();
|
|
3470
|
+
if (Host !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [cards, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Host, {
|
|
3471
|
+
...rest,
|
|
3472
|
+
node: displayNode
|
|
3473
|
+
})] });
|
|
3474
|
+
const images = imageAttachments(node.data?.content);
|
|
3475
|
+
const loadImage = props.loadImage;
|
|
3476
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [cards, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3262
3477
|
className: cx(A.colEnd, styles.userRow),
|
|
3263
|
-
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
3478
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3264
3479
|
className: cx(A.colEnd, styles.userStack),
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
]
|
|
3480
|
+
"data-dsq-fallback-content": "true",
|
|
3481
|
+
children: [images.length > 0 && loadImage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3482
|
+
className: cx(A.colEnd, styles.userImages),
|
|
3483
|
+
"data-message-attachments": true,
|
|
3484
|
+
children: images.map((attachment, index) => {
|
|
3485
|
+
const url = loadImage(attachment);
|
|
3486
|
+
return url === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3487
|
+
src: url,
|
|
3488
|
+
alt: ""
|
|
3489
|
+
}, `image-${index}`);
|
|
3490
|
+
})
|
|
3491
|
+
}) : null, text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3492
|
+
className: styles.userBubble,
|
|
3493
|
+
children: text
|
|
3494
|
+
}) : null]
|
|
3281
3495
|
})
|
|
3282
|
-
});
|
|
3496
|
+
})] });
|
|
3283
3497
|
}
|
|
3284
3498
|
//#endregion
|
|
3285
3499
|
//#region src/client/state/annotation-persistence.ts
|
|
@@ -3473,6 +3687,7 @@ function apply(ctx) {
|
|
|
3473
3687
|
ctx.effect(() => subscribeLocale(() => refreshDirectiveChips()), "dsh-select-quote: chip language");
|
|
3474
3688
|
ctx.effect(() => watchAnnotationDirectives(), "dsh-select-quote: annotation directives");
|
|
3475
3689
|
ctx.effect(() => watchAnnotationMarks(), "dsh-select-quote: annotation marks");
|
|
3690
|
+
ctx.effect(() => watchSubmissionEcho(), "dsh-select-quote: submission echo");
|
|
3476
3691
|
ctx.effect(() => {
|
|
3477
3692
|
syncPendingAnnotationJobs(getAnnotations());
|
|
3478
3693
|
return subscribeAnnotations(() => syncPendingAnnotationJobs(getAnnotations()));
|
|
@@ -3504,14 +3719,28 @@ function apply(ctx) {
|
|
|
3504
3719
|
order: 1
|
|
3505
3720
|
}, SessionBridge);
|
|
3506
3721
|
});
|
|
3722
|
+
const hostUserOf = () => {
|
|
3723
|
+
const candidates = (typeof ctx.slots.entries === "function" ? ctx.slots.entries("conversation.chat.node") : []).filter((entry) => entry.options?.key === "user" && entry.options?.priority !== -10);
|
|
3724
|
+
return (candidates.find((entry) => componentName(entry.component) === "UserMessageNodeView") ?? candidates[0])?.component;
|
|
3725
|
+
};
|
|
3726
|
+
const UserNode = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMessageDisplay, {
|
|
3727
|
+
...props,
|
|
3728
|
+
hostUserOf
|
|
3729
|
+
});
|
|
3507
3730
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
3508
3731
|
ctx.slots.register({
|
|
3509
3732
|
name: "conversation.chat.node",
|
|
3510
3733
|
key: "user",
|
|
3511
|
-
priority: -10
|
|
3512
|
-
|
|
3734
|
+
priority: -10,
|
|
3735
|
+
locale: "chat"
|
|
3736
|
+
}, UserNode);
|
|
3513
3737
|
});
|
|
3514
3738
|
}
|
|
3739
|
+
/** Component name across memo/forwardRef/plain function shapes. */
|
|
3740
|
+
function componentName(component) {
|
|
3741
|
+
const candidate = component;
|
|
3742
|
+
return candidate?.type?.name ?? candidate?.name ?? candidate?.displayName;
|
|
3743
|
+
}
|
|
3515
3744
|
/** Invisible session-scoped bridge that publishes sessionId for the root toolbar. */
|
|
3516
3745
|
function SessionBridge({ sessionId }) {
|
|
3517
3746
|
bindAnnotationSession(sessionId);
|