dsh-select-quote 0.3.10 → 0.3.12
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 +9 -6
- package/lib/client.js +752 -469
- package/package.json +5 -3
package/lib/client.js
CHANGED
|
@@ -64,6 +64,246 @@ function lastEditable() {
|
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
66
|
//#endregion
|
|
67
|
+
//#region src/client/i18n.ts
|
|
68
|
+
/**
|
|
69
|
+
* Copy for dsh-select-quote, served through the host locale service.
|
|
70
|
+
*
|
|
71
|
+
* The active language is owned by `@deepseek-ai/dsh-client-locale` (Settings →
|
|
72
|
+
* General → Language, persisted as `locale.preference`). This module owns only
|
|
73
|
+
* the dictionaries and the plural form selection that the host's plain-template
|
|
74
|
+
* lookup cannot express; it never resolves a language on its own.
|
|
75
|
+
*
|
|
76
|
+
* NOT localized on purpose:
|
|
77
|
+
* - wire markers (`> [选中文本]`, `<response-annotations>`,
|
|
78
|
+
* `:dsh-annotation{index="N"}`) — they are the message format the model
|
|
79
|
+
* and the transcript parser agree on;
|
|
80
|
+
* - the `# Response annotations:` header — model-facing instruction prose,
|
|
81
|
+
* not UI text.
|
|
82
|
+
*/
|
|
83
|
+
const LOCALE_NS = "select-quote";
|
|
84
|
+
const zh = {
|
|
85
|
+
"common.selectedText": "选中的文本",
|
|
86
|
+
"common.commentPlaceholder": "添加可选评论…",
|
|
87
|
+
"common.cancel": "取消",
|
|
88
|
+
"common.close": "关闭",
|
|
89
|
+
"common.save": "保存",
|
|
90
|
+
"toolbar.aria": "划词操作",
|
|
91
|
+
"toolbar.commentHint": "添加批注…",
|
|
92
|
+
"toolbar.copy": "复制",
|
|
93
|
+
"toolbar.openLink": "打开链接",
|
|
94
|
+
"toolbar.copyFailed": "复制失败",
|
|
95
|
+
"toolbar.addToTask": "添加到任务",
|
|
96
|
+
"toolbar.commentDialogAria": "添加可选评论",
|
|
97
|
+
"toolbar.skipComment": "暂不评论",
|
|
98
|
+
"toolbar.addComment": "添加评论",
|
|
99
|
+
"menu.aria": "划词菜单",
|
|
100
|
+
"menu.shortcutLabel": "打开划词菜单",
|
|
101
|
+
"menu.addShortcutLabel": "将选中文字添加到任务",
|
|
102
|
+
"menu.noSelection": "先在对话里选中一段文字",
|
|
103
|
+
"menu.busy": "先完成当前批注",
|
|
104
|
+
"summary.hint": "悬停查看批注",
|
|
105
|
+
"summary.hintTouch": "点按查看批注",
|
|
106
|
+
"summary.dialogAria": "{count} 条划词批注详情",
|
|
107
|
+
"summary.edit": "编辑批注 {index}",
|
|
108
|
+
"summary.remove": "移除批注 {index}",
|
|
109
|
+
"summary.userComment": "用户评论",
|
|
110
|
+
"summary.noComment": "未添加评论",
|
|
111
|
+
"summary.aria.other": "{count} 条划词批注。展开查看详情。",
|
|
112
|
+
"summary.title.other": "{count} 条批注",
|
|
113
|
+
"summary.removeAll": "移除全部划词批注",
|
|
114
|
+
"chip.annotation": "批注 {index}",
|
|
115
|
+
"chip.previewAria": "批注预览",
|
|
116
|
+
"mark.previewAria": "查看第 {index} 条批注",
|
|
117
|
+
"chip.jump": "原文",
|
|
118
|
+
"chip.jumpAria": "跳转到原文",
|
|
119
|
+
"settings.trigger": "划词入口",
|
|
120
|
+
"settings.triggerHint": "选中文字后何时显示操作条。触屏始终使用底部操作条。",
|
|
121
|
+
"settings.trigger.auto": "自动浮出",
|
|
122
|
+
"settings.trigger.contextmenu": "右键菜单",
|
|
123
|
+
"settings.trigger.both": "两者都开",
|
|
124
|
+
"settings.shortcut": "快捷键",
|
|
125
|
+
"settings.shortcutHint": "开启后,选中文字即可使用下面的快捷键;可在宿主的「快捷键」设置里改键。",
|
|
126
|
+
"settings.shortcutListAria": "当前快捷键",
|
|
127
|
+
"settings.shortcutUnavailable": "快捷键暂不可用(可能被其它插件占用)",
|
|
128
|
+
"settings.shortcutUnbound": "未绑定",
|
|
129
|
+
"settings.shortcutConflict": "与其它快捷键冲突"
|
|
130
|
+
};
|
|
131
|
+
const en = {
|
|
132
|
+
"common.selectedText": "Selected text",
|
|
133
|
+
"common.commentPlaceholder": "Add an optional comment…",
|
|
134
|
+
"common.cancel": "Cancel",
|
|
135
|
+
"common.close": "Close",
|
|
136
|
+
"common.save": "Save",
|
|
137
|
+
"toolbar.aria": "Selection actions",
|
|
138
|
+
"toolbar.commentHint": "Add annotation…",
|
|
139
|
+
"toolbar.copy": "Copy",
|
|
140
|
+
"toolbar.openLink": "Open link",
|
|
141
|
+
"toolbar.copyFailed": "Copy failed",
|
|
142
|
+
"toolbar.addToTask": "Add to task",
|
|
143
|
+
"toolbar.commentDialogAria": "Add an optional comment",
|
|
144
|
+
"toolbar.skipComment": "Skip comment",
|
|
145
|
+
"toolbar.addComment": "Add comment",
|
|
146
|
+
"menu.aria": "Selection menu",
|
|
147
|
+
"menu.shortcutLabel": "Open selection menu",
|
|
148
|
+
"menu.addShortcutLabel": "Add selected text to task",
|
|
149
|
+
"menu.noSelection": "Select text in the conversation first",
|
|
150
|
+
"menu.busy": "Finish the current annotation first",
|
|
151
|
+
"summary.hint": "Hover to view annotations",
|
|
152
|
+
"summary.hintTouch": "Tap to view annotations",
|
|
153
|
+
"summary.dialogAria": "Annotation details: {count}",
|
|
154
|
+
"summary.edit": "Edit annotation {index}",
|
|
155
|
+
"summary.remove": "Remove annotation {index}",
|
|
156
|
+
"summary.userComment": "User comment",
|
|
157
|
+
"summary.noComment": "No comment added",
|
|
158
|
+
"summary.aria.one": "{count} annotation. Open to view details.",
|
|
159
|
+
"summary.aria.other": "{count} annotations. Open to view details.",
|
|
160
|
+
"summary.title.one": "{count} annotation",
|
|
161
|
+
"summary.title.other": "{count} annotations",
|
|
162
|
+
"summary.removeAll": "Remove all annotations",
|
|
163
|
+
"chip.annotation": "Annotation {index}",
|
|
164
|
+
"chip.previewAria": "Annotation preview",
|
|
165
|
+
"mark.previewAria": "View annotation {index}",
|
|
166
|
+
"chip.jump": "Original",
|
|
167
|
+
"chip.jumpAria": "Jump to original",
|
|
168
|
+
"settings.trigger": "Trigger",
|
|
169
|
+
"settings.triggerHint": "When the action bar appears after you select text. Touch screens always use the bottom bar.",
|
|
170
|
+
"settings.trigger.auto": "Automatic",
|
|
171
|
+
"settings.trigger.contextmenu": "Right-click",
|
|
172
|
+
"settings.trigger.both": "Both",
|
|
173
|
+
"settings.shortcut": "Shortcut",
|
|
174
|
+
"settings.shortcutHint": "When on, the shortcuts below work on the selected text. Rebind them in the Host's Keyboard shortcuts settings.",
|
|
175
|
+
"settings.shortcutListAria": "Active shortcuts",
|
|
176
|
+
"settings.shortcutUnavailable": "Shortcuts are unavailable (another plugin may already use the keys).",
|
|
177
|
+
"settings.shortcutUnbound": "Unbound",
|
|
178
|
+
"settings.shortcutConflict": "Conflicts with another shortcut"
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Russian is not a shipped host locale: it is added through
|
|
182
|
+
* `ctx.locale.addLanguage()` and its dictionary through the three-argument
|
|
183
|
+
* `register()`. Its fallback chain ends at `en`, as the host requires.
|
|
184
|
+
*/
|
|
185
|
+
const ru = {
|
|
186
|
+
"common.selectedText": "Выделенный текст",
|
|
187
|
+
"common.commentPlaceholder": "Добавить необязательный комментарий…",
|
|
188
|
+
"common.cancel": "Отмена",
|
|
189
|
+
"common.close": "Закрыть",
|
|
190
|
+
"common.save": "Сохранить",
|
|
191
|
+
"toolbar.aria": "Действия с выделенным текстом",
|
|
192
|
+
"toolbar.commentHint": "Добавить аннотацию…",
|
|
193
|
+
"toolbar.copy": "Копировать",
|
|
194
|
+
"toolbar.openLink": "Открыть ссылку",
|
|
195
|
+
"toolbar.copyFailed": "Не удалось скопировать",
|
|
196
|
+
"toolbar.addToTask": "Добавить к задаче",
|
|
197
|
+
"toolbar.commentDialogAria": "Добавить необязательный комментарий",
|
|
198
|
+
"toolbar.skipComment": "Без комментария",
|
|
199
|
+
"toolbar.addComment": "Добавить комментарий",
|
|
200
|
+
"menu.aria": "Меню выделения",
|
|
201
|
+
"menu.shortcutLabel": "Открыть меню выделения",
|
|
202
|
+
"menu.addShortcutLabel": "Добавить выделенный текст к задаче",
|
|
203
|
+
"menu.noSelection": "Сначала выделите текст в диалоге",
|
|
204
|
+
"menu.busy": "Сначала завершите текущую аннотацию",
|
|
205
|
+
"summary.hint": "Наведите, чтобы увидеть аннотации",
|
|
206
|
+
"summary.hintTouch": "Нажмите, чтобы увидеть аннотации",
|
|
207
|
+
"summary.dialogAria": "Детали аннотаций: {count}",
|
|
208
|
+
"summary.edit": "Изменить аннотацию {index}",
|
|
209
|
+
"summary.remove": "Удалить аннотацию {index}",
|
|
210
|
+
"summary.userComment": "Комментарий пользователя",
|
|
211
|
+
"summary.noComment": "Комментарий не добавлен",
|
|
212
|
+
"summary.aria.one": "{count} аннотация. Откройте, чтобы увидеть подробности.",
|
|
213
|
+
"summary.aria.few": "{count} аннотации. Откройте, чтобы увидеть подробности.",
|
|
214
|
+
"summary.aria.many": "{count} аннотаций. Откройте, чтобы увидеть подробности.",
|
|
215
|
+
"summary.aria.other": "{count} аннотаций. Откройте, чтобы увидеть подробности.",
|
|
216
|
+
"summary.title.one": "{count} аннотация",
|
|
217
|
+
"summary.title.few": "{count} аннотации",
|
|
218
|
+
"summary.title.many": "{count} аннотаций",
|
|
219
|
+
"summary.title.other": "{count} аннотаций",
|
|
220
|
+
"summary.removeAll": "Удалить все аннотации",
|
|
221
|
+
"chip.annotation": "Аннотация {index}",
|
|
222
|
+
"chip.previewAria": "Просмотр аннотации",
|
|
223
|
+
"mark.previewAria": "Открыть аннотацию {index}",
|
|
224
|
+
"chip.jump": "Оригинал",
|
|
225
|
+
"chip.jumpAria": "Перейти к оригиналу",
|
|
226
|
+
"settings.trigger": "Триггер",
|
|
227
|
+
"settings.triggerHint": "Когда показывать панель действий после выделения текста. На сенсорных экранах всегда используется нижняя панель.",
|
|
228
|
+
"settings.trigger.auto": "Автоматически",
|
|
229
|
+
"settings.trigger.contextmenu": "По правому клику",
|
|
230
|
+
"settings.trigger.both": "Оба способа",
|
|
231
|
+
"settings.shortcut": "Горячая клавиша",
|
|
232
|
+
"settings.shortcutHint": "Когда включено, сочетания ниже работают с выделенным текстом. Изменить их можно в настройках горячих клавиш.",
|
|
233
|
+
"settings.shortcutListAria": "Текущие сочетания",
|
|
234
|
+
"settings.shortcutUnavailable": "Сочетания недоступны (возможно, клавиши заняты другим плагином).",
|
|
235
|
+
"settings.shortcutUnbound": "Не назначено",
|
|
236
|
+
"settings.shortcutConflict": "Конфликтует с другим сочетанием"
|
|
237
|
+
};
|
|
238
|
+
/** Both shipped host locales, required together by the typed `register()`. */
|
|
239
|
+
const SHIPPED_DICTS = Object.freeze({
|
|
240
|
+
zh,
|
|
241
|
+
en
|
|
242
|
+
});
|
|
243
|
+
/** Dictionary for the language registered through {@link LANGUAGE_RU}. */
|
|
244
|
+
const DICTIONARIES = Object.freeze({
|
|
245
|
+
zh,
|
|
246
|
+
en,
|
|
247
|
+
ru
|
|
248
|
+
});
|
|
249
|
+
const LANGUAGE_RU = Object.freeze({
|
|
250
|
+
id: "ru",
|
|
251
|
+
label: "Русский",
|
|
252
|
+
fallback: "en"
|
|
253
|
+
});
|
|
254
|
+
let api;
|
|
255
|
+
/**
|
|
256
|
+
* Install the host-bound translation entry point. Called once from `apply()`
|
|
257
|
+
* with `ctx.locale.bind(LOCALE_NS)` and the live snapshot accessors.
|
|
258
|
+
*/
|
|
259
|
+
function bindLocale(next) {
|
|
260
|
+
api = next;
|
|
261
|
+
}
|
|
262
|
+
/** Active language id, or `en` before the host service is bound. */
|
|
263
|
+
function activeLocale() {
|
|
264
|
+
return api?.getActive() ?? "en";
|
|
265
|
+
}
|
|
266
|
+
function pluralForm(count) {
|
|
267
|
+
const locale = activeLocale();
|
|
268
|
+
if (locale === "ru") {
|
|
269
|
+
const mod10 = count % 10;
|
|
270
|
+
const mod100 = count % 100;
|
|
271
|
+
if (mod10 === 1 && mod100 !== 11) return "one";
|
|
272
|
+
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
|
|
273
|
+
return "many";
|
|
274
|
+
}
|
|
275
|
+
if (locale === "en") return count === 1 ? "one" : "other";
|
|
276
|
+
return "other";
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Translate a `select-quote` key. A numeric `count` selects a plural form and
|
|
280
|
+
* the matching `key.form` entry is used, falling back to `key.other`.
|
|
281
|
+
*/
|
|
282
|
+
function t(key, params = {}) {
|
|
283
|
+
if (!api) return key;
|
|
284
|
+
if (typeof params.count === "number") {
|
|
285
|
+
const form = pluralForm(params.count);
|
|
286
|
+
const exact = api.translate(`${key}.${form}`, params);
|
|
287
|
+
if (exact !== `${key}.${form}`) return exact;
|
|
288
|
+
const other = api.translate(`${key}.other`, params);
|
|
289
|
+
if (other !== `${key}.other`) return other;
|
|
290
|
+
}
|
|
291
|
+
return api.translate(key, params);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Subscribe to language or dictionary changes. Components go through
|
|
295
|
+
* `useLocale()` instead; this is the non-React entry point (DOM decorations).
|
|
296
|
+
* Subscribing before the host service is bound is a no-op, so callers subscribe
|
|
297
|
+
* from `apply()`, after `bindLocale()`.
|
|
298
|
+
*/
|
|
299
|
+
function subscribeLocale(listener) {
|
|
300
|
+
return api ? api.subscribe(listener) : () => {};
|
|
301
|
+
}
|
|
302
|
+
/** Monotonic host revision counter, bumped on locale and dictionary changes. */
|
|
303
|
+
function localeRevision() {
|
|
304
|
+
return api?.getRevision() ?? 0;
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
67
307
|
//#region src/client/styles.ts
|
|
68
308
|
/**
|
|
69
309
|
* Every style the plugin ships, in four layers.
|
|
@@ -1168,7 +1408,9 @@ ${atomRules}
|
|
|
1168
1408
|
font-size: 10px;
|
|
1169
1409
|
font-weight: 600;
|
|
1170
1410
|
line-height: 1;
|
|
1171
|
-
|
|
1411
|
+
/* The only click target on a mark; the prose itself stays selectable. */
|
|
1412
|
+
pointer-events: auto;
|
|
1413
|
+
cursor: pointer;
|
|
1172
1414
|
}
|
|
1173
1415
|
|
|
1174
1416
|
.dsq_annMark:hover {
|
|
@@ -1567,13 +1809,41 @@ const styles = {
|
|
|
1567
1809
|
segOption: "dsq_segOption",
|
|
1568
1810
|
srOnly: "dsq_srOnly"
|
|
1569
1811
|
};
|
|
1570
|
-
|
|
1571
|
-
|
|
1812
|
+
const BADGE_MIN_WIDTH = 15;
|
|
1813
|
+
/** 10px/600 digits plus the 3px side padding. */
|
|
1814
|
+
const BADGE_DIGIT_WIDTH = 6;
|
|
1815
|
+
const BADGE_PADDING = 6;
|
|
1816
|
+
const HIT_SLOP = 3;
|
|
1572
1817
|
const STEP_X = 28;
|
|
1573
1818
|
const STEP_Y = 18;
|
|
1574
1819
|
/** Rest, then two steps right; after that the stack grows upward. */
|
|
1575
1820
|
const MAX_COLUMNS = 3;
|
|
1576
1821
|
const MAX_ROWS = 24;
|
|
1822
|
+
/** Rendered badge width, matching `min-width` + digit advance + padding. */
|
|
1823
|
+
function badgeWidth(index) {
|
|
1824
|
+
return Math.max(BADGE_MIN_WIDTH, String(index).length * BADGE_DIGIT_WIDTH + BADGE_PADDING);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Hit box of a badge as painted: anchored to the final fragment's bottom-right
|
|
1828
|
+
* corner, then shifted by the collision offset. Used to tell a badge click from
|
|
1829
|
+
* a click on the marked prose.
|
|
1830
|
+
*/
|
|
1831
|
+
function badgeHitBox(anchor, index, offset = {
|
|
1832
|
+
dx: 0,
|
|
1833
|
+
dy: 0
|
|
1834
|
+
}) {
|
|
1835
|
+
const left = anchor.right + 2 + offset.dx;
|
|
1836
|
+
const top = anchor.bottom - 8 - 15 + offset.dy;
|
|
1837
|
+
return {
|
|
1838
|
+
left: left - HIT_SLOP,
|
|
1839
|
+
top: top - HIT_SLOP,
|
|
1840
|
+
right: left + badgeWidth(index) + HIT_SLOP,
|
|
1841
|
+
bottom: top + 15 + HIT_SLOP
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function badgeContainsPoint(box, x, y) {
|
|
1845
|
+
return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
|
|
1846
|
+
}
|
|
1577
1847
|
function overlaps(a, b) {
|
|
1578
1848
|
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
1579
1849
|
}
|
|
@@ -1593,7 +1863,7 @@ function placeBadges(anchors, bounds = {}) {
|
|
|
1593
1863
|
return anchors.map((anchor) => {
|
|
1594
1864
|
for (let row = 0; row < MAX_ROWS; row += 1) for (let column = 0; column < MAX_COLUMNS; column += 1) {
|
|
1595
1865
|
const dx = column * STEP_X;
|
|
1596
|
-
const dy = -row * STEP_Y;
|
|
1866
|
+
const dy = row === 0 ? 0 : -row * STEP_Y;
|
|
1597
1867
|
if (!fits(anchor, dx, dy, bounds)) continue;
|
|
1598
1868
|
const candidate = {
|
|
1599
1869
|
...anchor,
|
|
@@ -1753,25 +2023,88 @@ function cleanTextOf(item) {
|
|
|
1753
2023
|
const MARK_ATTR = "data-dsq-ann-mark";
|
|
1754
2024
|
const INDEX_ATTR = "data-dsq-ann-index";
|
|
1755
2025
|
const FLASH_MS = 1500;
|
|
1756
|
-
/** Distance from the mark's bottom edge to the badge's bottom edge (CSS). */
|
|
1757
|
-
const BADGE_BOTTOM = 8;
|
|
1758
|
-
const BADGE_MARGIN = 2;
|
|
1759
2026
|
const jobs = /* @__PURE__ */ new Map();
|
|
1760
2027
|
let pendingIds = /* @__PURE__ */ new Set();
|
|
1761
2028
|
function attrValue(value) {
|
|
1762
2029
|
return value.replace(/["\\]/g, "\\$&");
|
|
1763
2030
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2031
|
+
const FLOW_SELECTOR = "[data-chat-flow-key]";
|
|
2032
|
+
/**
|
|
2033
|
+
* One pass over the rendered messages instead of one query per annotation.
|
|
2034
|
+
* `querySelectorAll` preserves document order, so the first copy of a key wins,
|
|
2035
|
+
* matching the old per-job `querySelector`.
|
|
2036
|
+
*/
|
|
2037
|
+
function flowLookup(scope) {
|
|
2038
|
+
const all = [...scope.querySelectorAll(FLOW_SELECTOR)];
|
|
2039
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2040
|
+
for (const item of all) {
|
|
2041
|
+
const key = item.getAttribute("data-chat-flow-key");
|
|
2042
|
+
if (key && !byKey.has(key)) byKey.set(key, item);
|
|
1768
2043
|
}
|
|
1769
|
-
return
|
|
2044
|
+
return {
|
|
2045
|
+
all,
|
|
2046
|
+
byKey
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
function flowKeyOf(item) {
|
|
2050
|
+
return item.getAttribute("data-chat-flow-key") ?? "";
|
|
1770
2051
|
}
|
|
1771
2052
|
/**
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
2053
|
+
* An annotation's source may sit in a block nested inside its message (for
|
|
2054
|
+
* example a reasoning panel), so a touched flow item answers to its own key
|
|
2055
|
+
* and every enclosing message key.
|
|
2056
|
+
*/
|
|
2057
|
+
function anchorKeysOf(item) {
|
|
2058
|
+
const keys = [];
|
|
2059
|
+
for (let el = item; el; el = el.parentElement?.closest(FLOW_SELECTOR) ?? null) {
|
|
2060
|
+
const key = el.getAttribute("data-chat-flow-key");
|
|
2061
|
+
if (key) keys.push(key);
|
|
2062
|
+
}
|
|
2063
|
+
return keys;
|
|
2064
|
+
}
|
|
2065
|
+
/** Items in a partial pass, grouped by flow key; other messages are not touched. */
|
|
2066
|
+
function groupByFlowKey(items) {
|
|
2067
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2068
|
+
for (const item of items) for (const key of anchorKeysOf(item)) {
|
|
2069
|
+
const list = grouped.get(key) ?? [];
|
|
2070
|
+
list.push(item);
|
|
2071
|
+
grouped.set(key, list);
|
|
2072
|
+
}
|
|
2073
|
+
return grouped;
|
|
2074
|
+
}
|
|
2075
|
+
/** The flow item that owns a mutated node, if any. */
|
|
2076
|
+
function ownerFlowItem(node) {
|
|
2077
|
+
if (!node) return null;
|
|
2078
|
+
if (node instanceof Element) return node.closest(FLOW_SELECTOR);
|
|
2079
|
+
return node.parentElement?.closest(FLOW_SELECTOR) ?? null;
|
|
2080
|
+
}
|
|
2081
|
+
function affectedFlowItems(records) {
|
|
2082
|
+
const items = /* @__PURE__ */ new Set();
|
|
2083
|
+
const addOwner = (node) => {
|
|
2084
|
+
const owner = ownerFlowItem(node);
|
|
2085
|
+
if (owner?.isConnected) items.add(owner);
|
|
2086
|
+
};
|
|
2087
|
+
/** An added wrapper can hold several newly rendered messages. */
|
|
2088
|
+
const addSubtree = (node) => {
|
|
2089
|
+
const owner = ownerFlowItem(node);
|
|
2090
|
+
if (owner) {
|
|
2091
|
+
if (owner.isConnected) items.add(owner);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (!(node instanceof Element)) return;
|
|
2095
|
+
for (const nested of node.querySelectorAll(FLOW_SELECTOR)) if (nested.isConnected) items.add(nested);
|
|
2096
|
+
};
|
|
2097
|
+
for (const record of records) {
|
|
2098
|
+
addOwner(record.target);
|
|
2099
|
+
if (record.type !== "childList") continue;
|
|
2100
|
+
for (const node of record.addedNodes) addSubtree(node);
|
|
2101
|
+
}
|
|
2102
|
+
return items;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Whitespace-insensitive comparison: `selection.toString()` inserts newlines
|
|
2106
|
+
* at block boundaries, while the clean-text concatenation has none, so both
|
|
2107
|
+
* sides are compared with all whitespace removed.
|
|
1775
2108
|
*/
|
|
1776
2109
|
function sameText(a, b) {
|
|
1777
2110
|
const norm = (value) => value.replace(/\s+/g, "");
|
|
@@ -1782,11 +2115,25 @@ function isMarked(pieces) {
|
|
|
1782
2115
|
if (!first) return false;
|
|
1783
2116
|
return first.entry.node.parentElement?.closest(`[${MARK_ATTR}]`) != null;
|
|
1784
2117
|
}
|
|
2118
|
+
function markLabel(index) {
|
|
2119
|
+
return t("mark.previewAria", { index });
|
|
2120
|
+
}
|
|
2121
|
+
/** The badge fragment is the keyboard entry into that annotation's preview. */
|
|
2122
|
+
function makeBadgeInteractive(mark, index) {
|
|
2123
|
+
mark.tabIndex = 0;
|
|
2124
|
+
mark.setAttribute("role", "button");
|
|
2125
|
+
mark.setAttribute("aria-haspopup", "dialog");
|
|
2126
|
+
const label = markLabel(index);
|
|
2127
|
+
if (mark.getAttribute("aria-label") !== label) mark.setAttribute("aria-label", label);
|
|
2128
|
+
}
|
|
1785
2129
|
function createMark(job, withIndex) {
|
|
1786
2130
|
const mark = document.createElement("span");
|
|
1787
2131
|
mark.className = styles.annMark;
|
|
1788
2132
|
mark.setAttribute(MARK_ATTR, job.markId);
|
|
1789
|
-
if (withIndex)
|
|
2133
|
+
if (withIndex) {
|
|
2134
|
+
mark.setAttribute(INDEX_ATTR, String(job.index));
|
|
2135
|
+
makeBadgeInteractive(mark, job.index);
|
|
2136
|
+
}
|
|
1790
2137
|
return mark;
|
|
1791
2138
|
}
|
|
1792
2139
|
/**
|
|
@@ -1863,14 +2210,24 @@ function refreshMarkIndex(item, job) {
|
|
|
1863
2210
|
const last = marks[marks.length - 1];
|
|
1864
2211
|
const next = String(job.index);
|
|
1865
2212
|
if (last.getAttribute(INDEX_ATTR) !== next) last.setAttribute(INDEX_ATTR, next);
|
|
2213
|
+
makeBadgeInteractive(last, job.index);
|
|
1866
2214
|
return true;
|
|
1867
2215
|
}
|
|
2216
|
+
/** Refresh the localized labels after a language switch (marks survive renders). */
|
|
2217
|
+
function refreshAnnotationMarkLabels() {
|
|
2218
|
+
if (typeof document === "undefined") return;
|
|
2219
|
+
for (const mark of document.querySelectorAll(`[${INDEX_ATTR}]`)) {
|
|
2220
|
+
const index = Number(mark.getAttribute(INDEX_ATTR) ?? 0);
|
|
2221
|
+
if (index > 0) makeBadgeInteractive(mark, index);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1868
2224
|
/**
|
|
1869
2225
|
* Resolve overlapping ordinal badges. Two annotations can end at the same
|
|
1870
2226
|
* offset (nested ranges), which puts their pseudo-element badges on the same
|
|
1871
2227
|
* pixel; nearby ends can also touch. Measure every last fragment, then write
|
|
1872
2228
|
* the offset back as variables inherited by `::after`. The badge's transform
|
|
1873
|
-
* never participates in text layout.
|
|
2229
|
+
* never participates in text layout. Collisions are resolved inside each
|
|
2230
|
+
* message block, and a stack may not climb into the message above.
|
|
1874
2231
|
*/
|
|
1875
2232
|
function layoutBadges(scope) {
|
|
1876
2233
|
const marks = Array.from(scope.querySelectorAll(`[${INDEX_ATTR}]`));
|
|
@@ -1882,37 +2239,57 @@ function layoutBadges(scope) {
|
|
|
1882
2239
|
const scroller = marks[0]?.closest("[data-conversation-scroll]");
|
|
1883
2240
|
const scrollerRect = scroller instanceof Element ? scroller.getBoundingClientRect() : null;
|
|
1884
2241
|
const rightEdge = scrollerRect && scrollerRect.width > 0 ? scrollerRect.right : document.documentElement.clientWidth;
|
|
1885
|
-
const
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2242
|
+
const scrollerTop = scrollerRect && scrollerRect.height > 0 ? scrollerRect.top : 0;
|
|
2243
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2244
|
+
for (const mark of marks) {
|
|
2245
|
+
const owner = mark.closest(FLOW_SELECTOR);
|
|
2246
|
+
const group = groups.get(owner);
|
|
2247
|
+
if (group) group.push(mark);
|
|
2248
|
+
else groups.set(owner, [mark]);
|
|
2249
|
+
}
|
|
2250
|
+
for (const [owner, group] of groups) {
|
|
2251
|
+
if (group.length < 2) continue;
|
|
2252
|
+
const ownerRect = owner?.getBoundingClientRect();
|
|
2253
|
+
const topEdge = Math.max(scrollerTop, ownerRect && ownerRect.height > 0 ? ownerRect.top : scrollerTop) + 4;
|
|
2254
|
+
const anchors = group.map((mark) => {
|
|
2255
|
+
const rect = mark.getBoundingClientRect();
|
|
2256
|
+
return {
|
|
2257
|
+
mark,
|
|
2258
|
+
index: Number(mark.getAttribute(INDEX_ATTR) ?? 0),
|
|
2259
|
+
x: rect.right + 2,
|
|
2260
|
+
y: rect.bottom - 8 - 15,
|
|
2261
|
+
width: 24,
|
|
2262
|
+
height: 15,
|
|
2263
|
+
visible: rect.width > 0 && rect.height > 0
|
|
2264
|
+
};
|
|
2265
|
+
}).filter((anchor) => anchor.visible).sort((a, b) => a.index - b.index);
|
|
2266
|
+
if (anchors.length < 2) continue;
|
|
2267
|
+
const offsets = placeBadges(anchors, {
|
|
2268
|
+
right: rightEdge - 4,
|
|
2269
|
+
top: topEdge
|
|
2270
|
+
});
|
|
2271
|
+
anchors.forEach((anchor, i) => {
|
|
2272
|
+
const { dx, dy } = offsets[i];
|
|
2273
|
+
if (dx !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-x", `${dx}px`);
|
|
2274
|
+
if (dy !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-y", `${dy}px`);
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
1908
2277
|
}
|
|
1909
|
-
/**
|
|
1910
|
-
|
|
2278
|
+
/**
|
|
2279
|
+
* Apply registered jobs. With `only`, touch just those rendered messages — the
|
|
2280
|
+
* mutation-driven path — instead of resolving and scanning every message for
|
|
2281
|
+
* every job. Badge collisions live inside one message's text flow, so both
|
|
2282
|
+
* paths resolve them per message.
|
|
2283
|
+
*/
|
|
2284
|
+
function applyAnnotationMarks(root, only) {
|
|
1911
2285
|
if (jobs.size === 0) return;
|
|
1912
2286
|
const scope = root ?? (typeof document === "undefined" ? void 0 : document.body);
|
|
1913
2287
|
if (!scope) return;
|
|
2288
|
+
const lookup = only ? null : flowLookup(scope);
|
|
2289
|
+
const onlyByKey = only ? groupByFlowKey(only) : null;
|
|
2290
|
+
const onlyItems = only ? [...only].filter((item) => flowKeyOf(item) !== "") : [];
|
|
1914
2291
|
for (const job of [...jobs.values()]) {
|
|
1915
|
-
const items =
|
|
2292
|
+
const items = only ? job.messageId === "" ? onlyItems : onlyByKey?.get(job.messageId) ?? [] : job.messageId === "" ? lookup?.all ?? [] : [lookup?.byKey.get(job.messageId)].filter((item) => item !== void 0);
|
|
1916
2293
|
let decorated = false;
|
|
1917
2294
|
for (const item of items) {
|
|
1918
2295
|
if (refreshMarkIndex(item, job)) {
|
|
@@ -1924,7 +2301,11 @@ function applyAnnotationMarks(root) {
|
|
|
1924
2301
|
if (decorateJob(item, job)) decorated = true;
|
|
1925
2302
|
}
|
|
1926
2303
|
}
|
|
1927
|
-
|
|
2304
|
+
if (!only) {
|
|
2305
|
+
layoutBadges(scope);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
for (const item of only) if (item.isConnected) layoutBadges(item);
|
|
1928
2309
|
}
|
|
1929
2310
|
/** Insert or refresh jobs; identical jobs are no-ops. */
|
|
1930
2311
|
function upsertAnnotationJobs(next) {
|
|
@@ -1971,26 +2352,46 @@ function syncPendingAnnotationJobs(annotations) {
|
|
|
1971
2352
|
endOffset: item.sources[0]?.endOffset
|
|
1972
2353
|
})));
|
|
1973
2354
|
}
|
|
1974
|
-
/**
|
|
2355
|
+
/**
|
|
2356
|
+
* Observe the transcript and re-apply marks after re-renders. Mutations are
|
|
2357
|
+
* narrowed to the flow items that actually changed; streaming a reply no
|
|
2358
|
+
* longer walks every annotation in the conversation on each frame. Resizes
|
|
2359
|
+
* still force a full pass because line wrapping moves every badge anchor.
|
|
2360
|
+
*/
|
|
1975
2361
|
function watchAnnotationMarks(root) {
|
|
1976
2362
|
const el = root ?? (typeof document === "undefined" ? void 0 : document.body);
|
|
1977
2363
|
if (!el || typeof MutationObserver === "undefined") return () => {};
|
|
1978
2364
|
applyAnnotationMarks(el);
|
|
1979
2365
|
let frame = 0;
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
frame =
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2366
|
+
let pending = /* @__PURE__ */ new Set();
|
|
2367
|
+
let needsFull = false;
|
|
2368
|
+
const run = () => {
|
|
2369
|
+
frame = 0;
|
|
2370
|
+
const only = pending;
|
|
2371
|
+
const full = needsFull;
|
|
2372
|
+
pending = /* @__PURE__ */ new Set();
|
|
2373
|
+
needsFull = false;
|
|
2374
|
+
if (full) applyAnnotationMarks(el);
|
|
2375
|
+
else if (only.size > 0) applyAnnotationMarks(el, only);
|
|
2376
|
+
};
|
|
2377
|
+
const schedule = (records) => {
|
|
2378
|
+
if (jobs.size === 0) {
|
|
2379
|
+
pending.clear();
|
|
2380
|
+
needsFull = false;
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (records) for (const item of affectedFlowItems(records)) pending.add(item);
|
|
2384
|
+
if (frame === 0) frame = window.requestAnimationFrame(run);
|
|
1987
2385
|
};
|
|
1988
|
-
const observer = new MutationObserver(schedule);
|
|
2386
|
+
const observer = new MutationObserver((records) => schedule(records));
|
|
1989
2387
|
observer.observe(el, {
|
|
1990
2388
|
childList: true,
|
|
1991
2389
|
subtree: true
|
|
1992
2390
|
});
|
|
1993
|
-
const onResize = () =>
|
|
2391
|
+
const onResize = () => {
|
|
2392
|
+
needsFull = true;
|
|
2393
|
+
schedule();
|
|
2394
|
+
};
|
|
1994
2395
|
window.addEventListener("resize", onResize);
|
|
1995
2396
|
return () => {
|
|
1996
2397
|
observer.disconnect();
|
|
@@ -2477,387 +2878,150 @@ function clearAnnotations(sessionId) {
|
|
|
2477
2878
|
*/
|
|
2478
2879
|
function bindAnnotationSession(sessionId) {
|
|
2479
2880
|
if (!sessionId || sessionId === runtime.sessionId) return;
|
|
2480
|
-
setRuntimeSession(sessionId);
|
|
2481
|
-
queueMicrotask(() => adoptPersistedAnnotations(sessionId));
|
|
2482
|
-
}
|
|
2483
|
-
/**
|
|
2484
|
-
* Adopt persisted annotations for a session.
|
|
2485
|
-
*
|
|
2486
|
-
* In-memory entries win on conflict — they can only be newer, because anything
|
|
2487
|
-
* added after the bind is persisted immediately. Annotations parked in the
|
|
2488
|
-
* memory-only bucket (added before the bind) move over as well.
|
|
2489
|
-
*/
|
|
2490
|
-
function adoptPersistedAnnotations(sessionId) {
|
|
2491
|
-
if (!persistence) return;
|
|
2492
|
-
const stored = persistence.load(sessionId);
|
|
2493
|
-
const current = bySession.get(sessionId) ?? EMPTY$1;
|
|
2494
|
-
const orphaned = bySession.get(MEMORY_KEY) ?? EMPTY$1;
|
|
2495
|
-
const merged = /* @__PURE__ */ new Map();
|
|
2496
|
-
for (const item of stored) merged.set(item.id, item);
|
|
2497
|
-
for (const item of [...current, ...orphaned]) merged.set(item.id, item);
|
|
2498
|
-
const next = [...merged.values()];
|
|
2499
|
-
if (next.length === current.length && next.every((item, i) => item.id === current[i]?.id) && orphaned.length === 0) return;
|
|
2500
|
-
bySession.set(sessionId, next);
|
|
2501
|
-
if (orphaned.length > 0) bySession.delete(MEMORY_KEY);
|
|
2502
|
-
notify();
|
|
2503
|
-
}
|
|
2504
|
-
/** Same ids in the same order, with the only mutable field (comment) equal. */
|
|
2505
|
-
function sameAnnotations(a, b) {
|
|
2506
|
-
return a.length === b.length && a.every((item, i) => {
|
|
2507
|
-
const other = b[i];
|
|
2508
|
-
return other !== void 0 && item.id === other.id && (item.comment ?? "") === (other.comment ?? "");
|
|
2509
|
-
});
|
|
2510
|
-
}
|
|
2511
|
-
/**
|
|
2512
|
-
* Re-read a session after another tab wrote its storage key.
|
|
2513
|
-
*
|
|
2514
|
-
* Storage is the shared source of truth: every local mutation persists
|
|
2515
|
-
* synchronously, so this tab's memory can only be stale relative to it. The
|
|
2516
|
-
* stored array replaces the local one (last writer wins) — merging would
|
|
2517
|
-
* resurrect annotations the other tab already sent and cleared.
|
|
2518
|
-
*
|
|
2519
|
-
* `null` is the `localStorage.clear()` event: reload every partition this tab
|
|
2520
|
-
* holds. Sessions that are neither cached nor active are ignored; binding the
|
|
2521
|
-
* session later reads storage anyway.
|
|
2522
|
-
*/
|
|
2523
|
-
function adoptStorageAnnotations(sessionId) {
|
|
2524
|
-
if (!persistence) return;
|
|
2525
|
-
if (sessionId === null) {
|
|
2526
|
-
let changed = false;
|
|
2527
|
-
for (const key of [...bySession.keys()]) {
|
|
2528
|
-
if (key === MEMORY_KEY) continue;
|
|
2529
|
-
const stored = persistence.load(key);
|
|
2530
|
-
const current = bySession.get(key);
|
|
2531
|
-
if (stored.length === 0) {
|
|
2532
|
-
bySession.delete(key);
|
|
2533
|
-
changed = true;
|
|
2534
|
-
} else if (current === void 0 || !sameAnnotations(stored, current)) {
|
|
2535
|
-
bySession.set(key, [...stored]);
|
|
2536
|
-
changed = true;
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
if (changed) notify();
|
|
2540
|
-
return;
|
|
2541
|
-
}
|
|
2542
|
-
if (!bySession.has(sessionId) && sessionId !== runtime.sessionId) return;
|
|
2543
|
-
const stored = persistence.load(sessionId);
|
|
2544
|
-
if (sameAnnotations(stored, bySession.get(sessionId) ?? EMPTY$1)) return;
|
|
2545
|
-
if (stored.length === 0) bySession.delete(sessionId);
|
|
2546
|
-
else bySession.set(sessionId, [...stored]);
|
|
2547
|
-
notify();
|
|
2548
|
-
}
|
|
2549
|
-
function annotationTitle(annotation) {
|
|
2550
|
-
return previewTitle(annotation.text);
|
|
2551
|
-
}
|
|
2552
|
-
//#endregion
|
|
2553
|
-
//#region src/client/dom/popoverPlacement.ts
|
|
2554
|
-
const GAP$2 = 8;
|
|
2555
|
-
const MARGIN = 8;
|
|
2556
|
-
/**
|
|
2557
|
-
* Place `panel` near `anchor`, flipping vertical side when the preferred side
|
|
2558
|
-
* lacks room. Keeps the panel inside `bounds` (the viewport by default).
|
|
2559
|
-
*/
|
|
2560
|
-
function placePopover(anchor, panel, preferred = "bottom", gap = GAP$2, bounds) {
|
|
2561
|
-
const box = bounds ?? {
|
|
2562
|
-
top: 0,
|
|
2563
|
-
left: 0,
|
|
2564
|
-
right: window.innerWidth,
|
|
2565
|
-
bottom: window.innerHeight
|
|
2566
|
-
};
|
|
2567
|
-
const spaceBelow = box.bottom - anchor.bottom;
|
|
2568
|
-
const spaceAbove = anchor.top - box.top;
|
|
2569
|
-
let side = preferred;
|
|
2570
|
-
const need = panel.height + gap;
|
|
2571
|
-
if (preferred === "bottom" && spaceBelow < need && spaceAbove > spaceBelow) side = "top";
|
|
2572
|
-
else if (preferred === "top" && spaceAbove < need && spaceBelow > spaceAbove) side = "bottom";
|
|
2573
|
-
return {
|
|
2574
|
-
top: side === "bottom" ? Math.min(anchor.bottom + gap, box.bottom - panel.height - MARGIN) : Math.max(anchor.top - panel.height - gap, box.top + MARGIN),
|
|
2575
|
-
left: Math.min(Math.max(anchor.left, box.left + MARGIN), Math.max(box.left + MARGIN, box.right - panel.width - MARGIN)),
|
|
2576
|
-
side
|
|
2577
|
-
};
|
|
2578
|
-
}
|
|
2579
|
-
//#endregion
|
|
2580
|
-
//#region src/client/ui/icons.tsx
|
|
2581
|
-
const SVG_ATTRS = {
|
|
2582
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
2583
|
-
viewBox: "0 0 24 24",
|
|
2584
|
-
fill: "none",
|
|
2585
|
-
stroke: "currentColor",
|
|
2586
|
-
strokeWidth: 2,
|
|
2587
|
-
strokeLinecap: "round",
|
|
2588
|
-
strokeLinejoin: "round",
|
|
2589
|
-
"aria-hidden": true,
|
|
2590
|
-
focusable: false
|
|
2591
|
-
};
|
|
2592
|
-
const SHAPES = {
|
|
2593
|
-
copy: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2594
|
-
width: 14,
|
|
2595
|
-
height: 14,
|
|
2596
|
-
x: 8,
|
|
2597
|
-
y: 8,
|
|
2598
|
-
rx: 2,
|
|
2599
|
-
ry: 2
|
|
2600
|
-
}), /* @__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" })] }),
|
|
2601
|
-
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" })] }),
|
|
2602
|
-
"trash-2": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
2603
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 11v6" }),
|
|
2604
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 11v6" }),
|
|
2605
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }),
|
|
2606
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
2607
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
|
|
2608
|
-
] }),
|
|
2609
|
-
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" })] }),
|
|
2610
|
-
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" })] }),
|
|
2611
|
-
"external-link": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
2612
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 3h6v6" }),
|
|
2613
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 14 21 3" }),
|
|
2614
|
-
/* @__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" })
|
|
2615
|
-
] })
|
|
2616
|
-
};
|
|
2617
|
-
function LucideIcon({ name, size = 15 }) {
|
|
2618
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2619
|
-
...SVG_ATTRS,
|
|
2620
|
-
width: size,
|
|
2621
|
-
height: size,
|
|
2622
|
-
children: SHAPES[name]
|
|
2623
|
-
});
|
|
2624
|
-
}
|
|
2625
|
-
//#endregion
|
|
2626
|
-
//#region src/client/i18n.ts
|
|
2627
|
-
/**
|
|
2628
|
-
* Copy for dsh-select-quote, served through the host locale service.
|
|
2629
|
-
*
|
|
2630
|
-
* The active language is owned by `@deepseek-ai/dsh-client-locale` (Settings →
|
|
2631
|
-
* General → Language, persisted as `locale.preference`). This module owns only
|
|
2632
|
-
* the dictionaries and the plural form selection that the host's plain-template
|
|
2633
|
-
* lookup cannot express; it never resolves a language on its own.
|
|
2634
|
-
*
|
|
2635
|
-
* NOT localized on purpose:
|
|
2636
|
-
* - wire markers (`> [选中文本]`, `<response-annotations>`,
|
|
2637
|
-
* `:dsh-annotation{index="N"}`) — they are the message format the model
|
|
2638
|
-
* and the transcript parser agree on;
|
|
2639
|
-
* - the `# Response annotations:` header — model-facing instruction prose,
|
|
2640
|
-
* not UI text.
|
|
2641
|
-
*/
|
|
2642
|
-
const LOCALE_NS = "select-quote";
|
|
2643
|
-
const zh = {
|
|
2644
|
-
"common.selectedText": "选中的文本",
|
|
2645
|
-
"common.commentPlaceholder": "添加可选评论…",
|
|
2646
|
-
"common.cancel": "取消",
|
|
2647
|
-
"common.close": "关闭",
|
|
2648
|
-
"common.save": "保存",
|
|
2649
|
-
"toolbar.aria": "划词操作",
|
|
2650
|
-
"toolbar.commentHint": "添加批注…",
|
|
2651
|
-
"toolbar.copy": "复制",
|
|
2652
|
-
"toolbar.openLink": "打开链接",
|
|
2653
|
-
"toolbar.copyFailed": "复制失败",
|
|
2654
|
-
"toolbar.addToTask": "添加到任务",
|
|
2655
|
-
"toolbar.commentDialogAria": "添加可选评论",
|
|
2656
|
-
"toolbar.skipComment": "暂不评论",
|
|
2657
|
-
"toolbar.addComment": "添加评论",
|
|
2658
|
-
"menu.aria": "划词菜单",
|
|
2659
|
-
"menu.shortcutLabel": "打开划词菜单",
|
|
2660
|
-
"menu.addShortcutLabel": "将选中文字添加到任务",
|
|
2661
|
-
"menu.noSelection": "先在对话里选中一段文字",
|
|
2662
|
-
"menu.busy": "先完成当前批注",
|
|
2663
|
-
"summary.hint": "悬停查看批注",
|
|
2664
|
-
"summary.hintTouch": "点按查看批注",
|
|
2665
|
-
"summary.dialogAria": "{count} 条划词批注详情",
|
|
2666
|
-
"summary.edit": "编辑批注 {index}",
|
|
2667
|
-
"summary.remove": "移除批注 {index}",
|
|
2668
|
-
"summary.userComment": "用户评论",
|
|
2669
|
-
"summary.noComment": "未添加评论",
|
|
2670
|
-
"summary.aria.other": "{count} 条划词批注。展开查看详情。",
|
|
2671
|
-
"summary.title.other": "{count} 条批注",
|
|
2672
|
-
"summary.removeAll": "移除全部划词批注",
|
|
2673
|
-
"chip.annotation": "批注 {index}",
|
|
2674
|
-
"chip.previewAria": "批注预览",
|
|
2675
|
-
"chip.jump": "原文",
|
|
2676
|
-
"chip.jumpAria": "跳转到原文",
|
|
2677
|
-
"settings.trigger": "划词入口",
|
|
2678
|
-
"settings.triggerHint": "选中文字后何时显示操作条。触屏始终使用底部操作条。",
|
|
2679
|
-
"settings.trigger.auto": "自动浮出",
|
|
2680
|
-
"settings.trigger.contextmenu": "右键菜单",
|
|
2681
|
-
"settings.trigger.both": "两者都开",
|
|
2682
|
-
"settings.shortcut": "快捷键",
|
|
2683
|
-
"settings.shortcutHint": "开启后,选中文字即可使用下面的快捷键;可在宿主的「快捷键」设置里改键。",
|
|
2684
|
-
"settings.shortcutListAria": "当前快捷键",
|
|
2685
|
-
"settings.shortcutUnavailable": "快捷键暂不可用(可能被其它插件占用)",
|
|
2686
|
-
"settings.shortcutUnbound": "未绑定",
|
|
2687
|
-
"settings.shortcutConflict": "与其它快捷键冲突"
|
|
2688
|
-
};
|
|
2689
|
-
const en = {
|
|
2690
|
-
"common.selectedText": "Selected text",
|
|
2691
|
-
"common.commentPlaceholder": "Add an optional comment…",
|
|
2692
|
-
"common.cancel": "Cancel",
|
|
2693
|
-
"common.close": "Close",
|
|
2694
|
-
"common.save": "Save",
|
|
2695
|
-
"toolbar.aria": "Selection actions",
|
|
2696
|
-
"toolbar.commentHint": "Add annotation…",
|
|
2697
|
-
"toolbar.copy": "Copy",
|
|
2698
|
-
"toolbar.openLink": "Open link",
|
|
2699
|
-
"toolbar.copyFailed": "Copy failed",
|
|
2700
|
-
"toolbar.addToTask": "Add to task",
|
|
2701
|
-
"toolbar.commentDialogAria": "Add an optional comment",
|
|
2702
|
-
"toolbar.skipComment": "Skip comment",
|
|
2703
|
-
"toolbar.addComment": "Add comment",
|
|
2704
|
-
"menu.aria": "Selection menu",
|
|
2705
|
-
"menu.shortcutLabel": "Open selection menu",
|
|
2706
|
-
"menu.addShortcutLabel": "Add selected text to task",
|
|
2707
|
-
"menu.noSelection": "Select text in the conversation first",
|
|
2708
|
-
"menu.busy": "Finish the current annotation first",
|
|
2709
|
-
"summary.hint": "Hover to view annotations",
|
|
2710
|
-
"summary.hintTouch": "Tap to view annotations",
|
|
2711
|
-
"summary.dialogAria": "Annotation details: {count}",
|
|
2712
|
-
"summary.edit": "Edit annotation {index}",
|
|
2713
|
-
"summary.remove": "Remove annotation {index}",
|
|
2714
|
-
"summary.userComment": "User comment",
|
|
2715
|
-
"summary.noComment": "No comment added",
|
|
2716
|
-
"summary.aria.one": "{count} annotation. Open to view details.",
|
|
2717
|
-
"summary.aria.other": "{count} annotations. Open to view details.",
|
|
2718
|
-
"summary.title.one": "{count} annotation",
|
|
2719
|
-
"summary.title.other": "{count} annotations",
|
|
2720
|
-
"summary.removeAll": "Remove all annotations",
|
|
2721
|
-
"chip.annotation": "Annotation {index}",
|
|
2722
|
-
"chip.previewAria": "Annotation preview",
|
|
2723
|
-
"chip.jump": "Original",
|
|
2724
|
-
"chip.jumpAria": "Jump to original",
|
|
2725
|
-
"settings.trigger": "Trigger",
|
|
2726
|
-
"settings.triggerHint": "When the action bar appears after you select text. Touch screens always use the bottom bar.",
|
|
2727
|
-
"settings.trigger.auto": "Automatic",
|
|
2728
|
-
"settings.trigger.contextmenu": "Right-click",
|
|
2729
|
-
"settings.trigger.both": "Both",
|
|
2730
|
-
"settings.shortcut": "Shortcut",
|
|
2731
|
-
"settings.shortcutHint": "When on, the shortcuts below work on the selected text. Rebind them in the Host's Keyboard shortcuts settings.",
|
|
2732
|
-
"settings.shortcutListAria": "Active shortcuts",
|
|
2733
|
-
"settings.shortcutUnavailable": "Shortcuts are unavailable (another plugin may already use the keys).",
|
|
2734
|
-
"settings.shortcutUnbound": "Unbound",
|
|
2735
|
-
"settings.shortcutConflict": "Conflicts with another shortcut"
|
|
2736
|
-
};
|
|
2737
|
-
/**
|
|
2738
|
-
* Russian is not a shipped host locale: it is added through
|
|
2739
|
-
* `ctx.locale.addLanguage()` and its dictionary through the three-argument
|
|
2740
|
-
* `register()`. Its fallback chain ends at `en`, as the host requires.
|
|
2741
|
-
*/
|
|
2742
|
-
const ru = {
|
|
2743
|
-
"common.selectedText": "Выделенный текст",
|
|
2744
|
-
"common.commentPlaceholder": "Добавить необязательный комментарий…",
|
|
2745
|
-
"common.cancel": "Отмена",
|
|
2746
|
-
"common.close": "Закрыть",
|
|
2747
|
-
"common.save": "Сохранить",
|
|
2748
|
-
"toolbar.aria": "Действия с выделенным текстом",
|
|
2749
|
-
"toolbar.commentHint": "Добавить аннотацию…",
|
|
2750
|
-
"toolbar.copy": "Копировать",
|
|
2751
|
-
"toolbar.openLink": "Открыть ссылку",
|
|
2752
|
-
"toolbar.copyFailed": "Не удалось скопировать",
|
|
2753
|
-
"toolbar.addToTask": "Добавить к задаче",
|
|
2754
|
-
"toolbar.commentDialogAria": "Добавить необязательный комментарий",
|
|
2755
|
-
"toolbar.skipComment": "Без комментария",
|
|
2756
|
-
"toolbar.addComment": "Добавить комментарий",
|
|
2757
|
-
"menu.aria": "Меню выделения",
|
|
2758
|
-
"menu.shortcutLabel": "Открыть меню выделения",
|
|
2759
|
-
"menu.addShortcutLabel": "Добавить выделенный текст к задаче",
|
|
2760
|
-
"menu.noSelection": "Сначала выделите текст в диалоге",
|
|
2761
|
-
"menu.busy": "Сначала завершите текущую аннотацию",
|
|
2762
|
-
"summary.hint": "Наведите, чтобы увидеть аннотации",
|
|
2763
|
-
"summary.hintTouch": "Нажмите, чтобы увидеть аннотации",
|
|
2764
|
-
"summary.dialogAria": "Детали аннотаций: {count}",
|
|
2765
|
-
"summary.edit": "Изменить аннотацию {index}",
|
|
2766
|
-
"summary.remove": "Удалить аннотацию {index}",
|
|
2767
|
-
"summary.userComment": "Комментарий пользователя",
|
|
2768
|
-
"summary.noComment": "Комментарий не добавлен",
|
|
2769
|
-
"summary.aria.one": "{count} аннотация. Откройте, чтобы увидеть подробности.",
|
|
2770
|
-
"summary.aria.few": "{count} аннотации. Откройте, чтобы увидеть подробности.",
|
|
2771
|
-
"summary.aria.many": "{count} аннотаций. Откройте, чтобы увидеть подробности.",
|
|
2772
|
-
"summary.aria.other": "{count} аннотаций. Откройте, чтобы увидеть подробности.",
|
|
2773
|
-
"summary.title.one": "{count} аннотация",
|
|
2774
|
-
"summary.title.few": "{count} аннотации",
|
|
2775
|
-
"summary.title.many": "{count} аннотаций",
|
|
2776
|
-
"summary.title.other": "{count} аннотаций",
|
|
2777
|
-
"summary.removeAll": "Удалить все аннотации",
|
|
2778
|
-
"chip.annotation": "Аннотация {index}",
|
|
2779
|
-
"chip.previewAria": "Просмотр аннотации",
|
|
2780
|
-
"chip.jump": "Оригинал",
|
|
2781
|
-
"chip.jumpAria": "Перейти к оригиналу",
|
|
2782
|
-
"settings.trigger": "Триггер",
|
|
2783
|
-
"settings.triggerHint": "Когда показывать панель действий после выделения текста. На сенсорных экранах всегда используется нижняя панель.",
|
|
2784
|
-
"settings.trigger.auto": "Автоматически",
|
|
2785
|
-
"settings.trigger.contextmenu": "По правому клику",
|
|
2786
|
-
"settings.trigger.both": "Оба способа",
|
|
2787
|
-
"settings.shortcut": "Горячая клавиша",
|
|
2788
|
-
"settings.shortcutHint": "Когда включено, сочетания ниже работают с выделенным текстом. Изменить их можно в настройках горячих клавиш.",
|
|
2789
|
-
"settings.shortcutListAria": "Текущие сочетания",
|
|
2790
|
-
"settings.shortcutUnavailable": "Сочетания недоступны (возможно, клавиши заняты другим плагином).",
|
|
2791
|
-
"settings.shortcutUnbound": "Не назначено",
|
|
2792
|
-
"settings.shortcutConflict": "Конфликтует с другим сочетанием"
|
|
2793
|
-
};
|
|
2794
|
-
/** Both shipped host locales, required together by the typed `register()`. */
|
|
2795
|
-
const SHIPPED_DICTS = Object.freeze({
|
|
2796
|
-
zh,
|
|
2797
|
-
en
|
|
2798
|
-
});
|
|
2799
|
-
/** Dictionary for the language registered through {@link LANGUAGE_RU}. */
|
|
2800
|
-
const DICTIONARIES = Object.freeze({
|
|
2801
|
-
zh,
|
|
2802
|
-
en,
|
|
2803
|
-
ru
|
|
2804
|
-
});
|
|
2805
|
-
const LANGUAGE_RU = Object.freeze({
|
|
2806
|
-
id: "ru",
|
|
2807
|
-
label: "Русский",
|
|
2808
|
-
fallback: "en"
|
|
2809
|
-
});
|
|
2810
|
-
let api;
|
|
2811
|
-
/**
|
|
2812
|
-
* Install the host-bound translation entry point. Called once from `apply()`
|
|
2813
|
-
* with `ctx.locale.bind(LOCALE_NS)` and the live snapshot accessors.
|
|
2814
|
-
*/
|
|
2815
|
-
function bindLocale(next) {
|
|
2816
|
-
api = next;
|
|
2881
|
+
setRuntimeSession(sessionId);
|
|
2882
|
+
queueMicrotask(() => adoptPersistedAnnotations(sessionId));
|
|
2817
2883
|
}
|
|
2818
|
-
/**
|
|
2819
|
-
|
|
2820
|
-
|
|
2884
|
+
/**
|
|
2885
|
+
* Adopt persisted annotations for a session.
|
|
2886
|
+
*
|
|
2887
|
+
* In-memory entries win on conflict — they can only be newer, because anything
|
|
2888
|
+
* added after the bind is persisted immediately. Annotations parked in the
|
|
2889
|
+
* memory-only bucket (added before the bind) move over as well.
|
|
2890
|
+
*/
|
|
2891
|
+
function adoptPersistedAnnotations(sessionId) {
|
|
2892
|
+
if (!persistence) return;
|
|
2893
|
+
const stored = persistence.load(sessionId);
|
|
2894
|
+
const current = bySession.get(sessionId) ?? EMPTY$1;
|
|
2895
|
+
const orphaned = bySession.get(MEMORY_KEY) ?? EMPTY$1;
|
|
2896
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2897
|
+
for (const item of stored) merged.set(item.id, item);
|
|
2898
|
+
for (const item of [...current, ...orphaned]) merged.set(item.id, item);
|
|
2899
|
+
const next = [...merged.values()];
|
|
2900
|
+
if (next.length === current.length && next.every((item, i) => item.id === current[i]?.id) && orphaned.length === 0) return;
|
|
2901
|
+
bySession.set(sessionId, next);
|
|
2902
|
+
if (orphaned.length > 0) bySession.delete(MEMORY_KEY);
|
|
2903
|
+
notify();
|
|
2821
2904
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
const
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
|
|
2829
|
-
return "many";
|
|
2830
|
-
}
|
|
2831
|
-
if (locale === "en") return count === 1 ? "one" : "other";
|
|
2832
|
-
return "other";
|
|
2905
|
+
/** Same ids in the same order, with the only mutable field (comment) equal. */
|
|
2906
|
+
function sameAnnotations(a, b) {
|
|
2907
|
+
return a.length === b.length && a.every((item, i) => {
|
|
2908
|
+
const other = b[i];
|
|
2909
|
+
return other !== void 0 && item.id === other.id && (item.comment ?? "") === (other.comment ?? "");
|
|
2910
|
+
});
|
|
2833
2911
|
}
|
|
2834
2912
|
/**
|
|
2835
|
-
*
|
|
2836
|
-
*
|
|
2913
|
+
* Re-read a session after another tab wrote its storage key.
|
|
2914
|
+
*
|
|
2915
|
+
* Storage is the shared source of truth: every local mutation persists
|
|
2916
|
+
* synchronously, so this tab's memory can only be stale relative to it. The
|
|
2917
|
+
* stored array replaces the local one (last writer wins) — merging would
|
|
2918
|
+
* resurrect annotations the other tab already sent and cleared.
|
|
2919
|
+
*
|
|
2920
|
+
* `null` is the `localStorage.clear()` event: reload every partition this tab
|
|
2921
|
+
* holds. Sessions that are neither cached nor active are ignored; binding the
|
|
2922
|
+
* session later reads storage anyway.
|
|
2837
2923
|
*/
|
|
2838
|
-
function
|
|
2839
|
-
if (!
|
|
2840
|
-
if (
|
|
2841
|
-
|
|
2842
|
-
const
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2924
|
+
function adoptStorageAnnotations(sessionId) {
|
|
2925
|
+
if (!persistence) return;
|
|
2926
|
+
if (sessionId === null) {
|
|
2927
|
+
let changed = false;
|
|
2928
|
+
for (const key of [...bySession.keys()]) {
|
|
2929
|
+
if (key === MEMORY_KEY) continue;
|
|
2930
|
+
const stored = persistence.load(key);
|
|
2931
|
+
const current = bySession.get(key);
|
|
2932
|
+
if (stored.length === 0) {
|
|
2933
|
+
bySession.delete(key);
|
|
2934
|
+
changed = true;
|
|
2935
|
+
} else if (current === void 0 || !sameAnnotations(stored, current)) {
|
|
2936
|
+
bySession.set(key, [...stored]);
|
|
2937
|
+
changed = true;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if (changed) notify();
|
|
2941
|
+
return;
|
|
2846
2942
|
}
|
|
2847
|
-
|
|
2943
|
+
if (!bySession.has(sessionId) && sessionId !== runtime.sessionId) return;
|
|
2944
|
+
const stored = persistence.load(sessionId);
|
|
2945
|
+
if (sameAnnotations(stored, bySession.get(sessionId) ?? EMPTY$1)) return;
|
|
2946
|
+
if (stored.length === 0) bySession.delete(sessionId);
|
|
2947
|
+
else bySession.set(sessionId, [...stored]);
|
|
2948
|
+
notify();
|
|
2949
|
+
}
|
|
2950
|
+
function annotationTitle(annotation) {
|
|
2951
|
+
return previewTitle(annotation.text);
|
|
2848
2952
|
}
|
|
2953
|
+
//#endregion
|
|
2954
|
+
//#region src/client/dom/popoverPlacement.ts
|
|
2955
|
+
const GAP$2 = 8;
|
|
2956
|
+
const MARGIN = 8;
|
|
2849
2957
|
/**
|
|
2850
|
-
*
|
|
2851
|
-
*
|
|
2852
|
-
* Subscribing before the host service is bound is a no-op, so callers subscribe
|
|
2853
|
-
* from `apply()`, after `bindLocale()`.
|
|
2958
|
+
* Place `panel` near `anchor`, flipping vertical side when the preferred side
|
|
2959
|
+
* lacks room. Keeps the panel inside `bounds` (the viewport by default).
|
|
2854
2960
|
*/
|
|
2855
|
-
function
|
|
2856
|
-
|
|
2961
|
+
function placePopover(anchor, panel, preferred = "bottom", gap = GAP$2, bounds) {
|
|
2962
|
+
const box = bounds ?? {
|
|
2963
|
+
top: 0,
|
|
2964
|
+
left: 0,
|
|
2965
|
+
right: window.innerWidth,
|
|
2966
|
+
bottom: window.innerHeight
|
|
2967
|
+
};
|
|
2968
|
+
const spaceBelow = box.bottom - anchor.bottom;
|
|
2969
|
+
const spaceAbove = anchor.top - box.top;
|
|
2970
|
+
let side = preferred;
|
|
2971
|
+
const need = panel.height + gap;
|
|
2972
|
+
if (preferred === "bottom" && spaceBelow < need && spaceAbove > spaceBelow) side = "top";
|
|
2973
|
+
else if (preferred === "top" && spaceAbove < need && spaceBelow > spaceAbove) side = "bottom";
|
|
2974
|
+
return {
|
|
2975
|
+
top: side === "bottom" ? Math.min(anchor.bottom + gap, box.bottom - panel.height - MARGIN) : Math.max(anchor.top - panel.height - gap, box.top + MARGIN),
|
|
2976
|
+
left: Math.min(Math.max(anchor.left, box.left + MARGIN), Math.max(box.left + MARGIN, box.right - panel.width - MARGIN)),
|
|
2977
|
+
side
|
|
2978
|
+
};
|
|
2857
2979
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2980
|
+
//#endregion
|
|
2981
|
+
//#region src/client/ui/icons.tsx
|
|
2982
|
+
const SVG_ATTRS = {
|
|
2983
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2984
|
+
viewBox: "0 0 24 24",
|
|
2985
|
+
fill: "none",
|
|
2986
|
+
stroke: "currentColor",
|
|
2987
|
+
strokeWidth: 2,
|
|
2988
|
+
strokeLinecap: "round",
|
|
2989
|
+
strokeLinejoin: "round",
|
|
2990
|
+
"aria-hidden": true,
|
|
2991
|
+
focusable: false
|
|
2992
|
+
};
|
|
2993
|
+
const SHAPES = {
|
|
2994
|
+
copy: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2995
|
+
width: 14,
|
|
2996
|
+
height: 14,
|
|
2997
|
+
x: 8,
|
|
2998
|
+
y: 8,
|
|
2999
|
+
rx: 2,
|
|
3000
|
+
ry: 2
|
|
3001
|
+
}), /* @__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" })] }),
|
|
3002
|
+
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" })] }),
|
|
3003
|
+
"trash-2": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
3004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 11v6" }),
|
|
3005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 11v6" }),
|
|
3006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }),
|
|
3007
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
3008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
|
|
3009
|
+
] }),
|
|
3010
|
+
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" })] }),
|
|
3011
|
+
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" })] }),
|
|
3012
|
+
"external-link": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
3013
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 3h6v6" }),
|
|
3014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 14 21 3" }),
|
|
3015
|
+
/* @__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" })
|
|
3016
|
+
] })
|
|
3017
|
+
};
|
|
3018
|
+
function LucideIcon({ name, size = 15 }) {
|
|
3019
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3020
|
+
...SVG_ATTRS,
|
|
3021
|
+
width: size,
|
|
3022
|
+
height: size,
|
|
3023
|
+
children: SHAPES[name]
|
|
3024
|
+
});
|
|
2861
3025
|
}
|
|
2862
3026
|
//#endregion
|
|
2863
3027
|
//#region src/client/useLocale.ts
|
|
@@ -5293,6 +5457,7 @@ let onScroll = null;
|
|
|
5293
5457
|
let onResize = null;
|
|
5294
5458
|
let leavingBox = null;
|
|
5295
5459
|
let leavingTimer = 0;
|
|
5460
|
+
let markBindingCleanup = null;
|
|
5296
5461
|
function cancelTimers() {
|
|
5297
5462
|
if (showTimer !== 0) window.clearTimeout(showTimer);
|
|
5298
5463
|
if (hideTimer !== 0) window.clearTimeout(hideTimer);
|
|
@@ -5453,12 +5618,14 @@ function jumpToMark(event) {
|
|
|
5453
5618
|
}
|
|
5454
5619
|
if (text) scrollToAnnotation(index, text);
|
|
5455
5620
|
}
|
|
5456
|
-
function buildPreview(index, quoteText, comment) {
|
|
5621
|
+
function buildPreview(index, quoteText, comment, withJump = true) {
|
|
5457
5622
|
const box = document.createElement("div");
|
|
5458
5623
|
box.className = cx(A.col, styles.chipPreview);
|
|
5459
5624
|
box.setAttribute("data-dsq-chip-preview", "true");
|
|
5460
5625
|
box.setAttribute("role", "dialog");
|
|
5461
5626
|
box.setAttribute("aria-label", t("chip.previewAria"));
|
|
5627
|
+
box.tabIndex = -1;
|
|
5628
|
+
jumpButton = null;
|
|
5462
5629
|
const body = document.createElement("div");
|
|
5463
5630
|
body.className = styles.chipPreviewBody;
|
|
5464
5631
|
body.style.animation = "none";
|
|
@@ -5467,18 +5634,21 @@ function buildPreview(index, quoteText, comment) {
|
|
|
5467
5634
|
const title = document.createElement("span");
|
|
5468
5635
|
title.className = styles.chipPreviewTitle;
|
|
5469
5636
|
title.textContent = t("chip.annotation", { index });
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5637
|
+
head.append(title);
|
|
5638
|
+
if (withJump) {
|
|
5639
|
+
jumpButton = document.createElement("button");
|
|
5640
|
+
jumpButton.type = "button";
|
|
5641
|
+
jumpButton.className = cx(A.plain, A.inlineRow, styles.chipPreviewJump);
|
|
5642
|
+
jumpButton.setAttribute("data-dsq-chip-jump", "true");
|
|
5643
|
+
jumpButton.setAttribute("aria-label", t("chip.jumpAria"));
|
|
5644
|
+
jumpButton.title = t("chip.jumpAria");
|
|
5645
|
+
jumpButton.insertAdjacentHTML("afterbegin", JUMP_ICON);
|
|
5646
|
+
const jumpLabel = document.createElement("span");
|
|
5647
|
+
jumpLabel.textContent = t("chip.jump");
|
|
5648
|
+
jumpButton.append(jumpLabel);
|
|
5649
|
+
jumpButton.addEventListener("click", jumpToMark);
|
|
5650
|
+
head.append(jumpButton);
|
|
5651
|
+
}
|
|
5482
5652
|
body.append(head);
|
|
5483
5653
|
appendLabel(body, t("common.selectedText"));
|
|
5484
5654
|
appendText(body, styles.chipPreviewQuote, quoteText);
|
|
@@ -5505,6 +5675,39 @@ function buildPreview(index, quoteText, comment) {
|
|
|
5505
5675
|
});
|
|
5506
5676
|
return box;
|
|
5507
5677
|
}
|
|
5678
|
+
/** Show and watch one preview; `activeChip` is the element it belongs to. */
|
|
5679
|
+
function mountPreview(box, anchor) {
|
|
5680
|
+
preview = box;
|
|
5681
|
+
box.style.visibility = "hidden";
|
|
5682
|
+
document.body.append(box);
|
|
5683
|
+
const { top, left } = placePopover(anchor, {
|
|
5684
|
+
width: box.offsetWidth || 320,
|
|
5685
|
+
height: box.offsetHeight || 180
|
|
5686
|
+
}, "top", GAP);
|
|
5687
|
+
box.style.top = `${top}px`;
|
|
5688
|
+
box.style.left = `${left}px`;
|
|
5689
|
+
box.style.visibility = "";
|
|
5690
|
+
const animated = box.querySelector(`.${styles.chipPreviewBody}`);
|
|
5691
|
+
if (animated) animated.style.animation = "";
|
|
5692
|
+
onDocPointerDown = (event) => {
|
|
5693
|
+
const target = event.target;
|
|
5694
|
+
if (!(target instanceof Node)) return;
|
|
5695
|
+
if (box.contains(target) || activeChip?.contains(target)) return;
|
|
5696
|
+
closeChipPreview(false);
|
|
5697
|
+
};
|
|
5698
|
+
document.addEventListener("pointerdown", onDocPointerDown, true);
|
|
5699
|
+
onScroll = scheduleReposition;
|
|
5700
|
+
document.addEventListener("scroll", onScroll, true);
|
|
5701
|
+
onResize = scheduleReposition;
|
|
5702
|
+
window.addEventListener("resize", onResize);
|
|
5703
|
+
observer = new MutationObserver(() => {
|
|
5704
|
+
if (activeChip && !activeChip.isConnected) closeChipPreview(false);
|
|
5705
|
+
});
|
|
5706
|
+
observer.observe(document.body, {
|
|
5707
|
+
childList: true,
|
|
5708
|
+
subtree: true
|
|
5709
|
+
});
|
|
5710
|
+
}
|
|
5508
5711
|
function showChipPreview(chip, index, nextPinned) {
|
|
5509
5712
|
cancelTimers();
|
|
5510
5713
|
if (!chip.isConnected) return;
|
|
@@ -5538,38 +5741,114 @@ function showChipPreview(chip, index, nextPinned) {
|
|
|
5538
5741
|
activeText = text;
|
|
5539
5742
|
activeIndex = index;
|
|
5540
5743
|
pinned = nextPinned;
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5744
|
+
mountPreview(buildPreview(index, text, quote ? (quote.comment ?? "").trim() : null), chip.getBoundingClientRect());
|
|
5745
|
+
}
|
|
5746
|
+
/**
|
|
5747
|
+
* Resolve the annotation behind a transcript mark. Durable marks carry a
|
|
5748
|
+
* `dqsm_` id and read their quote from the user turn's registry; pending marks
|
|
5749
|
+
* carry the store id and read the live list, so a comment edited after
|
|
5750
|
+
* decoration still shows up.
|
|
5751
|
+
*/
|
|
5752
|
+
function annotationOfMark(mark) {
|
|
5753
|
+
const id = mark.getAttribute("data-dsq-ann-mark") ?? "";
|
|
5754
|
+
const index = Number(mark.getAttribute("data-dsq-ann-index") ?? 0);
|
|
5755
|
+
if (index <= 0) return null;
|
|
5756
|
+
if (id.startsWith("dqsm_")) {
|
|
5757
|
+
const ownerKey = quotesOwnerOf(mark);
|
|
5758
|
+
const quote = ownerKey ? messageQuotes(ownerKey)?.[index - 1] : void 0;
|
|
5759
|
+
if (!ownerKey || !quote) return null;
|
|
5760
|
+
const text = quote.text.trim();
|
|
5761
|
+
return text ? {
|
|
5762
|
+
text,
|
|
5763
|
+
comment: (quote.comment ?? "").trim(),
|
|
5764
|
+
ownerKey,
|
|
5765
|
+
messageId: quote.messageId ?? null
|
|
5766
|
+
} : null;
|
|
5767
|
+
}
|
|
5768
|
+
const item = getAnnotations().find((annotation) => annotation.id === id);
|
|
5769
|
+
if (!item) return null;
|
|
5770
|
+
const text = item.text.trim();
|
|
5771
|
+
return text ? {
|
|
5772
|
+
text,
|
|
5773
|
+
comment: (item.comment ?? "").trim(),
|
|
5774
|
+
ownerKey: null,
|
|
5775
|
+
messageId: item.sources[0]?.messageId ?? null
|
|
5776
|
+
} : null;
|
|
5777
|
+
}
|
|
5778
|
+
function showMarkPreview(mark, focus) {
|
|
5779
|
+
if (!mark.isConnected) return;
|
|
5780
|
+
const index = Number(mark.getAttribute("data-dsq-ann-index") ?? 0);
|
|
5781
|
+
if (index <= 0) return;
|
|
5782
|
+
if (activeChip === mark && preview) {
|
|
5783
|
+
pinned = true;
|
|
5784
|
+
return;
|
|
5785
|
+
}
|
|
5786
|
+
const data = annotationOfMark(mark);
|
|
5787
|
+
if (!data) return;
|
|
5788
|
+
cancelTimers();
|
|
5789
|
+
closeChipPreview(false);
|
|
5790
|
+
flushLeavingPreview();
|
|
5791
|
+
activeChip = mark;
|
|
5792
|
+
activeMark = mark;
|
|
5793
|
+
activeOwnerKey = data.ownerKey;
|
|
5794
|
+
activeMessageId = data.messageId;
|
|
5795
|
+
activeText = data.text;
|
|
5796
|
+
activeIndex = index;
|
|
5797
|
+
pinned = true;
|
|
5798
|
+
const box = buildPreview(index, data.text, data.comment, false);
|
|
5799
|
+
mountPreview(box, mark.getBoundingClientRect());
|
|
5800
|
+
if (focus) box.focus({ preventScroll: true });
|
|
5801
|
+
}
|
|
5802
|
+
/**
|
|
5803
|
+
* Click/keyboard previews for the ordinal badge fragments in transcript
|
|
5804
|
+
* marks. Delegated on the document: marks are replaced by host re-renders and
|
|
5805
|
+
* binding every fragment would leak listeners across those passes.
|
|
5806
|
+
*/
|
|
5807
|
+
function bindAnnotationMarkPreview() {
|
|
5808
|
+
if (typeof document === "undefined") return () => {};
|
|
5809
|
+
if (markBindingCleanup) return markBindingCleanup;
|
|
5810
|
+
const markOf = (target) => {
|
|
5811
|
+
if (!(target instanceof Element)) return null;
|
|
5812
|
+
return target.closest("[data-dsq-ann-mark]");
|
|
5548
5813
|
};
|
|
5549
|
-
const
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5814
|
+
const onClick = (event) => {
|
|
5815
|
+
const mark = markOf(event.target);
|
|
5816
|
+
if (!mark) return;
|
|
5817
|
+
const index = Number(mark.getAttribute("data-dsq-ann-index"));
|
|
5818
|
+
if (!Number.isFinite(index) || index <= 0) return;
|
|
5819
|
+
if (event.detail !== 0) {
|
|
5820
|
+
const offset = {
|
|
5821
|
+
dx: Number.parseFloat(mark.style.getPropertyValue("--dsq-ann-badge-x")) || 0,
|
|
5822
|
+
dy: Number.parseFloat(mark.style.getPropertyValue("--dsq-ann-badge-y")) || 0
|
|
5823
|
+
};
|
|
5824
|
+
if (!badgeContainsPoint(badgeHitBox(mark.getBoundingClientRect(), index, offset), event.clientX, event.clientY)) return;
|
|
5825
|
+
}
|
|
5826
|
+
const selection = window.getSelection();
|
|
5827
|
+
if (selection && !selection.isCollapsed) return;
|
|
5828
|
+
showMarkPreview(mark, false);
|
|
5560
5829
|
};
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5830
|
+
const onKeyDown = (event) => {
|
|
5831
|
+
const mark = markOf(event.target);
|
|
5832
|
+
if (!mark) return;
|
|
5833
|
+
if (event.key === "Escape" && preview !== null) {
|
|
5834
|
+
event.preventDefault();
|
|
5835
|
+
closeChipPreview(true);
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5838
|
+
if ((event.key === "Enter" || event.key === " ") && mark.hasAttribute("data-dsq-ann-index")) {
|
|
5839
|
+
event.preventDefault();
|
|
5840
|
+
showMarkPreview(mark, true);
|
|
5841
|
+
}
|
|
5842
|
+
};
|
|
5843
|
+
document.addEventListener("click", onClick);
|
|
5844
|
+
document.addEventListener("keydown", onKeyDown);
|
|
5845
|
+
const cleanup = () => {
|
|
5846
|
+
document.removeEventListener("click", onClick);
|
|
5847
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
5848
|
+
markBindingCleanup = null;
|
|
5849
|
+
};
|
|
5850
|
+
markBindingCleanup = cleanup;
|
|
5851
|
+
return cleanup;
|
|
5573
5852
|
}
|
|
5574
5853
|
/** Close the preview. `restoreFocus` is for a deliberate Escape. */
|
|
5575
5854
|
function closeChipPreview(restoreFocus) {
|
|
@@ -6253,7 +6532,10 @@ function apply(ctx) {
|
|
|
6253
6532
|
getRevision: () => ctx.locale.getSnapshot().revision,
|
|
6254
6533
|
subscribe: (listener) => ctx.locale.subscribe(listener)
|
|
6255
6534
|
});
|
|
6256
|
-
ctx.effect(() => subscribeLocale(() =>
|
|
6535
|
+
ctx.effect(() => subscribeLocale(() => {
|
|
6536
|
+
refreshDirectiveChips();
|
|
6537
|
+
refreshAnnotationMarkLabels();
|
|
6538
|
+
}), "dsh-select-quote: chip language");
|
|
6257
6539
|
ctx.inject(["configForms"], (scope) => {
|
|
6258
6540
|
scope.effect(() => attachConfigForm(scope.configForms.get(ROW_ID)), "dsh-select-quote: config");
|
|
6259
6541
|
const form = scope.configForms.get(ROW_ID);
|
|
@@ -6269,6 +6551,7 @@ function apply(ctx) {
|
|
|
6269
6551
|
});
|
|
6270
6552
|
ctx.effect(() => watchAnnotationDirectives(), "dsh-select-quote: annotation directives");
|
|
6271
6553
|
ctx.effect(() => watchAnnotationMarks(), "dsh-select-quote: annotation marks");
|
|
6554
|
+
ctx.effect(() => bindAnnotationMarkPreview(), "dsh-select-quote: mark preview");
|
|
6272
6555
|
ctx.effect(() => watchSubmissionEcho(), "dsh-select-quote: submission echo");
|
|
6273
6556
|
ctx.effect(() => {
|
|
6274
6557
|
syncPendingAnnotationJobs(getAnnotations());
|