dsh-select-quote 0.3.7 → 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/lib/client.js +366 -365
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -223,8 +223,8 @@ ${atomRules}
|
|
|
223
223
|
内容里吸顶元素的 6(同 z 时 host 追加在滚动容器末尾,DOM 次序压过它们)。
|
|
224
224
|
host 内部次序:status 3 > toolbar 2 > commentPop 1。
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
(原因见 AnnotationCardView 里 contain:layout 的说明)。 */
|
|
226
|
+
摘要块(composer 与历史消息)在内容普通流里,不参与这套层带;悬停面板仍是
|
|
227
|
+
body 级 fixed(原因见 AnnotationCardView 里 contain:layout 的说明)。 */
|
|
228
228
|
|
|
229
229
|
.dsq_toolbar {
|
|
230
230
|
position: absolute;
|
|
@@ -334,78 +334,8 @@ ${atomRules}
|
|
|
334
334
|
pointer-events: auto;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
/*
|
|
338
|
-
|
|
339
|
-
numbers where the transcript really differs.
|
|
340
|
-
Geometry mirrors the product's own file card (\`.nyYjTG_file\` in
|
|
341
|
-
ui-deliverables): hairline border, neutral fill, 18px radius, fixed 72px row. */
|
|
342
|
-
.dsq_card {
|
|
343
|
-
--dsq-card-fill: var(--dsq-neutral-50);
|
|
344
|
-
--dsq-card-hover: var(--dsq-neutral-100);
|
|
345
|
-
flex: 0 0 auto;
|
|
346
|
-
gap: 10px;
|
|
347
|
-
width: fit-content;
|
|
348
|
-
max-width: calc(25% - 5px);
|
|
349
|
-
min-width: 0;
|
|
350
|
-
height: 64px;
|
|
351
|
-
margin: 0;
|
|
352
|
-
padding: 10px;
|
|
353
|
-
border: 0.5px solid var(--dsq-line);
|
|
354
|
-
border-radius: 18px;
|
|
355
|
-
background: var(--dsq-card-fill);
|
|
356
|
-
color: var(--dsq-label);
|
|
357
|
-
transition: background-color 0.12s;
|
|
358
|
-
position: relative;
|
|
359
|
-
overflow: hidden;
|
|
360
|
-
user-select: none;
|
|
361
|
-
pointer-events: auto;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.dsq_card:hover {
|
|
365
|
-
background: var(--dsq-card-hover);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* Scaled down from the reference 48px: the composer card is width-capped at a
|
|
369
|
-
quarter of the input, so 48px would leave no room for the quote text. */
|
|
370
|
-
.dsq_cardIcon {
|
|
371
|
-
z-index: 2;
|
|
372
|
-
width: 36px;
|
|
373
|
-
height: 36px;
|
|
374
|
-
border: 0.5px solid var(--dsq-line);
|
|
375
|
-
border-radius: 12px;
|
|
376
|
-
background: var(--dsq-card-fill);
|
|
377
|
-
color: var(--dsq-link);
|
|
378
|
-
font-size: 12px;
|
|
379
|
-
font-weight: 600;
|
|
380
|
-
letter-spacing: -0.02em;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
.dsq_cardBody {
|
|
384
|
-
z-index: 2;
|
|
385
|
-
justify-content: center;
|
|
386
|
-
gap: 2px;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/* Keeps the ellipsised title clear of the corner remove button, which only the
|
|
390
|
-
composer stack renders. */
|
|
391
|
-
.dsq_cardStack .dsq_cardBody {
|
|
392
|
-
padding-right: 14px;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
.dsq_cardTitle {
|
|
396
|
-
font-size: 14px;
|
|
397
|
-
font-weight: 500;
|
|
398
|
-
line-height: 22px;
|
|
399
|
-
color: var(--dsq-label);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
.dsq_cardSubtitle {
|
|
403
|
-
font-size: 12px;
|
|
404
|
-
line-height: 18px;
|
|
405
|
-
color: var(--dsq-label-3);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/* 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. */
|
|
409
339
|
.dsq_cardClose {
|
|
410
340
|
position: absolute;
|
|
411
341
|
top: 4px;
|
|
@@ -422,8 +352,6 @@ ${atomRules}
|
|
|
422
352
|
transition: opacity 0.12s, background-color 0.12s;
|
|
423
353
|
}
|
|
424
354
|
|
|
425
|
-
.dsq_card:hover .dsq_cardClose,
|
|
426
|
-
.dsq_card:focus-within .dsq_cardClose,
|
|
427
355
|
.dsq_summary:hover .dsq_cardClose,
|
|
428
356
|
.dsq_summary:focus-within .dsq_cardClose {
|
|
429
357
|
opacity: 1;
|
|
@@ -435,33 +363,6 @@ ${atomRules}
|
|
|
435
363
|
color: var(--dsq-label);
|
|
436
364
|
}
|
|
437
365
|
|
|
438
|
-
/* Secondary corner action (edit comment). */
|
|
439
|
-
.dsq_cardAction {
|
|
440
|
-
position: absolute;
|
|
441
|
-
top: 4px;
|
|
442
|
-
right: 28px;
|
|
443
|
-
z-index: 3;
|
|
444
|
-
width: 20px;
|
|
445
|
-
height: 20px;
|
|
446
|
-
border-radius: 6px;
|
|
447
|
-
color: var(--dsq-label-3);
|
|
448
|
-
font-size: 12px;
|
|
449
|
-
line-height: 1;
|
|
450
|
-
opacity: 0;
|
|
451
|
-
pointer-events: none;
|
|
452
|
-
transition: opacity 0.12s, background-color 0.12s;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
.dsq_card:hover .dsq_cardAction,
|
|
456
|
-
.dsq_card:focus-within .dsq_cardAction {
|
|
457
|
-
opacity: 1;
|
|
458
|
-
pointer-events: auto;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
.dsq_cardAction:hover {
|
|
462
|
-
background: var(--dsq-fill-hover);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
366
|
/* Qoder-style annotation summary (“N 条批注”) inside the composer. */
|
|
466
367
|
.dsq_summary {
|
|
467
368
|
position: relative;
|
|
@@ -770,98 +671,19 @@ ${atomRules}
|
|
|
770
671
|
gap: 6px;
|
|
771
672
|
}
|
|
772
673
|
|
|
773
|
-
/* Transcript
|
|
774
|
-
.
|
|
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 {
|
|
775
678
|
display: flex;
|
|
776
|
-
flex-direction: row;
|
|
777
|
-
flex-wrap: wrap;
|
|
778
679
|
justify-content: flex-end;
|
|
779
|
-
align-items: stretch;
|
|
780
|
-
gap: 6px;
|
|
781
680
|
margin: 4px 0 8px;
|
|
782
|
-
/* Cards lay out horizontally and wrap onto the next row when they run out of
|
|
783
|
-
width. Each card carries the selected text plus the user's comment, so they
|
|
784
|
-
stay compact and stay on the user-bubble side (right). */
|
|
785
|
-
max-width: 100%;
|
|
786
|
-
margin-left: auto;
|
|
787
|
-
margin-right: 0;
|
|
788
681
|
}
|
|
789
682
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
.dsq_tCard {
|
|
683
|
+
.dsq_tSummary .dsq_summary {
|
|
684
|
+
width: auto;
|
|
685
|
+
min-width: 220px;
|
|
794
686
|
max-width: min(calc(var(--dsq-chat-width) * 0.72), 520px);
|
|
795
|
-
margin-left: auto;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.dsq_tCardIcon {
|
|
799
|
-
width: 40px;
|
|
800
|
-
height: 40px;
|
|
801
|
-
font-size: 14px;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
.dsq_tCardButton {
|
|
805
|
-
/* Compact cards: several fit in one row before wrapping. */
|
|
806
|
-
display: block;
|
|
807
|
-
flex: 0 1 auto;
|
|
808
|
-
width: 230px;
|
|
809
|
-
max-width: 100%;
|
|
810
|
-
padding: 0;
|
|
811
|
-
font: inherit;
|
|
812
|
-
text-align: left;
|
|
813
|
-
border-radius: 14px;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
.dsq_tCardButton:focus-visible {
|
|
817
|
-
outline: 2px solid var(--dsq-accent);
|
|
818
|
-
outline-offset: 2px;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
.dsq_tCardButton .dsq_card {
|
|
822
|
-
height: auto;
|
|
823
|
-
min-height: 0;
|
|
824
|
-
align-items: flex-start;
|
|
825
|
-
width: 100%;
|
|
826
|
-
gap: 8px;
|
|
827
|
-
padding: 8px;
|
|
828
|
-
border-radius: 14px;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
.dsq_tCardButton .dsq_cardIcon {
|
|
832
|
-
width: 22px;
|
|
833
|
-
height: 22px;
|
|
834
|
-
border-radius: 8px;
|
|
835
|
-
font-size: 11px;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
.dsq_tCardButton .dsq_cardTitle {
|
|
839
|
-
display: -webkit-box;
|
|
840
|
-
-webkit-line-clamp: 2;
|
|
841
|
-
-webkit-box-orient: vertical;
|
|
842
|
-
overflow: hidden;
|
|
843
|
-
white-space: normal;
|
|
844
|
-
overflow-wrap: anywhere;
|
|
845
|
-
font-size: 13px;
|
|
846
|
-
line-height: 18px;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
.dsq_tCardButton .dsq_cardSubtitle {
|
|
850
|
-
display: -webkit-box;
|
|
851
|
-
-webkit-line-clamp: 3;
|
|
852
|
-
-webkit-box-orient: vertical;
|
|
853
|
-
overflow: hidden;
|
|
854
|
-
white-space: pre-wrap;
|
|
855
|
-
overflow-wrap: anywhere;
|
|
856
|
-
margin-top: 2px;
|
|
857
|
-
font-size: 12px;
|
|
858
|
-
line-height: 16px;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/* A comment is the user's own words — give it more presence than the neutral
|
|
862
|
-
"no comment" placeholder. */
|
|
863
|
-
.dsq_cardSubtitle[data-dsq-has-comment='true'] {
|
|
864
|
-
color: var(--dsq-label-2);
|
|
865
687
|
}
|
|
866
688
|
|
|
867
689
|
/* Replacement user bubble (quote block stripped from display). */
|
|
@@ -892,6 +714,12 @@ ${atomRules}
|
|
|
892
714
|
max-width: 100%;
|
|
893
715
|
}
|
|
894
716
|
|
|
717
|
+
.dsq_userImages img {
|
|
718
|
+
display: block;
|
|
719
|
+
max-width: 100%;
|
|
720
|
+
border-radius: 12px;
|
|
721
|
+
}
|
|
722
|
+
|
|
895
723
|
/* Model-written :dsh-annotation{index="N"} chips. */
|
|
896
724
|
.dsq_annDirective {
|
|
897
725
|
appearance: none;
|
|
@@ -974,13 +802,7 @@ ${atomRules}
|
|
|
974
802
|
}
|
|
975
803
|
|
|
976
804
|
/* The product flips its palette on body[data-ds-dark-theme], and every
|
|
977
|
-
--dsq-* alias follows it. Only values we own ourselves need restating
|
|
978
|
-
transcript card carries .dsq_card too, so one selector covers both. */
|
|
979
|
-
body[data-ds-dark-theme] .dsq_card {
|
|
980
|
-
--dsq-card-fill: var(--dsq-neutral-850);
|
|
981
|
-
--dsq-card-hover: var(--dsq-neutral-800);
|
|
982
|
-
}
|
|
983
|
-
|
|
805
|
+
--dsq-* alias follows it. Only values we own ourselves need restating. */
|
|
984
806
|
body[data-ds-dark-theme] .dsq_annMark {
|
|
985
807
|
--dsq-mark-tint: var(--dsq-accent-tint-dark);
|
|
986
808
|
}
|
|
@@ -1009,14 +831,8 @@ const styles = {
|
|
|
1009
831
|
divider: "dsq_divider",
|
|
1010
832
|
status: "dsq_status",
|
|
1011
833
|
cardStack: "dsq_cardStack",
|
|
1012
|
-
card: "dsq_card",
|
|
1013
|
-
cardIcon: "dsq_cardIcon",
|
|
1014
|
-
cardBody: "dsq_cardBody",
|
|
1015
|
-
cardTitle: "dsq_cardTitle",
|
|
1016
|
-
cardSubtitle: "dsq_cardSubtitle",
|
|
1017
834
|
cardClose: "dsq_cardClose",
|
|
1018
835
|
cardPad: "dsq_cardPad",
|
|
1019
|
-
cardAction: "dsq_cardAction",
|
|
1020
836
|
summary: "dsq_summary",
|
|
1021
837
|
summaryIcon: "dsq_summaryIcon",
|
|
1022
838
|
summaryBody: "dsq_summaryBody",
|
|
@@ -1048,10 +864,7 @@ const styles = {
|
|
|
1048
864
|
commentPop: "dsq_commentPop",
|
|
1049
865
|
commentInput: "dsq_commentInput",
|
|
1050
866
|
commentActions: "dsq_commentActions",
|
|
1051
|
-
|
|
1052
|
-
tCardButton: "dsq_tCardButton",
|
|
1053
|
-
tCard: "dsq_tCard",
|
|
1054
|
-
tCardIcon: "dsq_tCardIcon",
|
|
867
|
+
tSummary: "dsq_tSummary",
|
|
1055
868
|
userRow: "dsq_userRow",
|
|
1056
869
|
userStack: "dsq_userStack",
|
|
1057
870
|
userImages: "dsq_userImages",
|
|
@@ -1591,47 +1404,80 @@ function stripProtocolHeader(prefix) {
|
|
|
1591
1404
|
if (prefix.indexOf("# Response annotations:") >= 0) return "";
|
|
1592
1405
|
return prefix.replace(/\s+$/, "");
|
|
1593
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
|
+
}
|
|
1594
1453
|
/**
|
|
1595
1454
|
* Read plugin annotations out of a durable message. Tolerates the JSON block
|
|
1596
1455
|
* and strips it from `rest` so the bubble can show only the user's question.
|
|
1597
1456
|
*/
|
|
1598
1457
|
function scanAnnotatedMessage(text) {
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
if (open < 0 || close < 0 || close < open) return {
|
|
1458
|
+
const parts = blockParts(text);
|
|
1459
|
+
if (parts === null) return {
|
|
1602
1460
|
annotations: [],
|
|
1603
1461
|
rest: text
|
|
1604
1462
|
};
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
const rest = [stripProtocolHeader(text.slice(0, open)), text.slice(close + 23).replace(/^\s+/, "")].filter((part) => part.length > 0).join("\n\n").trim();
|
|
1608
|
-
let parsed;
|
|
1609
|
-
try {
|
|
1610
|
-
parsed = JSON.parse(jsonText);
|
|
1611
|
-
} catch {
|
|
1612
|
-
return {
|
|
1613
|
-
annotations: [],
|
|
1614
|
-
rest: text.replace(/^\s+/, "")
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
if (!Array.isArray(parsed)) return {
|
|
1463
|
+
const parsed = parseWire(parts);
|
|
1464
|
+
if (parsed === null) return {
|
|
1618
1465
|
annotations: [],
|
|
1619
|
-
rest
|
|
1466
|
+
rest: text.replace(/^\s+/, "")
|
|
1620
1467
|
};
|
|
1468
|
+
const rest = joinedRest(parts);
|
|
1621
1469
|
const annotations = [];
|
|
1622
1470
|
for (const entry of parsed) {
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
const
|
|
1626
|
-
if (!body) continue;
|
|
1627
|
-
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];
|
|
1628
1474
|
const messageId = typeof source?.messageId === "string" ? source.messageId : void 0;
|
|
1629
1475
|
const startOffset = typeof source?.startOffset === "number" ? source.startOffset : void 0;
|
|
1630
1476
|
const endOffset = typeof source?.endOffset === "number" ? source.endOffset : void 0;
|
|
1631
1477
|
annotations.push({
|
|
1632
|
-
title: previewTitle(body),
|
|
1633
|
-
text: body,
|
|
1634
|
-
comment:
|
|
1478
|
+
title: previewTitle(item.body),
|
|
1479
|
+
text: item.body,
|
|
1480
|
+
comment: item.comment,
|
|
1635
1481
|
...messageId ? { messageId } : {},
|
|
1636
1482
|
...startOffset !== void 0 ? { startOffset } : {},
|
|
1637
1483
|
...endOffset !== void 0 ? { endOffset } : {}
|
|
@@ -1642,6 +1488,33 @@ function scanAnnotatedMessage(text) {
|
|
|
1642
1488
|
rest
|
|
1643
1489
|
};
|
|
1644
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
|
+
}
|
|
1645
1518
|
//#endregion
|
|
1646
1519
|
//#region src/client/runtime.ts
|
|
1647
1520
|
/**
|
|
@@ -1689,6 +1562,9 @@ function persist(partitionKey) {
|
|
|
1689
1562
|
if (!persistence || partitionKey === MEMORY_KEY) return;
|
|
1690
1563
|
persistence.save(partitionKey, bySession.get(partitionKey) ?? EMPTY$1);
|
|
1691
1564
|
}
|
|
1565
|
+
function newAnnotationId() {
|
|
1566
|
+
return `ann_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1567
|
+
}
|
|
1692
1568
|
/** Append one selection; an identical text+comment pair is reused. */
|
|
1693
1569
|
function saveAnnotation(sessionId, input) {
|
|
1694
1570
|
const k = partition(sessionId);
|
|
@@ -1697,7 +1573,7 @@ function saveAnnotation(sessionId, input) {
|
|
|
1697
1573
|
const duplicate = current.find((item) => item.text === input.text && (item.comment ?? "") === comment);
|
|
1698
1574
|
if (duplicate) return duplicate;
|
|
1699
1575
|
const annotation = {
|
|
1700
|
-
id:
|
|
1576
|
+
id: newAnnotationId(),
|
|
1701
1577
|
text: input.text,
|
|
1702
1578
|
...comment ? { comment } : {},
|
|
1703
1579
|
sources: input.sources?.length ? input.sources : [{ text: input.text }]
|
|
@@ -1707,6 +1583,36 @@ function saveAnnotation(sessionId, input) {
|
|
|
1707
1583
|
notify();
|
|
1708
1584
|
return annotation;
|
|
1709
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
|
+
}
|
|
1710
1616
|
function getAnnotations(sessionId) {
|
|
1711
1617
|
return bySession.get(partition(sessionId)) ?? EMPTY$1;
|
|
1712
1618
|
}
|
|
@@ -1888,11 +1794,6 @@ const zh = {
|
|
|
1888
1794
|
"toolbar.commentDialogAria": "添加可选评论",
|
|
1889
1795
|
"toolbar.skipComment": "暂不评论",
|
|
1890
1796
|
"toolbar.addComment": "添加评论",
|
|
1891
|
-
"card.commentSubtitle": "评论 · {comment}",
|
|
1892
|
-
"card.ariaIndexed": "批注 {index}",
|
|
1893
|
-
"card.ariaPlain": "划词批注",
|
|
1894
|
-
"card.editComment": "编辑评论",
|
|
1895
|
-
"card.remove": "移除批注",
|
|
1896
1797
|
"summary.hint": "悬停查看批注",
|
|
1897
1798
|
"summary.dialogAria": "{count} 条划词批注详情",
|
|
1898
1799
|
"summary.edit": "编辑批注 {index}",
|
|
@@ -1918,11 +1819,6 @@ const en = {
|
|
|
1918
1819
|
"toolbar.commentDialogAria": "Add an optional comment",
|
|
1919
1820
|
"toolbar.skipComment": "Skip comment",
|
|
1920
1821
|
"toolbar.addComment": "Add comment",
|
|
1921
|
-
"card.commentSubtitle": "Comment · {comment}",
|
|
1922
|
-
"card.ariaIndexed": "Annotation {index}",
|
|
1923
|
-
"card.ariaPlain": "Selection annotation",
|
|
1924
|
-
"card.editComment": "Edit comment",
|
|
1925
|
-
"card.remove": "Remove annotation",
|
|
1926
1822
|
"summary.hint": "Hover to view annotations",
|
|
1927
1823
|
"summary.dialogAria": "Annotation details: {count}",
|
|
1928
1824
|
"summary.edit": "Edit annotation {index}",
|
|
@@ -1955,11 +1851,6 @@ const ru = {
|
|
|
1955
1851
|
"toolbar.commentDialogAria": "Добавить необязательный комментарий",
|
|
1956
1852
|
"toolbar.skipComment": "Без комментария",
|
|
1957
1853
|
"toolbar.addComment": "Добавить комментарий",
|
|
1958
|
-
"card.commentSubtitle": "Комментарий · {comment}",
|
|
1959
|
-
"card.ariaIndexed": "Аннотация {index}",
|
|
1960
|
-
"card.ariaPlain": "Аннотация к выделенному тексту",
|
|
1961
|
-
"card.editComment": "Изменить комментарий",
|
|
1962
|
-
"card.remove": "Удалить аннотацию",
|
|
1963
1854
|
"summary.hint": "Наведите, чтобы увидеть аннотации",
|
|
1964
1855
|
"summary.dialogAria": "Детали аннотаций: {count}",
|
|
1965
1856
|
"summary.edit": "Изменить аннотацию {index}",
|
|
@@ -2057,52 +1948,6 @@ function useLocale() {
|
|
|
2057
1948
|
}
|
|
2058
1949
|
//#endregion
|
|
2059
1950
|
//#region src/client/ui/AnnotationCardView.tsx
|
|
2060
|
-
function AnnotationCardView({ title, text, comment, variant, index, onRemove, onEditComment }) {
|
|
2061
|
-
useLocale();
|
|
2062
|
-
const composer = variant === "composer";
|
|
2063
|
-
const subtitle = comment ? composer ? t("card.commentSubtitle", { comment }) : comment : composer ? t("common.selectedText") : t("summary.noComment");
|
|
2064
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2065
|
-
className: cx(A.box, A.row, styles.card, !composer && styles.tCard),
|
|
2066
|
-
role: "group",
|
|
2067
|
-
"aria-label": index ? t("card.ariaIndexed", { index }) : t("card.ariaPlain"),
|
|
2068
|
-
children: [
|
|
2069
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2070
|
-
className: cx(A.keep, A.gridMid, styles.cardIcon, !composer && styles.tCardIcon),
|
|
2071
|
-
"aria-hidden": "true",
|
|
2072
|
-
children: index ? String(index) : "AI"
|
|
2073
|
-
}),
|
|
2074
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2075
|
-
className: cx(A.grow, A.col, styles.cardBody),
|
|
2076
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2077
|
-
className: cx(A.ell, styles.cardTitle),
|
|
2078
|
-
title: text,
|
|
2079
|
-
children: title
|
|
2080
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2081
|
-
className: cx(A.ell, styles.cardSubtitle),
|
|
2082
|
-
...comment ? {
|
|
2083
|
-
title: comment,
|
|
2084
|
-
"data-dsq-has-comment": "true"
|
|
2085
|
-
} : {},
|
|
2086
|
-
children: subtitle
|
|
2087
|
-
})]
|
|
2088
|
-
}),
|
|
2089
|
-
composer && onEditComment ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2090
|
-
type: "button",
|
|
2091
|
-
className: cx(A.plain, A.gridMid, styles.cardAction),
|
|
2092
|
-
"aria-label": t("card.editComment"),
|
|
2093
|
-
onClick: onEditComment,
|
|
2094
|
-
children: "✎"
|
|
2095
|
-
}) : null,
|
|
2096
|
-
composer && onRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2097
|
-
type: "button",
|
|
2098
|
-
className: cx(A.plain, A.gridMid, styles.cardClose),
|
|
2099
|
-
"aria-label": t("card.remove"),
|
|
2100
|
-
onClick: onRemove,
|
|
2101
|
-
children: "×"
|
|
2102
|
-
}) : null
|
|
2103
|
-
]
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
1951
|
/** Grace period so pointer transit between the summary and the panel never closes it. */
|
|
2107
1952
|
const CLOSE_DELAY = 320;
|
|
2108
1953
|
/** After a viewport change, wait this long before position-checking the pointer. */
|
|
@@ -2464,12 +2309,13 @@ function clearDraftMarker(draft, setDraft) {
|
|
|
2464
2309
|
* composer, or the primary send button) so the composer's submit path carries
|
|
2465
2310
|
* the JSON protocol block.
|
|
2466
2311
|
*/
|
|
2467
|
-
function useSendIntercept({ itemCount, draftRef, setDraft, onFolded }) {
|
|
2312
|
+
function useSendIntercept({ itemCount, draftRef, setDraft, onFolded, foldingRef }) {
|
|
2468
2313
|
(0, react.useEffect)(() => {
|
|
2469
2314
|
if (itemCount === 0) return;
|
|
2470
2315
|
const inject = () => {
|
|
2471
2316
|
const current = getAnnotations();
|
|
2472
2317
|
if (current.length === 0) return;
|
|
2318
|
+
if (foldingRef !== void 0) foldingRef.current = true;
|
|
2473
2319
|
setDraft(composeAnnotatedMessage(draftRef.current, current));
|
|
2474
2320
|
clearAnnotations();
|
|
2475
2321
|
onFolded();
|
|
@@ -2505,7 +2351,8 @@ function useSendIntercept({ itemCount, draftRef, setDraft, onFolded }) {
|
|
|
2505
2351
|
itemCount,
|
|
2506
2352
|
draftRef,
|
|
2507
2353
|
setDraft,
|
|
2508
|
-
onFolded
|
|
2354
|
+
onFolded,
|
|
2355
|
+
foldingRef
|
|
2509
2356
|
]);
|
|
2510
2357
|
}
|
|
2511
2358
|
//#endregion
|
|
@@ -2550,6 +2397,7 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2550
2397
|
const draftRef = (0, react.useRef)(draft);
|
|
2551
2398
|
const actionsRef = (0, react.useRef)(inputActions);
|
|
2552
2399
|
const sessionRef = (0, react.useRef)(sessionId);
|
|
2400
|
+
const foldingRef = (0, react.useRef)(false);
|
|
2553
2401
|
const composing = useComposing();
|
|
2554
2402
|
draftRef.current = draft;
|
|
2555
2403
|
actionsRef.current = inputActions;
|
|
@@ -2595,8 +2443,26 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2595
2443
|
itemCount: items.length,
|
|
2596
2444
|
draftRef,
|
|
2597
2445
|
setDraft,
|
|
2598
|
-
onFolded: () => setItems(EMPTY)
|
|
2446
|
+
onFolded: () => setItems(EMPTY),
|
|
2447
|
+
foldingRef
|
|
2599
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
|
+
]);
|
|
2600
2466
|
(0, react.useEffect)(() => {
|
|
2601
2467
|
if (composing.current) return;
|
|
2602
2468
|
if (items.length === 0) {
|
|
@@ -2683,6 +2549,21 @@ const layers = /* @__PURE__ */ new WeakMap();
|
|
|
2683
2549
|
/** flow item → layer, so scrolling does not re-walk the ancestor chain. */
|
|
2684
2550
|
const itemLayers = /* @__PURE__ */ new WeakMap();
|
|
2685
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
|
+
}
|
|
2686
2567
|
function contentCoords(scroller, left, top) {
|
|
2687
2568
|
const rect = scroller.getBoundingClientRect();
|
|
2688
2569
|
return {
|
|
@@ -2706,7 +2587,11 @@ function layerFor(scroller) {
|
|
|
2706
2587
|
scroller,
|
|
2707
2588
|
bounds: () => {
|
|
2708
2589
|
const rect = scroller.getBoundingClientRect();
|
|
2709
|
-
|
|
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));
|
|
2710
2595
|
},
|
|
2711
2596
|
toContent: (left, top) => contentCoords(scroller, left, top),
|
|
2712
2597
|
fromContent: (left, top) => {
|
|
@@ -2730,7 +2615,10 @@ function fallbackLayer() {
|
|
|
2730
2615
|
fallback = {
|
|
2731
2616
|
host,
|
|
2732
2617
|
scroller: document.body,
|
|
2733
|
-
bounds: () =>
|
|
2618
|
+
bounds: () => {
|
|
2619
|
+
const bottom = composerTop() ?? window.innerHeight;
|
|
2620
|
+
return new DOMRect(0, 0, window.innerWidth, Math.max(0, bottom));
|
|
2621
|
+
},
|
|
2734
2622
|
toContent: (left, top) => ({
|
|
2735
2623
|
left,
|
|
2736
2624
|
top
|
|
@@ -3389,16 +3277,82 @@ function watchAnnotationDirectives(root) {
|
|
|
3389
3277
|
return () => observer.disconnect();
|
|
3390
3278
|
}
|
|
3391
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
|
|
3392
3346
|
//#region src/client/transcript/TranscriptQuoteCard.tsx
|
|
3393
3347
|
/**
|
|
3394
|
-
* History-side
|
|
3348
|
+
* History-side summary for one user message's annotations.
|
|
3395
3349
|
*
|
|
3396
|
-
* One
|
|
3397
|
-
*
|
|
3398
|
-
*
|
|
3399
|
-
*
|
|
3400
|
-
*
|
|
3401
|
-
* 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.
|
|
3402
3356
|
*/
|
|
3403
3357
|
function TranscriptQuoteCard({ node }) {
|
|
3404
3358
|
ensureToolbarStyles();
|
|
@@ -3420,94 +3374,126 @@ function TranscriptQuoteCard({ node }) {
|
|
|
3420
3374
|
const ownerKey = stackRef.current?.closest("[data-chat-flow-key]")?.getAttribute("data-chat-flow-key");
|
|
3421
3375
|
return ownerKey ? `dqsm_${ownerKey}_${index}` : void 0;
|
|
3422
3376
|
}, []);
|
|
3423
|
-
const onSelect = (0, react.useCallback)((index,
|
|
3424
|
-
|
|
3425
|
-
|
|
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]);
|
|
3426
3382
|
if (quotes.length === 0) return null;
|
|
3427
3383
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3428
3384
|
ref: stackRef,
|
|
3429
|
-
className: styles.
|
|
3385
|
+
className: styles.tSummary,
|
|
3430
3386
|
"data-dsq-deco": "true",
|
|
3431
|
-
children:
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
"aria-label": quote.title,
|
|
3436
|
-
onClick: () => onSelect(index, quote.text),
|
|
3437
|
-
onKeyDown: (event) => {
|
|
3438
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
3439
|
-
event.preventDefault();
|
|
3440
|
-
onSelect(index, quote.text);
|
|
3441
|
-
},
|
|
3442
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationCardView, {
|
|
3443
|
-
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}`,
|
|
3444
3391
|
title: quote.title,
|
|
3445
3392
|
text: quote.text,
|
|
3446
|
-
comment: quote.comment
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
}
|
|
3393
|
+
...quote.comment ? { comment: quote.comment } : {}
|
|
3394
|
+
})),
|
|
3395
|
+
onItemSelect: onSelect
|
|
3396
|
+
})
|
|
3450
3397
|
});
|
|
3451
3398
|
}
|
|
3452
3399
|
//#endregion
|
|
3453
3400
|
//#region src/client/transcript/UserMessageDisplay.tsx
|
|
3454
|
-
function
|
|
3455
|
-
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 [];
|
|
3456
3424
|
const images = [];
|
|
3457
|
-
for (const block of
|
|
3425
|
+
for (const block of raw) {
|
|
3458
3426
|
if (block === null || typeof block !== "object") continue;
|
|
3459
3427
|
const candidate = block;
|
|
3460
|
-
if (candidate.type
|
|
3461
|
-
images.push({ attachment: candidate.attachment });
|
|
3428
|
+
if (candidate.type === "image" && candidate.attachment !== void 0) images.push(candidate.attachment);
|
|
3462
3429
|
}
|
|
3463
3430
|
return images;
|
|
3464
3431
|
}
|
|
3465
3432
|
/**
|
|
3466
3433
|
* Replacement for the built-in `user` Chat node view.
|
|
3467
3434
|
*
|
|
3468
|
-
*
|
|
3469
|
-
*
|
|
3470
|
-
*
|
|
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.
|
|
3471
3442
|
*
|
|
3472
|
-
*
|
|
3473
|
-
* `conversation.chat.node` is gated by the shipped
|
|
3474
|
-
* allowlist (system-prompt, user, steering,
|
|
3475
|
-
* turn-error, turn-max-tokens, turn-tail)
|
|
3476
|
-
*
|
|
3477
|
-
* process disclosure of a completed turn — mounted, then invisible. `user` is on
|
|
3478
|
-
* the list, so rendering the cards inside this component is the only placement
|
|
3479
|
-
* 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.
|
|
3480
3448
|
*/
|
|
3481
|
-
function UserMessageDisplay(
|
|
3449
|
+
function UserMessageDisplay(props) {
|
|
3482
3450
|
ensureToolbarStyles();
|
|
3451
|
+
const { node, hostUserOf, ...rest } = props;
|
|
3483
3452
|
const raw = contentBlocksToText(node.data?.content);
|
|
3484
3453
|
const scanned = scanAnnotatedMessage(raw);
|
|
3485
3454
|
const text = scanned.annotations.length > 0 ? scanned.rest : displayTextWithoutQuote(raw).trim();
|
|
3486
3455
|
const quotes = scanned.annotations;
|
|
3487
|
-
const
|
|
3488
|
-
|
|
3489
|
-
|
|
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", {
|
|
3490
3477
|
className: cx(A.colEnd, styles.userRow),
|
|
3491
3478
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3492
3479
|
className: cx(A.colEnd, styles.userStack),
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
]
|
|
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]
|
|
3509
3495
|
})
|
|
3510
|
-
});
|
|
3496
|
+
})] });
|
|
3511
3497
|
}
|
|
3512
3498
|
//#endregion
|
|
3513
3499
|
//#region src/client/state/annotation-persistence.ts
|
|
@@ -3701,6 +3687,7 @@ function apply(ctx) {
|
|
|
3701
3687
|
ctx.effect(() => subscribeLocale(() => refreshDirectiveChips()), "dsh-select-quote: chip language");
|
|
3702
3688
|
ctx.effect(() => watchAnnotationDirectives(), "dsh-select-quote: annotation directives");
|
|
3703
3689
|
ctx.effect(() => watchAnnotationMarks(), "dsh-select-quote: annotation marks");
|
|
3690
|
+
ctx.effect(() => watchSubmissionEcho(), "dsh-select-quote: submission echo");
|
|
3704
3691
|
ctx.effect(() => {
|
|
3705
3692
|
syncPendingAnnotationJobs(getAnnotations());
|
|
3706
3693
|
return subscribeAnnotations(() => syncPendingAnnotationJobs(getAnnotations()));
|
|
@@ -3732,14 +3719,28 @@ function apply(ctx) {
|
|
|
3732
3719
|
order: 1
|
|
3733
3720
|
}, SessionBridge);
|
|
3734
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
|
+
});
|
|
3735
3730
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
3736
3731
|
ctx.slots.register({
|
|
3737
3732
|
name: "conversation.chat.node",
|
|
3738
3733
|
key: "user",
|
|
3739
|
-
priority: -10
|
|
3740
|
-
|
|
3734
|
+
priority: -10,
|
|
3735
|
+
locale: "chat"
|
|
3736
|
+
}, UserNode);
|
|
3741
3737
|
});
|
|
3742
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
|
+
}
|
|
3743
3744
|
/** Invisible session-scoped bridge that publishes sessionId for the root toolbar. */
|
|
3744
3745
|
function SessionBridge({ sessionId }) {
|
|
3745
3746
|
bindAnnotationSession(sessionId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-select-quote",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "DeepSeek Harness web plugin: annotate text selected in the conversation — the selection rides along with your next message, is marked in the source text, and the model points back at it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|