browsertrack 0.1.2 → 0.2.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/AGENTS.md +4 -2
- package/dist/{chunk-ILRYKMME.js → chunk-3HOXPTM2.js} +97 -3
- package/dist/chunk-3HOXPTM2.js.map +1 -0
- package/dist/{chunk-SPCIROIU.js → chunk-7OCOQGDN.js} +24 -5
- package/dist/chunk-7OCOQGDN.js.map +1 -0
- package/dist/{chunk-G2Y3CXCY.js → chunk-UP5JKCFY.js} +84 -2
- package/dist/chunk-UP5JKCFY.js.map +1 -0
- package/dist/{chunk-SKCMT2DE.js → chunk-WB7ZKWK7.js} +453 -152
- package/dist/chunk-WB7ZKWK7.js.map +1 -0
- package/dist/cli/index.js +200 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +452 -151
- package/dist/client/index.d.ts +28 -9
- package/dist/client/index.js +1 -1
- package/dist/client.iife.js +185 -29
- package/dist/core/index.d.ts +2 -2
- package/dist/daemon/index.d.ts +3 -3
- package/dist/daemon/index.js +2 -2
- package/dist/{engine-CeT9URuN.d.ts → engine-B43IohQY.d.ts} +13 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +2 -2
- package/dist/{notes-CBvN91Wf.d.ts → notes-BMnonq46.d.ts} +24 -1
- package/dist/{projects-CY8ungMt.d.ts → projects-D5J-egVN.d.ts} +1 -1
- package/dist/{server-Dd8NX2Mk.d.ts → server-BztYp1Zc.d.ts} +1 -1
- package/docs/index.md +2 -1
- package/docs/mcp-reference.md +14 -1
- package/docs/scenarios-flows.md +86 -0
- package/package.json +1 -1
- package/packages/client/src/notes/inspector.ts +533 -161
- package/packages/core/src/types/notes.ts +25 -0
- package/packages/daemon/src/notes/engine.ts +6 -0
- package/packages/daemon/src/server/ws.ts +23 -1
- package/packages/daemon/src/storage/db.ts +106 -3
- package/packages/mcp/src/handlers.ts +58 -0
- package/packages/mcp/src/tools.ts +28 -0
- package/test/client/interceptors.test.ts +64 -0
- package/test/daemon/scenario-storage.test.ts +158 -0
- package/dist/chunk-G2Y3CXCY.js.map +0 -1
- package/dist/chunk-ILRYKMME.js.map +0 -1
- package/dist/chunk-SKCMT2DE.js.map +0 -1
- package/dist/chunk-SPCIROIU.js.map +0 -1
|
@@ -12,15 +12,22 @@ export interface InspectorOptions {
|
|
|
12
12
|
onNoteCreated?: (note: Partial<VisualNote>) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface ActiveScenarioState {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
stepNumber: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
|
-
* Isolated Visual Note Inspector rendering in Shadow DOM.
|
|
22
|
+
* Isolated Visual Note & Multi-Step Scenario Inspector rendering in Shadow DOM.
|
|
17
23
|
* Supports:
|
|
18
24
|
* 1. Element selection (hover highlight & click)
|
|
19
25
|
* 2. Region / Area selection (drag-and-drop rectangle on screen with cancel banner)
|
|
20
26
|
* 3. Whole page note
|
|
21
|
-
* 4.
|
|
22
|
-
* 5.
|
|
23
|
-
* 6.
|
|
27
|
+
* 4. Multi-Step Flow / Scenario Recording (sequential 'Save & Next Step' continuous capture)
|
|
28
|
+
* 5. Real-time Saved Note & Step Markers (pins with step sequencing numbers)
|
|
29
|
+
* 6. Interactive Note & Step Detail Card (with previous/next step walk-through navigation)
|
|
30
|
+
* 7. Floating quick dock / toolbar in bottom-right corner with Notes count and Flow controls
|
|
24
31
|
*/
|
|
25
32
|
export class NoteInspector {
|
|
26
33
|
private transport: WebSocketTransport;
|
|
@@ -37,8 +44,10 @@ export class NoteInspector {
|
|
|
37
44
|
private toolbarElement: HTMLDivElement | null = null;
|
|
38
45
|
private modalOverlay: HTMLDivElement | null = null;
|
|
39
46
|
private cardOverlay: HTMLDivElement | null = null;
|
|
47
|
+
private toastContainer: HTMLDivElement | null = null;
|
|
40
48
|
|
|
41
49
|
private activeMode: NoteInspectMode = 'idle';
|
|
50
|
+
private activeScenario: ActiveScenarioState | null = null;
|
|
42
51
|
private hoveredElement: HTMLElement | null = null;
|
|
43
52
|
private selectedElement: HTMLElement | null = null;
|
|
44
53
|
private selectedRegion: RegionContext | null = null;
|
|
@@ -90,6 +99,46 @@ export class NoteInspector {
|
|
|
90
99
|
this.renderMarkers();
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
public showToast(message: string, icon = '✨', durationMs = 2500): void {
|
|
103
|
+
if (typeof document === 'undefined') return;
|
|
104
|
+
const root = this.ensureContainer();
|
|
105
|
+
if (!root || !this.toastContainer) return;
|
|
106
|
+
|
|
107
|
+
const toast = document.createElement('div');
|
|
108
|
+
toast.className = 'bt-toast';
|
|
109
|
+
toast.innerHTML = `<span>${icon}</span> <span>${message}</span>`;
|
|
110
|
+
this.toastContainer.appendChild(toast);
|
|
111
|
+
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
toast.classList.add('bt-toast-fadeout');
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (toast.parentElement) {
|
|
116
|
+
toast.parentElement.removeChild(toast);
|
|
117
|
+
}
|
|
118
|
+
}, 250);
|
|
119
|
+
}, durationMs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public startScenario(title?: string): void {
|
|
123
|
+
const defaultTitle = title || `Scenario ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
124
|
+
this.activeScenario = {
|
|
125
|
+
id: `scen_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
126
|
+
title: defaultTitle,
|
|
127
|
+
stepNumber: 1,
|
|
128
|
+
};
|
|
129
|
+
this.updateToolbarState();
|
|
130
|
+
this.setMode('element');
|
|
131
|
+
this.showToast(`Started flow: "${defaultTitle}"`, '🎬');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public finishScenario(): void {
|
|
135
|
+
const title = this.activeScenario?.title;
|
|
136
|
+
this.activeScenario = null;
|
|
137
|
+
this.updateToolbarState();
|
|
138
|
+
this.setMode('idle');
|
|
139
|
+
this.showToast(title ? `Finished flow: "${title}"` : 'Flow recording finished', '✓');
|
|
140
|
+
}
|
|
141
|
+
|
|
93
142
|
public setMode(mode: NoteInspectMode): void {
|
|
94
143
|
this.activeMode = mode;
|
|
95
144
|
this.updateToolbarState();
|
|
@@ -276,6 +325,12 @@ export class NoteInspector {
|
|
|
276
325
|
z-index: 2147483641;
|
|
277
326
|
}
|
|
278
327
|
|
|
328
|
+
.bt-note-marker-step {
|
|
329
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
330
|
+
border-color: #fef3c7;
|
|
331
|
+
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.4), 0 0 0 1px rgba(217, 119, 6, 0.5);
|
|
332
|
+
}
|
|
333
|
+
|
|
279
334
|
@keyframes bt-pop-in {
|
|
280
335
|
0% { transform: scale(0.6); opacity: 0; }
|
|
281
336
|
100% { transform: scale(1); opacity: 1; }
|
|
@@ -286,6 +341,10 @@ export class NoteInspector {
|
|
|
286
341
|
box-shadow: 0 8px 20px rgba(37,99,235,0.6), 0 0 0 2px #60a5fa;
|
|
287
342
|
}
|
|
288
343
|
|
|
344
|
+
.bt-note-marker-step:hover {
|
|
345
|
+
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.7), 0 0 0 2px #fde68a;
|
|
346
|
+
}
|
|
347
|
+
|
|
289
348
|
.bt-marker-resolved {
|
|
290
349
|
background: linear-gradient(135deg, #475569, #334155);
|
|
291
350
|
border-color: #94a3b8;
|
|
@@ -382,6 +441,19 @@ export class NoteInspector {
|
|
|
382
441
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
|
|
383
442
|
}
|
|
384
443
|
|
|
444
|
+
.bt-toolbar-btn.active-scenario {
|
|
445
|
+
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
446
|
+
color: #ffffff;
|
|
447
|
+
font-weight: 700;
|
|
448
|
+
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4);
|
|
449
|
+
animation: bt-pulse 2s infinite;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
@keyframes bt-pulse {
|
|
453
|
+
0%, 100% { opacity: 1; }
|
|
454
|
+
50% { opacity: 0.85; }
|
|
455
|
+
}
|
|
456
|
+
|
|
385
457
|
.bt-count-pill {
|
|
386
458
|
background: rgba(255, 255, 255, 0.2);
|
|
387
459
|
color: #ffffff;
|
|
@@ -429,7 +501,7 @@ export class NoteInspector {
|
|
|
429
501
|
border: 1px solid #334155;
|
|
430
502
|
border-radius: 14px;
|
|
431
503
|
padding: 20px;
|
|
432
|
-
width:
|
|
504
|
+
width: 470px;
|
|
433
505
|
max-width: 92vw;
|
|
434
506
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
435
507
|
display: flex;
|
|
@@ -452,6 +524,7 @@ export class NoteInspector {
|
|
|
452
524
|
align-items: center;
|
|
453
525
|
gap: 8px;
|
|
454
526
|
color: #f8fafc;
|
|
527
|
+
flex-wrap: wrap;
|
|
455
528
|
}
|
|
456
529
|
|
|
457
530
|
.bt-mode-badge {
|
|
@@ -481,6 +554,12 @@ export class NoteInspector {
|
|
|
481
554
|
border: 1px solid rgba(168, 85, 247, 0.4);
|
|
482
555
|
}
|
|
483
556
|
|
|
557
|
+
.bt-badge-step {
|
|
558
|
+
background: rgba(245, 158, 11, 0.15);
|
|
559
|
+
color: #fbbf24;
|
|
560
|
+
border: 1px solid rgba(245, 158, 11, 0.4);
|
|
561
|
+
}
|
|
562
|
+
|
|
484
563
|
.bt-badge-status-open {
|
|
485
564
|
background: rgba(16, 185, 129, 0.15);
|
|
486
565
|
color: #34d399;
|
|
@@ -532,6 +611,43 @@ export class NoteInspector {
|
|
|
532
611
|
color: #cbd5e1;
|
|
533
612
|
}
|
|
534
613
|
|
|
614
|
+
.bt-scenario-stepper {
|
|
615
|
+
display: flex;
|
|
616
|
+
align-items: center;
|
|
617
|
+
justify-content: space-between;
|
|
618
|
+
background: #090d16;
|
|
619
|
+
border: 1px solid #1e293b;
|
|
620
|
+
border-radius: 8px;
|
|
621
|
+
padding: 6px 10px;
|
|
622
|
+
font-size: 12px;
|
|
623
|
+
font-weight: 600;
|
|
624
|
+
color: #f59e0b;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.bt-step-nav-btn {
|
|
628
|
+
background: #1e293b;
|
|
629
|
+
color: #f8fafc;
|
|
630
|
+
border: 1px solid #334155;
|
|
631
|
+
padding: 4px 10px;
|
|
632
|
+
border-radius: 6px;
|
|
633
|
+
cursor: pointer;
|
|
634
|
+
font-size: 11px;
|
|
635
|
+
font-weight: 600;
|
|
636
|
+
transition: all 0.12s ease;
|
|
637
|
+
font-family: inherit;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.bt-step-nav-btn:hover:not(:disabled) {
|
|
641
|
+
background: #2563eb;
|
|
642
|
+
border-color: #3b82f6;
|
|
643
|
+
color: #ffffff;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.bt-step-nav-btn:disabled {
|
|
647
|
+
opacity: 0.3;
|
|
648
|
+
cursor: not-allowed;
|
|
649
|
+
}
|
|
650
|
+
|
|
535
651
|
.bt-note-message-box {
|
|
536
652
|
background: #090d16;
|
|
537
653
|
border: 1px solid #1e293b;
|
|
@@ -576,8 +692,9 @@ export class NoteInspector {
|
|
|
576
692
|
display: flex;
|
|
577
693
|
justify-content: space-between;
|
|
578
694
|
align-items: center;
|
|
579
|
-
gap:
|
|
695
|
+
gap: 10px;
|
|
580
696
|
margin-top: 2px;
|
|
697
|
+
flex-wrap: wrap;
|
|
581
698
|
}
|
|
582
699
|
|
|
583
700
|
.bt-kbd-hint {
|
|
@@ -605,12 +722,13 @@ export class NoteInspector {
|
|
|
605
722
|
justify-content: flex-end;
|
|
606
723
|
align-items: center;
|
|
607
724
|
gap: 8px;
|
|
725
|
+
flex-wrap: wrap;
|
|
608
726
|
}
|
|
609
727
|
|
|
610
728
|
.bt-btn {
|
|
611
|
-
padding:
|
|
729
|
+
padding: 7px 14px;
|
|
612
730
|
border-radius: 7px;
|
|
613
|
-
font-size:
|
|
731
|
+
font-size: 12px;
|
|
614
732
|
font-weight: 600;
|
|
615
733
|
cursor: pointer;
|
|
616
734
|
border: 1px solid transparent;
|
|
@@ -619,7 +737,7 @@ export class NoteInspector {
|
|
|
619
737
|
display: inline-flex;
|
|
620
738
|
align-items: center;
|
|
621
739
|
justify-content: center;
|
|
622
|
-
gap:
|
|
740
|
+
gap: 5px;
|
|
623
741
|
}
|
|
624
742
|
|
|
625
743
|
.bt-btn-cancel {
|
|
@@ -641,6 +759,25 @@ export class NoteInspector {
|
|
|
641
759
|
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.45);
|
|
642
760
|
}
|
|
643
761
|
|
|
762
|
+
.bt-btn-next-step {
|
|
763
|
+
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
764
|
+
color: #ffffff;
|
|
765
|
+
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
|
|
766
|
+
}
|
|
767
|
+
.bt-btn-next-step:hover {
|
|
768
|
+
background: linear-gradient(135deg, #4f46e5, #4338ca);
|
|
769
|
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.55);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.bt-btn-finish-flow {
|
|
773
|
+
background: linear-gradient(135deg, #10b981, #059669);
|
|
774
|
+
color: #ffffff;
|
|
775
|
+
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.35);
|
|
776
|
+
}
|
|
777
|
+
.bt-btn-finish-flow:hover {
|
|
778
|
+
background: linear-gradient(135deg, #059669, #047857);
|
|
779
|
+
}
|
|
780
|
+
|
|
644
781
|
.bt-btn-resolve {
|
|
645
782
|
background: rgba(16, 185, 129, 0.15);
|
|
646
783
|
color: #6ee7b7;
|
|
@@ -660,6 +797,47 @@ export class NoteInspector {
|
|
|
660
797
|
background: rgba(239, 68, 68, 0.3);
|
|
661
798
|
color: #ffffff;
|
|
662
799
|
}
|
|
800
|
+
|
|
801
|
+
/* 6. Toast Notifications */
|
|
802
|
+
.bt-toast-container {
|
|
803
|
+
position: fixed;
|
|
804
|
+
bottom: 74px;
|
|
805
|
+
right: 24px;
|
|
806
|
+
display: flex;
|
|
807
|
+
flex-direction: column;
|
|
808
|
+
gap: 8px;
|
|
809
|
+
pointer-events: none;
|
|
810
|
+
z-index: 2147483647;
|
|
811
|
+
align-items: flex-end;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.bt-toast {
|
|
815
|
+
background: #0f172a;
|
|
816
|
+
color: #f8fafc;
|
|
817
|
+
border: 1px solid #334155;
|
|
818
|
+
border-radius: 30px;
|
|
819
|
+
padding: 8px 16px;
|
|
820
|
+
font-size: 12.5px;
|
|
821
|
+
font-weight: 500;
|
|
822
|
+
display: inline-flex;
|
|
823
|
+
align-items: center;
|
|
824
|
+
gap: 8px;
|
|
825
|
+
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
|
|
826
|
+
pointer-events: auto;
|
|
827
|
+
animation: bt-toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
828
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
829
|
+
user-select: none;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.bt-toast-fadeout {
|
|
833
|
+
opacity: 0;
|
|
834
|
+
transform: translateY(6px);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
@keyframes bt-toast-in {
|
|
838
|
+
from { opacity: 0; transform: translateY(8px) scale(0.95); }
|
|
839
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
840
|
+
}
|
|
663
841
|
`;
|
|
664
842
|
|
|
665
843
|
this.shadowRoot.appendChild(style);
|
|
@@ -675,6 +853,11 @@ export class NoteInspector {
|
|
|
675
853
|
this.markersContainer.className = 'bt-markers-layer';
|
|
676
854
|
this.shadowRoot.appendChild(this.markersContainer);
|
|
677
855
|
|
|
856
|
+
// Toast container layer
|
|
857
|
+
this.toastContainer = document.createElement('div');
|
|
858
|
+
this.toastContainer.className = 'bt-toast-container';
|
|
859
|
+
this.shadowRoot.appendChild(this.toastContainer);
|
|
860
|
+
|
|
678
861
|
// Floating Toolbar
|
|
679
862
|
if (this.options.showToolbar) {
|
|
680
863
|
this.createToolbar();
|
|
@@ -707,6 +890,10 @@ export class NoteInspector {
|
|
|
707
890
|
<span>📄</span> Page
|
|
708
891
|
</button>
|
|
709
892
|
<div class="bt-toolbar-divider"></div>
|
|
893
|
+
<button class="bt-toolbar-btn" id="bt-mode-flow" title="Record multi-step reproduction flow">
|
|
894
|
+
<span>🎬</span> Flow
|
|
895
|
+
</button>
|
|
896
|
+
<div class="bt-toolbar-divider"></div>
|
|
710
897
|
<button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
|
|
711
898
|
<span>📌</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
|
|
712
899
|
</button>
|
|
@@ -715,6 +902,7 @@ export class NoteInspector {
|
|
|
715
902
|
const btnElement = this.toolbarElement.querySelector('#bt-mode-element') as HTMLButtonElement;
|
|
716
903
|
const btnRegion = this.toolbarElement.querySelector('#bt-mode-region') as HTMLButtonElement;
|
|
717
904
|
const btnPage = this.toolbarElement.querySelector('#bt-mode-page') as HTMLButtonElement;
|
|
905
|
+
const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow') as HTMLButtonElement;
|
|
718
906
|
const btnToggleNotes = this.toolbarElement.querySelector('#bt-toggle-notes') as HTMLButtonElement;
|
|
719
907
|
|
|
720
908
|
btnElement.onclick = (e: MouseEvent) => {
|
|
@@ -732,6 +920,15 @@ export class NoteInspector {
|
|
|
732
920
|
this.setMode('page');
|
|
733
921
|
};
|
|
734
922
|
|
|
923
|
+
btnFlow.onclick = (e: MouseEvent) => {
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
if (this.activeScenario) {
|
|
926
|
+
this.finishScenario();
|
|
927
|
+
} else {
|
|
928
|
+
this.startScenario();
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
735
932
|
btnToggleNotes.onclick = (e: MouseEvent) => {
|
|
736
933
|
e.stopPropagation();
|
|
737
934
|
this.showMarkers = !this.showMarkers;
|
|
@@ -748,10 +945,23 @@ export class NoteInspector {
|
|
|
748
945
|
const btnElement = this.toolbarElement.querySelector('#bt-mode-element');
|
|
749
946
|
const btnRegion = this.toolbarElement.querySelector('#bt-mode-region');
|
|
750
947
|
const btnPage = this.toolbarElement.querySelector('#bt-mode-page');
|
|
948
|
+
const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow');
|
|
751
949
|
|
|
752
950
|
btnElement?.classList.toggle('active', this.activeMode === 'element');
|
|
753
951
|
btnRegion?.classList.toggle('active', this.activeMode === 'region');
|
|
754
952
|
btnPage?.classList.toggle('active', this.activeMode === 'page');
|
|
953
|
+
|
|
954
|
+
if (btnFlow) {
|
|
955
|
+
if (this.activeScenario) {
|
|
956
|
+
btnFlow.className = 'bt-toolbar-btn active-scenario';
|
|
957
|
+
btnFlow.innerHTML = `<span>🎬</span> Step ${this.activeScenario.stepNumber} (Finish)`;
|
|
958
|
+
btnFlow.setAttribute('title', `Click to finish recording "${this.activeScenario.title}"`);
|
|
959
|
+
} else {
|
|
960
|
+
btnFlow.className = 'bt-toolbar-btn';
|
|
961
|
+
btnFlow.innerHTML = `<span>🎬</span> Flow`;
|
|
962
|
+
btnFlow.setAttribute('title', 'Record multi-step reproduction flow');
|
|
963
|
+
}
|
|
964
|
+
}
|
|
755
965
|
}
|
|
756
966
|
|
|
757
967
|
private updateToolbarCount(): void {
|
|
@@ -784,6 +994,10 @@ export class NoteInspector {
|
|
|
784
994
|
return;
|
|
785
995
|
}
|
|
786
996
|
|
|
997
|
+
const isStep = !!note.scenarioId && note.stepNumber != null;
|
|
998
|
+
const stepLabel = isStep ? `🎬 Step ${note.stepNumber}` : `#${index + 1}`;
|
|
999
|
+
const markerClass = `bt-note-marker ${isStep ? 'bt-note-marker-step' : ''}`;
|
|
1000
|
+
|
|
787
1001
|
if (note.type === 'region' && note.region) {
|
|
788
1002
|
// Region box
|
|
789
1003
|
const regBox = document.createElement('div');
|
|
@@ -796,11 +1010,11 @@ export class NoteInspector {
|
|
|
796
1010
|
|
|
797
1011
|
// Pin on top-left of region
|
|
798
1012
|
const pin = document.createElement('div');
|
|
799
|
-
pin.className =
|
|
1013
|
+
pin.className = markerClass;
|
|
800
1014
|
pin.style.left = `${Math.max(4, note.region.x - 12)}px`;
|
|
801
1015
|
pin.style.top = `${Math.max(4, note.region.y - 12)}px`;
|
|
802
|
-
pin.title = note.message;
|
|
803
|
-
pin.innerHTML = `<span
|
|
1016
|
+
pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
|
|
1017
|
+
pin.innerHTML = `<span>${isStep ? '🎬' : '📐'}</span> <span>${stepLabel}</span>`;
|
|
804
1018
|
pin.onclick = (e) => {
|
|
805
1019
|
e.stopPropagation();
|
|
806
1020
|
this.openNoteCard(note);
|
|
@@ -820,12 +1034,12 @@ export class NoteInspector {
|
|
|
820
1034
|
const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
|
|
821
1035
|
if (rect) {
|
|
822
1036
|
const pin = document.createElement('div');
|
|
823
|
-
pin.className =
|
|
1037
|
+
pin.className = markerClass;
|
|
824
1038
|
pin.setAttribute('data-note-id', note.id);
|
|
825
|
-
pin.title = note.message;
|
|
1039
|
+
pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
|
|
826
1040
|
pin.style.left = `${Math.max(4, rect.left - 10)}px`;
|
|
827
1041
|
pin.style.top = `${Math.max(4, rect.top - 12)}px`;
|
|
828
|
-
pin.innerHTML = `<span
|
|
1042
|
+
pin.innerHTML = `<span>${isStep ? '🎬' : '📝'}</span> <span>${stepLabel}</span>`;
|
|
829
1043
|
|
|
830
1044
|
pin.onclick = (e) => {
|
|
831
1045
|
e.stopPropagation();
|
|
@@ -845,10 +1059,11 @@ export class NoteInspector {
|
|
|
845
1059
|
if (pageNotes.length > 0) {
|
|
846
1060
|
const pageDock = document.createElement('div');
|
|
847
1061
|
pageDock.className = 'bt-page-notes-dock';
|
|
848
|
-
pageNotes.forEach((pNote
|
|
1062
|
+
pageNotes.forEach((pNote) => {
|
|
849
1063
|
const pill = document.createElement('div');
|
|
850
1064
|
pill.className = 'bt-page-note-pill';
|
|
851
|
-
|
|
1065
|
+
const label = pNote.scenarioId && pNote.stepNumber ? `Step ${pNote.stepNumber}: ` : '';
|
|
1066
|
+
pill.innerHTML = `<span>📄</span> <span>${label}${truncate(pNote.message, 25)}</span>`;
|
|
852
1067
|
pill.onclick = (e) => {
|
|
853
1068
|
e.stopPropagation();
|
|
854
1069
|
this.openNoteCard(pNote);
|
|
@@ -889,17 +1104,45 @@ export class NoteInspector {
|
|
|
889
1104
|
|
|
890
1105
|
const formattedDate = new Date(note.createdAt).toLocaleString();
|
|
891
1106
|
|
|
1107
|
+
// Scenario flow steps navigation
|
|
1108
|
+
let scenarioStepsHtml = '';
|
|
1109
|
+
let scenarioSteps: VisualNote[] = [];
|
|
1110
|
+
if (note.scenarioId) {
|
|
1111
|
+
scenarioSteps = this.savedNotes
|
|
1112
|
+
.filter((n) => n.scenarioId === note.scenarioId)
|
|
1113
|
+
.sort((a, b) => (a.stepNumber || 0) - (b.stepNumber || 0));
|
|
1114
|
+
|
|
1115
|
+
const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
|
|
1116
|
+
const prevStep = currentIndex > 0 ? scenarioSteps[currentIndex - 1] : null;
|
|
1117
|
+
const nextStep = currentIndex >= 0 && currentIndex < scenarioSteps.length - 1 ? scenarioSteps[currentIndex + 1] : null;
|
|
1118
|
+
|
|
1119
|
+
scenarioStepsHtml = `
|
|
1120
|
+
<div class="bt-scenario-stepper">
|
|
1121
|
+
<button class="bt-step-nav-btn" id="btn-prev-step" ${!prevStep ? 'disabled' : ''}>
|
|
1122
|
+
◀ Step ${prevStep ? prevStep.stepNumber : ''}
|
|
1123
|
+
</button>
|
|
1124
|
+
<span>🎬 Step ${note.stepNumber || 1} of ${scenarioSteps.length}</span>
|
|
1125
|
+
<button class="bt-step-nav-btn" id="btn-next-step-card" ${!nextStep ? 'disabled' : ''}>
|
|
1126
|
+
Step ${nextStep ? nextStep.stepNumber : ''} ▶
|
|
1127
|
+
</button>
|
|
1128
|
+
</div>
|
|
1129
|
+
`;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
892
1132
|
backdrop.innerHTML = `
|
|
893
1133
|
<div class="bt-modal">
|
|
894
1134
|
<div class="bt-modal-header">
|
|
895
1135
|
<div class="bt-modal-title">
|
|
896
|
-
<span>${icon
|
|
1136
|
+
<span>${note.scenarioId ? '🎬 ' + (note.scenarioTitle || 'Scenario Flow') : icon + ' Visual Note Details'}</span>
|
|
897
1137
|
<span class="bt-mode-badge ${badgeClass}">${note.type.toUpperCase()}</span>
|
|
1138
|
+
${note.scenarioId && note.stepNumber ? `<span class="bt-mode-badge bt-badge-step">STEP ${note.stepNumber}</span>` : ''}
|
|
898
1139
|
<span class="bt-mode-badge ${statusClass}">${note.status}</span>
|
|
899
1140
|
</div>
|
|
900
1141
|
<button class="bt-close-btn" id="btn-card-close" title="Close (Esc)">✕</button>
|
|
901
1142
|
</div>
|
|
902
1143
|
|
|
1144
|
+
${scenarioStepsHtml}
|
|
1145
|
+
|
|
903
1146
|
<div class="bt-target-pill" title="${contextText}">
|
|
904
1147
|
<span>🏷️</span>
|
|
905
1148
|
<span class="bt-pill-content">${contextText}</span>
|
|
@@ -913,9 +1156,18 @@ export class NoteInspector {
|
|
|
913
1156
|
</div>
|
|
914
1157
|
|
|
915
1158
|
<div class="bt-modal-footer">
|
|
916
|
-
<
|
|
917
|
-
<
|
|
918
|
-
|
|
1159
|
+
<div style="display: flex; gap: 6px;">
|
|
1160
|
+
<button class="bt-btn bt-btn-delete" id="btn-card-delete" title="Delete this note">
|
|
1161
|
+
<span>🗑️</span> Delete
|
|
1162
|
+
</button>
|
|
1163
|
+
${
|
|
1164
|
+
note.scenarioId
|
|
1165
|
+
? `<button class="bt-btn bt-btn-delete" id="btn-card-delete-flow" title="Delete entire scenario flow">
|
|
1166
|
+
<span>🗑️</span> Delete Flow
|
|
1167
|
+
</button>`
|
|
1168
|
+
: ''
|
|
1169
|
+
}
|
|
1170
|
+
</div>
|
|
919
1171
|
<div class="bt-modal-actions">
|
|
920
1172
|
<button class="bt-btn bt-btn-cancel" id="btn-card-dismiss">Close</button>
|
|
921
1173
|
<button class="bt-btn ${note.status === 'OPEN' ? 'bt-btn-resolve' : 'bt-btn-save'}" id="btn-card-resolve">
|
|
@@ -930,6 +1182,9 @@ export class NoteInspector {
|
|
|
930
1182
|
const btnDismiss = backdrop.querySelector('#btn-card-dismiss') as HTMLButtonElement;
|
|
931
1183
|
const btnResolve = backdrop.querySelector('#btn-card-resolve') as HTMLButtonElement;
|
|
932
1184
|
const btnDelete = backdrop.querySelector('#btn-card-delete') as HTMLButtonElement;
|
|
1185
|
+
const btnDeleteFlow = backdrop.querySelector('#btn-card-delete-flow') as HTMLButtonElement | null;
|
|
1186
|
+
const btnPrevStep = backdrop.querySelector('#btn-prev-step') as HTMLButtonElement | null;
|
|
1187
|
+
const btnNextStepCard = backdrop.querySelector('#btn-next-step-card') as HTMLButtonElement | null;
|
|
933
1188
|
|
|
934
1189
|
const closeCard = () => {
|
|
935
1190
|
if (this.cardOverlay) {
|
|
@@ -945,20 +1200,49 @@ export class NoteInspector {
|
|
|
945
1200
|
if (e.target === backdrop) closeCard();
|
|
946
1201
|
};
|
|
947
1202
|
|
|
1203
|
+
if (btnPrevStep && scenarioSteps.length > 0) {
|
|
1204
|
+
const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
|
|
1205
|
+
if (currentIndex > 0) {
|
|
1206
|
+
btnPrevStep.onclick = () => {
|
|
1207
|
+
this.openNoteCard(scenarioSteps[currentIndex - 1]);
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (btnNextStepCard && scenarioSteps.length > 0) {
|
|
1213
|
+
const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
|
|
1214
|
+
if (currentIndex >= 0 && currentIndex < scenarioSteps.length - 1) {
|
|
1215
|
+
btnNextStepCard.onclick = () => {
|
|
1216
|
+
this.openNoteCard(scenarioSteps[currentIndex + 1]);
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
948
1221
|
btnResolve.onclick = () => {
|
|
949
1222
|
if (note.status === 'OPEN') {
|
|
950
1223
|
this.transport.send({ type: 'resolve_note', noteId: note.id });
|
|
1224
|
+
this.showToast('Note marked as resolved', '✅');
|
|
951
1225
|
} else {
|
|
952
1226
|
this.transport.send({ type: 'reopen_note', noteId: note.id });
|
|
1227
|
+
this.showToast('Note reopened', '↺');
|
|
953
1228
|
}
|
|
954
1229
|
closeCard();
|
|
955
1230
|
};
|
|
956
1231
|
|
|
957
1232
|
btnDelete.onclick = () => {
|
|
958
1233
|
this.transport.send({ type: 'delete_note', noteId: note.id });
|
|
1234
|
+
this.showToast('Note deleted', '🗑️');
|
|
959
1235
|
closeCard();
|
|
960
1236
|
};
|
|
961
1237
|
|
|
1238
|
+
if (btnDeleteFlow && note.scenarioId) {
|
|
1239
|
+
btnDeleteFlow.onclick = () => {
|
|
1240
|
+
this.transport.send({ type: 'delete_scenario', scenarioId: note.scenarioId });
|
|
1241
|
+
this.showToast('Scenario flow deleted', '🗑️');
|
|
1242
|
+
closeCard();
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
962
1246
|
root.appendChild(backdrop);
|
|
963
1247
|
}
|
|
964
1248
|
|
|
@@ -1008,7 +1292,7 @@ export class NoteInspector {
|
|
|
1008
1292
|
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
1009
1293
|
this.selectedElement = target;
|
|
1010
1294
|
this.openNoteEditor(target, 'element');
|
|
1011
|
-
if (this.activeMode === 'element') {
|
|
1295
|
+
if (this.activeMode === 'element' && !this.activeScenario) {
|
|
1012
1296
|
this.setMode('idle');
|
|
1013
1297
|
}
|
|
1014
1298
|
}
|
|
@@ -1022,7 +1306,9 @@ export class NoteInspector {
|
|
|
1022
1306
|
this.shadowRoot.removeChild(this.cardOverlay);
|
|
1023
1307
|
this.cardOverlay = null;
|
|
1024
1308
|
} else if (this.activeMode === 'region' || this.activeMode === 'element') {
|
|
1025
|
-
this.
|
|
1309
|
+
if (!this.activeScenario) {
|
|
1310
|
+
this.setMode('idle');
|
|
1311
|
+
}
|
|
1026
1312
|
}
|
|
1027
1313
|
}
|
|
1028
1314
|
};
|
|
@@ -1153,31 +1439,30 @@ export class NoteInspector {
|
|
|
1153
1439
|
width: Math.round(width),
|
|
1154
1440
|
height: Math.round(height),
|
|
1155
1441
|
};
|
|
1156
|
-
this.hideRegionOverlay();
|
|
1157
|
-
this.setMode('idle');
|
|
1158
1442
|
this.openRegionNoteEditor(this.selectedRegion);
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (this.regionBox) {
|
|
1446
|
+
this.regionBox.style.display = 'none';
|
|
1447
|
+
}
|
|
1448
|
+
if (!this.activeScenario) {
|
|
1161
1449
|
this.setMode('idle');
|
|
1162
1450
|
}
|
|
1163
1451
|
};
|
|
1164
|
-
}
|
|
1165
1452
|
|
|
1166
|
-
// Always ensure regionOverlay is mounted before toolbar so toolbar is interactable
|
|
1167
|
-
if (this.toolbarElement && this.toolbarElement.parentNode === root) {
|
|
1168
|
-
root.insertBefore(this.regionOverlay, this.toolbarElement);
|
|
1169
|
-
} else {
|
|
1170
1453
|
root.appendChild(this.regionOverlay);
|
|
1454
|
+
} else {
|
|
1455
|
+
this.regionOverlay.style.display = 'block';
|
|
1171
1456
|
}
|
|
1172
1457
|
}
|
|
1173
1458
|
|
|
1174
1459
|
private hideRegionOverlay(): void {
|
|
1175
|
-
this.
|
|
1176
|
-
|
|
1177
|
-
this.
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1460
|
+
if (this.regionOverlay) {
|
|
1461
|
+
this.regionOverlay.style.display = 'none';
|
|
1462
|
+
this.isDraggingRegion = false;
|
|
1463
|
+
if (this.regionBox) {
|
|
1464
|
+
this.regionBox.style.display = 'none';
|
|
1465
|
+
}
|
|
1181
1466
|
}
|
|
1182
1467
|
}
|
|
1183
1468
|
|
|
@@ -1187,21 +1472,21 @@ export class NoteInspector {
|
|
|
1187
1472
|
|
|
1188
1473
|
const rect = el.getBoundingClientRect();
|
|
1189
1474
|
this.highlightOverlay.style.display = 'block';
|
|
1190
|
-
this.highlightOverlay.style.top = `${rect.top}px`;
|
|
1191
1475
|
this.highlightOverlay.style.left = `${rect.left}px`;
|
|
1476
|
+
this.highlightOverlay.style.top = `${rect.top}px`;
|
|
1192
1477
|
this.highlightOverlay.style.width = `${rect.width}px`;
|
|
1193
1478
|
this.highlightOverlay.style.height = `${rect.height}px`;
|
|
1194
1479
|
|
|
1195
|
-
const selector = getSemanticSelector(el);
|
|
1196
|
-
const badgeText = `${selector} · ${Math.round(rect.width)}×${Math.round(rect.height)}`;
|
|
1197
|
-
|
|
1198
1480
|
let badge = this.highlightOverlay.querySelector('.bt-badge') as HTMLDivElement | null;
|
|
1199
1481
|
if (!badge) {
|
|
1200
1482
|
badge = document.createElement('div');
|
|
1201
1483
|
badge.className = 'bt-badge';
|
|
1202
1484
|
this.highlightOverlay.appendChild(badge);
|
|
1203
1485
|
}
|
|
1204
|
-
|
|
1486
|
+
|
|
1487
|
+
const selector = getSemanticSelector(el);
|
|
1488
|
+
const label = this.activeScenario ? `🎬 Step ${this.activeScenario.stepNumber} · ${selector}` : selector;
|
|
1489
|
+
badge.textContent = `${label} (${Math.round(rect.width)} × ${Math.round(rect.height)} px)`;
|
|
1205
1490
|
}
|
|
1206
1491
|
|
|
1207
1492
|
private hideHighlight(): void {
|
|
@@ -1221,25 +1506,73 @@ export class NoteInspector {
|
|
|
1221
1506
|
}
|
|
1222
1507
|
|
|
1223
1508
|
this.renderNoteModal({
|
|
1224
|
-
title:
|
|
1225
|
-
|
|
1509
|
+
title: this.activeScenario
|
|
1510
|
+
? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
|
|
1511
|
+
: 'Add Visual Note',
|
|
1512
|
+
modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : noteType.toUpperCase(),
|
|
1226
1513
|
pillText:
|
|
1227
1514
|
noteType === 'page'
|
|
1228
1515
|
? `Page Viewport: ${window.innerWidth} × ${window.innerHeight} px`
|
|
1229
1516
|
: `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
|
|
1230
|
-
onSave: async (message) => {
|
|
1231
|
-
|
|
1517
|
+
onSave: async (message, action) => {
|
|
1518
|
+
let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
|
|
1519
|
+
|
|
1520
|
+
if (action === 'next_step' && !this.activeScenario) {
|
|
1521
|
+
this.startScenario();
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (this.activeScenario) {
|
|
1525
|
+
scenarioParam = {
|
|
1526
|
+
scenarioId: this.activeScenario.id,
|
|
1527
|
+
stepNumber: this.activeScenario.stepNumber,
|
|
1528
|
+
scenarioTitle: this.activeScenario.title,
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
await this.saveVisualNote(targetEl, message, noteType, scenarioParam);
|
|
1533
|
+
|
|
1534
|
+
if (action === 'next_step' && this.activeScenario) {
|
|
1535
|
+
this.activeScenario.stepNumber++;
|
|
1536
|
+
this.updateToolbarState();
|
|
1537
|
+
this.setMode('element');
|
|
1538
|
+
} else if (action === 'finish_flow' && this.activeScenario) {
|
|
1539
|
+
this.finishScenario();
|
|
1540
|
+
}
|
|
1232
1541
|
},
|
|
1233
1542
|
});
|
|
1234
1543
|
}
|
|
1235
1544
|
|
|
1236
1545
|
public openRegionNoteEditor(region: RegionContext): void {
|
|
1237
1546
|
this.renderNoteModal({
|
|
1238
|
-
title:
|
|
1239
|
-
|
|
1547
|
+
title: this.activeScenario
|
|
1548
|
+
? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
|
|
1549
|
+
: 'Add Region Note',
|
|
1550
|
+
modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : 'REGION',
|
|
1240
1551
|
pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} × ${region.height} px)`,
|
|
1241
|
-
onSave: async (message) => {
|
|
1242
|
-
|
|
1552
|
+
onSave: async (message, action) => {
|
|
1553
|
+
let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
|
|
1554
|
+
|
|
1555
|
+
if (action === 'next_step' && !this.activeScenario) {
|
|
1556
|
+
this.startScenario();
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (this.activeScenario) {
|
|
1560
|
+
scenarioParam = {
|
|
1561
|
+
scenarioId: this.activeScenario.id,
|
|
1562
|
+
stepNumber: this.activeScenario.stepNumber,
|
|
1563
|
+
scenarioTitle: this.activeScenario.title,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
await this.saveRegionVisualNote(region, message, scenarioParam);
|
|
1568
|
+
|
|
1569
|
+
if (action === 'next_step' && this.activeScenario) {
|
|
1570
|
+
this.activeScenario.stepNumber++;
|
|
1571
|
+
this.updateToolbarState();
|
|
1572
|
+
this.setMode('element');
|
|
1573
|
+
} else if (action === 'finish_flow' && this.activeScenario) {
|
|
1574
|
+
this.finishScenario();
|
|
1575
|
+
}
|
|
1243
1576
|
},
|
|
1244
1577
|
});
|
|
1245
1578
|
}
|
|
@@ -1248,7 +1581,7 @@ export class NoteInspector {
|
|
|
1248
1581
|
title: string;
|
|
1249
1582
|
modeBadge: string;
|
|
1250
1583
|
pillText: string;
|
|
1251
|
-
onSave: (message: string) => Promise<void>;
|
|
1584
|
+
onSave: (message: string, action: 'save' | 'next_step' | 'finish_flow') => Promise<void>;
|
|
1252
1585
|
}): void {
|
|
1253
1586
|
const root = this.ensureContainer();
|
|
1254
1587
|
if (!root) return;
|
|
@@ -1263,15 +1596,16 @@ export class NoteInspector {
|
|
|
1263
1596
|
backdrop.className = 'bt-modal-backdrop';
|
|
1264
1597
|
this.modalOverlay = backdrop;
|
|
1265
1598
|
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1599
|
+
const isStep = options.modeBadge.startsWith('STEP');
|
|
1600
|
+
const badgeClass = isStep ? 'bt-badge-step' : `bt-badge-${options.modeBadge.toLowerCase()}`;
|
|
1601
|
+
const icon = isStep ? '🎬' : options.modeBadge === 'REGION' ? '📐' : options.modeBadge === 'PAGE' ? '📄' : '🎯';
|
|
1268
1602
|
const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
1269
1603
|
|
|
1270
1604
|
backdrop.innerHTML = `
|
|
1271
1605
|
<div class="bt-modal">
|
|
1272
1606
|
<div class="bt-modal-header">
|
|
1273
1607
|
<div class="bt-modal-title">
|
|
1274
|
-
<span
|
|
1608
|
+
<span>${icon} ${options.title}</span>
|
|
1275
1609
|
<span class="bt-mode-badge ${badgeClass}">${options.modeBadge}</span>
|
|
1276
1610
|
</div>
|
|
1277
1611
|
<button class="bt-close-btn" id="btn-close" title="Close (Esc)">✕</button>
|
|
@@ -1280,14 +1614,23 @@ export class NoteInspector {
|
|
|
1280
1614
|
<span>${icon}</span>
|
|
1281
1615
|
<span class="bt-pill-content">${options.pillText}</span>
|
|
1282
1616
|
</div>
|
|
1283
|
-
<textarea class="bt-textarea" placeholder="Describe the layout issue,
|
|
1617
|
+
<textarea class="bt-textarea" placeholder="Describe the layout issue, user action, or note for AI agent..." autofocus></textarea>
|
|
1284
1618
|
<div class="bt-modal-footer">
|
|
1285
1619
|
<div class="bt-kbd-hint">
|
|
1286
1620
|
<kbd>${isMac ? '⌘' : 'Ctrl'}+Enter</kbd> save · <kbd>Esc</kbd> cancel
|
|
1287
1621
|
</div>
|
|
1288
1622
|
<div class="bt-modal-actions">
|
|
1289
1623
|
<button class="bt-btn bt-btn-cancel" id="btn-cancel">Cancel</button>
|
|
1290
|
-
<button class="bt-btn bt-btn-
|
|
1624
|
+
<button class="bt-btn bt-btn-next-step" id="btn-next-step" title="Save this step and immediately select the next element">
|
|
1625
|
+
<span>➡️</span> ${this.activeScenario ? 'Save & Next Step' : 'Save as Step 1 (Flow)'}
|
|
1626
|
+
</button>
|
|
1627
|
+
${
|
|
1628
|
+
this.activeScenario
|
|
1629
|
+
? `<button class="bt-btn bt-btn-finish-flow" id="btn-finish-flow" title="Save final step and complete scenario">
|
|
1630
|
+
<span>✓</span> Save & Finish Flow
|
|
1631
|
+
</button>`
|
|
1632
|
+
: `<button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>`
|
|
1633
|
+
}
|
|
1291
1634
|
</div>
|
|
1292
1635
|
</div>
|
|
1293
1636
|
</div>
|
|
@@ -1296,7 +1639,9 @@ export class NoteInspector {
|
|
|
1296
1639
|
const textarea = backdrop.querySelector('textarea') as HTMLTextAreaElement;
|
|
1297
1640
|
const btnClose = backdrop.querySelector('#btn-close') as HTMLButtonElement;
|
|
1298
1641
|
const btnCancel = backdrop.querySelector('#btn-cancel') as HTMLButtonElement;
|
|
1299
|
-
const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement;
|
|
1642
|
+
const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement | null;
|
|
1643
|
+
const btnNextStep = backdrop.querySelector('#btn-next-step') as HTMLButtonElement;
|
|
1644
|
+
const btnFinishFlow = backdrop.querySelector('#btn-finish-flow') as HTMLButtonElement | null;
|
|
1300
1645
|
|
|
1301
1646
|
const closeModal = () => {
|
|
1302
1647
|
if (this.modalOverlay) {
|
|
@@ -1316,26 +1661,33 @@ export class NoteInspector {
|
|
|
1316
1661
|
}
|
|
1317
1662
|
};
|
|
1318
1663
|
|
|
1319
|
-
const
|
|
1664
|
+
const handleAction = async (action: 'save' | 'next_step' | 'finish_flow') => {
|
|
1320
1665
|
const message = textarea.value.trim();
|
|
1321
1666
|
if (!message) return;
|
|
1322
1667
|
|
|
1323
|
-
|
|
1324
|
-
btnSave.disabled = true;
|
|
1668
|
+
btnNextStep.disabled = true;
|
|
1669
|
+
if (btnSave) btnSave.disabled = true;
|
|
1670
|
+
if (btnFinishFlow) btnFinishFlow.disabled = true;
|
|
1325
1671
|
|
|
1326
1672
|
try {
|
|
1327
|
-
await options.onSave(message);
|
|
1673
|
+
await options.onSave(message, action);
|
|
1328
1674
|
} finally {
|
|
1329
1675
|
closeModal();
|
|
1330
1676
|
}
|
|
1331
1677
|
};
|
|
1332
1678
|
|
|
1333
|
-
btnSave
|
|
1679
|
+
if (btnSave) {
|
|
1680
|
+
btnSave.onclick = () => handleAction('save');
|
|
1681
|
+
}
|
|
1682
|
+
btnNextStep.onclick = () => handleAction('next_step');
|
|
1683
|
+
if (btnFinishFlow) {
|
|
1684
|
+
btnFinishFlow.onclick = () => handleAction('finish_flow');
|
|
1685
|
+
}
|
|
1334
1686
|
|
|
1335
1687
|
textarea.onkeydown = (e: KeyboardEvent) => {
|
|
1336
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey
|
|
1688
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
1337
1689
|
e.preventDefault();
|
|
1338
|
-
|
|
1690
|
+
handleAction(this.activeScenario ? 'next_step' : 'save');
|
|
1339
1691
|
}
|
|
1340
1692
|
if (e.key === 'Escape') {
|
|
1341
1693
|
closeModal();
|
|
@@ -1346,7 +1698,12 @@ export class NoteInspector {
|
|
|
1346
1698
|
setTimeout(() => textarea?.focus(), 50);
|
|
1347
1699
|
}
|
|
1348
1700
|
|
|
1349
|
-
public async saveVisualNote(
|
|
1701
|
+
public async saveVisualNote(
|
|
1702
|
+
targetEl: HTMLElement,
|
|
1703
|
+
message: string,
|
|
1704
|
+
noteType: 'element' | 'page' = 'element',
|
|
1705
|
+
scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
|
|
1706
|
+
): Promise<void> {
|
|
1350
1707
|
const rect = targetEl.getBoundingClientRect();
|
|
1351
1708
|
const selector = noteType === 'page' ? 'body' : getSemanticSelector(targetEl);
|
|
1352
1709
|
|
|
@@ -1381,25 +1738,30 @@ export class NoteInspector {
|
|
|
1381
1738
|
confidence: selector.startsWith('[data-test') ? 'high' : selector.startsWith('#') ? 'medium' : 'low',
|
|
1382
1739
|
};
|
|
1383
1740
|
|
|
1741
|
+
const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
1742
|
+
const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
|
|
1743
|
+
const viewport = typeof window !== 'undefined'
|
|
1744
|
+
? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
|
|
1745
|
+
: { width: 1280, height: 800, devicePixelRatio: 1 };
|
|
1746
|
+
const scroll = typeof window !== 'undefined'
|
|
1747
|
+
? { scrollX: window.scrollX, scrollY: window.scrollY }
|
|
1748
|
+
: { scrollX: 0, scrollY: 0 };
|
|
1749
|
+
|
|
1384
1750
|
const notePayload = {
|
|
1385
1751
|
type: 'create_note',
|
|
1386
1752
|
sessionId: this.transport.getSessionId() || '',
|
|
1387
1753
|
noteType,
|
|
1388
1754
|
message,
|
|
1389
|
-
route
|
|
1390
|
-
url
|
|
1391
|
-
viewport
|
|
1392
|
-
|
|
1393
|
-
height: window.innerHeight,
|
|
1394
|
-
devicePixelRatio: window.devicePixelRatio || 1,
|
|
1395
|
-
},
|
|
1396
|
-
scroll: {
|
|
1397
|
-
scrollX: window.scrollX,
|
|
1398
|
-
scrollY: window.scrollY,
|
|
1399
|
-
},
|
|
1755
|
+
route,
|
|
1756
|
+
url,
|
|
1757
|
+
viewport,
|
|
1758
|
+
scroll,
|
|
1400
1759
|
target,
|
|
1401
1760
|
elementContext,
|
|
1402
1761
|
screenshot: screenshotDataUrl,
|
|
1762
|
+
scenarioId: scenario?.scenarioId,
|
|
1763
|
+
stepNumber: scenario?.stepNumber,
|
|
1764
|
+
scenarioTitle: scenario?.scenarioTitle,
|
|
1403
1765
|
timestamp: Date.now(),
|
|
1404
1766
|
};
|
|
1405
1767
|
|
|
@@ -1407,9 +1769,19 @@ export class NoteInspector {
|
|
|
1407
1769
|
if (this.options.onNoteCreated) {
|
|
1408
1770
|
this.options.onNoteCreated(notePayload as any);
|
|
1409
1771
|
}
|
|
1772
|
+
|
|
1773
|
+
if (scenario?.scenarioId) {
|
|
1774
|
+
this.showToast(`Step ${scenario.stepNumber || 1} recorded`, '🎬');
|
|
1775
|
+
} else {
|
|
1776
|
+
this.showToast('Visual note saved', '✨');
|
|
1777
|
+
}
|
|
1410
1778
|
}
|
|
1411
1779
|
|
|
1412
|
-
public async saveRegionVisualNote(
|
|
1780
|
+
public async saveRegionVisualNote(
|
|
1781
|
+
region: RegionContext,
|
|
1782
|
+
message: string,
|
|
1783
|
+
scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
|
|
1784
|
+
): Promise<void> {
|
|
1413
1785
|
// 1. Capture full page screenshot and crop to region
|
|
1414
1786
|
let screenshotDataUrl: string | undefined;
|
|
1415
1787
|
try {
|
|
@@ -1421,24 +1793,29 @@ export class NoteInspector {
|
|
|
1421
1793
|
// Defensive
|
|
1422
1794
|
}
|
|
1423
1795
|
|
|
1796
|
+
const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
1797
|
+
const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
|
|
1798
|
+
const viewport = typeof window !== 'undefined'
|
|
1799
|
+
? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
|
|
1800
|
+
: { width: 1280, height: 800, devicePixelRatio: 1 };
|
|
1801
|
+
const scroll = typeof window !== 'undefined'
|
|
1802
|
+
? { scrollX: window.scrollX, scrollY: window.scrollY }
|
|
1803
|
+
: { scrollX: 0, scrollY: 0 };
|
|
1804
|
+
|
|
1424
1805
|
const notePayload = {
|
|
1425
1806
|
type: 'create_note',
|
|
1426
1807
|
sessionId: this.transport.getSessionId() || '',
|
|
1427
1808
|
noteType: 'region',
|
|
1428
1809
|
message,
|
|
1429
|
-
route
|
|
1430
|
-
url
|
|
1431
|
-
viewport
|
|
1432
|
-
|
|
1433
|
-
height: window.innerHeight,
|
|
1434
|
-
devicePixelRatio: window.devicePixelRatio || 1,
|
|
1435
|
-
},
|
|
1436
|
-
scroll: {
|
|
1437
|
-
scrollX: window.scrollX,
|
|
1438
|
-
scrollY: window.scrollY,
|
|
1439
|
-
},
|
|
1810
|
+
route,
|
|
1811
|
+
url,
|
|
1812
|
+
viewport,
|
|
1813
|
+
scroll,
|
|
1440
1814
|
region,
|
|
1441
1815
|
screenshot: screenshotDataUrl,
|
|
1816
|
+
scenarioId: scenario?.scenarioId,
|
|
1817
|
+
stepNumber: scenario?.stepNumber,
|
|
1818
|
+
scenarioTitle: scenario?.scenarioTitle,
|
|
1442
1819
|
timestamp: Date.now(),
|
|
1443
1820
|
};
|
|
1444
1821
|
|
|
@@ -1446,108 +1823,103 @@ export class NoteInspector {
|
|
|
1446
1823
|
if (this.options.onNoteCreated) {
|
|
1447
1824
|
this.options.onNoteCreated(notePayload as any);
|
|
1448
1825
|
}
|
|
1449
|
-
}
|
|
1450
1826
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
const canvas = document.createElement('canvas');
|
|
1457
|
-
canvas.width = region.width;
|
|
1458
|
-
canvas.height = region.height;
|
|
1459
|
-
const ctx = canvas.getContext('2d');
|
|
1460
|
-
if (!ctx) {
|
|
1461
|
-
resolve(dataUrl);
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
const dpr = window.devicePixelRatio || 1;
|
|
1466
|
-
ctx.drawImage(
|
|
1467
|
-
img,
|
|
1468
|
-
region.x * dpr,
|
|
1469
|
-
region.y * dpr,
|
|
1470
|
-
region.width * dpr,
|
|
1471
|
-
region.height * dpr,
|
|
1472
|
-
0,
|
|
1473
|
-
0,
|
|
1474
|
-
region.width,
|
|
1475
|
-
region.height
|
|
1476
|
-
);
|
|
1477
|
-
resolve(canvas.toDataURL('image/webp', 0.9));
|
|
1478
|
-
} catch {
|
|
1479
|
-
resolve(dataUrl);
|
|
1480
|
-
}
|
|
1481
|
-
};
|
|
1482
|
-
img.onerror = () => resolve(dataUrl);
|
|
1483
|
-
img.src = dataUrl;
|
|
1484
|
-
});
|
|
1827
|
+
if (scenario?.scenarioId) {
|
|
1828
|
+
this.showToast(`Step ${scenario.stepNumber || 1} region recorded`, '🎬');
|
|
1829
|
+
} else {
|
|
1830
|
+
this.showToast('Region note saved', '📐');
|
|
1831
|
+
}
|
|
1485
1832
|
}
|
|
1486
1833
|
|
|
1487
1834
|
private extractElementContext(el: HTMLElement): ElementContext {
|
|
1488
1835
|
const selector = getSemanticSelector(el);
|
|
1489
|
-
const tag = el.tagName.toLowerCase();
|
|
1490
|
-
|
|
1491
|
-
// Attributes (sanitize passwords/tokens)
|
|
1492
1836
|
const attributes: Record<string, string> = {};
|
|
1837
|
+
|
|
1493
1838
|
for (let i = 0; i < el.attributes.length; i++) {
|
|
1494
1839
|
const attr = el.attributes[i];
|
|
1495
|
-
if (
|
|
1840
|
+
if (
|
|
1841
|
+
this.options.maskSelectors?.some((mask) => {
|
|
1842
|
+
try {
|
|
1843
|
+
return el.matches(mask);
|
|
1844
|
+
} catch {
|
|
1845
|
+
return false;
|
|
1846
|
+
}
|
|
1847
|
+
}) &&
|
|
1848
|
+
(attr.name === 'value' || attr.name === 'data-secret')
|
|
1849
|
+
) {
|
|
1496
1850
|
attributes[attr.name] = '[REDACTED]';
|
|
1497
1851
|
} else {
|
|
1498
1852
|
attributes[attr.name] = attr.value;
|
|
1499
1853
|
}
|
|
1500
1854
|
}
|
|
1501
1855
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const clone = el.cloneNode(true) as HTMLElement;
|
|
1506
|
-
for (const passInput of Array.from(clone.querySelectorAll('input[type="password"]'))) {
|
|
1507
|
-
passInput.setAttribute('value', '[REDACTED]');
|
|
1508
|
-
}
|
|
1509
|
-
outerHTML = truncate(clone.outerHTML, 10240); // 10 KB max
|
|
1510
|
-
} catch {
|
|
1511
|
-
outerHTML = truncate(el.outerHTML, 10240);
|
|
1856
|
+
let outerHTML = el.outerHTML;
|
|
1857
|
+
if (outerHTML && outerHTML.length > 1000) {
|
|
1858
|
+
outerHTML = truncate(outerHTML, 1000);
|
|
1512
1859
|
}
|
|
1513
1860
|
|
|
1514
|
-
let
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
selector: getSemanticSelector(el.parentElement),
|
|
1518
|
-
tag: el.parentElement.tagName.toLowerCase(),
|
|
1519
|
-
};
|
|
1861
|
+
let innerText = el.innerText || el.textContent || '';
|
|
1862
|
+
if (innerText && innerText.length > 200) {
|
|
1863
|
+
innerText = truncate(innerText, 200);
|
|
1520
1864
|
}
|
|
1521
1865
|
|
|
1522
1866
|
return {
|
|
1523
1867
|
selector,
|
|
1524
|
-
tag,
|
|
1868
|
+
tag: el.tagName.toLowerCase(),
|
|
1525
1869
|
attributes,
|
|
1526
1870
|
outerHTML,
|
|
1527
|
-
innerText
|
|
1528
|
-
parent
|
|
1871
|
+
innerText,
|
|
1872
|
+
parent: el.parentElement
|
|
1873
|
+
? {
|
|
1874
|
+
selector: getSemanticSelector(el.parentElement),
|
|
1875
|
+
tag: el.parentElement.tagName.toLowerCase(),
|
|
1876
|
+
}
|
|
1877
|
+
: undefined,
|
|
1529
1878
|
};
|
|
1530
1879
|
}
|
|
1531
1880
|
|
|
1881
|
+
private async cropDataUrl(dataUrl: string, region: RegionContext): Promise<string> {
|
|
1882
|
+
return new Promise((resolve) => {
|
|
1883
|
+
const img = new Image();
|
|
1884
|
+
img.onload = () => {
|
|
1885
|
+
const canvas = document.createElement('canvas');
|
|
1886
|
+
canvas.width = region.width;
|
|
1887
|
+
canvas.height = region.height;
|
|
1888
|
+
const ctx = canvas.getContext('2d');
|
|
1889
|
+
if (!ctx) {
|
|
1890
|
+
resolve(dataUrl);
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1895
|
+
ctx.drawImage(
|
|
1896
|
+
img,
|
|
1897
|
+
region.x * dpr,
|
|
1898
|
+
region.y * dpr,
|
|
1899
|
+
region.width * dpr,
|
|
1900
|
+
region.height * dpr,
|
|
1901
|
+
0,
|
|
1902
|
+
0,
|
|
1903
|
+
region.width,
|
|
1904
|
+
region.height
|
|
1905
|
+
);
|
|
1906
|
+
resolve(canvas.toDataURL('image/png'));
|
|
1907
|
+
};
|
|
1908
|
+
img.onerror = () => resolve(dataUrl);
|
|
1909
|
+
img.src = dataUrl;
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1532
1913
|
public destroy(): void {
|
|
1533
1914
|
for (const cleanup of this.cleanups) {
|
|
1534
|
-
|
|
1535
|
-
cleanup();
|
|
1536
|
-
} catch {}
|
|
1915
|
+
cleanup();
|
|
1537
1916
|
}
|
|
1538
1917
|
this.cleanups = [];
|
|
1539
1918
|
|
|
1540
|
-
if (this.container && this.container.
|
|
1541
|
-
this.container.
|
|
1919
|
+
if (this.container && this.container.parentNode) {
|
|
1920
|
+
this.container.parentNode.removeChild(this.container);
|
|
1921
|
+
this.container = null;
|
|
1922
|
+
this.shadowRoot = null;
|
|
1542
1923
|
}
|
|
1543
|
-
this.container = null;
|
|
1544
|
-
this.shadowRoot = null;
|
|
1545
|
-
this.toolbarElement = null;
|
|
1546
|
-
this.regionOverlay = null;
|
|
1547
|
-
this.regionBox = null;
|
|
1548
|
-
this.regionBanner = null;
|
|
1549
|
-
this.markersContainer = null;
|
|
1550
|
-
this.modalOverlay = null;
|
|
1551
|
-
this.cardOverlay = null;
|
|
1552
1924
|
}
|
|
1553
1925
|
}
|