@jay-framework/aiditor 0.17.1 → 0.17.2

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.
@@ -1,5 +1,5 @@
1
1
  import { makeJayStackComponent } from "@jay-framework/fullstack-component";
2
- import { createSignal, createMemo } from "@jay-framework/component";
2
+ import { createSignal, createMemo, createEffect } from "@jay-framework/component";
3
3
  import { createStreamCaller, createActionCaller, setActionCallerOptions } from "@jay-framework/stack-client-runtime";
4
4
  const submitTaskAction = createStreamCaller("aiditor.submitTask", { acceptsFiles: true });
5
5
  const visualModeToForm = {
@@ -7919,6 +7919,93 @@ var parseBackgroundColor = function(context, element, backgroundColorOverride) {
7919
7919
  var defaultBackgroundColor = typeof backgroundColorOverride === "string" ? parseColor(context, backgroundColorOverride) : backgroundColorOverride === null ? COLORS.TRANSPARENT : 4294967295;
7920
7920
  return element === ownerDocument.documentElement ? isTransparent(documentBackgroundColor) ? isTransparent(bodyBackgroundColor) ? defaultBackgroundColor : bodyBackgroundColor : documentBackgroundColor : defaultBackgroundColor;
7921
7921
  };
7922
+ const TRANSIENT_PSEUDOCLASSES = [
7923
+ ":hover",
7924
+ ":focus",
7925
+ ":focus-within",
7926
+ ":focus-visible"
7927
+ ];
7928
+ const FREEZE_PROPS = [
7929
+ "background",
7930
+ "background-color",
7931
+ "background-image",
7932
+ "color",
7933
+ "opacity",
7934
+ "transform",
7935
+ "box-shadow",
7936
+ "border",
7937
+ "border-color",
7938
+ "border-width",
7939
+ "border-style",
7940
+ "border-radius",
7941
+ "outline",
7942
+ "outline-color",
7943
+ "outline-width",
7944
+ "outline-offset",
7945
+ "text-decoration",
7946
+ "text-decoration-color",
7947
+ "text-shadow",
7948
+ "filter",
7949
+ "visibility",
7950
+ "display",
7951
+ "scale",
7952
+ "cursor",
7953
+ "width",
7954
+ "height",
7955
+ "max-width",
7956
+ "max-height",
7957
+ "min-width",
7958
+ "min-height",
7959
+ "padding",
7960
+ "margin",
7961
+ "top",
7962
+ "left",
7963
+ "right",
7964
+ "bottom",
7965
+ "z-index",
7966
+ "overflow",
7967
+ "clip-path",
7968
+ "backdrop-filter"
7969
+ ];
7970
+ function freezeTransientStyles(iframeEl) {
7971
+ const doc = iframeEl.contentDocument;
7972
+ const win = iframeEl.contentWindow;
7973
+ if (!doc || !win) return () => {
7974
+ };
7975
+ const seen = /* @__PURE__ */ new Set();
7976
+ for (const pseudo of TRANSIENT_PSEUDOCLASSES) {
7977
+ try {
7978
+ for (const el of doc.querySelectorAll(pseudo)) seen.add(el);
7979
+ } catch {
7980
+ }
7981
+ }
7982
+ if (seen.size === 0) return () => {
7983
+ };
7984
+ const saved = [];
7985
+ for (const el of seen) {
7986
+ if (!("setAttribute" in el) || !("getAttribute" in el)) continue;
7987
+ let computed;
7988
+ try {
7989
+ computed = win.getComputedStyle(el);
7990
+ } catch {
7991
+ continue;
7992
+ }
7993
+ const prev = el.getAttribute("style");
7994
+ let inline = prev ?? "";
7995
+ for (const p of FREEZE_PROPS) {
7996
+ const v = computed.getPropertyValue(p);
7997
+ if (v) inline += `; ${p}: ${v} !important`;
7998
+ }
7999
+ saved.push({ el, prev });
8000
+ el.setAttribute("style", inline);
8001
+ }
8002
+ return () => {
8003
+ for (const { el, prev } of saved) {
8004
+ if (prev !== null) el.setAttribute("style", prev);
8005
+ else el.removeAttribute("style");
8006
+ }
8007
+ };
8008
+ }
7922
8009
  function getIframeViewportScrollForHtml2Canvas(win, doc) {
7923
8010
  let sx = win.pageXOffset;
7924
8011
  let sy = win.pageYOffset;
@@ -8283,6 +8370,123 @@ async function capturePreviewForVideoFrame(rootEl, videoEl, mode = "full") {
8283
8370
  if (mode === "full" && popoversEl) await drawLayer(popoversEl);
8284
8371
  return canvasToPng(canvas);
8285
8372
  }
8373
+ async function compositeMarkersOntoScreenshot(cleanBlob, annotations) {
8374
+ const img = await blobToImage(cleanBlob);
8375
+ const canvas = document.createElement("canvas");
8376
+ canvas.width = img.naturalWidth;
8377
+ canvas.height = img.naturalHeight;
8378
+ const ctx = canvas.getContext("2d");
8379
+ ctx.drawImage(img, 0, 0);
8380
+ const w = canvas.width;
8381
+ const h = canvas.height;
8382
+ const markerColor = "#c586ff";
8383
+ const markerBorder = "#ffffff";
8384
+ const fontSize2 = Math.max(12, Math.round(Math.min(w, h) * 0.018));
8385
+ for (const a2 of annotations) {
8386
+ const g = a2.geometry;
8387
+ if (g.kind === "point") {
8388
+ const px = g.x * w;
8389
+ const py = g.y * h;
8390
+ const r = Math.max(7, Math.round(Math.min(w, h) * 8e-3));
8391
+ ctx.beginPath();
8392
+ ctx.arc(px, py, r, 0, Math.PI * 2);
8393
+ ctx.fillStyle = markerColor;
8394
+ ctx.fill();
8395
+ ctx.lineWidth = 2;
8396
+ ctx.strokeStyle = markerBorder;
8397
+ ctx.stroke();
8398
+ drawMarkerLabel(ctx, a2.id, px + r + 2, py - r, fontSize2, markerColor);
8399
+ } else if (g.kind === "area") {
8400
+ const ax = g.x * w;
8401
+ const ay = g.y * h;
8402
+ const aw = g.w * w;
8403
+ const ah = g.h * h;
8404
+ ctx.strokeStyle = markerColor;
8405
+ ctx.lineWidth = 2;
8406
+ ctx.setLineDash([6, 4]);
8407
+ ctx.strokeRect(ax, ay, aw, ah);
8408
+ ctx.setLineDash([]);
8409
+ ctx.fillStyle = "rgba(197, 134, 255, 0.12)";
8410
+ ctx.fillRect(ax, ay, aw, ah);
8411
+ drawMarkerLabel(ctx, a2.id, ax + 4, ay + 4, fontSize2, markerColor);
8412
+ } else if (g.kind === "arrow") {
8413
+ const x1 = g.x1 * w;
8414
+ const y1 = g.y1 * h;
8415
+ const x2 = g.x2 * w;
8416
+ const y2 = g.y2 * h;
8417
+ ctx.strokeStyle = markerColor;
8418
+ ctx.lineWidth = 2;
8419
+ ctx.setLineDash([]);
8420
+ ctx.beginPath();
8421
+ ctx.moveTo(x1, y1);
8422
+ ctx.lineTo(x2, y2);
8423
+ ctx.stroke();
8424
+ drawArrowhead(ctx, x1, y1, x2, y2, 10, markerColor);
8425
+ const pinR = Math.max(11, Math.round(Math.min(w, h) * 0.014));
8426
+ ctx.beginPath();
8427
+ ctx.arc(x2, y2, pinR, 0, Math.PI * 2);
8428
+ ctx.fillStyle = markerColor;
8429
+ ctx.fill();
8430
+ ctx.lineWidth = 2;
8431
+ ctx.strokeStyle = markerBorder;
8432
+ ctx.stroke();
8433
+ ctx.fillStyle = "#1e1e1e";
8434
+ ctx.font = `bold ${Math.round(fontSize2 * 0.9)}px sans-serif`;
8435
+ ctx.textAlign = "center";
8436
+ ctx.textBaseline = "middle";
8437
+ ctx.fillText(a2.id, x2, y2);
8438
+ }
8439
+ }
8440
+ return canvasToPng(canvas);
8441
+ }
8442
+ function drawMarkerLabel(ctx, label, x, y, fontSize2, bgColor) {
8443
+ ctx.font = `bold ${fontSize2}px sans-serif`;
8444
+ const tm = ctx.measureText(label);
8445
+ const pad = 4;
8446
+ const lh = fontSize2 + 4;
8447
+ const lw = Math.max(tm.width + pad * 2, lh);
8448
+ const rx = x;
8449
+ const ry = y;
8450
+ ctx.fillStyle = bgColor;
8451
+ ctx.beginPath();
8452
+ ctx.roundRect(rx, ry, lw, lh, 4);
8453
+ ctx.fill();
8454
+ ctx.fillStyle = "#1e1e1e";
8455
+ ctx.textAlign = "center";
8456
+ ctx.textBaseline = "middle";
8457
+ ctx.fillText(label, rx + lw / 2, ry + lh / 2);
8458
+ }
8459
+ function drawArrowhead(ctx, x1, y1, x2, y2, size, color2) {
8460
+ const angle2 = Math.atan2(y2 - y1, x2 - x1);
8461
+ ctx.fillStyle = color2;
8462
+ ctx.beginPath();
8463
+ ctx.moveTo(x2, y2);
8464
+ ctx.lineTo(
8465
+ x2 - size * Math.cos(angle2 - Math.PI / 6),
8466
+ y2 - size * Math.sin(angle2 - Math.PI / 6)
8467
+ );
8468
+ ctx.lineTo(
8469
+ x2 - size * Math.cos(angle2 + Math.PI / 6),
8470
+ y2 - size * Math.sin(angle2 + Math.PI / 6)
8471
+ );
8472
+ ctx.closePath();
8473
+ ctx.fill();
8474
+ }
8475
+ function blobToImage(blob) {
8476
+ return new Promise((resolve, reject) => {
8477
+ const url = URL.createObjectURL(blob);
8478
+ const img = new Image();
8479
+ img.onload = () => {
8480
+ URL.revokeObjectURL(url);
8481
+ resolve(img);
8482
+ };
8483
+ img.onerror = () => {
8484
+ URL.revokeObjectURL(url);
8485
+ reject(new Error("Failed to load screenshot image for compositing"));
8486
+ };
8487
+ img.src = url;
8488
+ });
8489
+ }
8286
8490
  async function captureVideoInstantDual(rootEl, videoEl) {
8287
8491
  const clean = await capturePreviewForVideoFrame(rootEl, videoEl, "clean");
8288
8492
  const markersOnly = await capturePreviewForVideoFrame(
@@ -8774,6 +8978,20 @@ function aiditorConstructor(_props, refs) {
8774
8978
  const [videoRecordingNavLog, setVideoRecordingNavLog] = createSignal([]);
8775
8979
  const [recordingDraftAnnotation, setRecordingDraftAnnotation] = createSignal(null);
8776
8980
  const [videoPinsStagedDuringRecording, setVideoPinsStagedDuringRecording] = createSignal([]);
8981
+ const [showSnapshotModal, setShowSnapshotModal] = createSignal(false);
8982
+ const [snapshotCapture, setSnapshotCapture] = createSignal(null);
8983
+ const [snapshotAnnotations, setSnapshotAnnotations] = createSignal([]);
8984
+ const [snapshotTool, setSnapshotTool] = createSignal("point");
8985
+ const [snapshotAreaDraft, setSnapshotAreaDraft] = createSignal(null);
8986
+ const [snapshotDiscardConfirm, setSnapshotDiscardConfirm] = createSignal({
8987
+ visible: false
8988
+ });
8989
+ const [snapshotArrowPending, setSnapshotArrowPending] = createSignal(null);
8990
+ const [snapshotSubmitError, setSnapshotSubmitError] = createSignal("");
8991
+ let snapshotAreaDragActive = false;
8992
+ let snapshotAreaStartNorm = { x: 0, y: 0 };
8993
+ let snapshotAreaListeners = null;
8994
+ let snapshotAttachTargetPinId = null;
8777
8995
  let areaDragActive = false;
8778
8996
  let areaStartNorm = { x: 0, y: 0 };
8779
8997
  let areaListeners = null;
@@ -8790,6 +9008,7 @@ function aiditorConstructor(_props, refs) {
8790
9008
  let recordingStartedAt = 0;
8791
9009
  let recordingPreviewNavLog = [];
8792
9010
  let previewProbeTimer = null;
9011
+ let iframeShortcutDoc = null;
8793
9012
  let attachVideoTargetPinId = null;
8794
9013
  let nextVideoPinSeq = 1;
8795
9014
  let recordingArrowCarryFromDraft = null;
@@ -9210,6 +9429,126 @@ function aiditorConstructor(_props, refs) {
9210
9429
  return false;
9211
9430
  return list.some((a2) => a2.instruction.trim().length === 0);
9212
9431
  });
9432
+ const snapshotImageSrc = createMemo(() => snapshotCapture()?.imageUrl ?? "");
9433
+ const snapshotCanSubmit = createMemo(() => snapshotAnnotations().length > 0 && !isRunning());
9434
+ const snapshotShowDiscardConfirm = createMemo(() => snapshotDiscardConfirm().visible);
9435
+ const snapshotToolNoneOn = createMemo(() => snapshotTool() === "none");
9436
+ const snapshotToolPointOn = createMemo(() => snapshotTool() === "point");
9437
+ const snapshotToolAreaOn = createMemo(() => snapshotTool() === "area");
9438
+ const snapshotToolArrowOn = createMemo(() => snapshotTool() === "arrow");
9439
+ const snapshotOverlayPointerNone = createMemo(() => snapshotTool() === "none");
9440
+ const snapshotPointDisplayItems = createMemo(() => {
9441
+ const list = snapshotAnnotations();
9442
+ let pin = 0;
9443
+ const out = [];
9444
+ for (const a2 of list) {
9445
+ pin += 1;
9446
+ if (a2.kind === "point") {
9447
+ out.push({
9448
+ id: a2.id,
9449
+ leftPct: a2.x * 100,
9450
+ topPct: a2.y * 100,
9451
+ indexLabel: String(pin)
9452
+ });
9453
+ }
9454
+ }
9455
+ return out;
9456
+ });
9457
+ const snapshotAreaDisplayItems = createMemo(() => {
9458
+ const list = snapshotAnnotations();
9459
+ let pin = 0;
9460
+ const out = [];
9461
+ for (const a2 of list) {
9462
+ pin += 1;
9463
+ if (a2.kind === "area") {
9464
+ out.push({
9465
+ id: a2.id,
9466
+ leftPct: a2.x * 100,
9467
+ topPct: a2.y * 100,
9468
+ widthPct: a2.w * 100,
9469
+ heightPct: a2.h * 100,
9470
+ indexLabel: String(pin)
9471
+ });
9472
+ }
9473
+ }
9474
+ return out;
9475
+ });
9476
+ const snapshotArrowDisplayItems = createMemo(() => {
9477
+ const list = snapshotAnnotations();
9478
+ const out = [];
9479
+ for (const a2 of list) {
9480
+ if (a2.kind === "arrow") {
9481
+ out.push({
9482
+ id: a2.id,
9483
+ x1Pct: a2.x1 * 100,
9484
+ y1Pct: a2.y1 * 100,
9485
+ x2Pct: a2.x2 * 100,
9486
+ y2Pct: a2.y2 * 100
9487
+ });
9488
+ }
9489
+ }
9490
+ return out;
9491
+ });
9492
+ const snapshotArrowPinItems = createMemo(() => {
9493
+ const list = snapshotAnnotations();
9494
+ let pin = 0;
9495
+ const out = [];
9496
+ for (const a2 of list) {
9497
+ pin += 1;
9498
+ if (a2.kind === "arrow") {
9499
+ out.push({
9500
+ id: a2.id,
9501
+ leftPct: a2.x2 * 100,
9502
+ topPct: a2.y2 * 100,
9503
+ indexLabel: String(pin)
9504
+ });
9505
+ }
9506
+ }
9507
+ return out;
9508
+ });
9509
+ const hasSnapshotArrowLines = createMemo(() => snapshotArrowDisplayItems().length > 0);
9510
+ const showSnapshotArrowPending = createMemo(() => snapshotArrowPending() !== null);
9511
+ const snapshotArrowPendingLeftPct = createMemo(() => (snapshotArrowPending()?.x ?? 0) * 100);
9512
+ const snapshotArrowPendingTopPct = createMemo(() => (snapshotArrowPending()?.y ?? 0) * 100);
9513
+ const snapshotAreaDraftNorm = createMemo(() => {
9514
+ const d = snapshotAreaDraft();
9515
+ if (!d)
9516
+ return null;
9517
+ return normalizeAreaRect(d.startX, d.startY, d.currentX, d.currentY);
9518
+ });
9519
+ const showSnapshotAreaDraft = createMemo(() => snapshotAreaDraftNorm() !== null);
9520
+ const snapshotAreaDraftLeftPct = createMemo(() => (snapshotAreaDraftNorm()?.x ?? 0) * 100);
9521
+ const snapshotAreaDraftTopPct = createMemo(() => (snapshotAreaDraftNorm()?.y ?? 0) * 100);
9522
+ const snapshotAreaDraftWidthPct = createMemo(() => (snapshotAreaDraftNorm()?.w ?? 0) * 100);
9523
+ const snapshotAreaDraftHeightPct = createMemo(() => (snapshotAreaDraftNorm()?.h ?? 0) * 100);
9524
+ const snapshotAnnotationItems = createMemo(() => snapshotAnnotations().map((a2) => {
9525
+ const { nx, ny } = bubbleAnchorNorm(a2);
9526
+ const leftPct = nx * 100;
9527
+ const topPct = ny * 100;
9528
+ const { flipX, flipY } = popoverFlipFromPct(leftPct, topPct);
9529
+ const attachmentChips = a2.attachments.map((at) => ({
9530
+ key: at.id,
9531
+ name: at.file.name,
9532
+ thumbUrl: at.previewUrl && at.file.type.startsWith("image/") ? at.previewUrl : "",
9533
+ annotationId: a2.id
9534
+ }));
9535
+ return {
9536
+ id: a2.id,
9537
+ indexLabel: a2.id,
9538
+ kindLabel: `${a2.id} — ${kindLabel(a2.kind)}`,
9539
+ instruction: a2.instruction,
9540
+ kind: a2.kind,
9541
+ leftPct,
9542
+ topPct,
9543
+ popoverFlipX: flipX,
9544
+ popoverFlipY: flipY,
9545
+ attachmentChips
9546
+ };
9547
+ }));
9548
+ const showSnapshotAnnotationsPanel = createMemo(() => snapshotAnnotations().length > 0);
9549
+ const showSnapshotSubmitError = createMemo(() => snapshotSubmitError().trim().length > 0);
9550
+ const showSnapshotPointMarkers = createMemo(() => snapshotPointDisplayItems().length > 0);
9551
+ const showSnapshotAreaItems = createMemo(() => snapshotAreaDisplayItems().length > 0);
9213
9552
  const chunkItems = createMemo(() => chunks());
9214
9553
  const hasChunks = createMemo(() => chunks().length > 0);
9215
9554
  const outputEmpty = createMemo(() => chunks().length === 0 && !isRunning());
@@ -9267,6 +9606,8 @@ function aiditorConstructor(_props, refs) {
9267
9606
  mediaRecorder?.stop();
9268
9607
  }
9269
9608
  tearDownVideoReviewModal();
9609
+ if (showSnapshotModal())
9610
+ tearDownSnapshotModal();
9270
9611
  cancelRecordingDraftAndClear();
9271
9612
  setVideoPinsStagedDuringRecording([]);
9272
9613
  lastRecordingWallDurationSec = 0;
@@ -9287,6 +9628,152 @@ function aiditorConstructor(_props, refs) {
9287
9628
  cancelAreaDragListeners();
9288
9629
  setVisualTool(next);
9289
9630
  }
9631
+ function cancelSnapshotAreaDragListeners() {
9632
+ snapshotAreaDragActive = false;
9633
+ if (snapshotAreaListeners) {
9634
+ snapshotAreaListeners();
9635
+ snapshotAreaListeners = null;
9636
+ }
9637
+ }
9638
+ function tearDownSnapshotModal() {
9639
+ cancelSnapshotAreaDragListeners();
9640
+ revokeAttachmentUrls(snapshotAnnotations());
9641
+ const cap = snapshotCapture();
9642
+ if (cap?.imageUrl)
9643
+ URL.revokeObjectURL(cap.imageUrl);
9644
+ setSnapshotCapture(null);
9645
+ setSnapshotAnnotations([]);
9646
+ setSnapshotTool("point");
9647
+ setSnapshotAreaDraft(null);
9648
+ setSnapshotArrowPending(null);
9649
+ setSnapshotDiscardConfirm({ visible: false });
9650
+ setSnapshotSubmitError("");
9651
+ setShowSnapshotModal(false);
9652
+ }
9653
+ function onSelectSnapshotTool(next) {
9654
+ setSnapshotArrowPending(null);
9655
+ setSnapshotAreaDraft(null);
9656
+ setSnapshotSubmitError("");
9657
+ cancelSnapshotAreaDragListeners();
9658
+ setSnapshotTool(next);
9659
+ }
9660
+ function setSnapshotAnnotationInstruction(id, instruction) {
9661
+ setSnapshotAnnotations((prev) => prev.map((a2) => a2.id === id ? { ...a2, instruction } : a2));
9662
+ setSnapshotSubmitError("");
9663
+ }
9664
+ function removeSnapshotAnnotation(id) {
9665
+ setSnapshotAnnotations((prev) => {
9666
+ const victim = prev.find((a2) => a2.id === id);
9667
+ if (victim)
9668
+ revokeAttachmentUrls([victim]);
9669
+ return renumberAnnotations(prev.filter((a2) => a2.id !== id));
9670
+ });
9671
+ setSnapshotSubmitError("");
9672
+ }
9673
+ function snapshotUndo() {
9674
+ setSnapshotAnnotations((prev) => {
9675
+ if (prev.length === 0)
9676
+ return prev;
9677
+ const victim = prev[prev.length - 1];
9678
+ revokeAttachmentUrls([victim]);
9679
+ return renumberAnnotations(prev.slice(0, -1));
9680
+ });
9681
+ }
9682
+ function snapshotClearAll() {
9683
+ revokeAttachmentUrls(snapshotAnnotations());
9684
+ setSnapshotAnnotations([]);
9685
+ setSnapshotArrowPending(null);
9686
+ setSnapshotAreaDraft(null);
9687
+ cancelSnapshotAreaDragListeners();
9688
+ }
9689
+ function tryDismissSnapshotModal() {
9690
+ if (snapshotAnnotations().length > 0) {
9691
+ setSnapshotDiscardConfirm({ visible: true });
9692
+ } else {
9693
+ tearDownSnapshotModal();
9694
+ }
9695
+ }
9696
+ function addFilesToSnapshotAnnotation(annotationId, files) {
9697
+ setSnapshotAnnotations((prev) => {
9698
+ const ix = prev.findIndex((a2) => a2.id === annotationId);
9699
+ if (ix < 0)
9700
+ return prev;
9701
+ const ann = prev[ix];
9702
+ let nextAtt = [...ann.attachments];
9703
+ let byteSum = nextAtt.reduce((s, x) => s + x.file.size, 0);
9704
+ for (const file of files) {
9705
+ if (nextAtt.length >= MAX_ATTACHMENTS_PER_PIN) {
9706
+ setSnapshotSubmitError(`Each pin allows at most ${MAX_ATTACHMENTS_PER_PIN} files.`);
9707
+ break;
9708
+ }
9709
+ if (byteSum + file.size > MAX_ATTACHMENT_BYTES_PER_PIN) {
9710
+ setSnapshotSubmitError(`File "${file.name}" is ${mbLabel(file.size)}; max per pin is ${mbLabel(MAX_ATTACHMENT_BYTES_PER_PIN)}.`);
9711
+ break;
9712
+ }
9713
+ const id = newLocalAttachmentId();
9714
+ const loc = { id, file };
9715
+ if (file.type.startsWith("image/")) {
9716
+ loc.previewUrl = URL.createObjectURL(file);
9717
+ }
9718
+ nextAtt = [...nextAtt, loc];
9719
+ byteSum += file.size;
9720
+ }
9721
+ const nextAnn = { ...ann, attachments: nextAtt };
9722
+ const copy = [...prev];
9723
+ copy[ix] = nextAnn;
9724
+ return copy;
9725
+ });
9726
+ }
9727
+ function removeSnapshotAttachmentFile(annotationId, attachmentId) {
9728
+ setSnapshotAnnotations((prev) => {
9729
+ const ix = prev.findIndex((a2) => a2.id === annotationId);
9730
+ if (ix < 0)
9731
+ return prev;
9732
+ const ann = prev[ix];
9733
+ const victim = ann.attachments.find((x) => x.id === attachmentId);
9734
+ if (victim?.previewUrl)
9735
+ URL.revokeObjectURL(victim.previewUrl);
9736
+ const nextAtt = ann.attachments.filter((x) => x.id !== attachmentId);
9737
+ const nextAnn = { ...ann, attachments: nextAtt };
9738
+ const copy = [...prev];
9739
+ copy[ix] = nextAnn;
9740
+ return copy;
9741
+ });
9742
+ }
9743
+ function snapshotNormFromEvent(overlayEl, imgEl, e2) {
9744
+ const imgRect = imgEl.getBoundingClientRect();
9745
+ const imgNaturalAspect = imgEl.naturalWidth / imgEl.naturalHeight;
9746
+ const containerAspect = imgRect.width / imgRect.height;
9747
+ let renderedW, renderedH, offsetX, offsetY;
9748
+ if (imgNaturalAspect > containerAspect) {
9749
+ renderedW = imgRect.width;
9750
+ renderedH = imgRect.width / imgNaturalAspect;
9751
+ offsetX = 0;
9752
+ offsetY = (imgRect.height - renderedH) / 2;
9753
+ } else {
9754
+ renderedH = imgRect.height;
9755
+ renderedW = imgRect.height * imgNaturalAspect;
9756
+ offsetX = (imgRect.width - renderedW) / 2;
9757
+ offsetY = 0;
9758
+ }
9759
+ const imgContentLeft = imgRect.left + offsetX;
9760
+ const imgContentTop = imgRect.top + offsetY;
9761
+ const x = (e2.clientX - imgContentLeft) / renderedW;
9762
+ const y = (e2.clientY - imgContentTop) / renderedH;
9763
+ if (x < 0 || x > 1 || y < 0 || y > 1)
9764
+ return null;
9765
+ return { x, y };
9766
+ }
9767
+ function focusLastSnapshotAnnotationTextarea() {
9768
+ requestAnimationFrame(() => {
9769
+ tryJayRefExec(refs.snapshotBackdrop, (backdrop) => {
9770
+ const textareas = backdrop.querySelectorAll("textarea.visual-annotation-instruction");
9771
+ const last = textareas[textareas.length - 1];
9772
+ if (last)
9773
+ last.focus();
9774
+ });
9775
+ });
9776
+ }
9290
9777
  function validateVisualAnnotationsForSubmit() {
9291
9778
  const list = visualAnnotations();
9292
9779
  if (list.length === 0)
@@ -9593,6 +10080,24 @@ function aiditorConstructor(_props, refs) {
9593
10080
  }
9594
10081
  runBootstrap();
9595
10082
  ensurePreviewProbePoll();
10083
+ function handleSnapshotShortcut() {
10084
+ if (visualTool() !== "none" || !showPreviewIframe() || showSnapshotModal() || showVideoReviewModal() || isVideoRecording())
10085
+ return;
10086
+ void triggerSnapshotCapture();
10087
+ }
10088
+ if (typeof document !== "undefined") {
10089
+ document.addEventListener("keydown", (e2) => {
10090
+ if ((e2.key === "s" || e2.key === "S") && (e2.metaKey || e2.ctrlKey) && e2.shiftKey && !e2.altKey) {
10091
+ e2.preventDefault();
10092
+ handleSnapshotShortcut();
10093
+ }
10094
+ }, true);
10095
+ }
10096
+ if (typeof window !== "undefined") {
10097
+ window.addEventListener("aiditor:snapshot-shortcut", () => {
10098
+ handleSnapshotShortcut();
10099
+ });
10100
+ }
9596
10101
  if (typeof window !== "undefined") {
9597
10102
  window.addEventListener("message", (e2) => {
9598
10103
  if (e2.data?.type === "jay:freeze") {
@@ -9931,6 +10436,98 @@ function aiditorConstructor(_props, refs) {
9931
10436
  setIsRunning(false);
9932
10437
  }
9933
10438
  }
10439
+ async function runSnapshotSubmit() {
10440
+ const list = snapshotAnnotations();
10441
+ if (list.length === 0) {
10442
+ setSnapshotSubmitError("Add at least one marker on the screenshot.");
10443
+ return;
10444
+ }
10445
+ const cap = snapshotCapture();
10446
+ if (!cap) {
10447
+ setSnapshotSubmitError("Screenshot data is missing.");
10448
+ return;
10449
+ }
10450
+ const notesJson = serializeAiditorNotes(toNotesPayload(list, previewBreakpoint()));
10451
+ try {
10452
+ setIsRunning(true);
10453
+ setSnapshotSubmitError("");
10454
+ const markerGeometries = list.map((a2) => ({
10455
+ id: a2.id,
10456
+ geometry: serializeGeometry(a2)
10457
+ }));
10458
+ const markersBlob = await compositeMarkersOntoScreenshot(cap.cleanBlob, markerGeometries);
10459
+ const attachmentsByAnnotationId = /* @__PURE__ */ new Map();
10460
+ for (const a2 of list) {
10461
+ if (a2.attachments.length > 0) {
10462
+ attachmentsByAnnotationId.set(a2.id, a2.attachments.map((x) => x.file));
10463
+ }
10464
+ }
10465
+ const dual = { clean: cap.cleanBlob, markersOnly: markersBlob };
10466
+ const route = cap.routePattern;
10467
+ const rendered = cap.renderedUrl;
10468
+ tearDownSnapshotModal();
10469
+ clearVisualSession();
10470
+ startAgentStream(notesJson, {
10471
+ mode: "multi",
10472
+ pageRoute: route,
10473
+ renderedUrl: rendered,
10474
+ dual,
10475
+ attachmentsByAnnotationId: attachmentsByAnnotationId.size > 0 ? attachmentsByAnnotationId : void 0
10476
+ });
10477
+ } catch (err) {
10478
+ console.error("snapshot submit:", err);
10479
+ setSnapshotSubmitError(err instanceof Error ? err.message : String(err));
10480
+ } finally {
10481
+ setIsRunning(false);
10482
+ }
10483
+ }
10484
+ async function triggerSnapshotCapture() {
10485
+ if (showSnapshotModal() || isRunning())
10486
+ return;
10487
+ const route = selectedPageUrl();
10488
+ const rendered = previewUrlBar().trim();
10489
+ if (!route || !rendered)
10490
+ return;
10491
+ if (!previewIsSameOriginForCapture(previewSrc()))
10492
+ return;
10493
+ let rootEl = null;
10494
+ tryJayRefExec(refs.previewCaptureRoot, (el) => {
10495
+ rootEl = el;
10496
+ });
10497
+ if (!rootEl)
10498
+ return;
10499
+ let iframeEl = null;
10500
+ tryJayRefExec(refs.previewIframe, (el) => {
10501
+ iframeEl = el;
10502
+ });
10503
+ try {
10504
+ const unfreeze = iframeEl ? freezeTransientStyles(iframeEl) : () => {
10505
+ };
10506
+ let cleanBlob;
10507
+ try {
10508
+ cleanBlob = await capturePreviewForTask(rootEl, "clean");
10509
+ } finally {
10510
+ unfreeze();
10511
+ }
10512
+ const imageUrl = URL.createObjectURL(cleanBlob);
10513
+ setSnapshotCapture({
10514
+ cleanBlob,
10515
+ imageUrl,
10516
+ previewUrl: rendered,
10517
+ routePattern: route,
10518
+ renderedUrl: rendered
10519
+ });
10520
+ setSnapshotAnnotations([]);
10521
+ setSnapshotTool("point");
10522
+ setSnapshotAreaDraft(null);
10523
+ setSnapshotArrowPending(null);
10524
+ setSnapshotDiscardConfirm({ visible: false });
10525
+ setSnapshotSubmitError("");
10526
+ setShowSnapshotModal(true);
10527
+ } catch (err) {
10528
+ console.error("[aiditor] snapshot capture failed:", err);
10529
+ }
10530
+ }
9934
10531
  function formatClock(sec) {
9935
10532
  if (!Number.isFinite(sec))
9936
10533
  return "0:00";
@@ -9987,6 +10584,26 @@ function aiditorConstructor(_props, refs) {
9987
10584
  }
9988
10585
  }
9989
10586
  }
10587
+ function ensureIframeShortcutForwarder() {
10588
+ tryJayRefExec(refs.previewIframe, (fr) => {
10589
+ const iframe = fr;
10590
+ try {
10591
+ const doc = iframe.contentDocument;
10592
+ if (!doc)
10593
+ return;
10594
+ if (iframeShortcutDoc?.deref() === doc)
10595
+ return;
10596
+ iframeShortcutDoc = new WeakRef(doc);
10597
+ doc.addEventListener("keydown", (e2) => {
10598
+ if ((e2.key === "s" || e2.key === "S") && (e2.metaKey || e2.ctrlKey) && e2.shiftKey && !e2.altKey) {
10599
+ e2.preventDefault();
10600
+ window.dispatchEvent(new CustomEvent("aiditor:snapshot-shortcut"));
10601
+ }
10602
+ }, true);
10603
+ } catch {
10604
+ }
10605
+ });
10606
+ }
9990
10607
  function tickPreviewIframeProbe() {
9991
10608
  if (!isPreviewMode() || previewLoading())
9992
10609
  return;
@@ -9995,6 +10612,7 @@ function aiditorConstructor(_props, refs) {
9995
10612
  const src = previewSrc();
9996
10613
  if (!src || !previewIsSameOriginForCapture(src))
9997
10614
  return;
10615
+ ensureIframeShortcutForwarder();
9998
10616
  tryJayRefExec(refs.previewIframe, (fr) => {
9999
10617
  const iframe = fr;
10000
10618
  try {
@@ -10798,9 +11416,236 @@ function aiditorConstructor(_props, refs) {
10798
11416
  tryJayRefExec(refs.videoReviewCloseBtn, (b) => b.click());
10799
11417
  }
10800
11418
  });
11419
+ refs.snapshotToolPointBtn.onclick(() => onSelectSnapshotTool("point"));
11420
+ refs.snapshotToolAreaBtn.onclick(() => onSelectSnapshotTool("area"));
11421
+ refs.snapshotToolArrowBtn.onclick(() => onSelectSnapshotTool("arrow"));
11422
+ refs.snapshotToolNoneBtn.onclick(() => onSelectSnapshotTool("none"));
11423
+ refs.snapshotUndoBtn.onclick(() => snapshotUndo());
11424
+ refs.snapshotClearBtn.onclick(() => {
11425
+ if (snapshotAnnotations().length > 2) {
11426
+ if (!window.confirm("Clear all markers?"))
11427
+ return;
11428
+ }
11429
+ snapshotClearAll();
11430
+ });
11431
+ refs.snapshotCloseBtn.onclick(() => tryDismissSnapshotModal());
11432
+ refs.snapshotCancelBtn.onclick(() => tryDismissSnapshotModal());
11433
+ refs.snapshotDiscardBtn.onclick(() => tearDownSnapshotModal());
11434
+ refs.snapshotKeepEditingBtn.onclick(() => setSnapshotDiscardConfirm({ visible: false }));
11435
+ refs.snapshotSendBtn.onclick(() => {
11436
+ void runSnapshotSubmit();
11437
+ });
11438
+ refs.snapshotBackdrop.onkeydown(({ event }) => {
11439
+ if (event.key === "Escape") {
11440
+ event.preventDefault();
11441
+ tryDismissSnapshotModal();
11442
+ return;
11443
+ }
11444
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
11445
+ event.preventDefault();
11446
+ if (snapshotCanSubmit())
11447
+ void runSnapshotSubmit();
11448
+ return;
11449
+ }
11450
+ });
11451
+ refs.snapshotOverlay.onmousedown(({ event }) => {
11452
+ const mode = snapshotTool();
11453
+ if (mode !== "area")
11454
+ return;
11455
+ const el = event.currentTarget;
11456
+ let imgEl = null;
11457
+ tryJayRefExec(refs.snapshotImg, (img) => {
11458
+ imgEl = img;
11459
+ });
11460
+ if (!imgEl)
11461
+ return;
11462
+ const n = snapshotNormFromEvent(el, imgEl, event);
11463
+ if (!n)
11464
+ return;
11465
+ event.preventDefault();
11466
+ snapshotAreaDragActive = true;
11467
+ snapshotAreaStartNorm = { x: n.x, y: n.y };
11468
+ setSnapshotAreaDraft({
11469
+ startX: n.x,
11470
+ startY: n.y,
11471
+ currentX: n.x,
11472
+ currentY: n.y
11473
+ });
11474
+ const move = (ev) => {
11475
+ if (!snapshotAreaDragActive || !imgEl)
11476
+ return;
11477
+ const cur = snapshotNormFromEvent(el, imgEl, ev);
11478
+ if (!cur)
11479
+ return;
11480
+ setSnapshotAreaDraft({
11481
+ startX: snapshotAreaStartNorm.x,
11482
+ startY: snapshotAreaStartNorm.y,
11483
+ currentX: cur.x,
11484
+ currentY: cur.y
11485
+ });
11486
+ };
11487
+ const up = (ev) => {
11488
+ if (!snapshotAreaDragActive || !imgEl)
11489
+ return;
11490
+ snapshotAreaDragActive = false;
11491
+ document.removeEventListener("mousemove", move);
11492
+ document.removeEventListener("mouseup", up);
11493
+ snapshotAreaListeners = null;
11494
+ const cur = snapshotNormFromEvent(el, imgEl, ev);
11495
+ if (cur) {
11496
+ const r = normalizeAreaRect(snapshotAreaStartNorm.x, snapshotAreaStartNorm.y, cur.x, cur.y);
11497
+ if (r.w > 8e-3 && r.h > 8e-3) {
11498
+ setSnapshotAnnotations((prev) => renumberAnnotations([
11499
+ ...prev,
11500
+ {
11501
+ id: "_",
11502
+ kind: "area",
11503
+ ...r,
11504
+ instruction: "",
11505
+ attachments: []
11506
+ }
11507
+ ]));
11508
+ setSnapshotSubmitError("");
11509
+ focusLastSnapshotAnnotationTextarea();
11510
+ }
11511
+ }
11512
+ setSnapshotAreaDraft(null);
11513
+ };
11514
+ document.addEventListener("mousemove", move);
11515
+ document.addEventListener("mouseup", up);
11516
+ snapshotAreaListeners = () => {
11517
+ document.removeEventListener("mousemove", move);
11518
+ document.removeEventListener("mouseup", up);
11519
+ };
11520
+ });
11521
+ refs.snapshotOverlay.onclick(({ event }) => {
11522
+ const mode = snapshotTool();
11523
+ if (mode === "area")
11524
+ return;
11525
+ const el = event.currentTarget;
11526
+ let imgEl = null;
11527
+ tryJayRefExec(refs.snapshotImg, (img) => {
11528
+ imgEl = img;
11529
+ });
11530
+ if (!imgEl)
11531
+ return;
11532
+ const n = snapshotNormFromEvent(el, imgEl, event);
11533
+ if (!n)
11534
+ return;
11535
+ if (mode === "point") {
11536
+ setSnapshotAnnotations((prev) => renumberAnnotations([
11537
+ ...prev,
11538
+ {
11539
+ id: "_",
11540
+ kind: "point",
11541
+ x: n.x,
11542
+ y: n.y,
11543
+ instruction: "",
11544
+ attachments: []
11545
+ }
11546
+ ]));
11547
+ setSnapshotSubmitError("");
11548
+ focusLastSnapshotAnnotationTextarea();
11549
+ } else if (mode === "arrow") {
11550
+ const pending = snapshotArrowPending();
11551
+ if (!pending) {
11552
+ setSnapshotArrowPending(n);
11553
+ } else {
11554
+ const ln = { x1: pending.x, y1: pending.y, x2: n.x, y2: n.y };
11555
+ setSnapshotArrowPending(null);
11556
+ setSnapshotAnnotations((prev) => renumberAnnotations([
11557
+ ...prev,
11558
+ {
11559
+ id: "_",
11560
+ kind: "arrow",
11561
+ ...ln,
11562
+ instruction: "",
11563
+ attachments: []
11564
+ }
11565
+ ]));
11566
+ setSnapshotSubmitError("");
11567
+ focusLastSnapshotAnnotationTextarea();
11568
+ }
11569
+ }
11570
+ });
11571
+ refs.snapshotAnnotationItems.snapshotAnnotationRow.oninput(({ event }) => {
11572
+ const t = event.target;
11573
+ if (t.tagName !== "TEXTAREA" || !t.classList.contains("visual-annotation-instruction"))
11574
+ return;
11575
+ const id = t.dataset.annotationId;
11576
+ if (!id)
11577
+ return;
11578
+ setSnapshotAnnotationInstruction(id, t.value);
11579
+ });
11580
+ refs.snapshotAnnotationItems.snapshotAnnotationRow.onkeydown(({ event }) => {
11581
+ const t = event.target;
11582
+ if (t.tagName !== "TEXTAREA" || !t.classList.contains("visual-annotation-instruction"))
11583
+ return;
11584
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
11585
+ event.preventDefault();
11586
+ if (snapshotCanSubmit())
11587
+ void runSnapshotSubmit();
11588
+ } else if (event.key === "Escape") {
11589
+ event.preventDefault();
11590
+ const id = t.dataset.annotationId;
11591
+ if (id)
11592
+ removeSnapshotAnnotation(id);
11593
+ }
11594
+ });
11595
+ refs.snapshotAnnotationItems.snapshotAnnotationRow.onclick(({ event }) => {
11596
+ const btn = event.target.closest("button");
11597
+ if (!btn)
11598
+ return;
11599
+ if (btn.classList.contains("snapshot-annotation-remove")) {
11600
+ event.preventDefault();
11601
+ const card = btn.closest("[data-annotation-id]");
11602
+ const id = card?.getAttribute("data-annotation-id");
11603
+ if (id)
11604
+ removeSnapshotAnnotation(id);
11605
+ return;
11606
+ }
11607
+ if (btn.classList.contains("snapshot-annotation-attach")) {
11608
+ event.preventDefault();
11609
+ const id = btn.dataset.annotationId;
11610
+ if (!id)
11611
+ return;
11612
+ snapshotAttachTargetPinId = id;
11613
+ tryJayRefExec(refs.snapshotAttachFileInput, (inp) => inp.click());
11614
+ return;
11615
+ }
11616
+ if (btn.classList.contains("visual-attachment-remove-file")) {
11617
+ event.preventDefault();
11618
+ const aid = btn.dataset.annotationId;
11619
+ const key = btn.dataset.attKey;
11620
+ if (aid && key)
11621
+ removeSnapshotAttachmentFile(aid, key);
11622
+ return;
11623
+ }
11624
+ });
11625
+ refs.snapshotAttachFileInput.onchange(({ event }) => {
11626
+ const inp = event.target;
11627
+ const files = inp.files;
11628
+ const pin = snapshotAttachTargetPinId;
11629
+ snapshotAttachTargetPinId = null;
11630
+ if (!pin || !files?.length)
11631
+ return;
11632
+ addFilesToSnapshotAnnotation(pin, Array.from(files));
11633
+ inp.value = "";
11634
+ });
11635
+ createEffect(() => {
11636
+ if (showSnapshotModal()) {
11637
+ requestAnimationFrame(() => {
11638
+ tryJayRefExec(refs.snapshotBackdrop, (el) => el.focus());
11639
+ });
11640
+ }
11641
+ });
10801
11642
  function routePastedFilesToAnnotationField(id, ta, files) {
10802
11643
  if (files.length === 0)
10803
11644
  return;
11645
+ if (ta.closest(".snapshot-panel")) {
11646
+ addFilesToSnapshotAnnotation(id, files);
11647
+ return;
11648
+ }
10804
11649
  if (ta.closest(".video-review-capture-root")) {
10805
11650
  addFilesToVideoAnnotation(id, files);
10806
11651
  return;
@@ -11039,7 +11884,35 @@ function aiditorConstructor(_props, refs) {
11039
11884
  videoPlayPauseGlyph,
11040
11885
  breakpointDesktopOn,
11041
11886
  breakpointTabletOn,
11042
- breakpointMobileOn
11887
+ breakpointMobileOn,
11888
+ showSnapshotModal,
11889
+ snapshotImageSrc,
11890
+ snapshotToolNoneOn,
11891
+ snapshotToolPointOn,
11892
+ snapshotToolAreaOn,
11893
+ snapshotToolArrowOn,
11894
+ snapshotCanSubmit,
11895
+ snapshotShowDiscardConfirm,
11896
+ snapshotOverlayPointerNone,
11897
+ snapshotPointDisplayItems,
11898
+ showSnapshotPointMarkers,
11899
+ snapshotAreaDisplayItems,
11900
+ showSnapshotAreaItems,
11901
+ snapshotAreaDraftLeftPct,
11902
+ snapshotAreaDraftTopPct,
11903
+ snapshotAreaDraftWidthPct,
11904
+ snapshotAreaDraftHeightPct,
11905
+ showSnapshotAreaDraft,
11906
+ snapshotArrowDisplayItems,
11907
+ snapshotArrowPinItems,
11908
+ hasSnapshotArrowLines,
11909
+ showSnapshotArrowPending,
11910
+ snapshotArrowPendingLeftPct,
11911
+ snapshotArrowPendingTopPct,
11912
+ snapshotAnnotationItems,
11913
+ showSnapshotAnnotationsPanel,
11914
+ showSnapshotSubmitError,
11915
+ snapshotSubmitError
11043
11916
  })
11044
11917
  };
11045
11918
  }
package/dist/index.d.ts CHANGED
@@ -152,6 +152,57 @@ interface VideoTimelineMarkerOfPageViewState {
152
152
  label: string
153
153
  }
154
154
 
155
+ interface SnapshotPointDisplayItemOfPageViewState {
156
+ id: string,
157
+ leftPct: number,
158
+ topPct: number,
159
+ indexLabel: string
160
+ }
161
+
162
+ interface SnapshotAreaDisplayItemOfPageViewState {
163
+ id: string,
164
+ leftPct: number,
165
+ topPct: number,
166
+ widthPct: number,
167
+ heightPct: number,
168
+ indexLabel: string
169
+ }
170
+
171
+ interface SnapshotArrowDisplayItemOfPageViewState {
172
+ id: string,
173
+ x1Pct: number,
174
+ y1Pct: number,
175
+ x2Pct: number,
176
+ y2Pct: number
177
+ }
178
+
179
+ interface SnapshotArrowPinItemOfPageViewState {
180
+ id: string,
181
+ leftPct: number,
182
+ topPct: number,
183
+ indexLabel: string
184
+ }
185
+
186
+ interface AttachmentChipOfSnapshotAnnotationItemOfPageViewState {
187
+ key: string,
188
+ name: string,
189
+ thumbUrl: string,
190
+ annotationId: string
191
+ }
192
+
193
+ interface SnapshotAnnotationItemOfPageViewState {
194
+ id: string,
195
+ indexLabel: string,
196
+ kindLabel: string,
197
+ instruction: string,
198
+ kind: string,
199
+ leftPct: number,
200
+ topPct: number,
201
+ popoverFlipX: boolean,
202
+ popoverFlipY: boolean,
203
+ attachmentChips: Array<AttachmentChipOfSnapshotAnnotationItemOfPageViewState>
204
+ }
205
+
155
206
  interface PageViewState {
156
207
  isBootstrapping: boolean,
157
208
  showBootstrapError: boolean,
@@ -262,7 +313,35 @@ interface PageViewState {
262
313
  videoPlayPauseGlyph: string,
263
314
  breakpointDesktopOn: boolean,
264
315
  breakpointTabletOn: boolean,
265
- breakpointMobileOn: boolean
316
+ breakpointMobileOn: boolean,
317
+ showSnapshotModal: boolean,
318
+ snapshotImageSrc: string,
319
+ snapshotToolNoneOn: boolean,
320
+ snapshotToolPointOn: boolean,
321
+ snapshotToolAreaOn: boolean,
322
+ snapshotToolArrowOn: boolean,
323
+ snapshotCanSubmit: boolean,
324
+ snapshotShowDiscardConfirm: boolean,
325
+ snapshotOverlayPointerNone: boolean,
326
+ snapshotPointDisplayItems: Array<SnapshotPointDisplayItemOfPageViewState>,
327
+ showSnapshotPointMarkers: boolean,
328
+ snapshotAreaDisplayItems: Array<SnapshotAreaDisplayItemOfPageViewState>,
329
+ showSnapshotAreaItems: boolean,
330
+ snapshotAreaDraftLeftPct: number,
331
+ snapshotAreaDraftTopPct: number,
332
+ snapshotAreaDraftWidthPct: number,
333
+ snapshotAreaDraftHeightPct: number,
334
+ showSnapshotAreaDraft: boolean,
335
+ snapshotArrowDisplayItems: Array<SnapshotArrowDisplayItemOfPageViewState>,
336
+ snapshotArrowPinItems: Array<SnapshotArrowPinItemOfPageViewState>,
337
+ hasSnapshotArrowLines: boolean,
338
+ showSnapshotArrowPending: boolean,
339
+ snapshotArrowPendingLeftPct: number,
340
+ snapshotArrowPendingTopPct: number,
341
+ snapshotAnnotationItems: Array<SnapshotAnnotationItemOfPageViewState>,
342
+ showSnapshotAnnotationsPanel: boolean,
343
+ showSnapshotSubmitError: boolean,
344
+ snapshotSubmitError: string
266
345
  }
267
346
 
268
347
 
@@ -306,11 +385,29 @@ interface PageElementRefs {
306
385
  videoReviewDiscardBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
307
386
  videoReviewSendBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
308
387
  videoReviewBackdrop: HTMLElementProxy<PageViewState, HTMLDivElement>,
388
+ snapshotCloseBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
389
+ snapshotToolPointBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
390
+ snapshotToolAreaBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
391
+ snapshotToolArrowBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
392
+ snapshotToolNoneBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
393
+ snapshotUndoBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
394
+ snapshotClearBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
395
+ snapshotAttachFileInput: HTMLElementProxy<PageViewState, HTMLInputElement>,
396
+ snapshotImg: HTMLElementProxy<PageViewState, HTMLImageElement>,
397
+ snapshotOverlay: HTMLElementProxy<PageViewState, HTMLDivElement>,
398
+ snapshotDiscardBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
399
+ snapshotKeepEditingBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
400
+ snapshotCancelBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
401
+ snapshotSendBtn: HTMLElementProxy<PageViewState, HTMLButtonElement>,
402
+ snapshotBackdrop: HTMLElementProxy<PageViewState, HTMLDivElement>,
309
403
  visualAnnotationRows: {
310
404
  annotationRow: HTMLElementCollectionProxy<VisualAnnotationRowOfPageViewState, HTMLDivElement>
311
405
  },
312
406
  videoAnnotationRows: {
313
407
  videoAnnotationRow: HTMLElementCollectionProxy<VideoAnnotationRowOfPageViewState, HTMLDivElement>
408
+ },
409
+ snapshotAnnotationItems: {
410
+ snapshotAnnotationRow: HTMLElementCollectionProxy<SnapshotAnnotationItemOfPageViewState, HTMLDivElement>
314
411
  }
315
412
  }
316
413
 
@@ -146,6 +146,34 @@
146
146
  .video-review-primary { background: #0e639c; color: #fff; border: none; border-radius: 6px; padding: 8px 18px; font-size: 13px; cursor: pointer; }
147
147
  .video-review-primary:hover { background: #1177bb; }
148
148
  .video-review-primary:disabled { opacity: 0.45; cursor: not-allowed; }
149
+ .snapshot-overlay { position: fixed; inset: 0; z-index: 200; background: rgba(0,0,0,0.78); display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; }
150
+ .snapshot-panel { background: #252526; border: 1px solid #3e3e3e; border-radius: 10px; width: min(1200px, 96vw); max-height: 94vh; display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; overflow: hidden; }
151
+ .snapshot-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-shrink: 0; }
152
+ .snapshot-title { font-size: 15px; font-weight: 600; color: #9cdcfe; margin: 0; }
153
+ .snapshot-close { background: none; border: none; color: #888; font-size: 18px; cursor: pointer; padding: 0 4px; line-height: 1; }
154
+ .snapshot-close:hover { color: #fff; }
155
+ .snapshot-tools { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; flex-shrink: 0; }
156
+ .snapshot-body { display: flex; gap: 14px; flex: 1; min-height: 0; overflow: hidden; }
157
+ .snapshot-capture-area { flex: 2; min-width: 0; position: relative; display: flex; align-items: center; justify-content: center; background: #1e1e1e; border-radius: 8px; overflow: hidden; }
158
+ .snapshot-img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; user-select: none; -webkit-user-drag: none; }
159
+ .snapshot-img-overlay { position: absolute; inset: 0; z-index: 2; }
160
+ .snapshot-img-overlay-none { pointer-events: none; }
161
+ .snapshot-annotations-panel { flex: 1; min-width: 220px; max-width: 340px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
162
+ .snapshot-annotation-card { background: #1e1e1e; border: 1px solid #3e3e3e; border-radius: 8px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
163
+ .snapshot-annotation-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
164
+ .snapshot-annotation-kind { font-size: 11px; color: #9cdcfe; font-weight: 600; }
165
+ .snapshot-annotation-remove { background: transparent; color: #888; border: 1px solid #3e3e3e; border-radius: 4px; padding: 4px 8px; font-size: 11px; cursor: pointer; }
166
+ .snapshot-annotation-remove:hover { color: #f48771; border-color: #f48771; }
167
+ .snapshot-empty-hint { color: #888; font-size: 12px; font-style: italic; padding: 8px 0; }
168
+ .snapshot-footer { display: flex; gap: 10px; justify-content: flex-end; align-items: center; flex-wrap: wrap; flex-shrink: 0; margin-top: 4px; }
169
+ .snapshot-secondary { background: #3c3c3c; color: #ddd; border: 1px solid #555; border-radius: 6px; padding: 8px 16px; font-size: 13px; cursor: pointer; }
170
+ .snapshot-secondary:hover { background: #4a4a4a; }
171
+ .snapshot-primary { background: #0e639c; color: #fff; border: none; border-radius: 6px; padding: 8px 18px; font-size: 13px; cursor: pointer; }
172
+ .snapshot-primary:hover { background: #1177bb; }
173
+ .snapshot-primary:disabled { opacity: 0.45; cursor: not-allowed; }
174
+ .snapshot-discard-confirm { display: flex; align-items: center; gap: 10px; font-size: 13px; color: #f48771; }
175
+ .snapshot-discard-confirm-msg { flex: 1; }
176
+ .snapshot-overlay .arrow-svg line { marker-end: url(#aiditor-snapshot-arrowhead); }
149
177
  </style>
150
178
  <script type="application/jay-data">
151
179
  data:
@@ -350,6 +378,67 @@ data:
350
378
  breakpointDesktopOn: boolean
351
379
  breakpointTabletOn: boolean
352
380
  breakpointMobileOn: boolean
381
+ showSnapshotModal: boolean
382
+ snapshotImageSrc: string
383
+ snapshotToolNoneOn: boolean
384
+ snapshotToolPointOn: boolean
385
+ snapshotToolAreaOn: boolean
386
+ snapshotToolArrowOn: boolean
387
+ snapshotCanSubmit: boolean
388
+ snapshotShowDiscardConfirm: boolean
389
+ snapshotOverlayPointerNone: boolean
390
+ snapshotPointDisplayItems:
391
+ - id: string
392
+ leftPct: number
393
+ topPct: number
394
+ indexLabel: string
395
+ showSnapshotPointMarkers: boolean
396
+ snapshotAreaDisplayItems:
397
+ - id: string
398
+ leftPct: number
399
+ topPct: number
400
+ widthPct: number
401
+ heightPct: number
402
+ indexLabel: string
403
+ showSnapshotAreaItems: boolean
404
+ snapshotAreaDraftLeftPct: number
405
+ snapshotAreaDraftTopPct: number
406
+ snapshotAreaDraftWidthPct: number
407
+ snapshotAreaDraftHeightPct: number
408
+ showSnapshotAreaDraft: boolean
409
+ snapshotArrowDisplayItems:
410
+ - id: string
411
+ x1Pct: number
412
+ y1Pct: number
413
+ x2Pct: number
414
+ y2Pct: number
415
+ snapshotArrowPinItems:
416
+ - id: string
417
+ leftPct: number
418
+ topPct: number
419
+ indexLabel: string
420
+ hasSnapshotArrowLines: boolean
421
+ showSnapshotArrowPending: boolean
422
+ snapshotArrowPendingLeftPct: number
423
+ snapshotArrowPendingTopPct: number
424
+ snapshotAnnotationItems:
425
+ - id: string
426
+ indexLabel: string
427
+ kindLabel: string
428
+ instruction: string
429
+ kind: string
430
+ leftPct: number
431
+ topPct: number
432
+ popoverFlipX: boolean
433
+ popoverFlipY: boolean
434
+ attachmentChips:
435
+ - key: string
436
+ name: string
437
+ thumbUrl: string
438
+ annotationId: string
439
+ showSnapshotAnnotationsPanel: boolean
440
+ showSnapshotSubmitError: boolean
441
+ snapshotSubmitError: string
353
442
  </script>
354
443
  </head>
355
444
  <body>
@@ -754,6 +843,128 @@ data:
754
843
  </div>
755
844
  </div>
756
845
 
846
+ <div class="snapshot-overlay" if="showSnapshotModal" ref="snapshotBackdrop" tabindex="-1" role="dialog" aria-modal="true" aria-label="Snapshot annotation">
847
+ <div class="snapshot-panel">
848
+ <div class="snapshot-header">
849
+ <h2 class="snapshot-title">Snapshot Annotation</h2>
850
+ <button type="button" class="snapshot-close" ref="snapshotCloseBtn" aria-label="Close">✕</button>
851
+ </div>
852
+ <div class="snapshot-tools">
853
+ <button type="button" class="{snapshotToolPointOn ? visual-tool-on} visual-tool-btn" ref="snapshotToolPointBtn" title="Point" aria-label="Point tool">⊙</button>
854
+ <button type="button" class="{snapshotToolAreaOn ? visual-tool-on} visual-tool-btn" ref="snapshotToolAreaBtn" title="Area" aria-label="Area tool">▢</button>
855
+ <button type="button" class="{snapshotToolArrowOn ? visual-tool-on} visual-tool-btn" ref="snapshotToolArrowBtn" title="Arrow" aria-label="Arrow tool">↗</button>
856
+ <button type="button" class="{snapshotToolNoneOn ? visual-tool-on} visual-tool-btn" ref="snapshotToolNoneBtn" title="Select" aria-label="Select tool">⤢</button>
857
+ <span class="tool-group-sep"></span>
858
+ <button type="button" class="visual-tool-btn" ref="snapshotUndoBtn" title="Undo last marker" aria-label="Undo">↩</button>
859
+ <button type="button" class="visual-tool-btn" ref="snapshotClearBtn" title="Clear all markers" aria-label="Clear all">✕ Clear</button>
860
+ </div>
861
+ <input type="file" class="file-input-hidden" ref="snapshotAttachFileInput" multiple />
862
+ <div class="snapshot-body">
863
+ <div class="snapshot-capture-area">
864
+ <img class="snapshot-img" ref="snapshotImg" src="{snapshotImageSrc}" alt="Captured snapshot" draggable="false" />
865
+ <div class="{snapshotOverlayPointerNone ? snapshot-img-overlay-none} snapshot-img-overlay" ref="snapshotOverlay">
866
+ <div
867
+ class="point-marker"
868
+ forEach="snapshotPointDisplayItems"
869
+ trackBy="id"
870
+ style="left: {leftPct}%; top: {topPct}%;"
871
+ >
872
+ <span class="point-marker-idx">{indexLabel}</span>
873
+ </div>
874
+ <div
875
+ class="area-rect"
876
+ forEach="snapshotAreaDisplayItems"
877
+ trackBy="id"
878
+ style="left: {leftPct}%; top: {topPct}%; width: {widthPct}%; height: {heightPct}%;"
879
+ ><span class="area-marker-idx">{indexLabel}</span></div>
880
+ <div
881
+ class="area-rect area-rect-draft"
882
+ if="showSnapshotAreaDraft"
883
+ style="left: {snapshotAreaDraftLeftPct}%; top: {snapshotAreaDraftTopPct}%; width: {snapshotAreaDraftWidthPct}%; height: {snapshotAreaDraftHeightPct}%;"
884
+ ></div>
885
+ <div
886
+ class="arrow-pending-dot"
887
+ if="showSnapshotArrowPending"
888
+ style="left: {snapshotArrowPendingLeftPct}%; top: {snapshotArrowPendingTopPct}%;"
889
+ ></div>
890
+ <svg class="arrow-svg" if="hasSnapshotArrowLines" viewBox="0 0 100 100" preserveAspectRatio="none">
891
+ <defs>
892
+ <marker id="aiditor-snapshot-arrowhead" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
893
+ <polygon points="0 0, 8 4, 0 8" fill="#c586ff" />
894
+ </marker>
895
+ </defs>
896
+ <line
897
+ forEach="snapshotArrowDisplayItems"
898
+ trackBy="id"
899
+ x1="{x1Pct}"
900
+ y1="{y1Pct}"
901
+ x2="{x2Pct}"
902
+ y2="{y2Pct}"
903
+ />
904
+ </svg>
905
+ <div
906
+ class="arrow-end-pin"
907
+ forEach="snapshotArrowPinItems"
908
+ trackBy="id"
909
+ style="left: {leftPct}%; top: {topPct}%;"
910
+ >{indexLabel}</div>
911
+ </div>
912
+ </div>
913
+ <div class="snapshot-annotations-panel">
914
+ <p class="snapshot-empty-hint" if="!showSnapshotAnnotationsPanel">Click the screenshot to place a marker, or select a tool from the toolbar.</p>
915
+ <div
916
+ class="snapshot-annotation-card"
917
+ forEach="snapshotAnnotationItems"
918
+ trackBy="id"
919
+ ref="snapshotAnnotationRow"
920
+ data-annotation-id="{id}"
921
+ >
922
+ <div class="snapshot-annotation-card-head">
923
+ <span class="snapshot-annotation-kind">{kindLabel}</span>
924
+ <button type="button" class="snapshot-annotation-remove" title="Remove annotation" aria-label="Remove annotation">×</button>
925
+ </div>
926
+ <textarea
927
+ class="visual-annotation-instruction"
928
+ placeholder="Instruction for this annotation…"
929
+ data-annotation-id="{id}"
930
+ value="{instruction}"
931
+ ></textarea>
932
+ <div class="visual-annotation-files">
933
+ <div class="visual-attach-row">
934
+ <button type="button" class="visual-attach-btn snapshot-annotation-attach" data-annotation-id="{id}">
935
+ Attach file
936
+ </button>
937
+ </div>
938
+ <div class="visual-attachment-chips" forEach="attachmentChips" trackBy="key">
939
+ <div class="visual-attachment-chip">
940
+ <img if="thumbUrl" class="visual-attachment-thumb" src="{thumbUrl}" alt="" />
941
+ <span class="visual-attachment-name">{name}</span>
942
+ <button
943
+ type="button"
944
+ class="visual-attachment-remove-file"
945
+ data-annotation-id="{annotationId}"
946
+ data-att-key="{key}"
947
+ aria-label="Remove attachment"
948
+ >×</button>
949
+ </div>
950
+ </div>
951
+ </div>
952
+ </div>
953
+ </div>
954
+ </div>
955
+ <p class="visual-submit-error" if="showSnapshotSubmitError">{snapshotSubmitError}</p>
956
+ <div class="snapshot-footer">
957
+ <div class="snapshot-discard-confirm" if="snapshotShowDiscardConfirm">
958
+ <span class="snapshot-discard-confirm-msg">Discard annotations?</span>
959
+ <button type="button" class="snapshot-secondary" ref="snapshotDiscardBtn">Discard</button>
960
+ <button type="button" class="snapshot-secondary" ref="snapshotKeepEditingBtn">Keep editing</button>
961
+ </div>
962
+ <button type="button" class="snapshot-secondary" ref="snapshotCancelBtn" if="!snapshotShowDiscardConfirm">Cancel</button>
963
+ <button type="button" class="snapshot-primary" ref="snapshotSendBtn" disabled="!snapshotCanSubmit" if="!snapshotShowDiscardConfirm">Send to Agent</button>
964
+ </div>
965
+ </div>
966
+ </div>
967
+
757
968
  </div>
758
969
  </body>
759
970
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/aiditor",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "type": "module",
5
5
  "description": "AIditor — visual AI-driven code editor plugin for Jay Framework",
6
6
  "main": "dist/index.js",
@@ -42,16 +42,16 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@anthropic-ai/claude-agent-sdk": "0.2.119",
45
- "@jay-framework/fullstack-component": "^0.17.1",
46
- "@jay-framework/stack-client-runtime": "^0.17.1",
47
- "@jay-framework/stack-server-runtime": "^0.17.1",
45
+ "@jay-framework/fullstack-component": "^0.17.2",
46
+ "@jay-framework/stack-client-runtime": "^0.17.2",
47
+ "@jay-framework/stack-server-runtime": "^0.17.2",
48
48
  "busboy": "^1.6.0",
49
49
  "html2canvas": "^1.4.1",
50
50
  "zod": "^4.3.6"
51
51
  },
52
52
  "devDependencies": {
53
- "@jay-framework/compiler-jay-stack": "^0.17.1",
54
- "@jay-framework/jay-cli": "^0.17.1",
53
+ "@jay-framework/compiler-jay-stack": "^0.17.2",
54
+ "@jay-framework/jay-cli": "^0.17.2",
55
55
  "@types/busboy": "^1",
56
56
  "rimraf": "^5.0.5",
57
57
  "tsup": "^8.5.1",