@cfbender/cesium 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/prompt/system-fragment.md +68 -8
- package/src/render/annotate-frozen.ts +90 -0
- package/src/render/blocks/render.ts +20 -0
- package/src/render/blocks/renderers/callout.ts +3 -2
- package/src/render/blocks/renderers/code.ts +17 -2
- package/src/render/blocks/renderers/compare-table.ts +3 -2
- package/src/render/blocks/renderers/diagram.ts +3 -2
- package/src/render/blocks/renderers/diff.ts +23 -9
- package/src/render/blocks/renderers/hero.ts +3 -2
- package/src/render/blocks/renderers/kv.ts +3 -2
- package/src/render/blocks/renderers/list.ts +5 -4
- package/src/render/blocks/renderers/pill-row.ts +3 -2
- package/src/render/blocks/renderers/prose.ts +8 -2
- package/src/render/blocks/renderers/raw-html.ts +8 -2
- package/src/render/blocks/renderers/risk-table.ts +3 -2
- package/src/render/blocks/renderers/section.ts +4 -2
- package/src/render/blocks/renderers/timeline.ts +3 -2
- package/src/render/blocks/renderers/tldr.ts +3 -2
- package/src/render/client-js.ts +804 -6
- package/src/render/critique.ts +5 -335
- package/src/render/theme.ts +431 -6
- package/src/render/validate.ts +353 -97
- package/src/render/wrap.ts +67 -9
- package/src/server/api.ts +162 -3
- package/src/storage/index-gen.ts +4 -2
- package/src/storage/mutate.ts +433 -27
- package/src/tools/annotate.ts +336 -0
- package/src/tools/ask.ts +2 -6
- package/src/tools/critique.ts +15 -45
- package/src/tools/publish.ts +16 -56
- package/src/tools/styleguide.ts +7 -1
- package/src/tools/wait.ts +77 -24
package/src/render/client-js.ts
CHANGED
|
@@ -16,6 +16,16 @@ export function getClientJs(): string {
|
|
|
16
16
|
var m = window.location.pathname.match(/^\\/projects\\/([^\\/]+)\\/artifacts\\/([^\\/]+)$/);
|
|
17
17
|
var apiBase = m ? "/api/sessions/" + m[1] + "/" + m[2] : null;
|
|
18
18
|
|
|
19
|
+
// ─── Read cesium-meta ────────────────────────────────────────────────────────
|
|
20
|
+
function readMeta() {
|
|
21
|
+
var el = document.getElementById("cesium-meta");
|
|
22
|
+
if (!el) return null;
|
|
23
|
+
try { return JSON.parse(el.textContent || ""); } catch (e) { return null; }
|
|
24
|
+
}
|
|
25
|
+
var meta = readMeta();
|
|
26
|
+
var interactive = meta && meta.interactive;
|
|
27
|
+
var kind = interactive && interactive.kind;
|
|
28
|
+
|
|
19
29
|
// ─── File:// / offline banner ───────────────────────────────────────────────
|
|
20
30
|
if (!apiBase) {
|
|
21
31
|
document.addEventListener("DOMContentLoaded", function () {
|
|
@@ -23,18 +33,18 @@ export function getClientJs(): string {
|
|
|
23
33
|
var banner = document.createElement("div");
|
|
24
34
|
banner.className = "cs-banner cs-banner-offline";
|
|
25
35
|
banner.textContent =
|
|
26
|
-
"
|
|
36
|
+
"Review controls require viewing this artifact via the cesium HTTP server. " +
|
|
27
37
|
"Run cesium open or visit localhost:3030";
|
|
28
38
|
document.body.insertBefore(banner, document.body.firstChild);
|
|
29
39
|
});
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
// ─── Session-ended banner ───────────────────────────────────────────────────
|
|
33
|
-
function showSessionEndedBanner() {
|
|
43
|
+
function showSessionEndedBanner(msg) {
|
|
34
44
|
if (document.querySelector(".cs-banner-ended")) return;
|
|
35
45
|
var banner = document.createElement("div");
|
|
36
46
|
banner.className = "cs-banner cs-banner-ended";
|
|
37
|
-
banner.textContent = "Session ended — answers can no longer be submitted.";
|
|
47
|
+
banner.textContent = msg || "Session ended — answers can no longer be submitted.";
|
|
38
48
|
document.body.insertBefore(banner, document.body.firstChild);
|
|
39
49
|
// Disable all interactive controls
|
|
40
50
|
var disabled = document.querySelectorAll(
|
|
@@ -290,8 +300,8 @@ export function getClientJs(): string {
|
|
|
290
300
|
}
|
|
291
301
|
}
|
|
292
302
|
|
|
293
|
-
// ───
|
|
294
|
-
|
|
303
|
+
// ─── wireAsk ─────────────────────────────────────────────────────────────────
|
|
304
|
+
function wireAsk() {
|
|
295
305
|
var sections = document.querySelectorAll("section[data-question-id]");
|
|
296
306
|
for (var i = 0; i < sections.length; i++) {
|
|
297
307
|
var section = sections[i];
|
|
@@ -311,6 +321,794 @@ export function getClientJs(): string {
|
|
|
311
321
|
wireReact(section);
|
|
312
322
|
}
|
|
313
323
|
}
|
|
314
|
-
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
327
|
+
// ─── wireAnnotate ────────────────────────────────────────────────────────
|
|
328
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
329
|
+
|
|
330
|
+
function wireAnnotate(interactiveData) {
|
|
331
|
+
// ─── Status branch: frozen path for post-verdict artifacts ───────────────
|
|
332
|
+
//
|
|
333
|
+
// wireAnnotateFrozen is defined below but hoisted (function declaration).
|
|
334
|
+
// It handles positioning and hover linking only — no API calls.
|
|
335
|
+
if (interactiveData && interactiveData.status !== "open") {
|
|
336
|
+
wireAnnotateFrozen(interactiveData);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
341
|
+
function escapeHtml(str) {
|
|
342
|
+
return String(str)
|
|
343
|
+
.replace(/&/g, "&")
|
|
344
|
+
.replace(/</g, "<")
|
|
345
|
+
.replace(/>/g, ">")
|
|
346
|
+
.replace(/"/g, """);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function humanizeAnchor(anchor) {
|
|
350
|
+
var parts = String(anchor).split(".");
|
|
351
|
+
var blockPart = parts[0] || "";
|
|
352
|
+
var blockNum = blockPart.replace("block-", "");
|
|
353
|
+
if (parts.length === 1) {
|
|
354
|
+
return "Block " + blockNum;
|
|
355
|
+
}
|
|
356
|
+
var linePart = parts[1] || "";
|
|
357
|
+
var lineNum = linePart.replace("line-", "");
|
|
358
|
+
return "Block " + blockNum + " \u00b7 line " + lineNum;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function apiPost(path, body) {
|
|
362
|
+
return fetch(path, {
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: { "Content-Type": "application/json" },
|
|
365
|
+
body: JSON.stringify(body),
|
|
366
|
+
}).then(function (r) {
|
|
367
|
+
if (r.status === 410) {
|
|
368
|
+
showSessionEndedBanner("Review closed.");
|
|
369
|
+
return r.json().then(function (data) {
|
|
370
|
+
throw new Error(data && data.status ? "session ended: " + data.status : "session ended");
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return r.json().then(function (data) {
|
|
374
|
+
if (!r.ok) {
|
|
375
|
+
var msg = (data && (data.message || data.reason || data.error)) || ("HTTP " + r.status);
|
|
376
|
+
throw new Error(msg);
|
|
377
|
+
}
|
|
378
|
+
return data;
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function apiDelete(path) {
|
|
384
|
+
return fetch(path, { method: "DELETE" }).then(function (r) {
|
|
385
|
+
if (r.status === 410) {
|
|
386
|
+
showSessionEndedBanner("Review closed.");
|
|
387
|
+
return r.json().then(function (data) {
|
|
388
|
+
throw new Error(data && data.status ? "session ended: " + data.status : "session ended");
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return r.json().then(function (data) {
|
|
392
|
+
if (!r.ok) {
|
|
393
|
+
var msg = (data && (data.message || data.reason || data.error)) || ("HTTP " + r.status);
|
|
394
|
+
throw new Error(msg);
|
|
395
|
+
}
|
|
396
|
+
return data;
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─── State ──────────────────────────────────────────────────────────────
|
|
402
|
+
var state = {
|
|
403
|
+
comments: (interactiveData && Array.isArray(interactiveData.comments))
|
|
404
|
+
? interactiveData.comments.slice()
|
|
405
|
+
: [],
|
|
406
|
+
verdictMode: (interactiveData && interactiveData.verdictMode) || "approve",
|
|
407
|
+
status: (interactiveData && interactiveData.status) || "open",
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Active popup reference (only one at a time)
|
|
411
|
+
var activePopup = null;
|
|
412
|
+
|
|
413
|
+
// ─── Count display ───────────────────────────────────────────────────────
|
|
414
|
+
function updateCount() {
|
|
415
|
+
var countEl = document.querySelector("[data-cesium-comment-count]");
|
|
416
|
+
if (!countEl) return;
|
|
417
|
+
var n = state.comments.length;
|
|
418
|
+
countEl.textContent = n === 1 ? "1 comment" : n + " comments";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─── Verdict button enablement ───────────────────────────────────────────
|
|
422
|
+
function updateVerdictButtons() {
|
|
423
|
+
var hasComments = state.comments.length > 0;
|
|
424
|
+
var btns = document.querySelectorAll("button.cs-verdict-btn[data-verdict]");
|
|
425
|
+
for (var i = 0; i < btns.length; i++) {
|
|
426
|
+
var btn = btns[i];
|
|
427
|
+
if (!(btn instanceof HTMLButtonElement)) continue;
|
|
428
|
+
var v = btn.getAttribute("data-verdict");
|
|
429
|
+
// approve — always enabled; request_changes + comment — need comments
|
|
430
|
+
if (v === "approve") {
|
|
431
|
+
btn.disabled = false;
|
|
432
|
+
} else {
|
|
433
|
+
btn.disabled = !hasComments;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Banner error (toplevel) ─────────────────────────────────────────────
|
|
439
|
+
function showBannerError(message) {
|
|
440
|
+
var existing = document.querySelector(".cs-banner-error");
|
|
441
|
+
if (existing) existing.remove();
|
|
442
|
+
var banner = document.createElement("div");
|
|
443
|
+
banner.className = "cs-banner cs-banner-error cs-error";
|
|
444
|
+
banner.setAttribute("role", "alert");
|
|
445
|
+
banner.textContent = message;
|
|
446
|
+
banner.style.cssText =
|
|
447
|
+
"position:fixed;top:0;left:0;right:0;padding:0.75rem 1.25rem;" +
|
|
448
|
+
"text-align:center;z-index:200;";
|
|
449
|
+
document.body.insertBefore(banner, document.body.firstChild);
|
|
450
|
+
setTimeout(function () { if (banner.parentNode) banner.remove(); }, 6000);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ─── Comment rail ────────────────────────────────────────────────────────
|
|
454
|
+
function getRail() {
|
|
455
|
+
return document.querySelector("[data-cesium-comment-rail]");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function buildBubble(comment) {
|
|
459
|
+
var article = document.createElement("article");
|
|
460
|
+
article.className = "cs-comment-bubble";
|
|
461
|
+
article.setAttribute("data-comment-id", comment.id);
|
|
462
|
+
article.setAttribute("data-anchor", comment.anchor);
|
|
463
|
+
|
|
464
|
+
var head = document.createElement("header");
|
|
465
|
+
head.className = "cs-comment-bubble-head";
|
|
466
|
+
|
|
467
|
+
var label = document.createElement("span");
|
|
468
|
+
label.className = "cs-comment-anchor-label";
|
|
469
|
+
label.textContent = humanizeAnchor(comment.anchor);
|
|
470
|
+
|
|
471
|
+
var delBtn = document.createElement("button");
|
|
472
|
+
delBtn.type = "button";
|
|
473
|
+
delBtn.className = "cs-comment-delete";
|
|
474
|
+
delBtn.setAttribute("aria-label", "Delete comment");
|
|
475
|
+
delBtn.textContent = "\u00d7";
|
|
476
|
+
|
|
477
|
+
head.appendChild(label);
|
|
478
|
+
head.appendChild(delBtn);
|
|
479
|
+
|
|
480
|
+
var textEl = document.createElement("p");
|
|
481
|
+
textEl.className = "cs-comment-text";
|
|
482
|
+
textEl.textContent = comment.comment;
|
|
483
|
+
|
|
484
|
+
article.appendChild(head);
|
|
485
|
+
article.appendChild(textEl);
|
|
486
|
+
|
|
487
|
+
if (comment.selectedText && comment.selectedText.trim() !== "") {
|
|
488
|
+
var quoteEl = document.createElement("blockquote");
|
|
489
|
+
quoteEl.className = "cs-comment-bubble-quote";
|
|
490
|
+
quoteEl.textContent = comment.selectedText;
|
|
491
|
+
article.appendChild(quoteEl);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── Delete handler ────────────────────────────────────────────────────
|
|
495
|
+
delBtn.addEventListener("click", function () {
|
|
496
|
+
if (!apiBase) return;
|
|
497
|
+
// Optimistic: mark pending
|
|
498
|
+
article.classList.add("cs-saving");
|
|
499
|
+
delBtn.disabled = true;
|
|
500
|
+
apiDelete(apiBase + "/comments/" + comment.id)
|
|
501
|
+
.then(function () {
|
|
502
|
+
// Remove from state
|
|
503
|
+
state.comments = state.comments.filter(function (c) {
|
|
504
|
+
return c.id !== comment.id;
|
|
505
|
+
});
|
|
506
|
+
article.remove();
|
|
507
|
+
updateCount();
|
|
508
|
+
updateVerdictButtons();
|
|
509
|
+
positionBubbles();
|
|
510
|
+
})
|
|
511
|
+
.catch(function (err) {
|
|
512
|
+
// Restore
|
|
513
|
+
article.classList.remove("cs-saving");
|
|
514
|
+
delBtn.disabled = false;
|
|
515
|
+
showBannerError("Could not delete comment: " + (err instanceof Error ? err.message : String(err)));
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return article;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function mountBubble(comment) {
|
|
523
|
+
var rail = getRail();
|
|
524
|
+
if (!rail) return;
|
|
525
|
+
var bubble = buildBubble(comment);
|
|
526
|
+
rail.appendChild(bubble);
|
|
527
|
+
positionBubbles();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function mountAllSeededComments() {
|
|
531
|
+
for (var i = 0; i < state.comments.length; i++) {
|
|
532
|
+
mountBubble(state.comments[i]);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ─── Position bubbles aligned to anchors (marginalia style) ─────────────
|
|
537
|
+
function positionBubbles() {
|
|
538
|
+
var rail = getRail();
|
|
539
|
+
if (!rail) return;
|
|
540
|
+
var railParent = rail.offsetParent || document.body;
|
|
541
|
+
var railParentTop = railParent instanceof HTMLElement
|
|
542
|
+
? railParent.getBoundingClientRect().top + (window.scrollY || window.pageYOffset || 0)
|
|
543
|
+
: 0;
|
|
544
|
+
var bubbles = rail.querySelectorAll("[data-anchor]");
|
|
545
|
+
for (var i = 0; i < bubbles.length; i++) {
|
|
546
|
+
var bubble = bubbles[i];
|
|
547
|
+
if (!(bubble instanceof HTMLElement)) continue;
|
|
548
|
+
var anchorKey = bubble.getAttribute("data-anchor") || "";
|
|
549
|
+
var anchorEl = document.querySelector("[data-cesium-anchor=\\"" + anchorKey + "\\"]");
|
|
550
|
+
if (anchorEl instanceof HTMLElement) {
|
|
551
|
+
var anchorTop = anchorEl.getBoundingClientRect().top
|
|
552
|
+
+ (window.scrollY || window.pageYOffset || 0)
|
|
553
|
+
- railParentTop;
|
|
554
|
+
bubble.style.top = anchorTop + "px";
|
|
555
|
+
bubble.classList.remove("cs-comment-bubble-orphan");
|
|
556
|
+
} else {
|
|
557
|
+
bubble.style.top = "0px";
|
|
558
|
+
bubble.classList.add("cs-comment-bubble-orphan");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ─── Mutual hover linking ────────────────────────────────────────────────
|
|
564
|
+
function wireHoverLinking() {
|
|
565
|
+
var rail = getRail();
|
|
566
|
+
if (!rail) return;
|
|
567
|
+
|
|
568
|
+
// Bubble → anchor
|
|
569
|
+
rail.addEventListener("mouseover", function (e) {
|
|
570
|
+
var bubble = e.target instanceof Element
|
|
571
|
+
? e.target.closest("[data-anchor]")
|
|
572
|
+
: null;
|
|
573
|
+
if (!(bubble instanceof HTMLElement)) return;
|
|
574
|
+
var anchorKey = bubble.getAttribute("data-anchor") || "";
|
|
575
|
+
var anchorEl = document.querySelector("[data-cesium-anchor=\\"" + anchorKey + "\\"]");
|
|
576
|
+
bubble.classList.add("cs-comment-bubble-active");
|
|
577
|
+
if (anchorEl instanceof HTMLElement) {
|
|
578
|
+
anchorEl.classList.add("cs-anchor-active");
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
rail.addEventListener("mouseout", function (e) {
|
|
582
|
+
var bubble = e.target instanceof Element
|
|
583
|
+
? e.target.closest("[data-anchor]")
|
|
584
|
+
: null;
|
|
585
|
+
if (!(bubble instanceof HTMLElement)) return;
|
|
586
|
+
var anchorKey = bubble.getAttribute("data-anchor") || "";
|
|
587
|
+
var anchorEl = document.querySelector("[data-cesium-anchor=\\"" + anchorKey + "\\"]");
|
|
588
|
+
bubble.classList.remove("cs-comment-bubble-active");
|
|
589
|
+
if (anchorEl instanceof HTMLElement) {
|
|
590
|
+
anchorEl.classList.remove("cs-anchor-active");
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Anchor → bubble
|
|
595
|
+
var anchors = document.querySelectorAll("[data-cesium-anchor]");
|
|
596
|
+
for (var i = 0; i < anchors.length; i++) {
|
|
597
|
+
(function (anchorEl) {
|
|
598
|
+
var anchorKey = anchorEl.getAttribute("data-cesium-anchor") || "";
|
|
599
|
+
anchorEl.addEventListener("mouseenter", function () {
|
|
600
|
+
var bubble = rail.querySelector("[data-anchor=\\"" + anchorKey + "\\"]");
|
|
601
|
+
if (bubble instanceof HTMLElement) {
|
|
602
|
+
bubble.classList.add("cs-comment-bubble-active");
|
|
603
|
+
}
|
|
604
|
+
anchorEl.classList.add("cs-anchor-active");
|
|
605
|
+
});
|
|
606
|
+
anchorEl.addEventListener("mouseleave", function () {
|
|
607
|
+
var bubble = rail.querySelector("[data-anchor=\\"" + anchorKey + "\\"]");
|
|
608
|
+
if (bubble instanceof HTMLElement) {
|
|
609
|
+
bubble.classList.remove("cs-comment-bubble-active");
|
|
610
|
+
}
|
|
611
|
+
anchorEl.classList.remove("cs-anchor-active");
|
|
612
|
+
});
|
|
613
|
+
})(anchors[i]);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ─── Resize debounce ─────────────────────────────────────────────────────
|
|
618
|
+
var resizeTimer = null;
|
|
619
|
+
function onResize() {
|
|
620
|
+
if (resizeTimer !== null) clearTimeout(resizeTimer);
|
|
621
|
+
resizeTimer = setTimeout(function () {
|
|
622
|
+
resizeTimer = null;
|
|
623
|
+
positionBubbles();
|
|
624
|
+
}, 150);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─── Popup ──────────────────────────────────────────────────────────────
|
|
628
|
+
function closePopup() {
|
|
629
|
+
if (activePopup && activePopup.parentNode) {
|
|
630
|
+
activePopup.remove();
|
|
631
|
+
}
|
|
632
|
+
activePopup = null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function openPopup(anchorEl, anchorStr) {
|
|
636
|
+
closePopup();
|
|
637
|
+
|
|
638
|
+
var tmpl = document.getElementById("cs-annotate-comment-popup");
|
|
639
|
+
if (!(tmpl instanceof HTMLTemplateElement)) return;
|
|
640
|
+
var clone = tmpl.content.cloneNode(true);
|
|
641
|
+
var popup = clone instanceof DocumentFragment
|
|
642
|
+
? clone.firstElementChild
|
|
643
|
+
: null;
|
|
644
|
+
if (!popup) return;
|
|
645
|
+
if (!(popup instanceof HTMLElement)) return;
|
|
646
|
+
|
|
647
|
+
popup.setAttribute("data-popup-anchor", anchorStr);
|
|
648
|
+
popup.style.position = "absolute";
|
|
649
|
+
popup.style.zIndex = "200";
|
|
650
|
+
|
|
651
|
+
// Capture selection — also check data-prefill-text (set by selection menu)
|
|
652
|
+
var sel = window.getSelection ? window.getSelection() : null;
|
|
653
|
+
var selText = "";
|
|
654
|
+
if (sel && sel.rangeCount > 0 && sel.toString().trim() !== "") {
|
|
655
|
+
var range = sel.getRangeAt(0);
|
|
656
|
+
// Check containment: range must intersect anchor element
|
|
657
|
+
if (anchorEl.contains(range.commonAncestorContainer)
|
|
658
|
+
|| range.commonAncestorContainer === anchorEl) {
|
|
659
|
+
selText = sel.toString().slice(0, 4096);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Fallback: selection menu prefill (text captured before selection was cleared)
|
|
663
|
+
if (!selText) {
|
|
664
|
+
var prefill = anchorEl.getAttribute("data-prefill-text");
|
|
665
|
+
if (prefill && prefill.trim()) selText = prefill;
|
|
666
|
+
}
|
|
667
|
+
if (!selText) {
|
|
668
|
+
selText = (anchorEl.textContent || "").trim().slice(0, 300);
|
|
669
|
+
}
|
|
670
|
+
popup.setAttribute("data-selected-text", selText);
|
|
671
|
+
|
|
672
|
+
// Prepend quote block above textarea if we have text
|
|
673
|
+
if (selText) {
|
|
674
|
+
var quote = document.createElement("blockquote");
|
|
675
|
+
quote.className = "cs-comment-popup-quote";
|
|
676
|
+
quote.textContent = selText.slice(0, 200) + (selText.length > 200 ? "\u2026" : "");
|
|
677
|
+
popup.insertBefore(quote, popup.firstChild);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
document.body.appendChild(popup);
|
|
681
|
+
activePopup = popup;
|
|
682
|
+
|
|
683
|
+
// Position the popup
|
|
684
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
685
|
+
var scrollX = window.scrollX || window.pageXOffset || 0;
|
|
686
|
+
var scrollY = window.scrollY || window.pageYOffset || 0;
|
|
687
|
+
var viewportW = window.innerWidth || document.documentElement.clientWidth;
|
|
688
|
+
var popupW = 360;
|
|
689
|
+
|
|
690
|
+
var top = rect.bottom + scrollY + 4;
|
|
691
|
+
var left = rect.left + scrollX;
|
|
692
|
+
// Wide viewport: try to place to the right
|
|
693
|
+
if (viewportW > 900 && rect.right + popupW + 16 < viewportW) {
|
|
694
|
+
left = rect.right + scrollX + 12;
|
|
695
|
+
top = rect.top + scrollY;
|
|
696
|
+
}
|
|
697
|
+
// Clamp to viewport
|
|
698
|
+
if (left + popupW > scrollX + viewportW - 16) {
|
|
699
|
+
left = scrollX + viewportW - popupW - 16;
|
|
700
|
+
}
|
|
701
|
+
if (left < scrollX + 8) left = scrollX + 8;
|
|
702
|
+
|
|
703
|
+
popup.style.top = top + "px";
|
|
704
|
+
popup.style.left = left + "px";
|
|
705
|
+
|
|
706
|
+
// Wire save / cancel
|
|
707
|
+
var textarea = popup.querySelector("textarea.cs-comment-input");
|
|
708
|
+
var saveBtn = popup.querySelector("button.cs-comment-save");
|
|
709
|
+
var cancelBtn = popup.querySelector("button.cs-comment-cancel");
|
|
710
|
+
|
|
711
|
+
// Save starts disabled — enable when text is non-empty
|
|
712
|
+
if (saveBtn instanceof HTMLButtonElement) {
|
|
713
|
+
saveBtn.disabled = true;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (textarea instanceof HTMLTextAreaElement && saveBtn instanceof HTMLButtonElement) {
|
|
717
|
+
textarea.addEventListener("input", function () {
|
|
718
|
+
saveBtn.disabled = textarea.value.trim() === "";
|
|
719
|
+
});
|
|
720
|
+
// Cmd/Ctrl+Enter to submit
|
|
721
|
+
textarea.addEventListener("keydown", function (e) {
|
|
722
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "Enter" || e.keyCode === 13)) {
|
|
723
|
+
if (!saveBtn.disabled) saveBtn.click();
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
// Auto-focus
|
|
727
|
+
textarea.focus();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (saveBtn instanceof HTMLButtonElement) {
|
|
731
|
+
saveBtn.addEventListener("click", function () {
|
|
732
|
+
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
|
733
|
+
var commentText = textarea.value;
|
|
734
|
+
if (!commentText.trim()) return;
|
|
735
|
+
if (!apiBase) { closePopup(); return; }
|
|
736
|
+
|
|
737
|
+
// Disable buttons during save
|
|
738
|
+
saveBtn.disabled = true;
|
|
739
|
+
if (cancelBtn instanceof HTMLButtonElement) cancelBtn.disabled = true;
|
|
740
|
+
popup.classList.add("cs-saving");
|
|
741
|
+
|
|
742
|
+
// Remove any prior error
|
|
743
|
+
var priorErr = popup.querySelector(".cs-error");
|
|
744
|
+
if (priorErr) priorErr.remove();
|
|
745
|
+
|
|
746
|
+
apiPost(apiBase + "/comments", {
|
|
747
|
+
anchor: anchorStr,
|
|
748
|
+
selectedText: selText,
|
|
749
|
+
comment: commentText,
|
|
750
|
+
})
|
|
751
|
+
.then(function (resp) {
|
|
752
|
+
var newComment = resp && resp.comment;
|
|
753
|
+
if (newComment) {
|
|
754
|
+
state.comments.push(newComment);
|
|
755
|
+
mountBubble(newComment);
|
|
756
|
+
updateCount();
|
|
757
|
+
updateVerdictButtons();
|
|
758
|
+
}
|
|
759
|
+
closePopup();
|
|
760
|
+
})
|
|
761
|
+
.catch(function (err) {
|
|
762
|
+
popup.classList.remove("cs-saving");
|
|
763
|
+
saveBtn.disabled = textarea.value.trim() === "";
|
|
764
|
+
if (cancelBtn instanceof HTMLButtonElement) cancelBtn.disabled = false;
|
|
765
|
+
var errEl = document.createElement("p");
|
|
766
|
+
errEl.className = "cs-error";
|
|
767
|
+
errEl.setAttribute("role", "alert");
|
|
768
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
769
|
+
popup.appendChild(errEl);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (cancelBtn instanceof HTMLButtonElement) {
|
|
775
|
+
cancelBtn.addEventListener("click", closePopup);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Escape closes the popup
|
|
779
|
+
function onKeydown(e) {
|
|
780
|
+
if (e.key === "Escape" || e.keyCode === 27) {
|
|
781
|
+
closePopup();
|
|
782
|
+
document.removeEventListener("keydown", onKeydown);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
document.addEventListener("keydown", onKeydown);
|
|
786
|
+
|
|
787
|
+
// Click outside closes
|
|
788
|
+
setTimeout(function () {
|
|
789
|
+
function onOutside(e) {
|
|
790
|
+
if (activePopup && !activePopup.contains(e.target)) {
|
|
791
|
+
closePopup();
|
|
792
|
+
document.removeEventListener("click", onOutside);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
document.addEventListener("click", onOutside);
|
|
796
|
+
}, 0);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ─── Inject affordances ──────────────────────────────────────────────────
|
|
800
|
+
function injectAffordances() {
|
|
801
|
+
var anchors = document.querySelectorAll("[data-cesium-anchor]");
|
|
802
|
+
|
|
803
|
+
// Chat-bubble SVG glyph (inline, ~16px, currentColor)
|
|
804
|
+
var bubbleGlyphLg = '<svg class="cs-comment-glyph" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="14" height="10" rx="2.5"/><path d="M4 14l2-3"/></svg>';
|
|
805
|
+
var bubbleGlyphSm = '<svg class="cs-comment-glyph" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="14" height="10" rx="2.5"/><path d="M4 14l2-3"/></svg>';
|
|
806
|
+
|
|
807
|
+
for (var i = 0; i < anchors.length; i++) {
|
|
808
|
+
var anchorEl = anchors[i];
|
|
809
|
+
if (!(anchorEl instanceof HTMLElement)) continue;
|
|
810
|
+
var anchorStr = anchorEl.getAttribute("data-cesium-anchor") || "";
|
|
811
|
+
|
|
812
|
+
// Determine line vs block affordance
|
|
813
|
+
var isLine = /^block-\\d+\\.line-\\d+$/.test(anchorStr);
|
|
814
|
+
|
|
815
|
+
var btn = document.createElement("button");
|
|
816
|
+
btn.type = "button";
|
|
817
|
+
btn.className = isLine
|
|
818
|
+
? "cs-anchor-affordance cs-anchor-affordance-line"
|
|
819
|
+
: "cs-anchor-affordance cs-anchor-affordance-block";
|
|
820
|
+
btn.setAttribute("aria-label", "Add comment");
|
|
821
|
+
btn.setAttribute("data-anchor", anchorStr);
|
|
822
|
+
|
|
823
|
+
if (isLine) {
|
|
824
|
+
// Line affordance: icon only, gutter-positioned, visibility:hidden until hover
|
|
825
|
+
btn.innerHTML = bubbleGlyphSm;
|
|
826
|
+
} else {
|
|
827
|
+
// Block affordance: "Comment" button, always visible, top-right of block
|
|
828
|
+
btn.innerHTML = bubbleGlyphLg + " Comment";
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Wire click: open popup for this anchor
|
|
832
|
+
(function (el, aStr, affordBtn) {
|
|
833
|
+
affordBtn.addEventListener("click", function (e) {
|
|
834
|
+
e.stopPropagation();
|
|
835
|
+
openPopup(el, aStr);
|
|
836
|
+
});
|
|
837
|
+
})(anchorEl, anchorStr, btn);
|
|
838
|
+
|
|
839
|
+
anchorEl.insertBefore(btn, anchorEl.firstChild);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ─── Floating selection menu ─────────────────────────────────────────────
|
|
844
|
+
function wireSelectionMenu() {
|
|
845
|
+
// Skip in frozen or offline mode
|
|
846
|
+
if (!apiBase) return;
|
|
847
|
+
|
|
848
|
+
// Create menu element (injected once into <body>)
|
|
849
|
+
var menu = document.createElement("div");
|
|
850
|
+
menu.className = "cs-selection-menu";
|
|
851
|
+
menu.setAttribute("role", "toolbar");
|
|
852
|
+
menu.hidden = true;
|
|
853
|
+
menu.innerHTML = '<button type="button" class="cs-selection-comment-btn">' +
|
|
854
|
+
'<svg class="cs-comment-glyph" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="14" height="10" rx="2.5"/><path d="M4 14l2-3"/></svg>' +
|
|
855
|
+
' Comment</button>';
|
|
856
|
+
document.body.appendChild(menu);
|
|
857
|
+
|
|
858
|
+
var selectionCommentBtn = menu.querySelector(".cs-selection-comment-btn");
|
|
859
|
+
|
|
860
|
+
// Prevent clearing selection when clicking the button
|
|
861
|
+
menu.addEventListener("mousedown", function (e) {
|
|
862
|
+
e.preventDefault();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
function findAnchorEl(node) {
|
|
866
|
+
var el = node instanceof Element ? node : node.parentElement;
|
|
867
|
+
while (el) {
|
|
868
|
+
if (el.hasAttribute("data-cesium-anchor")) return el;
|
|
869
|
+
el = el.parentElement;
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function hideMenu() {
|
|
875
|
+
menu.hidden = true;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function showMenuNearSelection() {
|
|
879
|
+
// Don't show if popup already open
|
|
880
|
+
if (activePopup !== null) { hideMenu(); return; }
|
|
881
|
+
|
|
882
|
+
var sel = window.getSelection ? window.getSelection() : null;
|
|
883
|
+
if (!sel || sel.rangeCount === 0) { hideMenu(); return; }
|
|
884
|
+
|
|
885
|
+
var text = sel.toString().trim();
|
|
886
|
+
if (text.length < 3) { hideMenu(); return; }
|
|
887
|
+
|
|
888
|
+
var range = sel.getRangeAt(0);
|
|
889
|
+
var commonNode = range.commonAncestorContainer;
|
|
890
|
+
var anchorEl = findAnchorEl(commonNode);
|
|
891
|
+
if (!anchorEl) { hideMenu(); return; }
|
|
892
|
+
|
|
893
|
+
// Position at bottom-right of selection rect
|
|
894
|
+
var rect = range.getBoundingClientRect();
|
|
895
|
+
if (!rect || rect.width === 0 && rect.height === 0) { hideMenu(); return; }
|
|
896
|
+
|
|
897
|
+
menu.hidden = false;
|
|
898
|
+
var menuRect = menu.getBoundingClientRect();
|
|
899
|
+
var top = rect.bottom + window.scrollY + 6;
|
|
900
|
+
var left = rect.right + window.scrollX - menuRect.width;
|
|
901
|
+
// Clamp to viewport
|
|
902
|
+
var vpW = window.innerWidth || document.documentElement.clientWidth;
|
|
903
|
+
if (left + menuRect.width > vpW - 8) left = vpW - menuRect.width - 8;
|
|
904
|
+
if (left < 8) left = 8;
|
|
905
|
+
menu.style.top = top + "px";
|
|
906
|
+
menu.style.left = left + "px";
|
|
907
|
+
|
|
908
|
+
// Store resolved anchor for click handler
|
|
909
|
+
menu.setAttribute("data-resolved-anchor", anchorEl.getAttribute("data-cesium-anchor") || "");
|
|
910
|
+
menu.setAttribute("data-resolved-text", text.slice(0, 4096));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
document.addEventListener("selectionchange", function () {
|
|
914
|
+
// selectionchange fires frequently; use a short debounce
|
|
915
|
+
showMenuNearSelection();
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// Also reposition on mouseup (for initial selection end)
|
|
919
|
+
document.addEventListener("mouseup", function () {
|
|
920
|
+
setTimeout(showMenuNearSelection, 10);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
if (selectionCommentBtn instanceof HTMLButtonElement) {
|
|
924
|
+
selectionCommentBtn.addEventListener("click", function () {
|
|
925
|
+
var anchorStr = menu.getAttribute("data-resolved-anchor") || "";
|
|
926
|
+
var selText = menu.getAttribute("data-resolved-text") || "";
|
|
927
|
+
var anchorEl = anchorStr
|
|
928
|
+
? document.querySelector("[data-cesium-anchor=\\"" + anchorStr + "\\"]")
|
|
929
|
+
: null;
|
|
930
|
+
hideMenu();
|
|
931
|
+
if (!anchorEl || !(anchorEl instanceof HTMLElement)) return;
|
|
932
|
+
// Store the selText so openPopup can use it
|
|
933
|
+
var sel = window.getSelection ? window.getSelection() : null;
|
|
934
|
+
// openPopup reads the live selection — if it was cleared by focus
|
|
935
|
+
// change we inject it back via a temporary attribute
|
|
936
|
+
anchorEl.setAttribute("data-prefill-text", selText);
|
|
937
|
+
openPopup(anchorEl, anchorStr);
|
|
938
|
+
anchorEl.removeAttribute("data-prefill-text");
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Hide when popup opens (activePopup watcher via close/open hooks handled inline above)
|
|
943
|
+
// Hide menu when selection is cleared or moves outside anchored content
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ─── Verdict button wiring ───────────────────────────────────────────────
|
|
948
|
+
function wireVerdictButtons() {
|
|
949
|
+
var btns = document.querySelectorAll("button.cs-verdict-btn[data-verdict]");
|
|
950
|
+
for (var i = 0; i < btns.length; i++) {
|
|
951
|
+
(function (btn) {
|
|
952
|
+
btn.addEventListener("click", function () {
|
|
953
|
+
if (!(btn instanceof HTMLButtonElement)) return;
|
|
954
|
+
if (!apiBase) return;
|
|
955
|
+
var verdictValue = btn.getAttribute("data-verdict");
|
|
956
|
+
if (!verdictValue) return;
|
|
957
|
+
|
|
958
|
+
// Two-step confirmation: show confirm/cancel pair inline
|
|
959
|
+
if (btn.getAttribute("data-confirming") === "true") return;
|
|
960
|
+
btn.setAttribute("data-confirming", "true");
|
|
961
|
+
|
|
962
|
+
var originalText = btn.textContent;
|
|
963
|
+
btn.textContent = "Confirm " + originalText + "?";
|
|
964
|
+
btn.setAttribute("aria-label", "Confirm " + originalText);
|
|
965
|
+
|
|
966
|
+
var cancelConfirmBtn = document.createElement("button");
|
|
967
|
+
cancelConfirmBtn.type = "button";
|
|
968
|
+
cancelConfirmBtn.className = "cs-verdict-btn cs-comment-cancel";
|
|
969
|
+
cancelConfirmBtn.style.cssText = "margin-left:8px;";
|
|
970
|
+
cancelConfirmBtn.textContent = "Cancel";
|
|
971
|
+
|
|
972
|
+
btn.parentNode && btn.parentNode.insertBefore(cancelConfirmBtn, btn.nextSibling);
|
|
973
|
+
|
|
974
|
+
var cancelConfirmTimeout = setTimeout(function () {
|
|
975
|
+
btn.removeAttribute("data-confirming");
|
|
976
|
+
btn.textContent = originalText;
|
|
977
|
+
btn.removeAttribute("aria-label");
|
|
978
|
+
if (cancelConfirmBtn.parentNode) cancelConfirmBtn.remove();
|
|
979
|
+
}, 6000);
|
|
980
|
+
|
|
981
|
+
cancelConfirmBtn.addEventListener("click", function () {
|
|
982
|
+
clearTimeout(cancelConfirmTimeout);
|
|
983
|
+
btn.removeAttribute("data-confirming");
|
|
984
|
+
btn.textContent = originalText;
|
|
985
|
+
btn.removeAttribute("aria-label");
|
|
986
|
+
cancelConfirmBtn.remove();
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
btn.addEventListener("click", function onConfirm() {
|
|
990
|
+
clearTimeout(cancelConfirmTimeout);
|
|
991
|
+
cancelConfirmBtn.remove();
|
|
992
|
+
|
|
993
|
+
// Disable all verdict buttons
|
|
994
|
+
var allVerdictBtns = document.querySelectorAll("button.cs-verdict-btn[data-verdict]");
|
|
995
|
+
for (var j = 0; j < allVerdictBtns.length; j++) {
|
|
996
|
+
var vb = allVerdictBtns[j];
|
|
997
|
+
if (vb instanceof HTMLButtonElement) vb.disabled = true;
|
|
998
|
+
}
|
|
999
|
+
btn.textContent = "Submitting\u2026";
|
|
1000
|
+
btn.removeEventListener("click", onConfirm);
|
|
1001
|
+
|
|
1002
|
+
apiPost(apiBase + "/verdict", { verdict: verdictValue })
|
|
1003
|
+
.then(function () {
|
|
1004
|
+
window.location.reload();
|
|
1005
|
+
})
|
|
1006
|
+
.catch(function (err) {
|
|
1007
|
+
// Re-enable buttons on error
|
|
1008
|
+
for (var j2 = 0; j2 < allVerdictBtns.length; j2++) {
|
|
1009
|
+
var vb2 = allVerdictBtns[j2];
|
|
1010
|
+
if (vb2 instanceof HTMLButtonElement) vb2.disabled = false;
|
|
1011
|
+
}
|
|
1012
|
+
btn.removeAttribute("data-confirming");
|
|
1013
|
+
btn.textContent = originalText;
|
|
1014
|
+
updateVerdictButtons();
|
|
1015
|
+
showBannerError("Could not submit verdict: " + (err instanceof Error ? err.message : String(err)));
|
|
1016
|
+
});
|
|
1017
|
+
}, { once: true });
|
|
1018
|
+
});
|
|
1019
|
+
})(btns[i]);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ─── Freeze UI (non-open status) ─────────────────────────────────────────
|
|
1024
|
+
function freezeUi() {
|
|
1025
|
+
// Hide affordances, disable verdict buttons
|
|
1026
|
+
var affordances = document.querySelectorAll(".cs-anchor-affordance");
|
|
1027
|
+
for (var i = 0; i < affordances.length; i++) {
|
|
1028
|
+
var a = affordances[i];
|
|
1029
|
+
if (a instanceof HTMLElement) a.style.display = "none";
|
|
1030
|
+
}
|
|
1031
|
+
var vBtns = document.querySelectorAll("button.cs-verdict-btn[data-verdict]");
|
|
1032
|
+
for (var j = 0; j < vBtns.length; j++) {
|
|
1033
|
+
var vb = vBtns[j];
|
|
1034
|
+
if (vb instanceof HTMLButtonElement) vb.disabled = true;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ─── wireAnnotateFrozen: read-only path for post-verdict artifacts ────────
|
|
1039
|
+
//
|
|
1040
|
+
// Runs when status !== "open". The client script is kept in the document
|
|
1041
|
+
// so that positionBubbles() and wireHoverLinking() can still run.
|
|
1042
|
+
// All interactive affordances are hidden by CSS (data-cesium-status="complete").
|
|
1043
|
+
function wireAnnotateFrozen(frozenData) {
|
|
1044
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
1045
|
+
// skip frozen: API wiring is intentionally not registered here.
|
|
1046
|
+
// affordances are hidden by CSS; verdict footer is hidden by CSS.
|
|
1047
|
+
document.body.classList.add("cs-annotate-active");
|
|
1048
|
+
|
|
1049
|
+
showSessionEndedBanner("This review is closed.");
|
|
1050
|
+
|
|
1051
|
+
var rail = getRail();
|
|
1052
|
+
|
|
1053
|
+
// If the rail is already populated server-side, skip client-side mounting.
|
|
1054
|
+
// Defensive: if the rail is empty but interactive.comments has entries
|
|
1055
|
+
// (e.g. older artifact), fall back to mounting from state.
|
|
1056
|
+
if (rail && rail.querySelector(".cs-comment-bubble")) {
|
|
1057
|
+
// Rail pre-populated by setVerdict — positioning only
|
|
1058
|
+
requestAnimationFrame(positionBubbles);
|
|
1059
|
+
} else {
|
|
1060
|
+
// Fallback: mount from state (older artifact without server-side render)
|
|
1061
|
+
var comments = (frozenData && Array.isArray(frozenData.comments))
|
|
1062
|
+
? frozenData.comments
|
|
1063
|
+
: [];
|
|
1064
|
+
for (var i = 0; i < comments.length; i++) {
|
|
1065
|
+
mountBubble(comments[i]);
|
|
1066
|
+
}
|
|
1067
|
+
requestAnimationFrame(positionBubbles);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
window.addEventListener("resize", onResize);
|
|
1071
|
+
wireHoverLinking();
|
|
1072
|
+
// NOTE: no injectAffordances, no wireVerdictButtons, no API calls.
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// ─── DOMContentLoaded: main init ─────────────────────────────────────────
|
|
1077
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
1078
|
+
// Add body class for padding-bottom (fallback for browsers without :has)
|
|
1079
|
+
document.body.classList.add("cs-annotate-active");
|
|
1080
|
+
|
|
1081
|
+
if (!apiBase) {
|
|
1082
|
+
// Offline: render seeded comments but hide affordances, disable verdict btns
|
|
1083
|
+
mountAllSeededComments();
|
|
1084
|
+
updateCount();
|
|
1085
|
+
freezeUi();
|
|
1086
|
+
requestAnimationFrame(positionBubbles);
|
|
1087
|
+
window.addEventListener("resize", onResize);
|
|
1088
|
+
wireHoverLinking();
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Normal wiring
|
|
1093
|
+
injectAffordances();
|
|
1094
|
+
mountAllSeededComments();
|
|
1095
|
+
updateCount();
|
|
1096
|
+
updateVerdictButtons();
|
|
1097
|
+
wireVerdictButtons();
|
|
1098
|
+
wireSelectionMenu();
|
|
1099
|
+
requestAnimationFrame(positionBubbles);
|
|
1100
|
+
window.addEventListener("resize", onResize);
|
|
1101
|
+
wireHoverLinking();
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// ─── Dispatch on kind ────────────────────────────────────────────────────────
|
|
1106
|
+
if (kind === "annotate") {
|
|
1107
|
+
wireAnnotate(interactive);
|
|
1108
|
+
} else {
|
|
1109
|
+
// ask (or legacy without kind) — existing wiring
|
|
1110
|
+
document.addEventListener("DOMContentLoaded", wireAsk);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
315
1113
|
})();`;
|
|
316
1114
|
}
|