@shellraining/dsh-local-annotation 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1824 @@
1
+ // Generated from TypeScript by pnpm run build.
2
+ window.__ModuleLoader__.load({
3
+ id: "@shellraining/dsh-local-annotation",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name2 in all)
14
+ __defProp(target, name2, { get: all[name2], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/client.tsx
27
+ var client_exports = {};
28
+ __export(client_exports, {
29
+ apply: () => apply,
30
+ inject: () => inject,
31
+ name: () => name
32
+ });
33
+ module.exports = __toCommonJS(client_exports);
34
+ var import_client = require("react-dom/client");
35
+
36
+ // src/appearance.ts
37
+ var STROKES = [
38
+ { id: "marker", label: "荧光笔" },
39
+ { id: "underline", label: "实线" },
40
+ { id: "dashed", label: "虚线" },
41
+ { id: "dotted", label: "点线" },
42
+ { id: "wavy", label: "波浪线" }
43
+ ];
44
+ var ICONS = [
45
+ { id: "message", label: "气泡" },
46
+ { id: "bookmark", label: "书签" },
47
+ { id: "note", label: "便签" },
48
+ { id: "number", label: "纯编号" }
49
+ ];
50
+ var COLORS = [
51
+ { value: "theme", label: "主题色" },
52
+ { value: "#fbf719", label: "柠檬黄" },
53
+ { value: "#77dba9", label: "薄荷绿" },
54
+ { value: "#f5a6c8", label: "樱花粉" },
55
+ { value: "#b9a3f5", label: "薰衣紫" },
56
+ { value: "#80c8f5", label: "晴空蓝" }
57
+ ];
58
+ var DEFAULT_APPEARANCE = { stroke: "marker", color: "#fbf719", icon: "message" };
59
+ var PREFIX = "dsh-local-annotation.appearance.v1.";
60
+ var FIELDS = ["stroke", "color", "icon"];
61
+ function valid(field, value) {
62
+ if (typeof value !== "string") return false;
63
+ if (field === "color") return value === "theme" || /^#[\da-f]{6}$/i.test(value);
64
+ return (field === "stroke" ? STROKES : ICONS).some((option) => option.id === value);
65
+ }
66
+ function ink(color) {
67
+ return color === "theme" ? "var(--dsw-alias-brand-primary)" : /^#[\da-f]{6}$/i.test(color) ? color : DEFAULT_APPEARANCE.color;
68
+ }
69
+ function highlightCSS(stroke, color) {
70
+ const tint = ink(color);
71
+ if (stroke === "marker") return `background-color:color-mix(in srgb, ${tint} 32%, transparent);text-decoration:none;`;
72
+ const line = stroke === "underline" ? "solid" : stroke;
73
+ return `background-color:transparent;text-decoration:underline ${line} ${tint};text-decoration-thickness:2px;text-underline-offset:.2em;`;
74
+ }
75
+ var AppearanceStore = class {
76
+ constructor(changed) {
77
+ this.changed = changed;
78
+ this.read();
79
+ this.style.dataset.localAnnotationAppearance = "";
80
+ this.render();
81
+ document.head.append(this.style);
82
+ window.addEventListener("storage", this.sync);
83
+ }
84
+ changed;
85
+ state = { ...DEFAULT_APPEARANCE };
86
+ style = document.createElement("style");
87
+ sync = (event) => {
88
+ if (event.key !== null && !FIELDS.some((field) => event.key === PREFIX + field)) return;
89
+ try {
90
+ if (event.storageArea !== window.localStorage) return;
91
+ } catch {
92
+ return;
93
+ }
94
+ this.read();
95
+ this.render();
96
+ this.changed(this.state);
97
+ };
98
+ getSnapshot() {
99
+ return this.state;
100
+ }
101
+ read() {
102
+ const next = { ...DEFAULT_APPEARANCE };
103
+ try {
104
+ for (const field of FIELDS) {
105
+ const value = window.localStorage.getItem(PREFIX + field);
106
+ if (valid(field, value)) Object.assign(next, { [field]: value });
107
+ }
108
+ } catch {
109
+ }
110
+ this.state = next;
111
+ }
112
+ set(field, value) {
113
+ if (!valid(field, value)) throw new Error("无效的批注外观设置。");
114
+ try {
115
+ window.localStorage.setItem(PREFIX + field, value);
116
+ } catch {
117
+ throw new Error("无法保存外观设置,请检查浏览器存储权限或可用空间。");
118
+ }
119
+ this.read();
120
+ this.render();
121
+ this.changed(this.state);
122
+ }
123
+ render() {
124
+ const { stroke, color, icon } = this.state;
125
+ this.style.textContent = `
126
+ ::highlight(dsh-local-annotation) { ${highlightCSS(stroke, color)} }
127
+ [data-local-annotation-marker], [data-local-annotation-status] .dla-number {
128
+ --dla-ink:${ink(color)}; --dla-marker-mask:var(--dla-mask-${icon});
129
+ --dla-marker-outline:var(--dla-outline-${icon});
130
+ --dla-marker-radius:${icon === "number" ? "var(--dsw-radius-sm)" : "0"};
131
+ }
132
+ `;
133
+ }
134
+ dispose() {
135
+ window.removeEventListener("storage", this.sync);
136
+ this.style.remove();
137
+ }
138
+ };
139
+
140
+ // src/model.ts
141
+ var MAX_ITEMS = 200;
142
+ var MAX_TEXT = 16e3;
143
+ var MAX_NOTE = 8e3;
144
+ var MAX_ID = 1024;
145
+ var MAX_SOURCE = 8192;
146
+ function boundedString(value, name2, max, nonempty = false) {
147
+ if (typeof value !== "string" || value.length > max || nonempty && !value.trim()) {
148
+ throw new TypeError(`${name2} 必须是${nonempty ? "非空" : ""}字符串,且不超过 ${max} 个字符`);
149
+ }
150
+ return value;
151
+ }
152
+ function annotationKey(value, name2) {
153
+ return boundedString(value, name2, MAX_ID, true);
154
+ }
155
+ function object(value, name2) {
156
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${name2} 格式无效`);
157
+ return value;
158
+ }
159
+ function annotationSource(value, sessionId) {
160
+ const source = object(value, "批注来源");
161
+ if (source.kind === "message") return { kind: "message", key: boundedString(source.key, "消息来源", MAX_SOURCE, true) };
162
+ if (source.kind !== "file") throw new TypeError("批注必须具有明确的消息或文件来源");
163
+ const url = boundedString(source.url, "文件来源 URL", MAX_SOURCE, true);
164
+ const path = boundedString(source.path, "文件来源路径", MAX_SOURCE, true);
165
+ if (/[?#\u0000-\u001f\u007f]/.test(url) || /[\u0000-\u001f\u007f]/.test(path)) throw new TypeError("文件来源包含无效字符");
166
+ const match = /^dsh-resource:\/\/file\/session\/([^/]+)\/(.+)$/.exec(url);
167
+ let owner;
168
+ let decodedPath;
169
+ try {
170
+ owner = match?.[1] === void 0 ? void 0 : decodeURIComponent(match[1]);
171
+ decodedPath = match?.[2]?.split("/").map(decodeURIComponent).join("/");
172
+ } catch {
173
+ throw new TypeError("文件来源 URL 编码无效");
174
+ }
175
+ if (owner !== sessionId || decodedPath !== path) throw new TypeError("文件来源必须属于当前会话且与路径一致");
176
+ return { kind: "file", url, path };
177
+ }
178
+ function timestamp(value, name2) {
179
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name2} 必须是非负安全整数`);
180
+ return value;
181
+ }
182
+ function copyAnnotation(value) {
183
+ const item = object(value, "批注");
184
+ const sessionId = annotationKey(item.sessionId, "会话 ID");
185
+ const text = boundedString(item.text, "批注原文", MAX_TEXT, true);
186
+ const start = item.start;
187
+ const end = item.end;
188
+ const unknownPosition = start === -1 && end === -1;
189
+ if (!unknownPosition && (typeof start !== "number" || typeof end !== "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start)) {
190
+ throw new TypeError("批注位置必须是有效区间或 -1/-1");
191
+ }
192
+ const createdAt = timestamp(item.createdAt, "创建时间");
193
+ const updatedAt = timestamp(item.updatedAt, "更新时间");
194
+ if (updatedAt < createdAt) throw new TypeError("更新时间不能早于创建时间");
195
+ return {
196
+ id: annotationKey(item.id, "批注 ID"),
197
+ sessionId,
198
+ text,
199
+ note: boundedString(item.note, "批注内容", MAX_NOTE),
200
+ source: annotationSource(item.source, sessionId),
201
+ start,
202
+ end,
203
+ before: boundedString(item.before, "前文", MAX_TEXT),
204
+ after: boundedString(item.after, "后文", MAX_TEXT),
205
+ createdAt,
206
+ updatedAt
207
+ };
208
+ }
209
+
210
+ // src/anchors.ts
211
+ var MESSAGE = '[data-chat-flow-kind="assistant-step"][data-chat-anchor-key]';
212
+ var FILE_TEXT = "[data-document-markdown], [data-textpreview-plain], [data-code-preview] [data-code-block-content]";
213
+ var EXCLUDED = 'button,input,textarea,select,[contenteditable="true"],[hidden],[aria-hidden="true"],[data-turn-process-hidden="true"],[data-local-annotation-panel],[data-local-annotation-card],[data-local-annotation-toolbar],[data-local-annotation-markers],[data-local-annotation-status]';
214
+ var MAX_SOURCE_TEXT = 2e6;
215
+ var MAX_NODES = 5e4;
216
+ function sourceKey(source) {
217
+ return source.kind === "message" ? `message:${source.key}` : `file:${source.url}`;
218
+ }
219
+ function sourceOf(node, sessionId) {
220
+ const element = node instanceof Element ? node : node.parentElement;
221
+ if (!element || element.closest(EXCLUDED)) return null;
222
+ const file = element.closest(FILE_TEXT);
223
+ if (file) {
224
+ const preview = file.closest('[data-textpreview-state="text"][data-textpreview-url]');
225
+ const url = preview?.getAttribute("data-textpreview-url") ?? "";
226
+ const match = /^dsh-resource:\/\/file\/session\/([^/]+)\/(.+)$/.exec(url);
227
+ if (!match?.[1] || !match[2] || /[?#\u0000-\u001f]/.test(url)) return null;
228
+ try {
229
+ if (decodeURIComponent(match[1]) !== sessionId) return null;
230
+ const path = match[2].split("/").map(decodeURIComponent).join("/");
231
+ if (/[\u0000-\u001f\u007f]/.test(path)) return null;
232
+ return { root: file, source: { kind: "file", url, path } };
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+ const root = element.closest(MESSAGE);
238
+ if (!root || root.dataset.chatGroupPart === "reasoning" || root.closest(EXCLUDED)) return null;
239
+ if (root.closest("[data-conversation-session]")?.getAttribute("data-conversation-session") !== sessionId) return null;
240
+ return { root, source: { kind: "message", key: root.dataset.chatAnchorKey } };
241
+ }
242
+ function findRoot(source, sessionId) {
243
+ const selector = source.kind === "message" ? `${MESSAGE}[data-chat-anchor-key="${CSS.escape(source.key)}"]` : `[data-textpreview-url="${CSS.escape(source.url)}"] :is(${FILE_TEXT})`;
244
+ for (const element of document.querySelectorAll(selector)) {
245
+ const found = sourceOf(element, sessionId);
246
+ if (found && sourceKey(found.source) === sourceKey(source)) return found.root;
247
+ }
248
+ return null;
249
+ }
250
+ function indexText(root) {
251
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
252
+ acceptNode: (node) => node.parentElement?.closest(EXCLUDED) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
253
+ });
254
+ const segments = [];
255
+ const parts = [];
256
+ let length = 0;
257
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
258
+ if (!(node instanceof Text)) continue;
259
+ if (segments.length >= MAX_NODES || length + node.length > MAX_SOURCE_TEXT) return null;
260
+ segments.push({ node, start: length, end: length + node.length });
261
+ parts.push(node.data);
262
+ length += node.length;
263
+ }
264
+ return { text: parts.join(""), segments };
265
+ }
266
+ function offsetAt(index, node, offset) {
267
+ const point = document.createRange();
268
+ point.setStart(node, offset);
269
+ point.collapse(true);
270
+ for (const segment of index.segments) {
271
+ if (segment.node === node) return segment.start + offset;
272
+ if (point.comparePoint(segment.node, 0) >= 0) return segment.start;
273
+ }
274
+ return index.text.length;
275
+ }
276
+ function rangeAt(index, start, end) {
277
+ const first = index.segments.find((s) => s.start <= start && s.end > start);
278
+ const last = index.segments.find((s) => s.start < end && s.end >= end);
279
+ if (!first || !last) return null;
280
+ const range = document.createRange();
281
+ range.setStart(first.node, start - first.start);
282
+ range.setEnd(last.node, end - last.start);
283
+ return range;
284
+ }
285
+ function capture(range, sessionId) {
286
+ const startRoot = sourceOf(range.startContainer, sessionId);
287
+ const endRoot = sourceOf(range.endContainer, sessionId);
288
+ if (!startRoot || !endRoot || startRoot.root !== endRoot.root) return null;
289
+ const index = indexText(startRoot.root);
290
+ if (!index) throw new Error("这段正文过大,请在较短的消息或文件预览中批注。");
291
+ const start = offsetAt(index, range.startContainer, range.startOffset);
292
+ const end = offsetAt(index, range.endContainer, range.endOffset);
293
+ if (end <= start || end - start > MAX_TEXT) throw new Error(`每条引文最多 ${MAX_TEXT} 个字符。`);
294
+ const text = index.text.slice(start, end);
295
+ if (!text.trim() || range.toString() !== text) return null;
296
+ const now = Date.now();
297
+ return {
298
+ id: crypto.randomUUID(),
299
+ sessionId,
300
+ text,
301
+ note: "",
302
+ source: startRoot.source,
303
+ start,
304
+ end,
305
+ before: index.text.slice(Math.max(0, start - 32), start),
306
+ after: index.text.slice(end, end + 32),
307
+ createdAt: now,
308
+ updatedAt: now
309
+ };
310
+ }
311
+ var Anchors = class {
312
+ indices = /* @__PURE__ */ new WeakMap();
313
+ invalidate(root) {
314
+ this.indices.delete(root);
315
+ }
316
+ clear() {
317
+ this.indices = /* @__PURE__ */ new WeakMap();
318
+ }
319
+ resolve(item, root) {
320
+ let index = this.indices.get(root);
321
+ if (index === void 0) {
322
+ index = indexText(root);
323
+ this.indices.set(root, index);
324
+ }
325
+ if (!index) return null;
326
+ let start = item.start;
327
+ if (start < 0 || index.text.slice(start, item.end) !== item.text) {
328
+ let found = -1;
329
+ let from = 0;
330
+ while (from <= index.text.length) {
331
+ const pos = index.text.indexOf(item.text, from);
332
+ if (pos < 0) break;
333
+ const before = index.text.slice(Math.max(0, pos - item.before.length), pos);
334
+ const after = index.text.slice(pos + item.text.length, pos + item.text.length + item.after.length);
335
+ if ((!item.before || before === item.before) && (!item.after || after === item.after)) {
336
+ if (found >= 0) return null;
337
+ found = pos;
338
+ }
339
+ from = pos + 1;
340
+ }
341
+ if (found < 0) return null;
342
+ start = found;
343
+ }
344
+ return rangeAt(index, start, start + item.text.length);
345
+ }
346
+ };
347
+
348
+ // src/markers.ts
349
+ var WIDTH = 26;
350
+ var HEIGHT = 24;
351
+ var Markers = class {
352
+ constructor(open) {
353
+ this.open = open;
354
+ this.fonts?.addEventListener("loadingdone", this.schedule);
355
+ }
356
+ open;
357
+ groups = /* @__PURE__ */ new Map();
358
+ frame = 0;
359
+ disposed = false;
360
+ resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.schedule());
361
+ intersection = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
362
+ for (const entry of entries) {
363
+ const group = this.groups.get(entry.target);
364
+ if (!group) continue;
365
+ group.visible = entry.isIntersecting;
366
+ if (group.visible) this.schedule();
367
+ }
368
+ });
369
+ fonts = document.fonts;
370
+ schedule = () => {
371
+ if (!this.disposed && this.groups.size && !this.frame) {
372
+ this.frame = requestAnimationFrame(() => {
373
+ this.frame = 0;
374
+ this.position();
375
+ });
376
+ }
377
+ };
378
+ update(entries) {
379
+ if (this.disposed) return;
380
+ const scrollable = /* @__PURE__ */ new Map();
381
+ const positionRoot = (node, source) => {
382
+ let element = node instanceof HTMLElement ? node : node.parentElement;
383
+ for (; element && element !== source && source.contains(element); element = element.parentElement) {
384
+ let scroll = scrollable.get(element);
385
+ if (scroll === void 0) {
386
+ const style = getComputedStyle(element);
387
+ scroll = /^(auto|scroll)$/.test(style.overflowX) || /^(auto|scroll)$/.test(style.overflowY);
388
+ scrollable.set(element, scroll);
389
+ }
390
+ if (scroll) return element;
391
+ }
392
+ return source;
393
+ };
394
+ const limited = [];
395
+ for (const entry of entries.slice(0, MAX_ITEMS)) {
396
+ const root = positionRoot(entry.range.startContainer, entry.root);
397
+ if (root.contains(entry.range.endContainer) && root === positionRoot(entry.range.endContainer, entry.root)) {
398
+ limited.push({ ...entry, root });
399
+ }
400
+ }
401
+ const roots = new Set(limited.map((entry) => entry.root));
402
+ this.resize?.disconnect();
403
+ for (const [root, group] of this.groups) {
404
+ if (!roots.has(root)) this.remove(root, group);
405
+ else {
406
+ group.node.replaceChildren();
407
+ group.markers = [];
408
+ }
409
+ }
410
+ const layout = /* @__PURE__ */ new Set([document.documentElement]);
411
+ for (const entry of limited) {
412
+ const { root, item, number } = entry;
413
+ let group = this.groups.get(root);
414
+ if (!group) {
415
+ const position = getComputedStyle(root).position === "static" ? { value: root.style.getPropertyValue("position"), priority: root.style.getPropertyPriority("position") } : null;
416
+ if (position) root.style.setProperty("position", "relative");
417
+ const node = document.createElement("div");
418
+ node.dataset.localAnnotationMarkers = "";
419
+ node.style.cssText = "all:initial;position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;z-index:1;";
420
+ root.append(node);
421
+ group = { node, position, markers: [], visible: true };
422
+ this.groups.set(root, group);
423
+ this.intersection?.observe(root);
424
+ }
425
+ if (group.node.parentElement !== root) root.append(group.node);
426
+ const button = document.createElement("button");
427
+ button.type = "button";
428
+ button.dataset.localAnnotationMarker = "";
429
+ button.dataset.digits = String(number).length.toString();
430
+ const label = document.createElement("span");
431
+ label.textContent = String(number);
432
+ button.append(label);
433
+ button.setAttribute("aria-label", `编辑批注 ${number}`);
434
+ button.title = (item.note || item.text).slice(0, 240);
435
+ button.style.cssText = `position:absolute;box-sizing:border-box;width:${WIDTH}px;height:${HEIGHT}px;pointer-events:auto;visibility:hidden;user-select:none;text-align:center;`;
436
+ button.onpointerdown = (event) => {
437
+ event.preventDefault();
438
+ event.stopPropagation();
439
+ };
440
+ button.onclick = (event) => {
441
+ event.preventDefault();
442
+ event.stopPropagation();
443
+ this.open(item);
444
+ };
445
+ group.node.append(button);
446
+ group.markers.push({ entry, button });
447
+ for (let element = root; element; element = element.parentElement) layout.add(element);
448
+ for (let element = entry.range.startContainer.parentElement; element && root.contains(element); element = element.parentElement) {
449
+ layout.add(element);
450
+ if (element === root) break;
451
+ }
452
+ }
453
+ if (this.groups.size) for (const element of layout) this.resize?.observe(element);
454
+ if (!this.groups.size && this.frame) {
455
+ cancelAnimationFrame(this.frame);
456
+ this.frame = 0;
457
+ }
458
+ this.schedule();
459
+ }
460
+ position() {
461
+ const writes = [];
462
+ for (const [root, group] of this.groups) {
463
+ if (!group.visible || !root.isConnected) continue;
464
+ const box = root.getBoundingClientRect();
465
+ const sx = root.offsetWidth ? box.width / root.offsetWidth : 0;
466
+ const sy = root.offsetHeight ? box.height / root.offsetHeight : 0;
467
+ const left = root.scrollLeft - root.clientLeft;
468
+ const top = root.scrollTop - root.clientTop;
469
+ const maxX = Math.max(0, root.scrollWidth - WIDTH);
470
+ const maxY = Math.max(0, root.scrollHeight - HEIGHT);
471
+ const overlaps = /* @__PURE__ */ new Map();
472
+ for (const { entry, button } of group.markers) {
473
+ const write = { button };
474
+ writes.push(write);
475
+ if (!sx || !sy || !root.contains(entry.range.startContainer) || !root.contains(entry.range.endContainer)) continue;
476
+ const caret = entry.range.cloneRange();
477
+ caret.collapse(true);
478
+ const rect = caret.getClientRects()[0] ?? caret.getBoundingClientRect();
479
+ if (!rect.height) continue;
480
+ const x = (rect.left - box.left) / sx + left;
481
+ const y = Math.max(0, Math.min(maxY, (rect.top - box.top) / sy + top - HEIGHT + 2));
482
+ const key = `${Math.round(x / WIDTH)}:${Math.round(y / HEIGHT)}`;
483
+ const count = overlaps.get(key) ?? 0;
484
+ overlaps.set(key, count + 1);
485
+ write.x = Math.max(0, Math.min(maxX, x + count * WIDTH));
486
+ write.y = y;
487
+ }
488
+ }
489
+ for (const { button, x, y } of writes) {
490
+ button.style.visibility = x === void 0 ? "hidden" : "visible";
491
+ if (x !== void 0 && y !== void 0) {
492
+ button.style.left = `${x}px`;
493
+ button.style.top = `${y}px`;
494
+ }
495
+ }
496
+ }
497
+ remove(root, group) {
498
+ this.intersection?.unobserve(root);
499
+ group.node.remove();
500
+ if (group.position && root.style.getPropertyValue("position") === "relative" && root.style.getPropertyPriority("position") === "") {
501
+ if (group.position.value) root.style.setProperty("position", group.position.value, group.position.priority);
502
+ else root.style.removeProperty("position");
503
+ }
504
+ this.groups.delete(root);
505
+ }
506
+ dispose() {
507
+ this.disposed = true;
508
+ this.resize?.disconnect();
509
+ this.intersection?.disconnect();
510
+ this.fonts?.removeEventListener("loadingdone", this.schedule);
511
+ if (this.frame) cancelAnimationFrame(this.frame);
512
+ this.frame = 0;
513
+ for (const [root, group] of this.groups) this.remove(root, group);
514
+ }
515
+ };
516
+
517
+ // src/highlights.ts
518
+ var NAME = "dsh-local-annotation";
519
+ var api = CSS;
520
+ var HighlightClass = globalThis.Highlight;
521
+ var Highlights = class {
522
+ supported = !!api.highlights && !!HighlightClass;
523
+ highlight = HighlightClass ? new HighlightClass() : null;
524
+ groups = /* @__PURE__ */ new Map();
525
+ anchors = new Anchors();
526
+ dirty = /* @__PURE__ */ new Set();
527
+ timer;
528
+ sessionId = "";
529
+ markers;
530
+ numbers = /* @__PURE__ */ new Map();
531
+ observer = new MutationObserver((records) => {
532
+ let addedSource = false;
533
+ for (const record of records) {
534
+ const target = record.target instanceof Element ? record.target : record.target.parentElement;
535
+ if (target?.closest("[data-local-annotation-markers],[data-local-annotation-status]")) continue;
536
+ const changed = [...record.addedNodes, ...record.removedNodes];
537
+ if (record.type === "childList" && changed.length && changed.every((node) => node instanceof Element && node.matches("[data-local-annotation-markers]"))) continue;
538
+ for (const group of this.groups.values()) {
539
+ if (group.root && (!group.root.isConnected || group.root.contains(record.target) || record.type === "attributes" && record.target instanceof Element && record.target.contains(group.root))) this.dirty.add(group);
540
+ }
541
+ if (record.type === "attributes") addedSource = true;
542
+ if (!addedSource) for (const node of record.addedNodes) {
543
+ if (node instanceof Element && (node.matches(`${MESSAGE},[data-textpreview-url],${FILE_TEXT}`) || node.querySelector(`${MESSAGE},[data-textpreview-url],${FILE_TEXT}`))) {
544
+ addedSource = true;
545
+ break;
546
+ }
547
+ }
548
+ }
549
+ if (addedSource) {
550
+ for (const group of this.groups.values()) if (!group.root?.isConnected) this.dirty.add(group);
551
+ }
552
+ if (this.dirty.size && this.timer === void 0) this.timer = setTimeout(() => this.flush(), 100);
553
+ });
554
+ constructor(open) {
555
+ this.markers = new Markers(open);
556
+ if (this.highlight) api.highlights?.set(NAME, this.highlight);
557
+ }
558
+ update(sessionId, items) {
559
+ this.reset();
560
+ this.sessionId = sessionId;
561
+ items.forEach((item, index) => this.numbers.set(item.id, index + 1));
562
+ for (const item of items) {
563
+ const key = sourceKey(item.source);
564
+ let group = this.groups.get(key);
565
+ if (!group) {
566
+ group = { source: item.source, items: [], root: null, ranges: /* @__PURE__ */ new Map() };
567
+ this.groups.set(key, group);
568
+ }
569
+ group.items.push(item);
570
+ this.dirty.add(group);
571
+ }
572
+ this.flush();
573
+ if (this.groups.size) this.observer.observe(document.getElementById("root") ?? document.body, {
574
+ subtree: true,
575
+ childList: true,
576
+ characterData: true,
577
+ attributes: true,
578
+ attributeFilter: ["hidden", "aria-hidden", "data-turn-process-hidden", "data-textpreview-state", "data-textpreview-url"]
579
+ });
580
+ }
581
+ flush() {
582
+ clearTimeout(this.timer);
583
+ this.timer = void 0;
584
+ for (const group of this.dirty) {
585
+ for (const range of group.ranges.values()) this.highlight?.delete(range);
586
+ group.ranges.clear();
587
+ if (group.root) this.anchors.invalidate(group.root);
588
+ group.root = findRoot(group.source, this.sessionId);
589
+ if (!group.root) continue;
590
+ for (const item of group.items) {
591
+ const range = this.anchors.resolve(item, group.root);
592
+ if (range) {
593
+ group.ranges.set(item.id, range);
594
+ this.highlight?.add(range);
595
+ }
596
+ }
597
+ }
598
+ this.dirty.clear();
599
+ const entries = [];
600
+ for (const group of this.groups.values()) {
601
+ if (!group.root) continue;
602
+ for (const item of group.items) {
603
+ const range = group.ranges.get(item.id);
604
+ if (range) entries.push({ item, range, root: group.root, number: this.numbers.get(item.id) ?? 1 });
605
+ }
606
+ }
607
+ this.markers.update(entries);
608
+ }
609
+ range(item) {
610
+ const root = findRoot(item.source, item.sessionId);
611
+ if (!root) return null;
612
+ this.anchors.invalidate(root);
613
+ return this.anchors.resolve(item, root);
614
+ }
615
+ // Hit testing only on a click inside one source, never on pointermove/scroll.
616
+ hit(target, x, y) {
617
+ const source = sourceOf(target, this.sessionId);
618
+ if (!source) return null;
619
+ const group = this.groups.get(sourceKey(source.source));
620
+ if (!group) return null;
621
+ if (this.dirty.has(group)) this.flush();
622
+ for (const item of group.items) {
623
+ const range = group.ranges.get(item.id);
624
+ if (!range?.startContainer.isConnected) continue;
625
+ for (const rect of range.getClientRects()) {
626
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return item;
627
+ }
628
+ }
629
+ return null;
630
+ }
631
+ locate(item) {
632
+ const root = findRoot(item.source, item.sessionId);
633
+ if (!root) return false;
634
+ this.anchors.invalidate(root);
635
+ const range = this.anchors.resolve(item, root);
636
+ if (!range) return false;
637
+ const element = range.startContainer.parentElement;
638
+ element?.scrollIntoView({ block: "center", behavior: "instant" });
639
+ if (!this.supported) {
640
+ const selection = window.getSelection();
641
+ selection?.removeAllRanges();
642
+ selection?.addRange(range);
643
+ }
644
+ return true;
645
+ }
646
+ reset() {
647
+ this.observer.disconnect();
648
+ clearTimeout(this.timer);
649
+ this.timer = void 0;
650
+ this.markers.update([]);
651
+ this.numbers.clear();
652
+ this.groups.clear();
653
+ this.dirty.clear();
654
+ this.anchors.clear();
655
+ this.highlight?.clear();
656
+ }
657
+ dispose() {
658
+ this.reset();
659
+ this.markers.dispose();
660
+ if (api.highlights?.get(NAME) === this.highlight) api.highlights.delete(NAME);
661
+ }
662
+ };
663
+
664
+ // src/selection.ts
665
+ function installSelection(current, onSelect, onError) {
666
+ const events = new AbortController();
667
+ const button = document.createElement("button");
668
+ button.type = "button";
669
+ button.textContent = "批注";
670
+ button.title = "批注所选文字";
671
+ const toolbar = document.createElement("div");
672
+ toolbar.dataset.localAnnotationToolbar = "";
673
+ toolbar.hidden = true;
674
+ toolbar.append(button);
675
+ document.body.append(toolbar);
676
+ let pointerDown = false;
677
+ let composing = false;
678
+ let frame = 0;
679
+ let timer;
680
+ let range = null;
681
+ let sessionId;
682
+ let scrollEvents = null;
683
+ const hide = () => {
684
+ toolbar.hidden = true;
685
+ range = null;
686
+ sessionId = void 0;
687
+ scrollEvents?.abort();
688
+ scrollEvents = null;
689
+ clearTimeout(timer);
690
+ cancelAnimationFrame(frame);
691
+ frame = 0;
692
+ };
693
+ const show = () => {
694
+ frame = 0;
695
+ if (pointerDown || composing) return;
696
+ const selection = window.getSelection();
697
+ const id = current();
698
+ if (!id || !selection?.rangeCount || selection.isCollapsed) {
699
+ hide();
700
+ return;
701
+ }
702
+ const selected = selection.getRangeAt(0);
703
+ const start = sourceOf(selected.startContainer, id);
704
+ const end = sourceOf(selected.endContainer, id);
705
+ if (!start || !end || start.root !== end.root) {
706
+ hide();
707
+ return;
708
+ }
709
+ hide();
710
+ const caret = document.createRange();
711
+ if (!selection.focusNode) return;
712
+ caret.setStart(selection.focusNode, selection.focusOffset);
713
+ caret.collapse(true);
714
+ const rect = caret.getBoundingClientRect();
715
+ if (!rect.height || rect.bottom < 40 || rect.top > window.innerHeight) return;
716
+ range = selected.cloneRange();
717
+ sessionId = id;
718
+ toolbar.style.left = `${Math.max(8, Math.min(window.innerWidth - 80, rect.right + 8))}px`;
719
+ toolbar.style.top = `${Math.max(48, Math.min(window.innerHeight - 44, rect.bottom + 6))}px`;
720
+ toolbar.hidden = false;
721
+ scrollEvents = new AbortController();
722
+ document.addEventListener("scroll", hide, { capture: true, passive: true, signal: scrollEvents.signal });
723
+ window.addEventListener("resize", hide, { passive: true, signal: scrollEvents.signal });
724
+ };
725
+ const schedule = () => {
726
+ cancelAnimationFrame(frame);
727
+ frame = requestAnimationFrame(show);
728
+ };
729
+ const signal = events.signal;
730
+ document.addEventListener("pointerdown", (event) => {
731
+ if (toolbar.contains(event.target)) return;
732
+ pointerDown = true;
733
+ hide();
734
+ }, { capture: true, signal });
735
+ document.addEventListener("pointerup", () => {
736
+ pointerDown = false;
737
+ schedule();
738
+ }, { signal });
739
+ document.addEventListener("pointercancel", () => {
740
+ pointerDown = false;
741
+ hide();
742
+ }, { signal });
743
+ document.addEventListener("selectionchange", () => {
744
+ if (pointerDown || composing || toolbar.contains(document.activeElement)) return;
745
+ hide();
746
+ timer = setTimeout(schedule, 120);
747
+ }, { signal });
748
+ document.addEventListener("compositionstart", () => {
749
+ composing = true;
750
+ hide();
751
+ }, { signal });
752
+ document.addEventListener("compositionend", () => {
753
+ composing = false;
754
+ }, { signal });
755
+ document.addEventListener("keydown", (event) => {
756
+ if (event.key === "Escape") hide();
757
+ }, { signal });
758
+ window.addEventListener("blur", () => {
759
+ pointerDown = false;
760
+ hide();
761
+ }, { signal });
762
+ toolbar.addEventListener("pointerdown", (event) => event.preventDefault(), { signal });
763
+ button.addEventListener("click", () => {
764
+ if (!range || !sessionId || current() !== sessionId || !range.startContainer.isConnected) {
765
+ hide();
766
+ return;
767
+ }
768
+ try {
769
+ const selected = range.cloneRange();
770
+ const item = capture(selected, sessionId);
771
+ hide();
772
+ if (item) {
773
+ window.getSelection()?.removeAllRanges();
774
+ onSelect(item, selected);
775
+ }
776
+ } catch (error) {
777
+ hide();
778
+ onError(error);
779
+ }
780
+ }, { signal });
781
+ return { hide, dispose() {
782
+ hide();
783
+ events.abort();
784
+ toolbar.remove();
785
+ } };
786
+ }
787
+
788
+ // src/storage.ts
789
+ var DATABASE = "dsh-local-annotation-v1";
790
+ var ITEMS = "annotations";
791
+ var IMPORTS = "legacy-imports";
792
+ var LEGACY_PREFIX = "dsh.annotation.pending.v1.";
793
+ var MAX_LEGACY_CHARS = 16 * 1024 * 1024;
794
+ var OPEN_TIMEOUT = 1e4;
795
+ function failure(error, fallback) {
796
+ if (error instanceof Error) return error;
797
+ return new Error(fallback, { cause: error });
798
+ }
799
+ function expectedVersion(value) {
800
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new TypeError("预期版本必须是非负安全整数");
801
+ }
802
+ function conflict() {
803
+ return new Error("批注已在其他窗口修改或删除,请刷新后重试");
804
+ }
805
+ function nextVersion(previous) {
806
+ const next = Math.max(Date.now(), previous === void 0 ? 0 : previous + 1);
807
+ if (!Number.isSafeInteger(next)) throw new Error("批注版本已超出安全范围");
808
+ return next;
809
+ }
810
+ function legacyItems(raw, sessionId) {
811
+ if (raw.length > MAX_LEGACY_CHARS) throw new Error("旧批注数据过大,未导入任何内容");
812
+ const parsed = JSON.parse(raw);
813
+ if (!Array.isArray(parsed) || parsed.length > MAX_ITEMS) throw new Error(`旧批注必须是至多 ${MAX_ITEMS} 项的数组`);
814
+ const seen = /* @__PURE__ */ new Set();
815
+ const now = nextVersion();
816
+ return parsed.map((value, index) => {
817
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`旧批注第 ${index + 1} 项格式无效`);
818
+ const item = value;
819
+ const originalId = annotationKey(item.id, "旧批注 ID");
820
+ const id = annotationKey(`legacy-${originalId}`, "导入批注 ID");
821
+ if (seen.has(id)) throw new Error(`旧批注存在重复 ID:${originalId}`);
822
+ seen.add(id);
823
+ const text = boundedString(item.text, "旧批注原文", MAX_TEXT, true);
824
+ let source;
825
+ if (typeof item.sourceUrl === "string" && typeof item.sourcePath === "string") {
826
+ try {
827
+ source = annotationSource({ kind: "file", url: item.sourceUrl, path: item.sourcePath }, sessionId);
828
+ } catch {
829
+ }
830
+ }
831
+ if (!source && typeof item.seqKey === "string" && item.seqKey.trim()) source = annotationSource({ kind: "message", key: item.seqKey }, sessionId);
832
+ if (!source) throw new Error(`旧批注第 ${index + 1} 项没有属于当前会话的明确来源,整批导入已取消`);
833
+ const start = item.textOffset === void 0 ? -1 : item.textOffset;
834
+ if (typeof start !== "number" || !Number.isSafeInteger(start) || start < -1) throw new TypeError(`旧批注第 ${index + 1} 项位置无效`);
835
+ return copyAnnotation({
836
+ id,
837
+ sessionId,
838
+ text,
839
+ note: item.note ?? "",
840
+ source,
841
+ start,
842
+ end: start === -1 ? -1 : start + text.length,
843
+ before: item.ctxBefore ?? "",
844
+ after: item.ctxAfter ?? "",
845
+ createdAt: now,
846
+ updatedAt: now
847
+ });
848
+ });
849
+ }
850
+ var AnnotationStore = class {
851
+ listeners = /* @__PURE__ */ new Set();
852
+ connections = /* @__PURE__ */ new Set();
853
+ pending = /* @__PURE__ */ new Set();
854
+ environment;
855
+ channel;
856
+ disposed = false;
857
+ constructor(environment = globalThis) {
858
+ this.environment = environment;
859
+ try {
860
+ if (environment.BroadcastChannel) {
861
+ this.channel = new environment.BroadcastChannel(DATABASE);
862
+ this.channel.onmessage = (event) => {
863
+ const data = event.data;
864
+ if (!data || typeof data !== "object" || !("sessionId" in data)) return;
865
+ try {
866
+ this.notify(annotationKey(data.sessionId, "会话 ID"));
867
+ } catch {
868
+ }
869
+ };
870
+ }
871
+ } catch {
872
+ }
873
+ }
874
+ ensureActive() {
875
+ if (this.disposed) throw new Error("批注存储已关闭");
876
+ }
877
+ open() {
878
+ this.ensureActive();
879
+ return new Promise((resolve, reject) => {
880
+ let factory;
881
+ try {
882
+ factory = this.environment.indexedDB;
883
+ } catch (error) {
884
+ reject(failure(error, "无法访问 IndexedDB"));
885
+ return;
886
+ }
887
+ if (!factory) {
888
+ reject(new Error("当前环境不支持 IndexedDB,批注未保存"));
889
+ return;
890
+ }
891
+ let request;
892
+ try {
893
+ request = factory.open(DATABASE, 1);
894
+ } catch (error) {
895
+ reject(failure(error, "无法打开批注存储"));
896
+ return;
897
+ }
898
+ let settled = false;
899
+ const stop = (error) => {
900
+ if (settled) return;
901
+ settled = true;
902
+ clearTimeout(timer);
903
+ this.pending.delete(cancel);
904
+ reject(error);
905
+ };
906
+ const cancel = () => stop(new Error("批注存储已关闭"));
907
+ const timer = setTimeout(() => stop(new Error("打开批注存储超时,请关闭占用数据库的旧窗口后重试")), OPEN_TIMEOUT);
908
+ this.pending.add(cancel);
909
+ request.onblocked = () => stop(new Error("批注数据库升级被其他窗口阻塞,请关闭旧窗口后重试"));
910
+ request.onerror = () => stop(failure(request.error, "打开批注数据库失败"));
911
+ request.onupgradeneeded = () => {
912
+ if (settled || this.disposed) {
913
+ request.transaction?.abort();
914
+ return;
915
+ }
916
+ try {
917
+ const db = request.result;
918
+ const items = db.createObjectStore(ITEMS, { keyPath: ["sessionId", "id"] });
919
+ items.createIndex("sessionId", "sessionId");
920
+ db.createObjectStore(IMPORTS, { keyPath: "sessionId" });
921
+ } catch (error) {
922
+ request.transaction?.abort();
923
+ stop(failure(error, "初始化批注数据库失败"));
924
+ }
925
+ };
926
+ request.onsuccess = () => {
927
+ const db = request.result;
928
+ if (settled || this.disposed) {
929
+ db.close();
930
+ return;
931
+ }
932
+ settled = true;
933
+ clearTimeout(timer);
934
+ this.pending.delete(cancel);
935
+ this.connections.add(db);
936
+ db.onversionchange = () => {
937
+ db.close();
938
+ this.connections.delete(db);
939
+ };
940
+ resolve(db);
941
+ };
942
+ });
943
+ }
944
+ async transaction(mode, action) {
945
+ const db = await this.open();
946
+ try {
947
+ this.ensureActive();
948
+ return await new Promise((resolve, reject) => {
949
+ const tx = db.transaction([ITEMS, IMPORTS], mode);
950
+ let result;
951
+ let ready = false;
952
+ let error;
953
+ const abort = (cause) => {
954
+ error ??= failure(cause, "批注事务失败");
955
+ try {
956
+ tx.abort();
957
+ } catch {
958
+ reject(error);
959
+ }
960
+ };
961
+ tx.oncomplete = () => {
962
+ if (ready) resolve(result);
963
+ else reject(new Error("批注事务未产生结果"));
964
+ };
965
+ tx.onabort = () => reject(error ?? failure(tx.error, "批注事务已中止,数据未保存"));
966
+ tx.onerror = () => {
967
+ error ??= failure(tx.error, "批注读写失败,可能是存储空间不足");
968
+ };
969
+ try {
970
+ action(tx.objectStore(ITEMS), tx.objectStore(IMPORTS), (value) => {
971
+ result = value;
972
+ ready = true;
973
+ }, abort);
974
+ } catch (cause) {
975
+ abort(cause);
976
+ }
977
+ });
978
+ } finally {
979
+ db.close();
980
+ this.connections.delete(db);
981
+ }
982
+ }
983
+ async list(sessionId) {
984
+ annotationKey(sessionId, "会话 ID");
985
+ return this.transaction("readonly", (items, _imports, finish, abort) => {
986
+ const request = items.index("sessionId").getAll(sessionId, MAX_ITEMS + 1);
987
+ request.onsuccess = () => {
988
+ try {
989
+ if (request.result.length > MAX_ITEMS) throw new Error("批注数量超限,未截断数据");
990
+ const rows = request.result.map(copyAnnotation);
991
+ finish(rows.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)));
992
+ } catch (error) {
993
+ abort(error);
994
+ }
995
+ };
996
+ });
997
+ }
998
+ async put(item, expectedUpdatedAt) {
999
+ const value = copyAnnotation(item);
1000
+ expectedVersion(expectedUpdatedAt);
1001
+ await this.transaction("readwrite", (items, _imports, finish, abort) => {
1002
+ const request = items.get([value.sessionId, value.id]);
1003
+ request.onsuccess = () => {
1004
+ try {
1005
+ const old = request.result === void 0 ? void 0 : copyAnnotation(request.result);
1006
+ if (expectedUpdatedAt === void 0 ? old !== void 0 : old === void 0 || old.updatedAt !== expectedUpdatedAt) throw conflict();
1007
+ const save = () => {
1008
+ value.updatedAt = nextVersion(old?.updatedAt);
1009
+ value.createdAt = old?.createdAt ?? value.updatedAt;
1010
+ items.put(value);
1011
+ finish(void 0);
1012
+ };
1013
+ if (old) {
1014
+ save();
1015
+ return;
1016
+ }
1017
+ const count = items.index("sessionId").count(value.sessionId);
1018
+ count.onsuccess = () => {
1019
+ try {
1020
+ if (count.result >= MAX_ITEMS) throw new Error(`每个会话最多保存 ${MAX_ITEMS} 条批注`);
1021
+ save();
1022
+ } catch (error) {
1023
+ abort(error);
1024
+ }
1025
+ };
1026
+ } catch (error) {
1027
+ abort(error);
1028
+ }
1029
+ };
1030
+ });
1031
+ this.changed(value.sessionId);
1032
+ return value;
1033
+ }
1034
+ async remove(sessionId, id, expectedUpdatedAt) {
1035
+ annotationKey(sessionId, "会话 ID");
1036
+ annotationKey(id, "批注 ID");
1037
+ expectedVersion(expectedUpdatedAt);
1038
+ const changed = await this.transaction("readwrite", (items, _imports, finish, abort) => {
1039
+ const request = items.get([sessionId, id]);
1040
+ request.onsuccess = () => {
1041
+ try {
1042
+ const old = request.result === void 0 ? void 0 : copyAnnotation(request.result);
1043
+ if (expectedUpdatedAt !== void 0 && (!old || old.updatedAt !== expectedUpdatedAt)) throw conflict();
1044
+ if (old) items.delete([sessionId, id]);
1045
+ finish(old !== void 0);
1046
+ } catch (error) {
1047
+ abort(error);
1048
+ }
1049
+ };
1050
+ });
1051
+ if (changed) this.changed(sessionId);
1052
+ }
1053
+ async importLegacy(sessionId) {
1054
+ this.ensureActive();
1055
+ annotationKey(sessionId, "会话 ID");
1056
+ const storage = this.environment.localStorage;
1057
+ if (!storage) throw new Error("当前环境无法读取旧批注存储");
1058
+ const raw = storage.getItem(LEGACY_PREFIX + encodeURIComponent(sessionId));
1059
+ if (raw === null || raw === "") return 0;
1060
+ const candidates = legacyItems(raw, sessionId);
1061
+ if (!candidates.length) return 0;
1062
+ const imported = await this.transaction("readwrite", (items, imports, finish, abort) => {
1063
+ const metadata = imports.get(sessionId);
1064
+ metadata.onsuccess = () => {
1065
+ try {
1066
+ const record = metadata.result;
1067
+ let remembered = [];
1068
+ if (record !== void 0) {
1069
+ if (!record || typeof record !== "object" || !("ids" in record) || !Array.isArray(record.ids) || record.ids.length > MAX_ITEMS) throw new Error("旧批注导入记录损坏");
1070
+ remembered = record.ids.map((id) => annotationKey(id, "导入批注 ID"));
1071
+ }
1072
+ const done = new Set(remembered);
1073
+ const fresh = candidates.filter((item) => !done.has(item.id));
1074
+ if (!fresh.length) {
1075
+ finish(0);
1076
+ return;
1077
+ }
1078
+ if (done.size + fresh.length > MAX_ITEMS) throw new Error(`旧批注累计导入数量不能超过 ${MAX_ITEMS}`);
1079
+ const count = items.index("sessionId").count(sessionId);
1080
+ count.onsuccess = () => {
1081
+ let total = count.result;
1082
+ let added = 0;
1083
+ let cursor = 0;
1084
+ const next = () => {
1085
+ try {
1086
+ const item = fresh[cursor++];
1087
+ if (!item) {
1088
+ imports.put({ sessionId, ids: [...done] });
1089
+ finish(added);
1090
+ return;
1091
+ }
1092
+ const request = items.get([sessionId, item.id]);
1093
+ request.onsuccess = () => {
1094
+ try {
1095
+ if (request.result === void 0) {
1096
+ if (total >= MAX_ITEMS) throw new Error(`导入后批注超过 ${MAX_ITEMS} 条,整批导入已取消`);
1097
+ items.add(item);
1098
+ total++;
1099
+ added++;
1100
+ }
1101
+ done.add(item.id);
1102
+ next();
1103
+ } catch (error) {
1104
+ abort(error);
1105
+ }
1106
+ };
1107
+ } catch (error) {
1108
+ abort(error);
1109
+ }
1110
+ };
1111
+ next();
1112
+ };
1113
+ } catch (error) {
1114
+ abort(error);
1115
+ }
1116
+ };
1117
+ });
1118
+ if (imported) this.changed(sessionId);
1119
+ return imported;
1120
+ }
1121
+ subscribe(listener) {
1122
+ this.ensureActive();
1123
+ this.listeners.add(listener);
1124
+ return () => {
1125
+ this.listeners.delete(listener);
1126
+ };
1127
+ }
1128
+ notify(sessionId) {
1129
+ if (this.disposed) return;
1130
+ for (const listener of [...this.listeners]) {
1131
+ try {
1132
+ listener(sessionId);
1133
+ } catch (error) {
1134
+ console.error("批注存储订阅回调失败", error);
1135
+ }
1136
+ }
1137
+ }
1138
+ changed(sessionId) {
1139
+ if (this.disposed) return;
1140
+ this.notify(sessionId);
1141
+ try {
1142
+ this.channel?.postMessage({ sessionId });
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ dispose() {
1147
+ if (this.disposed) return;
1148
+ this.disposed = true;
1149
+ for (const cancel of [...this.pending]) cancel();
1150
+ for (const db of this.connections) db.close();
1151
+ this.connections.clear();
1152
+ this.listeners.clear();
1153
+ this.channel?.close();
1154
+ this.channel = void 0;
1155
+ }
1156
+ };
1157
+
1158
+ // src/floating.ts
1159
+ function floatingCard(host, close) {
1160
+ let anchor = null;
1161
+ let frame = 0;
1162
+ let events = null;
1163
+ const position = () => {
1164
+ frame = 0;
1165
+ if (!anchor || host.hidden) return;
1166
+ let { x, y } = anchor;
1167
+ if (anchor.range) {
1168
+ if (!anchor.range.endContainer.isConnected) {
1169
+ close();
1170
+ return;
1171
+ }
1172
+ const caret = anchor.range.cloneRange();
1173
+ caret.collapse(false);
1174
+ let rect = caret.getBoundingClientRect();
1175
+ if (!rect.height || rect.bottom < 48 || rect.top > window.innerHeight) {
1176
+ caret.setStart(anchor.range.startContainer, anchor.range.startOffset);
1177
+ caret.collapse(true);
1178
+ rect = caret.getBoundingClientRect();
1179
+ }
1180
+ if (!rect.height || rect.bottom < 48 || rect.top > window.innerHeight) {
1181
+ close();
1182
+ return;
1183
+ }
1184
+ x = rect.right;
1185
+ y = rect.bottom;
1186
+ }
1187
+ const width = host.offsetWidth;
1188
+ const height = host.offsetHeight;
1189
+ const left = Math.max(8, Math.min(window.innerWidth - width - 8, x + 8));
1190
+ const below = y + 8;
1191
+ const top = Math.max(48, Math.min(
1192
+ window.innerHeight - height - 8,
1193
+ below + height <= window.innerHeight - 8 ? below : y - height - 24
1194
+ ));
1195
+ host.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
1196
+ };
1197
+ const schedule = () => {
1198
+ if (!frame) frame = requestAnimationFrame(position);
1199
+ };
1200
+ const observer = new ResizeObserver(schedule);
1201
+ const hide = () => {
1202
+ host.hidden = true;
1203
+ anchor = null;
1204
+ events?.abort();
1205
+ events = null;
1206
+ observer.disconnect();
1207
+ cancelAnimationFrame(frame);
1208
+ frame = 0;
1209
+ };
1210
+ return {
1211
+ show(next) {
1212
+ anchor = next;
1213
+ host.hidden = false;
1214
+ if (!events) {
1215
+ events = new AbortController();
1216
+ const signal = events.signal;
1217
+ document.addEventListener("scroll", (event) => {
1218
+ if (event.target instanceof Node && host.contains(event.target)) return;
1219
+ schedule();
1220
+ }, { capture: true, passive: true, signal });
1221
+ window.addEventListener("resize", schedule, { passive: true, signal });
1222
+ document.addEventListener("pointerdown", (event) => {
1223
+ const target = event.target;
1224
+ if (target instanceof Element && !host.contains(target) && !target.closest("[data-local-annotation-toolbar]")) close();
1225
+ }, { capture: true, signal });
1226
+ document.addEventListener("keydown", (event) => {
1227
+ if (event.key === "Escape" && !event.isComposing && event.keyCode !== 229) {
1228
+ event.preventDefault();
1229
+ close();
1230
+ }
1231
+ }, { signal });
1232
+ observer.observe(host);
1233
+ }
1234
+ schedule();
1235
+ },
1236
+ hide,
1237
+ dispose: hide
1238
+ };
1239
+ }
1240
+
1241
+ // src/controller.ts
1242
+ var Controller = class {
1243
+ constructor(ctx, host) {
1244
+ this.ctx = ctx;
1245
+ this.floating = floatingCard(host, () => this.close());
1246
+ this.selection = installSelection(() => this.state.sessionId, (item, range) => {
1247
+ this.anchor = { range, x: 24, y: 64 };
1248
+ if (this.state.loading) return;
1249
+ if (this.state.pending) {
1250
+ this.showItem(this.state.pending);
1251
+ return;
1252
+ }
1253
+ if (this.state.items.length >= MAX_ITEMS) {
1254
+ this.error(new Error(`每个会话最多 ${MAX_ITEMS} 条批注。`));
1255
+ this.showError();
1256
+ return;
1257
+ }
1258
+ if (this.pendingBySession.size >= 8 && !this.pendingBySession.has(item.sessionId)) {
1259
+ this.error(new Error("已有 8 个会话保留未保存批注,请先保存或取消其中一条。"));
1260
+ this.showError();
1261
+ return;
1262
+ }
1263
+ this.pendingBySession.set(item.sessionId, item);
1264
+ this.patch({ pending: item, error: "", card: { item, editing: true } });
1265
+ this.paint();
1266
+ this.floating.show(this.anchor);
1267
+ }, (error) => {
1268
+ this.error(error);
1269
+ this.showError();
1270
+ });
1271
+ const click = (event) => {
1272
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || !(event.target instanceof Element) || event.target.closest('a,button,input,textarea,[contenteditable="true"],[data-local-annotation-card]')) return;
1273
+ if (!window.getSelection()?.isCollapsed) return;
1274
+ const item = this.highlights.hit(event.target, event.clientX, event.clientY);
1275
+ if (!item) return;
1276
+ this.anchor = { range: this.highlights.range(item), x: event.clientX, y: event.clientY };
1277
+ this.showItem(item);
1278
+ };
1279
+ document.addEventListener("click", click);
1280
+ this.disposers.push(() => document.removeEventListener("click", click));
1281
+ const sync = () => {
1282
+ const id = this.ctx.uiSession.adapter.current.getSnapshot().key;
1283
+ if (id === this.state.sessionId) return;
1284
+ ++this.generation;
1285
+ this.selection.hide();
1286
+ this.close();
1287
+ this.patch({ sessionId: id, items: [], pending: id ? this.pendingBySession.get(id) ?? null : null, error: "", loading: !!id });
1288
+ this.paint();
1289
+ if (id) void this.refresh(id);
1290
+ };
1291
+ this.disposers.push(ctx.uiSession.adapter.current.subscribe(sync));
1292
+ this.disposers.push(this.store.subscribe((id) => {
1293
+ if (id === this.state.sessionId) void this.refresh(id);
1294
+ }));
1295
+ const onFocus = () => {
1296
+ if (this.state.sessionId) void this.refresh(this.state.sessionId);
1297
+ };
1298
+ window.addEventListener("focus", onFocus);
1299
+ this.disposers.push(() => window.removeEventListener("focus", onFocus));
1300
+ sync();
1301
+ }
1302
+ ctx;
1303
+ store = new AnnotationStore();
1304
+ highlights = new Highlights((item) => this.showItem(item));
1305
+ listeners = /* @__PURE__ */ new Set();
1306
+ appearance = new AppearanceStore((appearance) => this.patch({ appearance }));
1307
+ state = {
1308
+ sessionId: void 0,
1309
+ items: [],
1310
+ pending: null,
1311
+ error: "",
1312
+ loading: false,
1313
+ highlightSupported: this.highlights.supported,
1314
+ appearance: this.appearance.getSnapshot(),
1315
+ card: null
1316
+ };
1317
+ pendingBySession = /* @__PURE__ */ new Map();
1318
+ generation = 0;
1319
+ disposed = false;
1320
+ disposers = [];
1321
+ selection;
1322
+ floating;
1323
+ anchor = { range: null, x: 24, y: 64 };
1324
+ drafts = /* @__PURE__ */ new Map();
1325
+ subscribe = (listener) => {
1326
+ this.listeners.add(listener);
1327
+ return () => this.listeners.delete(listener);
1328
+ };
1329
+ getSnapshot = () => this.state;
1330
+ setAppearance(field, value) {
1331
+ this.appearance.set(field, value);
1332
+ }
1333
+ patch(patch) {
1334
+ if (this.disposed) return;
1335
+ this.state = { ...this.state, ...patch };
1336
+ for (const listener of this.listeners) listener();
1337
+ }
1338
+ error(error) {
1339
+ this.patch({ error: error instanceof Error ? error.message : String(error) });
1340
+ }
1341
+ paint() {
1342
+ this.highlights.update(this.state.sessionId ?? "", this.state.pending ? [...this.state.items, this.state.pending] : this.state.items);
1343
+ }
1344
+ async refresh(id) {
1345
+ const generation = ++this.generation;
1346
+ try {
1347
+ const items = await this.store.list(id);
1348
+ if (this.disposed || generation !== this.generation || id !== this.state.sessionId) return;
1349
+ this.patch({ items, loading: false });
1350
+ this.paint();
1351
+ } catch (error) {
1352
+ if (!this.disposed && generation === this.generation && id === this.state.sessionId) {
1353
+ this.patch({ loading: false });
1354
+ this.error(error);
1355
+ }
1356
+ }
1357
+ }
1358
+ close() {
1359
+ const pending = this.state.pending;
1360
+ if (pending && !this.draft(pending).trim()) {
1361
+ this.pendingBySession.delete(pending.sessionId);
1362
+ this.drafts.delete(this.draftKey(pending));
1363
+ this.patch({ pending: null });
1364
+ this.paint();
1365
+ }
1366
+ this.floating.hide();
1367
+ this.patch({ card: null });
1368
+ }
1369
+ showError() {
1370
+ if (!this.state.sessionId) return;
1371
+ this.patch({ card: { item: null, editing: false } });
1372
+ if (!this.anchor.range?.endContainer.isConnected) this.anchor = { range: null, x: Math.min(window.innerWidth / 2, 480), y: 80 };
1373
+ this.floating.show(this.anchor);
1374
+ }
1375
+ showItem(item) {
1376
+ if (item.sessionId !== this.state.sessionId) return;
1377
+ const base = this.drafts.get(this.draftKey(item))?.item ?? item;
1378
+ this.patch({ card: { item: base, editing: true }, error: "" });
1379
+ this.anchor = { ...this.anchor, range: this.highlights.range(item) };
1380
+ this.floating.show(this.anchor);
1381
+ }
1382
+ draftKey(item) {
1383
+ return JSON.stringify([item.sessionId, item.id]);
1384
+ }
1385
+ draft(item) {
1386
+ return this.drafts.get(this.draftKey(item))?.note ?? item.note;
1387
+ }
1388
+ setDraft(item, note) {
1389
+ const key = this.draftKey(item);
1390
+ if (!this.drafts.has(key) && this.drafts.size >= 32) this.drafts.delete(this.drafts.keys().next().value);
1391
+ this.drafts.set(key, { item, note: note.slice(0, MAX_NOTE) });
1392
+ }
1393
+ async save(item, note) {
1394
+ if (item.sessionId !== this.state.sessionId || this.disposed) return;
1395
+ const id = item.sessionId;
1396
+ try {
1397
+ if (note.length > MAX_NOTE) throw new Error(`批注最多 ${MAX_NOTE} 个字符。`);
1398
+ const pending = this.state.pending?.id === item.id;
1399
+ const saved = await this.store.put({ ...item, note }, pending ? void 0 : item.updatedAt);
1400
+ this.drafts.delete(this.draftKey(item));
1401
+ if (this.pendingBySession.get(id)?.id === item.id) this.pendingBySession.delete(id);
1402
+ if (this.disposed || id !== this.state.sessionId) return;
1403
+ if (this.state.pending?.id === item.id) this.patch({ pending: null });
1404
+ const items = this.state.items.filter((value) => value.id !== saved.id);
1405
+ items.push(saved);
1406
+ items.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
1407
+ this.patch({ items, error: "" });
1408
+ if (this.state.card?.item?.id === item.id) this.close();
1409
+ this.paint();
1410
+ await this.refresh(id);
1411
+ } catch (error) {
1412
+ if (id === this.state.sessionId) this.error(error);
1413
+ throw error;
1414
+ }
1415
+ }
1416
+ cancelPending() {
1417
+ if (this.state.sessionId) this.pendingBySession.delete(this.state.sessionId);
1418
+ if (this.state.pending) this.drafts.delete(this.draftKey(this.state.pending));
1419
+ this.close();
1420
+ this.patch({ pending: null, error: "" });
1421
+ this.paint();
1422
+ }
1423
+ async remove(item) {
1424
+ if (item.sessionId !== this.state.sessionId || this.disposed) return;
1425
+ try {
1426
+ await this.store.remove(item.sessionId, item.id, item.updatedAt);
1427
+ this.drafts.delete(this.draftKey(item));
1428
+ if (this.state.card?.item?.id === item.id) this.close();
1429
+ if (item.sessionId === this.state.sessionId) {
1430
+ this.patch({ error: "" });
1431
+ await this.refresh(item.sessionId);
1432
+ }
1433
+ } catch (error) {
1434
+ if (item.sessionId === this.state.sessionId) this.error(error);
1435
+ throw error;
1436
+ }
1437
+ }
1438
+ insert(items) {
1439
+ try {
1440
+ const binding = this.ctx.uiSession.adapter.current.getSnapshot();
1441
+ if (!items.length || !binding.key || binding.key !== this.state.sessionId || items.some((item) => item.sessionId !== binding.key)) return false;
1442
+ const actions = binding.props.inputActions;
1443
+ if (!actions) throw new Error("输入框尚未就绪,请稍后重试。");
1444
+ const span = actions.captureInsertion();
1445
+ const text = "\n\n" + items.map((item, index) => {
1446
+ const source = item.source.kind === "file" ? `文件:${item.source.path}
1447
+ ` : "";
1448
+ return `批注 ${index + 1}
1449
+ ${source}${item.text.split("\n").map((line) => `> ${line}`).join("\n")}` + (item.note.trim() ? `
1450
+ 批注:${item.note}` : "");
1451
+ }).join("\n\n") + "\n\n";
1452
+ if (text.length > 256e3) throw new Error("引用总量过大,请分批插入。");
1453
+ if (this.ctx.uiSession.adapter.current.getSnapshot().key !== binding.key || !actions.insertText(text, span)) {
1454
+ throw new Error("输入框正在更新,未插入批注;请稍后重试。");
1455
+ }
1456
+ this.patch({ error: "" });
1457
+ return true;
1458
+ } catch (error) {
1459
+ this.error(error);
1460
+ return false;
1461
+ }
1462
+ }
1463
+ dispose() {
1464
+ this.disposed = true;
1465
+ ++this.generation;
1466
+ this.selection.dispose();
1467
+ this.floating.dispose();
1468
+ this.drafts.clear();
1469
+ for (const dispose of this.disposers) dispose();
1470
+ this.highlights.dispose();
1471
+ this.appearance.dispose();
1472
+ this.store.dispose();
1473
+ this.pendingBySession.clear();
1474
+ this.listeners.clear();
1475
+ }
1476
+ };
1477
+
1478
+ // src/view.tsx
1479
+ var import_react = require("react");
1480
+ var import_jsx_runtime = require("react/jsx-runtime");
1481
+ function Trash() {
1482
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" }) });
1483
+ }
1484
+ function Close() {
1485
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m6 6 12 12M18 6 6 18" }) });
1486
+ }
1487
+ function brief(text, length) {
1488
+ return text.length > length ? text.slice(0, length) + "…" : text;
1489
+ }
1490
+ function Card({ controller }) {
1491
+ const state = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1492
+ if (!state.card) return null;
1493
+ if (!state.card.item) return state.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-local-annotation-panel": "", role: "alert", children: [
1494
+ state.error,
1495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dla-icon", "aria-label": "关闭", onClick: () => controller.close(), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Close, {}) })
1496
+ ] }) : null;
1497
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Editor, { controller, item: state.card.item }, JSON.stringify([state.sessionId, state.card.item.id]));
1498
+ }
1499
+ function Editor({ controller, item }) {
1500
+ const state = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1501
+ const [base] = (0, import_react.useState)(item);
1502
+ const [note, setNote] = (0, import_react.useState)(() => controller.draft(item));
1503
+ const [busy, setBusy] = (0, import_react.useState)(false);
1504
+ const [error, setError] = (0, import_react.useState)("");
1505
+ const textarea = (0, import_react.useRef)(null);
1506
+ const composing = (0, import_react.useRef)(false);
1507
+ const inFlight = (0, import_react.useRef)(false);
1508
+ const newItem = state.pending?.id === item.id;
1509
+ const quote = (item.source.kind === "file" ? `${item.source.path}
1510
+ ` : "") + item.text;
1511
+ (0, import_react.useEffect)(() => {
1512
+ textarea.current?.focus({ preventScroll: true });
1513
+ textarea.current?.setSelectionRange(note.length, note.length);
1514
+ }, []);
1515
+ async function run(action) {
1516
+ if (inFlight.current) return;
1517
+ inFlight.current = true;
1518
+ setBusy(true);
1519
+ setError("");
1520
+ try {
1521
+ await action();
1522
+ } catch (cause) {
1523
+ setError(cause instanceof Error ? cause.message : "操作失败,请重试。");
1524
+ } finally {
1525
+ inFlight.current = false;
1526
+ setBusy(false);
1527
+ }
1528
+ }
1529
+ function keyDown(event) {
1530
+ if (composing.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1531
+ if (event.key === "Enter" && !event.shiftKey) {
1532
+ event.preventDefault();
1533
+ event.stopPropagation();
1534
+ void run(() => controller.save(base, note));
1535
+ } else if (event.key === "Escape") {
1536
+ event.preventDefault();
1537
+ event.stopPropagation();
1538
+ controller.close();
1539
+ }
1540
+ }
1541
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "data-local-annotation-panel": "", role: "dialog", "aria-label": newItem ? "添加批注" : "编辑批注", "aria-modal": false, children: [
1542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "dla-head", children: [
1543
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: newItem ? "添加批注" : "编辑批注" }),
1544
+ !newItem ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dla-icon dla-danger", title: "删除批注", "aria-label": "删除批注", disabled: busy, onClick: () => {
1545
+ void run(() => controller.remove(base));
1546
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash, {}) }) : null,
1547
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dla-icon dla-close", title: "关闭", "aria-label": "关闭批注", onClick: () => controller.close(), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Close, {}) })
1548
+ ] }),
1549
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("blockquote", { className: "dla-quote", title: quote, children: brief(quote, 200) }),
1550
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1551
+ "textarea",
1552
+ {
1553
+ ref: textarea,
1554
+ "aria-label": "批注内容",
1555
+ placeholder: "写下批注(可留空,仅标记原文)",
1556
+ value: note,
1557
+ maxLength: MAX_NOTE,
1558
+ rows: 3,
1559
+ disabled: busy,
1560
+ spellCheck: false,
1561
+ onChange: (event) => {
1562
+ setNote(event.target.value);
1563
+ controller.setDraft(base, event.target.value);
1564
+ },
1565
+ onKeyDown: keyDown,
1566
+ onCompositionStart: () => {
1567
+ composing.current = true;
1568
+ },
1569
+ onCompositionEnd: () => {
1570
+ composing.current = false;
1571
+ }
1572
+ }
1573
+ ),
1574
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("footer", { className: "dla-footer", children: [
1575
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppearancePicker, { controller, disabled: busy }),
1576
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dla-save", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", disabled: busy || state.loading, onClick: () => {
1577
+ void run(() => controller.save(base, note));
1578
+ }, children: [
1579
+ "✓ ",
1580
+ busy ? "保存中…" : "保存"
1581
+ ] }) })
1582
+ ] }),
1583
+ error || state.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dla-error", role: "alert", children: error || state.error }) : null
1584
+ ] });
1585
+ }
1586
+ function AppearancePicker({ controller, disabled }) {
1587
+ const { appearance } = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1588
+ const [error, setError] = (0, import_react.useState)("");
1589
+ function change(field, value) {
1590
+ try {
1591
+ controller.setAppearance(field, value);
1592
+ setError("");
1593
+ } catch (cause) {
1594
+ setError(cause instanceof Error ? cause.message : "外观设置未保存。");
1595
+ }
1596
+ }
1597
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dla-appearance", style: { "--dla-ink": ink(appearance.color) }, children: [
1598
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: "批注外观" }),
1599
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled, children: [
1600
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: "划线" }),
1601
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dla-options", children: STROKES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-pressed": appearance.stroke === option.id, onClick: () => change("stroke", option.id), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dla-stroke-sample", "data-stroke": option.id, children: option.label }) }, option.id)) })
1602
+ ] }),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled, children: [
1604
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: "颜色" }),
1605
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dla-options", children: [
1606
+ COLORS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dla-swatch", title: option.label, "aria-label": option.label, "aria-pressed": appearance.color.toLowerCase() === option.value, style: { "--dla-swatch": ink(option.value) }, onClick: () => change("color", option.value) }, option.value)),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dla-custom-color", title: "自定义荧光色", children: [
1608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "color", "aria-label": "自定义荧光色", value: appearance.color === "theme" ? COLORS[1].value : appearance.color, onChange: (event) => change("color", event.target.value) }),
1609
+ "自选"
1610
+ ] })
1611
+ ] })
1612
+ ] }),
1613
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled, children: [
1614
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: "旁注图标" }),
1615
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dla-options", children: ICONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", title: option.label, "aria-label": option.label, "aria-pressed": appearance.icon === option.id, onClick: () => change("icon", option.id), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", className: "dla-marker-sample", style: { "--dla-marker-mask": `var(--dla-mask-${option.id})`, "--dla-marker-outline": `var(--dla-outline-${option.id})`, "--dla-marker-radius": option.id === "number" ? "var(--dsw-radius-sm)" : "0" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "1" }) }) }, option.id)) })
1616
+ ] }),
1617
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dla-appearance-hint", children: "立即生效,应用于所有批注" }),
1618
+ error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dla-error", role: "alert", children: error }) : null
1619
+ ] });
1620
+ }
1621
+ function ComposerStatus({ controller, sessionId }) {
1622
+ const state = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1623
+ const [open, setOpen] = (0, import_react.useState)(false);
1624
+ const [error, setError] = (0, import_react.useState)("");
1625
+ (0, import_react.useEffect)(() => {
1626
+ setOpen(false);
1627
+ setError("");
1628
+ }, [sessionId, state.sessionId]);
1629
+ if (sessionId !== state.sessionId || !state.items.length) return null;
1630
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1631
+ "div",
1632
+ {
1633
+ "data-local-annotation-status": "",
1634
+ onMouseEnter: () => setOpen(true),
1635
+ onMouseLeave: () => setOpen(false),
1636
+ onFocus: () => setOpen(true),
1637
+ onBlur: (event) => {
1638
+ if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
1639
+ },
1640
+ onKeyDown: (event) => {
1641
+ if (event.key === "Escape") {
1642
+ event.stopPropagation();
1643
+ setOpen(false);
1644
+ }
1645
+ },
1646
+ children: [
1647
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "dla-chip", "aria-expanded": open, onClick: () => setOpen((value) => !value), children: [
1648
+ "批注 ×",
1649
+ state.items.length
1650
+ ] }),
1651
+ open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dla-preview", role: "region", "aria-label": "已保存批注", children: [
1652
+ state.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dla-preview-item", children: [
1653
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "dla-summary", onClick: () => {
1654
+ setOpen(false);
1655
+ controller.showItem(item);
1656
+ }, children: [
1657
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dla-number", "data-digits": String(index + 1).length, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: index + 1 }) }),
1658
+ brief(item.text, 100),
1659
+ item.note ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dla-preview-note", children: brief(item.note, 60) }) : null
1660
+ ] }),
1661
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dla-icon dla-danger", "aria-label": `删除批注 ${index + 1}`, onClick: () => {
1662
+ void controller.remove(item).catch((cause) => setError(String(cause)));
1663
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash, {}) })
1664
+ ] }, item.id)),
1665
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dla-attach", onClick: () => {
1666
+ if (controller.insert(state.items)) setOpen(false);
1667
+ }, children: "附加到输入框" }),
1668
+ error || state.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dla-error", role: "alert", children: error || state.error }) : null
1669
+ ] }) : null
1670
+ ]
1671
+ }
1672
+ );
1673
+ }
1674
+
1675
+ // src/icons.ts
1676
+ var paths = {
1677
+ message: ["M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"],
1678
+ bookmark: ["M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z"],
1679
+ note: ["M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z", "M15 3v5a1 1 0 0 0 1 1h5"]
1680
+ };
1681
+ function mask(parts, outline, viewBox) {
1682
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" preserveAspectRatio="none" fill="${outline ? "none" : "white"}" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${parts.map((d) => `<path d="${d}"/>`).join("")}</svg>`;
1683
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
1684
+ }
1685
+ var ICON_MASKS = Object.entries(paths).map(([id, parts]) => {
1686
+ const viewBox = id === "bookmark" ? "4 0 16 24" : "1 0 22 24";
1687
+ return `--dla-mask-${id}:${mask(parts, false, viewBox)};--dla-outline-${id}:${mask(parts, true, viewBox)};`;
1688
+ }).join("\n") + "--dla-mask-number:none;--dla-outline-number:linear-gradient(transparent,transparent);";
1689
+
1690
+ // src/styles.ts
1691
+ var STYLE = `
1692
+ :root { ${ICON_MASKS} }
1693
+ ::highlight(dsh-local-annotation) { background-color: var(--dsw-alias-bg-document-selection); }
1694
+ [data-local-annotation-card] { position: fixed; z-index: 1000; top: 0; left: 0; width: fit-content; }
1695
+ [data-local-annotation-card][hidden], [data-local-annotation-toolbar][hidden] { display: none; }
1696
+ [data-local-annotation-toolbar], [data-local-annotation-panel], .dla-preview {
1697
+ box-sizing: border-box; color: var(--dsw-alias-label-primary); background: var(--dsw-menu-surface-fill);
1698
+ backdrop-filter: var(--dsw-menu-backdrop-filter); border: 1px solid var(--dsw-alias-border-l2);
1699
+ border-radius: var(--dsw-radius-panel); corner-shape: var(--dsw-corner-shape); box-shadow: var(--dsw-elevation-panel);
1700
+ font: var(--dsw-font-xs-13); font-family: var(--dsw-font-family); -webkit-app-region: no-drag;
1701
+ }
1702
+ [data-local-annotation-toolbar] { position: fixed; z-index: 1000; padding: 3px; max-width: calc(100vw - 16px); }
1703
+ [data-local-annotation-panel] { width: min(320px, calc(100vw - 16px)); max-height: calc(100dvh - 64px); overflow: auto; padding: 12px; }
1704
+ [data-local-annotation-panel] .dla-head { display: flex; align-items: center; gap: 6px; font: var(--dsw-font-xs-strong-13); }
1705
+ [data-local-annotation-panel] .dla-close { margin-left: auto; }
1706
+ [data-local-annotation-panel] .dla-quote { margin: 10px 0; padding: 4px 9px; border-left: 2px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxs-12); max-height: 76px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
1707
+ [data-local-annotation-panel] textarea { box-sizing: border-box; width: 100%; min-height: 74px; resize: vertical; padding: 8px; font: inherit; color: var(--dsw-alias-label-primary); background: var(--dsw-specific-input-major); border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); corner-shape: var(--dsw-corner-shape); }
1708
+ [data-local-annotation-panel] button, [data-local-annotation-toolbar] button, [data-local-annotation-status] button { font: inherit; color: inherit; border: 0; background: transparent; border-radius: var(--dsw-radius-sm); corner-shape: var(--dsw-corner-shape); cursor: pointer; padding: 4px 8px; -webkit-app-region: no-drag; }
1709
+ :is([data-local-annotation-panel], [data-local-annotation-toolbar], [data-local-annotation-status]) button:hover { background: var(--dsw-alias-interactive-bg-hover); }
1710
+ :is([data-local-annotation-panel], [data-local-annotation-status]) .dla-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 24px; height: 24px; padding: 4px; }
1711
+ :is([data-local-annotation-panel], [data-local-annotation-status]) .dla-danger { color: var(--dsw-alias-label-tertiary); }
1712
+ :is([data-local-annotation-panel], [data-local-annotation-status]) .dla-danger:hover { color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-interactive-bg-hover-danger); }
1713
+ :is([data-local-annotation-panel], [data-local-annotation-toolbar], [data-local-annotation-status]) :is(button,textarea):focus-visible, [data-local-annotation-marker]:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
1714
+ [data-local-annotation-panel] button:disabled { opacity: .5; cursor: default; }
1715
+ .dla-error { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxs-12); overflow-wrap: anywhere; }
1716
+ [data-local-annotation-status] { position: relative; display: inline-flex; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary); }
1717
+ [data-local-annotation-status] .dla-chip { border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-xl); padding: 2px 8px; white-space: nowrap; }
1718
+ [data-local-annotation-status] .dla-preview { position: absolute; bottom: 100%; left: 0; z-index: 1000; width: min(300px,calc(100vw - 32px)); max-height: min(360px,50dvh); overflow: auto; padding: 8px 10px; }
1719
+ [data-local-annotation-status] .dla-preview-item { display: flex; align-items: flex-start; gap: 4px; padding: 6px 0; border-bottom: 1px solid var(--dsw-alias-border-l2); }
1720
+ [data-local-annotation-status] .dla-summary { flex: 1; min-width: 0; padding: 0; text-align: left; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; }
1721
+ [data-local-annotation-status] .dla-preview-note { display: block; color: var(--dsw-alias-label-secondary); margin: 3px 0 0 22px; }
1722
+ [data-local-annotation-status] .dla-number { display: inline-grid; vertical-align: middle; width: 26px; height: 24px; margin-right: 6px; }
1723
+ [data-local-annotation-status] .dla-attach { margin-top: 6px; color: var(--dsw-alias-brand-text); }
1724
+ [data-local-annotation-marker] { display: grid; width: 26px; height: 24px; padding: 0; border: 0; background: transparent; cursor: pointer; pointer-events: auto; -webkit-app-region: no-drag; }
1725
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample) {
1726
+ position: relative; isolation: isolate; place-items: center; box-sizing: border-box;
1727
+ color: var(--dsw-alias-label-primary); font: var(--dsw-font-xxxs-strong-11); font-variant-numeric: tabular-nums;
1728
+ }
1729
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::before,
1730
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::after {
1731
+ content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
1732
+ border-radius: var(--dla-marker-radius, 0);
1733
+ mask: var(--dla-marker-mask, var(--dla-mask-message)) center / 100% 100% no-repeat;
1734
+ }
1735
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::before { background: color-mix(in srgb, var(--dla-ink) 26%, var(--dsw-specific-input-major)); }
1736
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::after {
1737
+ background: color-mix(in srgb, var(--dla-ink) 55%, var(--dsw-alias-label-primary));
1738
+ mask-image: var(--dla-marker-outline, var(--dla-outline-message));
1739
+ }
1740
+ [data-local-annotation-marker]:hover::before { background: color-mix(in srgb, var(--dla-ink) 45%, var(--dsw-specific-input-major)); }
1741
+ [data-local-annotation-marker]:hover { filter: drop-shadow(0 1px 2px var(--dsw-alias-border-l2)); }
1742
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample) > span { position: relative; padding-bottom: 2px; }
1743
+ :is([data-local-annotation-marker], .dla-number)[data-digits='3'] > span { font-size: 9px; }
1744
+ [data-local-annotation-panel] .dla-footer { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-top: 8px; }
1745
+ [data-local-annotation-panel] .dla-save { margin: 0; margin-left: auto; flex: none; }
1746
+ .dla-appearance { min-width: 0; flex: 1; }
1747
+ .dla-appearance summary { width: fit-content; padding: 4px 0; cursor: pointer; color: var(--dsw-alias-label-secondary); font: var(--dsw-font-xxs-12); }
1748
+ .dla-appearance summary:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
1749
+ .dla-appearance fieldset { border: 0; padding: 0; margin: 8px 0; min-width: 0; }
1750
+ .dla-appearance legend { padding: 0; margin-bottom: 5px; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary); }
1751
+ .dla-appearance .dla-options { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
1752
+ [data-local-annotation-panel] .dla-appearance button { border: 1px solid transparent; padding: 4px 6px; }
1753
+ [data-local-annotation-panel] .dla-appearance button[aria-pressed='true'] { border-color: var(--dsw-alias-brand-primary); background: var(--dsw-alias-interactive-bg-hover); }
1754
+ .dla-stroke-sample { display: inline-block; padding: 0 1px 3px; font: var(--dsw-font-xxs-12); }
1755
+ ${STROKES.map((option) => `.dla-stroke-sample[data-stroke='${option.id}'] { ${highlightCSS(option.id, "theme").replaceAll("var(--dsw-alias-brand-primary)", "var(--dla-ink)")} }`).join("\n")}
1756
+ [data-local-annotation-panel] .dla-appearance .dla-swatch { padding: 4px; width: 26px; height: 26px; border-radius: var(--dsw-radius-xl); }
1757
+ .dla-swatch::before { content: ''; display: block; width: 16px; height: 16px; border-radius: 50%; background: var(--dla-swatch); box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l2); }
1758
+ .dla-custom-color { display: inline-flex; align-items: center; gap: 4px; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary); cursor: pointer; }
1759
+ .dla-custom-color input { width: 24px; height: 24px; padding: 0; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: transparent; cursor: pointer; }
1760
+ .dla-custom-color input:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
1761
+ .dla-marker-sample { display: inline-grid; width: 26px; height: 24px; }
1762
+ .dla-appearance-hint { font: var(--dsw-font-xxxs-strong-11); color: var(--dsw-alias-label-tertiary); margin: 4px 0; }
1763
+ `;
1764
+
1765
+ // src/client.tsx
1766
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1767
+ var name = "dsh-local-annotation";
1768
+ var inject = ["uiSession", "slots"];
1769
+ function apply(ctx) {
1770
+ ctx.effect(() => {
1771
+ const style = document.createElement("style");
1772
+ style.dataset.plugin = name;
1773
+ style.textContent = STYLE;
1774
+ document.head.append(style);
1775
+ const host = document.createElement("div");
1776
+ host.dataset.localAnnotationCard = "";
1777
+ host.hidden = true;
1778
+ document.body.append(host);
1779
+ let controller;
1780
+ let root;
1781
+ let disposeStatus;
1782
+ const dispose = () => {
1783
+ disposeStatus?.();
1784
+ root?.unmount();
1785
+ controller?.dispose();
1786
+ host.remove();
1787
+ style.remove();
1788
+ };
1789
+ try {
1790
+ controller = new Controller(ctx, host);
1791
+ root = (0, import_client.createRoot)(host);
1792
+ root.render(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Card, { controller }));
1793
+ const active = controller;
1794
+ disposeStatus = ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
1795
+ name: "conversation.input.left",
1796
+ id: name,
1797
+ inject: (sessionId) => ({ controller: active, sessionId })
1798
+ }, ComposerStatus));
1799
+ return dispose;
1800
+ } catch (error) {
1801
+ dispose();
1802
+ throw error;
1803
+ }
1804
+ }, `${name}: inline annotation card`);
1805
+ }
1806
+ /*! Lucide icon paths: https://lucide.dev — ISC License
1807
+ * Copyright (c) 2026 Lucide Icons and Contributors
1808
+ *
1809
+ * Permission to use, copy, modify, and/or distribute this software for any
1810
+ * purpose with or without fee is hereby granted, provided that the above
1811
+ * copyright notice and this permission notice appear in all copies.
1812
+ *
1813
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1814
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1815
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1816
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1817
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1818
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1819
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1820
+ */
1821
+
1822
+ return module.exports;
1823
+ }
1824
+ });