dsh-select-quote 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/lib/client.js +412 -134
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -19,11 +19,19 @@
|
|
|
19
19
|
| 项 | 值 |
|
|
20
20
|
|---|---|
|
|
21
21
|
| dsh | `0.1.7-rc.1`(2026-09-24 实测:划词、批注卡片、正文标记、对话记录渲染全通) |
|
|
22
|
-
| profile | web(`dsh web
|
|
22
|
+
| profile | web(`dsh web`)与桌面端(desktop)都可用;桌面端在 0.2.0 及更早版本会楔死渲染进程,本仓库已修复,详见下节 |
|
|
23
23
|
| 运行时 | 只有浏览器半有业务;Node 半仅用于让包被 Loader 挂载 |
|
|
24
24
|
|
|
25
25
|
实现依赖若干产品内部约定(槽位名、composer DOM、深色主题属性等),升级 dsh 后请先跑 `npm run check`,再照 [`docs/internals.md`](docs/internals.md) 末尾的复核清单人工回归一遍。
|
|
26
26
|
|
|
27
|
+
### 桌面端(desktop profile)
|
|
28
|
+
|
|
29
|
+
桌面端是 Electron 外壳 + 同一个 Web 客户端,插件声明的 `dsh.client.platform: "web"` 在桌面端同样加载,槽位与 DOM 契约都在,功能本身可用(实测批注随消息送达模型、模型回指渲染为 chip 均正常)。
|
|
30
|
+
|
|
31
|
+
> ⚠️ **0.2.0 及更早版本在桌面端会楔死渲染进程**,见 [#1](https://github.com/latte03/dsh-select-quote/issues/1):App 能启动、界面完整渲染,但整个输入区无法点击,renderer 子进程单核 100% 持续旋转。根因在插件自己的装饰器里——协议头部逐字写出了 `:dsh-annotation{index="N"}`,当文本含有这个字面量而严格正则一条都没匹配上时,装饰器仍然替换了一次内容完全相同的 Text 节点,于是被自己的 `MutationObserver` 反复触发,微任务队列永远排不空,宏任务和定时器再也得不到执行。触发条件是文本内容,与 Electron 或 `dsh-app://` 无关,桌面端只是恰好把那段文本送进了 Text 节点。完整机制与两道防线记录在 [`docs/internals.md`](docs/internals.md) 关键机制 8。
|
|
32
|
+
|
|
33
|
+
> **现在的状态**:修复已进主干,Windows 桌面端 0.2.0-rc.2 实测重新启用插件后批注正常送达、Turn 正常结束、输入框可点击、没有新的崩溃日志。npm 上可安装的仍是 `0.2.0`,也就是带缺陷的那一版,因此桌面端用户在修复版发布之前请在 `dsh.profile.bundles` 中摘除本插件,或改用浏览器访问同一个端口。[#1](https://github.com/latte03/dsh-select-quote/issues/1) 保持 open,等报告人复验后关闭。
|
|
34
|
+
|
|
27
35
|
## 安装
|
|
28
36
|
|
|
29
37
|
### 方式一:从 npm 安装(bundle)
|
|
@@ -33,7 +41,7 @@ dsh plugin --profile web add dsh-select-quote
|
|
|
33
41
|
dsh --profile web
|
|
34
42
|
```
|
|
35
43
|
|
|
36
|
-
>
|
|
44
|
+
> npm 上目前最新是 `0.2.0`(2026-09-24 发布):带批注卡片、正文标记与模型回指,但界面只有简体中文,桌面端还会楔死渲染进程(见兼容性一节)。本仓库的 `0.3.0` 修掉了楔死、加了简体中文 / English / Русский 三语,并把对话记录里的批注卡片改到用户气泡内,**还没发布到 npm**。要用 `0.3.0` 的行为,先按方式二从本地 checkout 加载;发布完成后这一节需要更新。仓库里另有 `0.1.0`,那是旧版「引用块」,不要装错。
|
|
37
45
|
|
|
38
46
|
### 方式二:本地 checkout 加载(`--patch`)
|
|
39
47
|
|
|
@@ -53,6 +61,27 @@ dsh web --patch "$PWD/dev.patch.yml"
|
|
|
53
61
|
4. 正常打字、按 Enter 发送。批注随消息一起交给模型,卡片随之消失
|
|
54
62
|
5. 模型回答里指向某条批注的位置,会显示成可点的 chip
|
|
55
63
|
|
|
64
|
+
## 语言 / Localization
|
|
65
|
+
|
|
66
|
+
界面文案支持 **简体中文 / English / Русский**,默认跟随浏览器语言:
|
|
67
|
+
|
|
68
|
+
| 语言 | 默认值 |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `zh` | 原文案,同时作为缺失键的兜底词典 |
|
|
71
|
+
| `en` | 非中文环境 |
|
|
72
|
+
| `ru` | `ru` / `ru-RU` 等标签 |
|
|
73
|
+
|
|
74
|
+
想固定语言,在 Web GUI 控制台里设置后刷新页面:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
localStorage.setItem('dsq-locale', 'ru') // 'ru' | 'en' | 'zh'
|
|
78
|
+
localStorage.removeItem('dsq-locale') // 恢复跟随浏览器
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**刻意不翻译的部分**:`> [选中文本]`、`<response-annotations>`、`:dsh-annotation{index="N"}` 这些是模型与解析器约定的传输格式;`# Response annotations:` 那段是给模型的指令正文,不是界面文案。只有面向人的 chrome 走 `t()`。
|
|
82
|
+
|
|
83
|
+
新增文案:在 `src/client/i18n.ts` 的三份词典里各加一条,再在组件里 `t('key', { ... })` 调用。带 `{count}` 的键可以写成复数形式对象(中文用 `other`,俄语用 `one` / `few` / `many`)。
|
|
84
|
+
|
|
56
85
|
## 已知限制
|
|
57
86
|
|
|
58
87
|
- **待发批注只在内存里**:刷新页面或切换会话后会丢,卡片不持久化(已发送消息里的批注不受影响)。
|
package/lib/client.js
CHANGED
|
@@ -642,17 +642,19 @@ const CSS = `
|
|
|
642
642
|
gap: 6px;
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
/* Transcript cards — in-flow
|
|
645
|
+
/* Transcript cards — in-flow rows under the user bubble, side by side. */
|
|
646
646
|
.dsq_tCardStack {
|
|
647
647
|
display: flex;
|
|
648
|
-
flex-direction:
|
|
648
|
+
flex-direction: row;
|
|
649
|
+
flex-wrap: wrap;
|
|
650
|
+
justify-content: flex-end;
|
|
649
651
|
align-items: stretch;
|
|
650
652
|
gap: 6px;
|
|
651
653
|
margin: 4px 0 8px;
|
|
652
|
-
/*
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
max-width:
|
|
654
|
+
/* Cards lay out horizontally and wrap onto the next row when they run out of
|
|
655
|
+
width. Each card carries the selected text plus the user's comment, so they
|
|
656
|
+
stay compact and stay on the user-bubble side (right). */
|
|
657
|
+
max-width: 100%;
|
|
656
658
|
margin-left: auto;
|
|
657
659
|
margin-right: 0;
|
|
658
660
|
}
|
|
@@ -729,6 +731,77 @@ const CSS = `
|
|
|
729
731
|
white-space: nowrap;
|
|
730
732
|
}
|
|
731
733
|
|
|
734
|
+
/* Transcript card wrapper: the card itself is a click target that scrolls back
|
|
735
|
+
to the marked text. Unlike the composer summary (one collapsed row per
|
|
736
|
+
message) each history card shows its own comment, so the fixed 64px height
|
|
737
|
+
and the single-line clamps are lifted here. */
|
|
738
|
+
.dsq_tCardButton {
|
|
739
|
+
appearance: none;
|
|
740
|
+
display: block;
|
|
741
|
+
/* Compact cards: several fit in one row before wrapping. */
|
|
742
|
+
flex: 0 1 auto;
|
|
743
|
+
width: 230px;
|
|
744
|
+
max-width: 100%;
|
|
745
|
+
padding: 0;
|
|
746
|
+
border: none;
|
|
747
|
+
background: transparent;
|
|
748
|
+
font: inherit;
|
|
749
|
+
text-align: left;
|
|
750
|
+
cursor: pointer;
|
|
751
|
+
border-radius: 14px;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.dsq_tCardButton:focus-visible {
|
|
755
|
+
outline: 2px solid var(--dsw-static-blue-500, #3b82f6);
|
|
756
|
+
outline-offset: 2px;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.dsq_tCardButton .dsq_tCard {
|
|
760
|
+
height: auto;
|
|
761
|
+
min-height: 0;
|
|
762
|
+
align-items: flex-start;
|
|
763
|
+
width: 100%;
|
|
764
|
+
gap: 8px;
|
|
765
|
+
padding: 8px;
|
|
766
|
+
border-radius: 14px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.dsq_tCardButton .dsq_tCardIcon {
|
|
770
|
+
width: 22px;
|
|
771
|
+
height: 22px;
|
|
772
|
+
border-radius: 8px;
|
|
773
|
+
font-size: 11px;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.dsq_tCardButton .dsq_tCardTitle {
|
|
777
|
+
display: -webkit-box;
|
|
778
|
+
-webkit-line-clamp: 2;
|
|
779
|
+
-webkit-box-orient: vertical;
|
|
780
|
+
overflow: hidden;
|
|
781
|
+
white-space: normal;
|
|
782
|
+
overflow-wrap: anywhere;
|
|
783
|
+
font-size: 13px;
|
|
784
|
+
line-height: 18px;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.dsq_tCardButton .dsq_tCardSubtitle {
|
|
788
|
+
display: -webkit-box;
|
|
789
|
+
-webkit-line-clamp: 3;
|
|
790
|
+
-webkit-box-orient: vertical;
|
|
791
|
+
overflow: hidden;
|
|
792
|
+
white-space: pre-wrap;
|
|
793
|
+
overflow-wrap: anywhere;
|
|
794
|
+
margin-top: 2px;
|
|
795
|
+
font-size: 12px;
|
|
796
|
+
line-height: 16px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/* A comment is the user's own words — give it more presence than the neutral
|
|
800
|
+
"no comment" placeholder. */
|
|
801
|
+
.dsq_tCardButton .dsq_tCardSubtitle[data-dsq-has-comment='true'] {
|
|
802
|
+
color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, 0.65));
|
|
803
|
+
}
|
|
804
|
+
|
|
732
805
|
/* Replacement user bubble (quote block stripped from display). */
|
|
733
806
|
.dsq_userRow {
|
|
734
807
|
display: flex;
|
|
@@ -916,6 +989,7 @@ const styles = {
|
|
|
916
989
|
commentInput: "dsq_commentInput",
|
|
917
990
|
commentActions: "dsq_commentActions",
|
|
918
991
|
tCardStack: "dsq_tCardStack",
|
|
992
|
+
tCardButton: "dsq_tCardButton",
|
|
919
993
|
tCard: "dsq_tCard",
|
|
920
994
|
tCardIcon: "dsq_tCardIcon",
|
|
921
995
|
tCardBody: "dsq_tCardBody",
|
|
@@ -1338,16 +1412,6 @@ function contentBlocksToText(content) {
|
|
|
1338
1412
|
return "";
|
|
1339
1413
|
}).join("");
|
|
1340
1414
|
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Best-effort plain text from a durable `user/message` event. A surface event
|
|
1343
|
-
* carries its blocks directly on `data.content`; `data.message.content` is only
|
|
1344
|
-
* a fallback for the older shape.
|
|
1345
|
-
*/
|
|
1346
|
-
function extractUserText(event) {
|
|
1347
|
-
const data = event?.data;
|
|
1348
|
-
if (!data) return "";
|
|
1349
|
-
return contentBlocksToText(data.content ?? data.message?.content);
|
|
1350
|
-
}
|
|
1351
1415
|
/** Short one-line card title from selected body text. */
|
|
1352
1416
|
function previewTitle$1(text, max = 48) {
|
|
1353
1417
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
@@ -1526,10 +1590,6 @@ function scanAnnotatedMessage(text) {
|
|
|
1526
1590
|
rest
|
|
1527
1591
|
};
|
|
1528
1592
|
}
|
|
1529
|
-
/** Convenience: annotations only. */
|
|
1530
|
-
function parseAnnotatedMessage(text) {
|
|
1531
|
-
return scanAnnotatedMessage(text).annotations;
|
|
1532
|
-
}
|
|
1533
1593
|
//#endregion
|
|
1534
1594
|
//#region src/client/state/annotation-store.ts
|
|
1535
1595
|
/** Per-session pending selection annotations (composer-side store). */
|
|
@@ -1704,7 +1764,220 @@ function LucideIcon({ name, size = 15 }) {
|
|
|
1704
1764
|
});
|
|
1705
1765
|
}
|
|
1706
1766
|
//#endregion
|
|
1767
|
+
//#region src/client/i18n.ts
|
|
1768
|
+
const LOCALES = [
|
|
1769
|
+
"ru",
|
|
1770
|
+
"en",
|
|
1771
|
+
"zh"
|
|
1772
|
+
];
|
|
1773
|
+
const STORAGE_KEY = "dsq-locale";
|
|
1774
|
+
const MESSAGES = {
|
|
1775
|
+
ru: {
|
|
1776
|
+
"common.selectedText": "Выделенный текст",
|
|
1777
|
+
"common.commentPlaceholder": "Добавить необязательный комментарий…",
|
|
1778
|
+
"common.cancel": "Отмена",
|
|
1779
|
+
"common.save": "Сохранить",
|
|
1780
|
+
"toolbar.aria": "Действия с выделенным текстом",
|
|
1781
|
+
"toolbar.commentHint": "Добавить аннотацию…",
|
|
1782
|
+
"toolbar.copy": "Копировать",
|
|
1783
|
+
"toolbar.copyFailed": "Не удалось скопировать",
|
|
1784
|
+
"toolbar.addToTask": "Добавить к задаче",
|
|
1785
|
+
"toolbar.commentDialogAria": "Добавить необязательный комментарий",
|
|
1786
|
+
"toolbar.skipComment": "Без комментария",
|
|
1787
|
+
"toolbar.addComment": "Добавить комментарий",
|
|
1788
|
+
"card.commentSubtitle": "Комментарий · {comment}",
|
|
1789
|
+
"card.ariaIndexed": "Аннотация {index}",
|
|
1790
|
+
"card.ariaPlain": "Аннотация к выделенному тексту",
|
|
1791
|
+
"card.editComment": "Изменить комментарий",
|
|
1792
|
+
"card.remove": "Удалить аннотацию",
|
|
1793
|
+
"summary.hint": "Наведите, чтобы увидеть аннотации",
|
|
1794
|
+
"summary.dialogAria": "Детали аннотаций: {count}",
|
|
1795
|
+
"summary.edit": "Изменить аннотацию {index}",
|
|
1796
|
+
"summary.remove": "Удалить аннотацию {index}",
|
|
1797
|
+
"summary.userComment": "Комментарий пользователя",
|
|
1798
|
+
"summary.noComment": "Комментарий не добавлен",
|
|
1799
|
+
"summary.aria": {
|
|
1800
|
+
one: "{count} аннотация. Наведите, чтобы увидеть детали.",
|
|
1801
|
+
few: "{count} аннотации. Наведите, чтобы увидеть детали.",
|
|
1802
|
+
many: "{count} аннотаций. Наведите, чтобы увидеть детали."
|
|
1803
|
+
},
|
|
1804
|
+
"summary.title": {
|
|
1805
|
+
one: "{count} аннотация",
|
|
1806
|
+
few: "{count} аннотации",
|
|
1807
|
+
many: "{count} аннотаций"
|
|
1808
|
+
},
|
|
1809
|
+
"summary.removeAll": "Удалить все аннотации",
|
|
1810
|
+
"chip.annotation": "Аннотация {index}"
|
|
1811
|
+
},
|
|
1812
|
+
en: {
|
|
1813
|
+
"common.selectedText": "Selected text",
|
|
1814
|
+
"common.commentPlaceholder": "Add an optional comment…",
|
|
1815
|
+
"common.cancel": "Cancel",
|
|
1816
|
+
"common.save": "Save",
|
|
1817
|
+
"toolbar.aria": "Selection actions",
|
|
1818
|
+
"toolbar.commentHint": "Add annotation…",
|
|
1819
|
+
"toolbar.copy": "Copy",
|
|
1820
|
+
"toolbar.copyFailed": "Copy failed",
|
|
1821
|
+
"toolbar.addToTask": "Add to task",
|
|
1822
|
+
"toolbar.commentDialogAria": "Add an optional comment",
|
|
1823
|
+
"toolbar.skipComment": "Skip comment",
|
|
1824
|
+
"toolbar.addComment": "Add comment",
|
|
1825
|
+
"card.commentSubtitle": "Comment · {comment}",
|
|
1826
|
+
"card.ariaIndexed": "Annotation {index}",
|
|
1827
|
+
"card.ariaPlain": "Selection annotation",
|
|
1828
|
+
"card.editComment": "Edit comment",
|
|
1829
|
+
"card.remove": "Remove annotation",
|
|
1830
|
+
"summary.hint": "Hover to view annotations",
|
|
1831
|
+
"summary.dialogAria": "Annotation details: {count}",
|
|
1832
|
+
"summary.edit": "Edit annotation {index}",
|
|
1833
|
+
"summary.remove": "Remove annotation {index}",
|
|
1834
|
+
"summary.userComment": "User comment",
|
|
1835
|
+
"summary.noComment": "No comment added",
|
|
1836
|
+
"summary.aria": {
|
|
1837
|
+
one: "{count} annotation. Hover to view details.",
|
|
1838
|
+
other: "{count} annotations. Hover to view details."
|
|
1839
|
+
},
|
|
1840
|
+
"summary.title": {
|
|
1841
|
+
one: "{count} annotation",
|
|
1842
|
+
other: "{count} annotations"
|
|
1843
|
+
},
|
|
1844
|
+
"summary.removeAll": "Remove all annotations",
|
|
1845
|
+
"chip.annotation": "Annotation {index}"
|
|
1846
|
+
},
|
|
1847
|
+
zh: {
|
|
1848
|
+
"common.selectedText": "选中的文本",
|
|
1849
|
+
"common.commentPlaceholder": "添加可选评论…",
|
|
1850
|
+
"common.cancel": "取消",
|
|
1851
|
+
"common.save": "保存",
|
|
1852
|
+
"toolbar.aria": "划词操作",
|
|
1853
|
+
"toolbar.commentHint": "添加批注…",
|
|
1854
|
+
"toolbar.copy": "复制",
|
|
1855
|
+
"toolbar.copyFailed": "复制失败",
|
|
1856
|
+
"toolbar.addToTask": "添加到任务",
|
|
1857
|
+
"toolbar.commentDialogAria": "添加可选评论",
|
|
1858
|
+
"toolbar.skipComment": "暂不评论",
|
|
1859
|
+
"toolbar.addComment": "添加评论",
|
|
1860
|
+
"card.commentSubtitle": "评论 · {comment}",
|
|
1861
|
+
"card.ariaIndexed": "批注 {index}",
|
|
1862
|
+
"card.ariaPlain": "划词批注",
|
|
1863
|
+
"card.editComment": "编辑评论",
|
|
1864
|
+
"card.remove": "移除批注",
|
|
1865
|
+
"summary.hint": "悬停查看批注",
|
|
1866
|
+
"summary.dialogAria": "{count} 条划词批注详情",
|
|
1867
|
+
"summary.edit": "编辑批注 {index}",
|
|
1868
|
+
"summary.remove": "移除批注 {index}",
|
|
1869
|
+
"summary.userComment": "用户评论",
|
|
1870
|
+
"summary.noComment": "未添加评论",
|
|
1871
|
+
"summary.aria": { other: "{count} 条划词批注。悬停查看详情。" },
|
|
1872
|
+
"summary.title": { other: "{count} 条批注" },
|
|
1873
|
+
"summary.removeAll": "移除全部划词批注",
|
|
1874
|
+
"chip.annotation": "批注 {index}"
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
let cached = null;
|
|
1878
|
+
function matchLocale(tag) {
|
|
1879
|
+
const lower = tag.toLowerCase();
|
|
1880
|
+
for (const locale of LOCALES) if (lower === locale || lower.startsWith(`${locale}-`) || lower.startsWith(`${locale}_`)) return locale;
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
function localStore() {
|
|
1884
|
+
return globalThis.localStorage;
|
|
1885
|
+
}
|
|
1886
|
+
function readOverride() {
|
|
1887
|
+
try {
|
|
1888
|
+
const raw = localStore()?.getItem(STORAGE_KEY);
|
|
1889
|
+
return raw ? matchLocale(raw) : null;
|
|
1890
|
+
} catch {
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function detectLocale() {
|
|
1895
|
+
const override = readOverride();
|
|
1896
|
+
if (override) return override;
|
|
1897
|
+
const nav = globalThis.navigator;
|
|
1898
|
+
const tags = nav ? [...nav.languages ?? [], nav.language].filter(Boolean) : [];
|
|
1899
|
+
for (const tag of tags) {
|
|
1900
|
+
const hit = matchLocale(tag);
|
|
1901
|
+
if (hit) return hit;
|
|
1902
|
+
}
|
|
1903
|
+
return "en";
|
|
1904
|
+
}
|
|
1905
|
+
/** Active locale (resolved once, then cached). */
|
|
1906
|
+
function getLocale() {
|
|
1907
|
+
cached ??= detectLocale();
|
|
1908
|
+
return cached;
|
|
1909
|
+
}
|
|
1910
|
+
function pluralize(forms, locale, count) {
|
|
1911
|
+
if (locale === "ru") {
|
|
1912
|
+
const mod10 = count % 10;
|
|
1913
|
+
const mod100 = count % 100;
|
|
1914
|
+
if (mod10 === 1 && mod100 !== 11) return forms.one ?? forms.other ?? "";
|
|
1915
|
+
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms.few ?? forms.other ?? "";
|
|
1916
|
+
return forms.many ?? forms.other ?? "";
|
|
1917
|
+
}
|
|
1918
|
+
if (locale === "en") return (count === 1 ? forms.one : void 0) ?? forms.other ?? "";
|
|
1919
|
+
return forms.other ?? forms.many ?? "";
|
|
1920
|
+
}
|
|
1921
|
+
function interpolate(template, params) {
|
|
1922
|
+
return template.replace(/\{(\w+)\}/g, (whole, name) => name in params ? String(params[name]) : whole);
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Translate a key for the active locale.
|
|
1926
|
+
* `{name}` placeholders are filled from `params`; a numeric `count` also
|
|
1927
|
+
* selects the right plural form when the message is a `PluralForms` object.
|
|
1928
|
+
*/
|
|
1929
|
+
function t(key, params = {}) {
|
|
1930
|
+
const locale = getLocale();
|
|
1931
|
+
const value = MESSAGES[locale][key] ?? MESSAGES.zh[key] ?? key;
|
|
1932
|
+
return interpolate(typeof value === "string" ? value : pluralize(value, locale, typeof params.count === "number" ? params.count : 0), params);
|
|
1933
|
+
}
|
|
1934
|
+
//#endregion
|
|
1707
1935
|
//#region src/client/ui/AnnotationCardView.tsx
|
|
1936
|
+
function AnnotationCardView({ title, text, comment, variant, index, onRemove, onEditComment }) {
|
|
1937
|
+
const composer = variant === "composer";
|
|
1938
|
+
const subtitle = comment ? composer ? t("card.commentSubtitle", { comment }) : comment : composer ? t("common.selectedText") : t("summary.noComment");
|
|
1939
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1940
|
+
className: composer ? styles.card : styles.tCard,
|
|
1941
|
+
role: "group",
|
|
1942
|
+
"aria-label": index ? t("card.ariaIndexed", { index }) : t("card.ariaPlain"),
|
|
1943
|
+
children: [
|
|
1944
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1945
|
+
className: composer ? styles.cardIcon : styles.tCardIcon,
|
|
1946
|
+
"aria-hidden": true,
|
|
1947
|
+
children: index ? String(index) : "AI"
|
|
1948
|
+
}),
|
|
1949
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1950
|
+
className: composer ? styles.cardBody : styles.tCardBody,
|
|
1951
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
1952
|
+
className: composer ? styles.cardTitle : styles.tCardTitle,
|
|
1953
|
+
title: text,
|
|
1954
|
+
children: title
|
|
1955
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
1956
|
+
className: composer ? styles.cardSubtitle : styles.tCardSubtitle,
|
|
1957
|
+
...comment ? {
|
|
1958
|
+
title: comment,
|
|
1959
|
+
"data-dsq-has-comment": "true"
|
|
1960
|
+
} : {},
|
|
1961
|
+
children: subtitle
|
|
1962
|
+
})]
|
|
1963
|
+
}),
|
|
1964
|
+
composer && onEditComment ? (0, react_jsx_runtime.jsx)("button", {
|
|
1965
|
+
type: "button",
|
|
1966
|
+
className: styles.cardAction,
|
|
1967
|
+
"aria-label": t("card.editComment"),
|
|
1968
|
+
onClick: onEditComment,
|
|
1969
|
+
children: "✎"
|
|
1970
|
+
}) : null,
|
|
1971
|
+
composer && onRemove ? (0, react_jsx_runtime.jsx)("button", {
|
|
1972
|
+
type: "button",
|
|
1973
|
+
className: styles.cardClose,
|
|
1974
|
+
"aria-label": t("card.remove"),
|
|
1975
|
+
onClick: onRemove,
|
|
1976
|
+
children: "×"
|
|
1977
|
+
}) : null
|
|
1978
|
+
]
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1708
1981
|
/** Grace period so pointer transit between the summary and the panel never closes it. */
|
|
1709
1982
|
const CLOSE_DELAY = 320;
|
|
1710
1983
|
/** After a viewport change, wait this long before position-checking the pointer. */
|
|
@@ -1720,7 +1993,7 @@ const SETTLE_DELAY = 600;
|
|
|
1720
1993
|
* other cancels. Comment editing happens inline in the list — the panel stays
|
|
1721
1994
|
* put and auto-close is suppressed while an edit is open.
|
|
1722
1995
|
*/
|
|
1723
|
-
function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemove, onItemEdit, hint = "
|
|
1996
|
+
function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemove, onItemEdit, hint = t("summary.hint") }) {
|
|
1724
1997
|
const rootRef = (0, react.useRef)(null);
|
|
1725
1998
|
const popRef = (0, react.useRef)(null);
|
|
1726
1999
|
const closeTimerRef = (0, react.useRef)(null);
|
|
@@ -1876,7 +2149,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
1876
2149
|
ref: popRef,
|
|
1877
2150
|
className: `${styles.summaryHover} ${styles.summaryHoverOpen}`,
|
|
1878
2151
|
role: "dialog",
|
|
1879
|
-
"aria-label":
|
|
2152
|
+
"aria-label": t("summary.dialogAria", { count }),
|
|
1880
2153
|
onPointerEnter: openPop,
|
|
1881
2154
|
onPointerLeave,
|
|
1882
2155
|
children: (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -1895,20 +2168,20 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
1895
2168
|
}),
|
|
1896
2169
|
(0, react_jsx_runtime.jsx)("div", {
|
|
1897
2170
|
className: styles.summaryItemLabel,
|
|
1898
|
-
children: "
|
|
2171
|
+
children: t("common.selectedText")
|
|
1899
2172
|
}),
|
|
1900
2173
|
!isEditing && (onItemRemove || onItemEdit) ? (0, react_jsx_runtime.jsxs)("div", {
|
|
1901
2174
|
className: styles.summaryItemActions,
|
|
1902
2175
|
children: [onItemEdit ? (0, react_jsx_runtime.jsx)("button", {
|
|
1903
2176
|
type: "button",
|
|
1904
2177
|
className: styles.summaryItemAction,
|
|
1905
|
-
"aria-label":
|
|
2178
|
+
"aria-label": t("summary.edit", { index: i + 1 }),
|
|
1906
2179
|
onClick: () => startEdit(i),
|
|
1907
2180
|
children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
|
|
1908
2181
|
}) : null, onItemRemove ? (0, react_jsx_runtime.jsx)("button", {
|
|
1909
2182
|
type: "button",
|
|
1910
2183
|
className: styles.summaryItemAction,
|
|
1911
|
-
"aria-label":
|
|
2184
|
+
"aria-label": t("summary.remove", { index: i + 1 }),
|
|
1912
2185
|
onClick: () => onItemRemove(i, item),
|
|
1913
2186
|
children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
|
|
1914
2187
|
}) : null]
|
|
@@ -1923,12 +2196,12 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
1923
2196
|
}),
|
|
1924
2197
|
(0, react_jsx_runtime.jsx)("div", {
|
|
1925
2198
|
className: styles.summaryItemLabel,
|
|
1926
|
-
children: "
|
|
2199
|
+
children: t("summary.userComment")
|
|
1927
2200
|
}),
|
|
1928
2201
|
(0, react_jsx_runtime.jsx)("textarea", {
|
|
1929
2202
|
ref: editInputRef,
|
|
1930
2203
|
className: styles.annEditInput,
|
|
1931
|
-
placeholder: "
|
|
2204
|
+
placeholder: t("common.commentPlaceholder"),
|
|
1932
2205
|
value: editDraft,
|
|
1933
2206
|
onChange: (e) => setEditDraft(e.target.value),
|
|
1934
2207
|
onKeyDown: (e) => {
|
|
@@ -1949,12 +2222,12 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
1949
2222
|
type: "button",
|
|
1950
2223
|
className: styles.annEditCancel,
|
|
1951
2224
|
onClick: () => setEditing(null),
|
|
1952
|
-
children: "
|
|
2225
|
+
children: t("common.cancel")
|
|
1953
2226
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
1954
2227
|
type: "button",
|
|
1955
2228
|
className: styles.annEditSave,
|
|
1956
2229
|
onClick: commitEdit,
|
|
1957
|
-
children: "
|
|
2230
|
+
children: t("common.save")
|
|
1958
2231
|
})]
|
|
1959
2232
|
})
|
|
1960
2233
|
]
|
|
@@ -1974,11 +2247,11 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
1974
2247
|
}),
|
|
1975
2248
|
(0, react_jsx_runtime.jsx)("div", {
|
|
1976
2249
|
className: styles.summaryItemLabel,
|
|
1977
|
-
children: "
|
|
2250
|
+
children: t("summary.userComment")
|
|
1978
2251
|
}),
|
|
1979
2252
|
(0, react_jsx_runtime.jsx)("div", {
|
|
1980
2253
|
className: item.comment ? styles.summaryItemText : `${styles.summaryItemText} ${styles.summaryItemTextEmpty}`,
|
|
1981
|
-
children: item.comment || "
|
|
2254
|
+
children: item.comment || t("summary.noComment")
|
|
1982
2255
|
})
|
|
1983
2256
|
]
|
|
1984
2257
|
})
|
|
@@ -1992,7 +2265,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
1992
2265
|
className: styles.summary,
|
|
1993
2266
|
tabIndex: 0,
|
|
1994
2267
|
"data-selection-annotation-summary": "true",
|
|
1995
|
-
"aria-label":
|
|
2268
|
+
"aria-label": t("summary.aria", { count }),
|
|
1996
2269
|
"aria-expanded": open,
|
|
1997
2270
|
onPointerEnter,
|
|
1998
2271
|
onPointerLeave,
|
|
@@ -2009,7 +2282,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2009
2282
|
className: styles.summaryBody,
|
|
2010
2283
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
2011
2284
|
className: styles.summaryTitle,
|
|
2012
|
-
children:
|
|
2285
|
+
children: t("summary.title", { count })
|
|
2013
2286
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
2014
2287
|
className: styles.summaryHint,
|
|
2015
2288
|
children: hint
|
|
@@ -2018,7 +2291,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2018
2291
|
onRemoveAll ? (0, react_jsx_runtime.jsx)("button", {
|
|
2019
2292
|
type: "button",
|
|
2020
2293
|
className: styles.cardClose,
|
|
2021
|
-
"aria-label": "
|
|
2294
|
+
"aria-label": t("summary.removeAll"),
|
|
2022
2295
|
onClick: onRemoveAll,
|
|
2023
2296
|
children: (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2024
2297
|
name: "x",
|
|
@@ -2310,6 +2583,8 @@ async function copyText(text) {
|
|
|
2310
2583
|
}
|
|
2311
2584
|
//#endregion
|
|
2312
2585
|
//#region src/client/SelectionToolbar.tsx
|
|
2586
|
+
/** Cap for the auto-growing comment box; beyond this it scrolls internally. */
|
|
2587
|
+
const COMMENT_MAX_HEIGHT = 220;
|
|
2313
2588
|
function SelectionToolbar({ sessionId }) {
|
|
2314
2589
|
const toolbarRef = (0, react.useRef)(null);
|
|
2315
2590
|
const commentWrapRef = (0, react.useRef)(null);
|
|
@@ -2376,19 +2651,24 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2376
2651
|
if (dragging) return;
|
|
2377
2652
|
schedule();
|
|
2378
2653
|
};
|
|
2654
|
+
const onScroll = (event) => {
|
|
2655
|
+
const target = event.target;
|
|
2656
|
+
if (target instanceof Node && (toolbarRef.current?.contains(target) === true || commentWrapRef.current?.contains(target) === true)) return;
|
|
2657
|
+
hide();
|
|
2658
|
+
};
|
|
2379
2659
|
document.addEventListener("pointerdown", onPointerDown, true);
|
|
2380
2660
|
document.addEventListener("pointerup", onPointerUp, true);
|
|
2381
2661
|
document.addEventListener("keyup", schedule, true);
|
|
2382
2662
|
document.addEventListener("selectionchange", onSelectionChange);
|
|
2383
2663
|
window.addEventListener("resize", hide);
|
|
2384
|
-
document.addEventListener("scroll",
|
|
2664
|
+
document.addEventListener("scroll", onScroll, true);
|
|
2385
2665
|
return () => {
|
|
2386
2666
|
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
2387
2667
|
document.removeEventListener("pointerup", onPointerUp, true);
|
|
2388
2668
|
document.removeEventListener("keyup", schedule, true);
|
|
2389
2669
|
document.removeEventListener("selectionchange", onSelectionChange);
|
|
2390
2670
|
window.removeEventListener("resize", hide);
|
|
2391
|
-
document.removeEventListener("scroll",
|
|
2671
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
2392
2672
|
};
|
|
2393
2673
|
}, [hide, refresh]);
|
|
2394
2674
|
const placeCommentPop = (0, react.useCallback)(() => {
|
|
@@ -2415,6 +2695,18 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2415
2695
|
commentRef.current?.focus();
|
|
2416
2696
|
}
|
|
2417
2697
|
}, [phase, placeCommentPop]);
|
|
2698
|
+
(0, react.useEffect)(() => {
|
|
2699
|
+
if (phase !== "comment") return;
|
|
2700
|
+
const area = commentRef.current;
|
|
2701
|
+
if (!area) return;
|
|
2702
|
+
area.style.height = "auto";
|
|
2703
|
+
area.style.height = `${Math.min(area.scrollHeight, COMMENT_MAX_HEIGHT)}px`;
|
|
2704
|
+
placeCommentPop();
|
|
2705
|
+
}, [
|
|
2706
|
+
phase,
|
|
2707
|
+
comment,
|
|
2708
|
+
placeCommentPop
|
|
2709
|
+
]);
|
|
2418
2710
|
const handleToolbarPointerDown = (0, react.useCallback)((event) => {
|
|
2419
2711
|
event.preventDefault();
|
|
2420
2712
|
event.stopPropagation();
|
|
@@ -2427,7 +2719,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2427
2719
|
clearNativeSelection();
|
|
2428
2720
|
hide();
|
|
2429
2721
|
} catch {
|
|
2430
|
-
setStatus("
|
|
2722
|
+
setStatus(t("toolbar.copyFailed"));
|
|
2431
2723
|
}
|
|
2432
2724
|
}, [hide]);
|
|
2433
2725
|
const openComment = (0, react.useCallback)(() => {
|
|
@@ -2436,7 +2728,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2436
2728
|
setComment("");
|
|
2437
2729
|
setStatus(null);
|
|
2438
2730
|
}, []);
|
|
2439
|
-
/** Quick-add without opening the comment step (double-tap /
|
|
2731
|
+
/** Quick-add without opening the comment step (double-tap / “Skip comment”). */
|
|
2440
2732
|
const commitAnnotation = (0, react.useCallback)((body) => {
|
|
2441
2733
|
const snapshot = activeRef.current;
|
|
2442
2734
|
if (!snapshot) {
|
|
@@ -2471,7 +2763,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2471
2763
|
className: styles.toolbar,
|
|
2472
2764
|
style: toolbarStyle,
|
|
2473
2765
|
role: "toolbar",
|
|
2474
|
-
"aria-label": "
|
|
2766
|
+
"aria-label": t("toolbar.aria"),
|
|
2475
2767
|
"data-dsq-toolbar": "true",
|
|
2476
2768
|
onPointerDown: handleToolbarPointerDown,
|
|
2477
2769
|
children: phase === "comment" ? (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -2486,7 +2778,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2486
2778
|
fontSize: 13,
|
|
2487
2779
|
opacity: .8
|
|
2488
2780
|
},
|
|
2489
|
-
children: "
|
|
2781
|
+
children: t("toolbar.commentHint")
|
|
2490
2782
|
})
|
|
2491
2783
|
}) : (0, react_jsx_runtime.jsxs)("div", {
|
|
2492
2784
|
style: {
|
|
@@ -2502,7 +2794,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2502
2794
|
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2503
2795
|
name: "copy",
|
|
2504
2796
|
size: 14
|
|
2505
|
-
}), "
|
|
2797
|
+
}), t("toolbar.copy")]
|
|
2506
2798
|
}),
|
|
2507
2799
|
(0, react_jsx_runtime.jsx)("span", {
|
|
2508
2800
|
className: styles.divider,
|
|
@@ -2515,7 +2807,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2515
2807
|
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2516
2808
|
name: "plus",
|
|
2517
2809
|
size: 14
|
|
2518
|
-
}), "
|
|
2810
|
+
}), t("toolbar.addToTask")]
|
|
2519
2811
|
})
|
|
2520
2812
|
]
|
|
2521
2813
|
})
|
|
@@ -2525,12 +2817,12 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2525
2817
|
className: styles.commentPop,
|
|
2526
2818
|
style: void 0,
|
|
2527
2819
|
role: "dialog",
|
|
2528
|
-
"aria-label": "
|
|
2820
|
+
"aria-label": t("toolbar.commentDialogAria"),
|
|
2529
2821
|
onPointerDown: handleToolbarPointerDown,
|
|
2530
2822
|
children: [(0, react_jsx_runtime.jsx)("textarea", {
|
|
2531
2823
|
ref: commentRef,
|
|
2532
2824
|
className: styles.commentInput,
|
|
2533
|
-
placeholder: "
|
|
2825
|
+
placeholder: t("common.commentPlaceholder"),
|
|
2534
2826
|
value: comment,
|
|
2535
2827
|
onChange: (e) => setComment(e.target.value),
|
|
2536
2828
|
onKeyDown: (e) => {
|
|
@@ -2550,13 +2842,13 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2550
2842
|
type: "button",
|
|
2551
2843
|
className: styles.button,
|
|
2552
2844
|
onClick: () => commitAnnotation(void 0),
|
|
2553
|
-
children: "
|
|
2845
|
+
children: t("toolbar.skipComment")
|
|
2554
2846
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
2555
2847
|
type: "button",
|
|
2556
2848
|
className: styles.button,
|
|
2557
2849
|
disabled: !comment.trim(),
|
|
2558
2850
|
onClick: () => commitAnnotation(comment),
|
|
2559
|
-
children: "
|
|
2851
|
+
children: t("toolbar.addComment")
|
|
2560
2852
|
})]
|
|
2561
2853
|
})]
|
|
2562
2854
|
}) : null,
|
|
@@ -2581,11 +2873,14 @@ function decorateTextNode(node) {
|
|
|
2581
2873
|
const parent = node.parentElement;
|
|
2582
2874
|
if (!parent || parent.closest(`[data-${DECO}]`)) return;
|
|
2583
2875
|
if (parent.closest("pre, code, [data-dsq-toolbar]")) return;
|
|
2876
|
+
if (parent.closest("textarea, input, [data-lexical-editor=\"true\"]")) return;
|
|
2584
2877
|
const frag = document.createDocumentFragment();
|
|
2585
2878
|
let last = 0;
|
|
2879
|
+
let matched = false;
|
|
2586
2880
|
const re = new RegExp(ANNOTATION_DIRECTIVE_RE.source, "g");
|
|
2587
2881
|
let m;
|
|
2588
2882
|
while ((m = re.exec(value)) !== null) {
|
|
2883
|
+
matched = true;
|
|
2589
2884
|
if (m.index > last) frag.append(document.createTextNode(value.slice(last, m.index)));
|
|
2590
2885
|
const index = Number(m[1]);
|
|
2591
2886
|
const chip = document.createElement("button");
|
|
@@ -2593,7 +2888,7 @@ function decorateTextNode(node) {
|
|
|
2593
2888
|
chip.className = "dsq_annDirective";
|
|
2594
2889
|
chip.dataset[DECO] = String(index);
|
|
2595
2890
|
chip.setAttribute("data-dsq-deco", "true");
|
|
2596
|
-
chip.textContent =
|
|
2891
|
+
chip.textContent = t("chip.annotation", { index });
|
|
2597
2892
|
chip.addEventListener("click", () => {
|
|
2598
2893
|
const mark = findAnnotationMarkBefore(index, chip);
|
|
2599
2894
|
if (mark) flashAnnotationMark(mark);
|
|
@@ -2601,6 +2896,7 @@ function decorateTextNode(node) {
|
|
|
2601
2896
|
frag.append(chip);
|
|
2602
2897
|
last = m.index + m[0].length;
|
|
2603
2898
|
}
|
|
2899
|
+
if (!matched) return;
|
|
2604
2900
|
if (last < value.length) frag.append(document.createTextNode(value.slice(last)));
|
|
2605
2901
|
parent.replaceChild(frag, node);
|
|
2606
2902
|
}
|
|
@@ -2635,7 +2931,16 @@ function watchAnnotationDirectives(root) {
|
|
|
2635
2931
|
}
|
|
2636
2932
|
//#endregion
|
|
2637
2933
|
//#region src/client/transcript/TranscriptQuoteCard.tsx
|
|
2638
|
-
/**
|
|
2934
|
+
/**
|
|
2935
|
+
* History-side cards for one user message's annotations.
|
|
2936
|
+
*
|
|
2937
|
+
* One card per annotation, each showing the selected text **and** the user's
|
|
2938
|
+
* comment. The comment is how the user recognizes their own annotation, so it
|
|
2939
|
+
* belongs on the visible card — not behind a hover panel that has to be
|
|
2940
|
+
* discovered. Clicking a card scrolls back to the marked text in the source
|
|
2941
|
+
* message; the ordinal badge matches the `:dsh-annotation{index="N"}` the model
|
|
2942
|
+
* refers to.
|
|
2943
|
+
*/
|
|
2639
2944
|
function TranscriptQuoteCard({ node }) {
|
|
2640
2945
|
ensureToolbarStyles();
|
|
2641
2946
|
const stackRef = (0, react.useRef)(null);
|
|
@@ -2652,28 +2957,37 @@ function TranscriptQuoteCard({ node }) {
|
|
|
2652
2957
|
endOffset: quote.endOffset
|
|
2653
2958
|
})));
|
|
2654
2959
|
}, [quotes]);
|
|
2655
|
-
const onItemSelect = (0, react.useCallback)((index, item) => {
|
|
2656
|
-
scrollToAnnotation(index + 1, item.text, item.id);
|
|
2657
|
-
}, []);
|
|
2658
2960
|
const markIdOf = (0, react.useCallback)((index) => {
|
|
2659
2961
|
const ownerKey = stackRef.current?.closest("[data-chat-flow-key]")?.getAttribute("data-chat-flow-key");
|
|
2660
2962
|
return ownerKey ? `dqsm_${ownerKey}_${index}` : void 0;
|
|
2661
2963
|
}, []);
|
|
2964
|
+
const onSelect = (0, react.useCallback)((index, text) => {
|
|
2965
|
+
scrollToAnnotation(index + 1, text, markIdOf(index));
|
|
2966
|
+
}, [markIdOf]);
|
|
2662
2967
|
if (quotes.length === 0) return null;
|
|
2663
2968
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
2664
2969
|
ref: stackRef,
|
|
2665
2970
|
className: styles.tCardStack,
|
|
2666
2971
|
"data-dsq-deco": "true",
|
|
2667
|
-
children: (0, react_jsx_runtime.jsx)(
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2972
|
+
children: quotes.map((quote, index) => (0, react_jsx_runtime.jsx)("div", {
|
|
2973
|
+
className: styles.tCardButton,
|
|
2974
|
+
role: "button",
|
|
2975
|
+
tabIndex: 0,
|
|
2976
|
+
"aria-label": quote.title,
|
|
2977
|
+
onClick: () => onSelect(index, quote.text),
|
|
2978
|
+
onKeyDown: (event) => {
|
|
2979
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2980
|
+
event.preventDefault();
|
|
2981
|
+
onSelect(index, quote.text);
|
|
2982
|
+
},
|
|
2983
|
+
children: (0, react_jsx_runtime.jsx)(AnnotationCardView, {
|
|
2984
|
+
variant: "transcript",
|
|
2671
2985
|
title: quote.title,
|
|
2672
2986
|
text: quote.text,
|
|
2673
|
-
comment: quote.comment || void 0
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
})
|
|
2987
|
+
comment: quote.comment || void 0,
|
|
2988
|
+
index: index + 1
|
|
2989
|
+
})
|
|
2990
|
+
}, `ann-${index}`))
|
|
2677
2991
|
});
|
|
2678
2992
|
}
|
|
2679
2993
|
//#endregion
|
|
@@ -2691,101 +3005,69 @@ function contentImages(content) {
|
|
|
2691
3005
|
}
|
|
2692
3006
|
/**
|
|
2693
3007
|
* Replacement for the built-in `user` Chat node view.
|
|
2694
|
-
*
|
|
3008
|
+
*
|
|
3009
|
+
* Strips the `<response-annotations>` protocol block from the bubble **and**
|
|
3010
|
+
* renders one card per annotation right under it.
|
|
3011
|
+
*
|
|
3012
|
+
* Why the cards live here instead of on a `select-quote` Chat node:
|
|
3013
|
+
* `conversation.chat.node` is gated by the shipped `TURN_PROCESS_INDEPENDENT_KINDS`
|
|
3014
|
+
* allowlist (system-prompt, user, steering, turn-trigger, turn-process,
|
|
3015
|
+
* turn-error, turn-max-tokens, turn-tail). A node whose kind is not on that list
|
|
3016
|
+
* is classified as a turn-process member and `hidden` inside the collapsed
|
|
3017
|
+
* process disclosure of a completed turn — mounted, then invisible. `user` is on
|
|
3018
|
+
* the list, so rendering the cards inside this component is the only placement
|
|
3019
|
+
* that survives turn folding.
|
|
2695
3020
|
*/
|
|
2696
3021
|
function UserMessageDisplay({ node, renderMessageImages }) {
|
|
2697
3022
|
ensureToolbarStyles();
|
|
2698
3023
|
const raw = contentBlocksToText(node.data?.content);
|
|
2699
3024
|
const scanned = scanAnnotatedMessage(raw);
|
|
2700
3025
|
const text = scanned.annotations.length > 0 ? scanned.rest : displayTextWithoutQuote(raw).trim();
|
|
3026
|
+
const quotes = scanned.annotations;
|
|
2701
3027
|
const images = contentImages(node.data?.content);
|
|
2702
|
-
if (!text && images.length === 0) return null;
|
|
3028
|
+
if (!text && images.length === 0 && quotes.length === 0) return null;
|
|
2703
3029
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
2704
3030
|
className: styles.userRow,
|
|
2705
3031
|
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
2706
3032
|
className: styles.userStack,
|
|
2707
|
-
children: [
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
3033
|
+
children: [
|
|
3034
|
+
images.length > 0 && renderMessageImages !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
|
|
3035
|
+
className: styles.userImages,
|
|
3036
|
+
"data-message-attachments": true,
|
|
3037
|
+
children: renderMessageImages({
|
|
3038
|
+
images,
|
|
3039
|
+
align: "end",
|
|
3040
|
+
compact: images.length > 1
|
|
3041
|
+
})
|
|
3042
|
+
}, "images") : null,
|
|
3043
|
+
text ? (0, react_jsx_runtime.jsx)("div", {
|
|
3044
|
+
className: styles.userBubble,
|
|
3045
|
+
children: text
|
|
3046
|
+
}, "bubble") : null,
|
|
3047
|
+
quotes.length > 0 ? (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }, "quotes") : null
|
|
3048
|
+
]
|
|
2719
3049
|
})
|
|
2720
3050
|
});
|
|
2721
3051
|
}
|
|
2722
3052
|
//#endregion
|
|
2723
|
-
//#region src/client/transcript/transcript-node.ts
|
|
2724
|
-
const KIND = "select-quote";
|
|
2725
|
-
function createSelectQuoteDefinition() {
|
|
2726
|
-
return {
|
|
2727
|
-
kind: KIND,
|
|
2728
|
-
target: "chat",
|
|
2729
|
-
match(event) {
|
|
2730
|
-
if (event.type !== "user/message") return null;
|
|
2731
|
-
if (parseAnnotatedMessage(extractUserText(event)).length === 0) return null;
|
|
2732
|
-
return {
|
|
2733
|
-
id: `${KIND}-${event.seq ?? 0}`,
|
|
2734
|
-
role: "start"
|
|
2735
|
-
};
|
|
2736
|
-
},
|
|
2737
|
-
start(_context, match) {
|
|
2738
|
-
const quotes = parseAnnotatedMessage(extractUserText(match.event));
|
|
2739
|
-
if (quotes.length === 0) throw new Error("select-quote: missing annotation payload on start");
|
|
2740
|
-
return {
|
|
2741
|
-
quotes,
|
|
2742
|
-
seq: match.event.seq ?? 0
|
|
2743
|
-
};
|
|
2744
|
-
},
|
|
2745
|
-
update(context) {
|
|
2746
|
-
return context.state;
|
|
2747
|
-
},
|
|
2748
|
-
buildViewNode(context) {
|
|
2749
|
-
if (context.state === void 0) return null;
|
|
2750
|
-
const anchor = context.start?.event.seq ?? context.matches?.[0]?.event.seq ?? context.state.seq ?? 0;
|
|
2751
|
-
const location = context.start?.location ?? context.matches?.[0]?.location ?? { kind: "unresolved" };
|
|
2752
|
-
return {
|
|
2753
|
-
key: context.key,
|
|
2754
|
-
kind: KIND,
|
|
2755
|
-
id: context.id,
|
|
2756
|
-
target: "chat",
|
|
2757
|
-
anchorSeq: anchor,
|
|
2758
|
-
location,
|
|
2759
|
-
visibility: "visible",
|
|
2760
|
-
data: context.state
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
};
|
|
2764
|
-
}
|
|
2765
|
-
function registerSelectQuoteNode(ctx) {
|
|
2766
|
-
const uiConversation = ctx.uiConversation;
|
|
2767
|
-
if (!uiConversation) {
|
|
2768
|
-
console.warn("[dsh-select-quote] uiConversation unavailable; transcript card disabled");
|
|
2769
|
-
return;
|
|
2770
|
-
}
|
|
2771
|
-
ctx.effect(() => uiConversation.events.register(createSelectQuoteDefinition()), "dsh-select-quote: conversation node definition");
|
|
2772
|
-
}
|
|
2773
|
-
//#endregion
|
|
2774
3053
|
//#region src/client/index.tsx
|
|
2775
3054
|
/**
|
|
2776
3055
|
* Browser half of dsh-select-quote.
|
|
2777
3056
|
*
|
|
2778
3057
|
* Selection toolbar is root-scoped (`shell.overlay`) so transcript selections
|
|
2779
3058
|
* are always observed. Annotation summary stays on the session composer.
|
|
3059
|
+
*
|
|
3060
|
+
* History cards render **inside** the `user` Chat node view, not on a
|
|
3061
|
+
* `select-quote` Chat node. `conversation.chat.node` is gated by the shipped
|
|
3062
|
+
* `TURN_PROCESS_INDEPENDENT_KINDS` allowlist (system-prompt, user, steering,
|
|
3063
|
+
* turn-trigger, turn-process, turn-error, turn-max-tokens, turn-tail): any other
|
|
3064
|
+
* kind is classified as a turn-process member and `hidden` when the completed
|
|
3065
|
+
* turn's process disclosure is folded — mounted, but invisible. `user` is on
|
|
3066
|
+
* that allowlist, so the bubble is the only placement that is reliably visible.
|
|
2780
3067
|
*/
|
|
2781
|
-
const inject = [
|
|
2782
|
-
"slots",
|
|
2783
|
-
"sessions",
|
|
2784
|
-
"uiConversation"
|
|
2785
|
-
];
|
|
3068
|
+
const inject = ["slots", "sessions"];
|
|
2786
3069
|
function apply(ctx) {
|
|
2787
3070
|
ensureToolbarStyles();
|
|
2788
|
-
registerSelectQuoteNode(ctx);
|
|
2789
3071
|
ctx.effect(() => watchAnnotationDirectives(), "dsh-select-quote: annotation directives");
|
|
2790
3072
|
ctx.effect(() => watchAnnotationMarks(), "dsh-select-quote: annotation marks");
|
|
2791
3073
|
ctx.effect(() => {
|
|
@@ -2814,10 +3096,6 @@ function apply(ctx) {
|
|
|
2814
3096
|
}, SessionBridge);
|
|
2815
3097
|
});
|
|
2816
3098
|
ctx.slots.inject("conversation.chat.node", () => {
|
|
2817
|
-
ctx.slots.register({
|
|
2818
|
-
name: "conversation.chat.node",
|
|
2819
|
-
key: "select-quote"
|
|
2820
|
-
}, TranscriptQuoteCard);
|
|
2821
3099
|
ctx.slots.register({
|
|
2822
3100
|
name: "conversation.chat.node",
|
|
2823
3101
|
key: "user",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-select-quote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@types/react-dom": "~18.3",
|
|
69
69
|
"react": "^18.2.0",
|
|
70
70
|
"react-dom": "^18.2.0",
|
|
71
|
+
"tsdown": "0.23.0",
|
|
71
72
|
"typescript": "^5.8.0"
|
|
72
73
|
}
|
|
73
74
|
}
|