agentation 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +118 -0
- data/app/assets/stylesheets/agentation.css +1456 -0
- data/lib/agentation/engine.rb +38 -0
- data/lib/agentation/version.rb +5 -0
- data/lib/agentation/view_helpers.rb +30 -0
- data/lib/agentation.rb +8 -0
- data/lib/generators/agentation/install_generator.rb +71 -0
- data/vendor/javascript/agentation.esm.js +2129 -0
- metadata +96 -0
|
@@ -0,0 +1,2129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentation - Visual Feedback Toolbar for Rails
|
|
3
|
+
* ES Module version for importmaps
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
const STORAGE_PREFIX = 'feedback-annotations-';
|
|
14
|
+
const DEFAULT_RETENTION_DAYS = 7;
|
|
15
|
+
const BLUE = '#3c82f7';
|
|
16
|
+
const RED = '#ff3b30';
|
|
17
|
+
const GREEN = '#34c759';
|
|
18
|
+
const VERSION = '1.0.0';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SETTINGS = {
|
|
21
|
+
outputDetail: 'standard',
|
|
22
|
+
autoClearAfterCopy: false,
|
|
23
|
+
annotationColor: BLUE,
|
|
24
|
+
blockInteractions: false
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const OUTPUT_DETAIL_OPTIONS = [
|
|
28
|
+
{ value: 'compact', label: 'Compact' },
|
|
29
|
+
{ value: 'standard', label: 'Standard' },
|
|
30
|
+
{ value: 'detailed', label: 'Detailed' },
|
|
31
|
+
{ value: 'forensic', label: 'Forensic' }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const COLOR_OPTIONS = [
|
|
35
|
+
{ value: '#AF52DE', label: 'Purple' },
|
|
36
|
+
{ value: '#3c82f7', label: 'Blue' },
|
|
37
|
+
{ value: '#5AC8FA', label: 'Cyan' },
|
|
38
|
+
{ value: '#34C759', label: 'Green' },
|
|
39
|
+
{ value: '#FFD60A', label: 'Yellow' },
|
|
40
|
+
{ value: '#FF9500', label: 'Orange' },
|
|
41
|
+
{ value: '#FF3B30', label: 'Red' }
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Storage Utilities
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
function getStorageKey(pathname) {
|
|
49
|
+
return STORAGE_PREFIX + pathname;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadAnnotations(pathname) {
|
|
53
|
+
try {
|
|
54
|
+
const stored = localStorage.getItem(getStorageKey(pathname));
|
|
55
|
+
if (!stored) return [];
|
|
56
|
+
const data = JSON.parse(stored);
|
|
57
|
+
const cutoff = Date.now() - DEFAULT_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
58
|
+
return data.filter(a => !a.timestamp || a.timestamp > cutoff);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveAnnotations(pathname, annotations) {
|
|
65
|
+
try {
|
|
66
|
+
localStorage.setItem(getStorageKey(pathname), JSON.stringify(annotations));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// localStorage might be full or disabled
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadSettings() {
|
|
73
|
+
try {
|
|
74
|
+
const stored = localStorage.getItem('feedback-toolbar-settings');
|
|
75
|
+
if (stored) {
|
|
76
|
+
return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {}
|
|
79
|
+
return { ...DEFAULT_SETTINGS };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveSettings(settings) {
|
|
83
|
+
try {
|
|
84
|
+
localStorage.setItem('feedback-toolbar-settings', JSON.stringify(settings));
|
|
85
|
+
} catch (e) {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function loadTheme() {
|
|
89
|
+
try {
|
|
90
|
+
const saved = localStorage.getItem('feedback-toolbar-theme');
|
|
91
|
+
if (saved !== null) {
|
|
92
|
+
return saved === 'dark';
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {}
|
|
95
|
+
return true; // default dark
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function saveTheme(isDark) {
|
|
99
|
+
try {
|
|
100
|
+
localStorage.setItem('feedback-toolbar-theme', isDark ? 'dark' : 'light');
|
|
101
|
+
} catch (e) {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Element Identification Utilities
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
function getElementPath(target, maxDepth = 4) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
let current = target;
|
|
111
|
+
let depth = 0;
|
|
112
|
+
|
|
113
|
+
while (current && depth < maxDepth) {
|
|
114
|
+
const tag = current.tagName.toLowerCase();
|
|
115
|
+
if (tag === 'html' || tag === 'body') break;
|
|
116
|
+
|
|
117
|
+
let identifier = tag;
|
|
118
|
+
if (current.id) {
|
|
119
|
+
identifier = '#' + current.id;
|
|
120
|
+
} else if (current.className && typeof current.className === 'string') {
|
|
121
|
+
const classes = current.className.split(/\s+/);
|
|
122
|
+
const meaningfulClass = classes.find(c =>
|
|
123
|
+
c.length > 2 && !c.match(/^[a-z]{1,2}$/) && !c.match(/[A-Z0-9]{5,}/)
|
|
124
|
+
);
|
|
125
|
+
if (meaningfulClass) {
|
|
126
|
+
identifier = '.' + meaningfulClass.split('_')[0];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
parts.unshift(identifier);
|
|
131
|
+
current = current.parentElement;
|
|
132
|
+
depth++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parts.join(' > ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function identifyElement(target) {
|
|
139
|
+
const path = getElementPath(target);
|
|
140
|
+
|
|
141
|
+
if (target.dataset?.element) {
|
|
142
|
+
return { name: target.dataset.element, path };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tag = target.tagName.toLowerCase();
|
|
146
|
+
|
|
147
|
+
// SVG elements
|
|
148
|
+
if (['path', 'circle', 'rect', 'line', 'g'].includes(tag)) {
|
|
149
|
+
const svg = target.closest('svg');
|
|
150
|
+
if (svg) {
|
|
151
|
+
const parent = svg.parentElement;
|
|
152
|
+
if (parent) {
|
|
153
|
+
const parentName = identifyElement(parent).name;
|
|
154
|
+
return { name: `graphic in ${parentName}`, path };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { name: 'graphic element', path };
|
|
158
|
+
}
|
|
159
|
+
if (tag === 'svg') {
|
|
160
|
+
const btnParent = target.parentElement;
|
|
161
|
+
if (btnParent?.tagName.toLowerCase() === 'button') {
|
|
162
|
+
const btnText = btnParent.textContent?.trim();
|
|
163
|
+
return { name: btnText ? `icon in "${btnText}" button` : 'button icon', path };
|
|
164
|
+
}
|
|
165
|
+
return { name: 'icon', path };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Interactive elements
|
|
169
|
+
if (tag === 'button') {
|
|
170
|
+
const text = target.textContent?.trim();
|
|
171
|
+
const ariaLabel = target.getAttribute('aria-label');
|
|
172
|
+
if (ariaLabel) return { name: `button [${ariaLabel}]`, path };
|
|
173
|
+
return { name: text ? `button "${text.slice(0, 25)}"` : 'button', path };
|
|
174
|
+
}
|
|
175
|
+
if (tag === 'a') {
|
|
176
|
+
const linkText = target.textContent?.trim();
|
|
177
|
+
const href = target.getAttribute('href');
|
|
178
|
+
if (linkText) return { name: `link "${linkText.slice(0, 25)}"`, path };
|
|
179
|
+
if (href) return { name: `link to ${href.slice(0, 30)}`, path };
|
|
180
|
+
return { name: 'link', path };
|
|
181
|
+
}
|
|
182
|
+
if (tag === 'input') {
|
|
183
|
+
const type = target.getAttribute('type') || 'text';
|
|
184
|
+
const placeholder = target.getAttribute('placeholder');
|
|
185
|
+
const inputName = target.getAttribute('name');
|
|
186
|
+
if (placeholder) return { name: `input "${placeholder}"`, path };
|
|
187
|
+
if (inputName) return { name: `input [${inputName}]`, path };
|
|
188
|
+
return { name: `${type} input`, path };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Headings
|
|
192
|
+
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
|
|
193
|
+
const headingText = target.textContent?.trim();
|
|
194
|
+
return { name: headingText ? `${tag} "${headingText.slice(0, 35)}"` : tag, path };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Text elements
|
|
198
|
+
if (tag === 'p') {
|
|
199
|
+
const pText = target.textContent?.trim();
|
|
200
|
+
if (pText) return { name: `paragraph: "${pText.slice(0, 40)}${pText.length > 40 ? '...' : ''}"`, path };
|
|
201
|
+
return { name: 'paragraph', path };
|
|
202
|
+
}
|
|
203
|
+
if (tag === 'span' || tag === 'label') {
|
|
204
|
+
const spanText = target.textContent?.trim();
|
|
205
|
+
if (spanText && spanText.length < 40) return { name: `"${spanText}"`, path };
|
|
206
|
+
return { name: tag, path };
|
|
207
|
+
}
|
|
208
|
+
if (tag === 'li') {
|
|
209
|
+
const liText = target.textContent?.trim();
|
|
210
|
+
if (liText && liText.length < 40) return { name: `list item: "${liText.slice(0, 35)}"`, path };
|
|
211
|
+
return { name: 'list item', path };
|
|
212
|
+
}
|
|
213
|
+
if (tag === 'blockquote') return { name: 'blockquote', path };
|
|
214
|
+
if (tag === 'code') {
|
|
215
|
+
const codeText = target.textContent?.trim();
|
|
216
|
+
if (codeText && codeText.length < 30) return { name: `code: \`${codeText}\``, path };
|
|
217
|
+
return { name: 'code', path };
|
|
218
|
+
}
|
|
219
|
+
if (tag === 'pre') return { name: 'code block', path };
|
|
220
|
+
|
|
221
|
+
// Media
|
|
222
|
+
if (tag === 'img') {
|
|
223
|
+
const alt = target.getAttribute('alt');
|
|
224
|
+
return { name: alt ? `image "${alt.slice(0, 30)}"` : 'image', path };
|
|
225
|
+
}
|
|
226
|
+
if (tag === 'video') return { name: 'video', path };
|
|
227
|
+
|
|
228
|
+
// Containers
|
|
229
|
+
if (['div', 'section', 'article', 'nav', 'header', 'footer', 'aside', 'main'].includes(tag)) {
|
|
230
|
+
const className = target.className;
|
|
231
|
+
const role = target.getAttribute('role');
|
|
232
|
+
const containerAriaLabel = target.getAttribute('aria-label');
|
|
233
|
+
|
|
234
|
+
if (containerAriaLabel) return { name: `${tag} [${containerAriaLabel}]`, path };
|
|
235
|
+
if (role) return { name: role, path };
|
|
236
|
+
|
|
237
|
+
if (typeof className === 'string' && className) {
|
|
238
|
+
const words = className
|
|
239
|
+
.split(/[\s_-]+/)
|
|
240
|
+
.map(c => c.replace(/[A-Z0-9]{5,}.*$/, ''))
|
|
241
|
+
.filter(c => c.length > 2 && !/^[a-z]{1,2}$/.test(c))
|
|
242
|
+
.slice(0, 2);
|
|
243
|
+
if (words.length > 0) return { name: words.join(' '), path };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { name: tag === 'div' ? 'container' : tag, path };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { name: tag, path };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getNearbyText(element) {
|
|
253
|
+
const texts = [];
|
|
254
|
+
const ownText = element.textContent?.trim();
|
|
255
|
+
if (ownText && ownText.length < 100) {
|
|
256
|
+
texts.push(ownText);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const prev = element.previousElementSibling;
|
|
260
|
+
if (prev) {
|
|
261
|
+
const prevText = prev.textContent?.trim();
|
|
262
|
+
if (prevText && prevText.length < 50) {
|
|
263
|
+
texts.unshift(`[before: "${prevText.slice(0, 40)}"]`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const next = element.nextElementSibling;
|
|
268
|
+
if (next) {
|
|
269
|
+
const nextText = next.textContent?.trim();
|
|
270
|
+
if (nextText && nextText.length < 50) {
|
|
271
|
+
texts.push(`[after: "${nextText.slice(0, 40)}"]`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return texts.join(' ');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getElementClasses(target) {
|
|
279
|
+
const className = target.className;
|
|
280
|
+
if (typeof className !== 'string' || !className) return '';
|
|
281
|
+
|
|
282
|
+
const classes = className
|
|
283
|
+
.split(/\s+/)
|
|
284
|
+
.filter(c => c.length > 0)
|
|
285
|
+
.map(c => {
|
|
286
|
+
const match = c.match(/^([a-zA-Z][a-zA-Z0-9_-]*?)(?:_[a-zA-Z0-9]{5,})?$/);
|
|
287
|
+
return match ? match[1] : c;
|
|
288
|
+
})
|
|
289
|
+
.filter((c, i, arr) => arr.indexOf(c) === i);
|
|
290
|
+
|
|
291
|
+
return classes.join(', ');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const DEFAULT_STYLE_VALUES = new Set(['none', 'normal', 'auto', '0px', 'rgba(0, 0, 0, 0)', 'transparent', 'static', 'visible']);
|
|
295
|
+
const TEXT_ELEMENTS = new Set(['p', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'li', 'td', 'th', 'blockquote', 'figcaption', 'caption', 'legend', 'dt', 'dd', 'pre', 'code', 'em', 'strong', 'b', 'i', 'a', 'time', 'cite', 'q']);
|
|
296
|
+
const FORM_INPUT_ELEMENTS = new Set(['input', 'textarea', 'select']);
|
|
297
|
+
const MEDIA_ELEMENTS = new Set(['img', 'video', 'canvas', 'svg']);
|
|
298
|
+
const CONTAINER_ELEMENTS = new Set(['div', 'section', 'article', 'nav', 'header', 'footer', 'aside', 'main', 'ul', 'ol', 'form', 'fieldset']);
|
|
299
|
+
|
|
300
|
+
function getDetailedComputedStyles(target) {
|
|
301
|
+
const styles = window.getComputedStyle(target);
|
|
302
|
+
const result = {};
|
|
303
|
+
const tag = target.tagName.toLowerCase();
|
|
304
|
+
|
|
305
|
+
let properties;
|
|
306
|
+
if (TEXT_ELEMENTS.has(tag)) {
|
|
307
|
+
properties = ['color', 'fontSize', 'fontWeight', 'fontFamily', 'lineHeight'];
|
|
308
|
+
} else if (tag === 'button' || (tag === 'a' && target.getAttribute('role') === 'button')) {
|
|
309
|
+
properties = ['backgroundColor', 'color', 'padding', 'borderRadius', 'fontSize'];
|
|
310
|
+
} else if (FORM_INPUT_ELEMENTS.has(tag)) {
|
|
311
|
+
properties = ['backgroundColor', 'color', 'padding', 'borderRadius', 'fontSize'];
|
|
312
|
+
} else if (MEDIA_ELEMENTS.has(tag)) {
|
|
313
|
+
properties = ['width', 'height', 'objectFit', 'borderRadius'];
|
|
314
|
+
} else if (CONTAINER_ELEMENTS.has(tag)) {
|
|
315
|
+
properties = ['display', 'padding', 'margin', 'gap', 'backgroundColor'];
|
|
316
|
+
} else {
|
|
317
|
+
properties = ['color', 'fontSize', 'margin', 'padding', 'backgroundColor'];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const prop of properties) {
|
|
321
|
+
const cssPropertyName = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
322
|
+
const value = styles.getPropertyValue(cssPropertyName);
|
|
323
|
+
if (value && !DEFAULT_STYLE_VALUES.has(value)) {
|
|
324
|
+
result[prop] = value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const FORENSIC_PROPERTIES = [
|
|
332
|
+
'color', 'backgroundColor', 'borderColor',
|
|
333
|
+
'fontSize', 'fontWeight', 'fontFamily', 'lineHeight', 'letterSpacing', 'textAlign',
|
|
334
|
+
'width', 'height', 'padding', 'margin', 'border', 'borderRadius',
|
|
335
|
+
'display', 'position', 'top', 'right', 'bottom', 'left', 'zIndex',
|
|
336
|
+
'flexDirection', 'justifyContent', 'alignItems', 'gap',
|
|
337
|
+
'opacity', 'visibility', 'overflow', 'boxShadow',
|
|
338
|
+
'transform'
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
function getForensicComputedStyles(target) {
|
|
342
|
+
const styles = window.getComputedStyle(target);
|
|
343
|
+
const parts = [];
|
|
344
|
+
|
|
345
|
+
for (const prop of FORENSIC_PROPERTIES) {
|
|
346
|
+
const cssPropertyName = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
347
|
+
const value = styles.getPropertyValue(cssPropertyName);
|
|
348
|
+
if (value && !DEFAULT_STYLE_VALUES.has(value)) {
|
|
349
|
+
parts.push(`${cssPropertyName}: ${value}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return parts.join('; ');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getAccessibilityInfo(target) {
|
|
357
|
+
const parts = [];
|
|
358
|
+
const role = target.getAttribute('role');
|
|
359
|
+
const ariaLabel = target.getAttribute('aria-label');
|
|
360
|
+
const ariaDescribedBy = target.getAttribute('aria-describedby');
|
|
361
|
+
const tabIndex = target.getAttribute('tabindex');
|
|
362
|
+
const ariaHidden = target.getAttribute('aria-hidden');
|
|
363
|
+
|
|
364
|
+
if (role) parts.push(`role="${role}"`);
|
|
365
|
+
if (ariaLabel) parts.push(`aria-label="${ariaLabel}"`);
|
|
366
|
+
if (ariaDescribedBy) parts.push(`aria-describedby="${ariaDescribedBy}"`);
|
|
367
|
+
if (tabIndex) parts.push(`tabindex=${tabIndex}`);
|
|
368
|
+
if (ariaHidden === 'true') parts.push('aria-hidden');
|
|
369
|
+
|
|
370
|
+
const focusable = target.matches('a, button, input, select, textarea, [tabindex]');
|
|
371
|
+
if (focusable) parts.push('focusable');
|
|
372
|
+
|
|
373
|
+
return parts.join(', ');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getFullElementPath(target) {
|
|
377
|
+
const parts = [];
|
|
378
|
+
let current = target;
|
|
379
|
+
|
|
380
|
+
while (current && current.tagName.toLowerCase() !== 'html') {
|
|
381
|
+
const tag = current.tagName.toLowerCase();
|
|
382
|
+
let identifier = tag;
|
|
383
|
+
|
|
384
|
+
if (current.id) {
|
|
385
|
+
identifier = `${tag}#${current.id}`;
|
|
386
|
+
} else if (current.className && typeof current.className === 'string') {
|
|
387
|
+
const cls = current.className
|
|
388
|
+
.split(/\s+/)
|
|
389
|
+
.map(c => c.replace(/[_][a-zA-Z0-9]{5,}.*$/, ''))
|
|
390
|
+
.find(c => c.length > 2);
|
|
391
|
+
if (cls) identifier = `${tag}.${cls}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
parts.unshift(identifier);
|
|
395
|
+
current = current.parentElement;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return parts.join(' > ');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function getNearbyElements(element) {
|
|
402
|
+
const parent = element.parentElement;
|
|
403
|
+
if (!parent) return '';
|
|
404
|
+
|
|
405
|
+
const siblings = Array.from(parent.children).filter(
|
|
406
|
+
child => child !== element && child instanceof HTMLElement
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (siblings.length === 0) return '';
|
|
410
|
+
|
|
411
|
+
const siblingIds = siblings.slice(0, 4).map(sib => {
|
|
412
|
+
const tag = sib.tagName.toLowerCase();
|
|
413
|
+
const className = sib.className;
|
|
414
|
+
let cls = '';
|
|
415
|
+
|
|
416
|
+
if (typeof className === 'string' && className) {
|
|
417
|
+
const meaningful = className
|
|
418
|
+
.split(/\s+/)
|
|
419
|
+
.map(c => c.replace(/[_][a-zA-Z0-9]{5,}.*$/, ''))
|
|
420
|
+
.find(c => c.length > 2 && !/^[a-z]{1,2}$/.test(c));
|
|
421
|
+
if (meaningful) cls = '.' + meaningful;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (tag === 'button' || tag === 'a') {
|
|
425
|
+
const text = sib.textContent?.trim().slice(0, 15);
|
|
426
|
+
if (text) return `${tag}${cls} "${text}"`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return tag + cls;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
433
|
+
let parentId = parentTag;
|
|
434
|
+
if (typeof parent.className === 'string' && parent.className) {
|
|
435
|
+
const parentCls = parent.className
|
|
436
|
+
.split(/\s+/)
|
|
437
|
+
.map(c => c.replace(/[_][a-zA-Z0-9]{5,}.*$/, ''))
|
|
438
|
+
.find(c => c.length > 2 && !/^[a-z]{1,2}$/.test(c));
|
|
439
|
+
if (parentCls) parentId = '.' + parentCls;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const total = parent.children.length;
|
|
443
|
+
const suffix = total > siblingIds.length + 1 ? ` (${total} total in ${parentId})` : '';
|
|
444
|
+
|
|
445
|
+
return siblingIds.join(', ') + suffix;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// =============================================================================
|
|
449
|
+
// Utility Functions
|
|
450
|
+
// =============================================================================
|
|
451
|
+
|
|
452
|
+
function isElementFixed(element) {
|
|
453
|
+
let current = element;
|
|
454
|
+
while (current && current !== document.body) {
|
|
455
|
+
const style = window.getComputedStyle(current);
|
|
456
|
+
const position = style.position;
|
|
457
|
+
if (position === 'fixed' || position === 'sticky') {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
current = current.parentElement;
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function hexToRgba(hex, alpha) {
|
|
466
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
467
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
468
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
469
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function darkenColor(hex, percent) {
|
|
473
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
474
|
+
const amt = Math.round(2.55 * percent);
|
|
475
|
+
const R = Math.max((num >> 16) - amt, 0);
|
|
476
|
+
const G = Math.max((num >> 8 & 0x00FF) - amt, 0);
|
|
477
|
+
const B = Math.max((num & 0x0000FF) - amt, 0);
|
|
478
|
+
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function generateId() {
|
|
482
|
+
return 'agnt-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function generateOutput(annotations, pathname, detailLevel = 'standard') {
|
|
486
|
+
if (annotations.length === 0) return '';
|
|
487
|
+
|
|
488
|
+
const viewport = `${window.innerWidth}\u00D7${window.innerHeight}`;
|
|
489
|
+
let output = `## Page Feedback: ${pathname}\n`;
|
|
490
|
+
|
|
491
|
+
if (detailLevel === 'forensic') {
|
|
492
|
+
output += `\n**Environment:**\n`;
|
|
493
|
+
output += `- Viewport: ${viewport}\n`;
|
|
494
|
+
output += `- URL: ${window.location.href}\n`;
|
|
495
|
+
output += `- User Agent: ${navigator.userAgent}\n`;
|
|
496
|
+
output += `- Timestamp: ${new Date().toISOString()}\n`;
|
|
497
|
+
output += `- Device Pixel Ratio: ${window.devicePixelRatio}\n`;
|
|
498
|
+
output += `\n---\n`;
|
|
499
|
+
} else if (detailLevel !== 'compact') {
|
|
500
|
+
output += `**Viewport:** ${viewport}\n`;
|
|
501
|
+
}
|
|
502
|
+
output += '\n';
|
|
503
|
+
|
|
504
|
+
annotations.forEach((a, i) => {
|
|
505
|
+
if (detailLevel === 'compact') {
|
|
506
|
+
output += `${i + 1}. **${a.element}**: ${a.comment}`;
|
|
507
|
+
if (a.selectedText) {
|
|
508
|
+
output += ` (re: "${a.selectedText.slice(0, 30)}${a.selectedText.length > 30 ? '...' : ''}")`;
|
|
509
|
+
}
|
|
510
|
+
output += '\n';
|
|
511
|
+
} else if (detailLevel === 'forensic') {
|
|
512
|
+
output += `### ${i + 1}. ${a.element}\n`;
|
|
513
|
+
if (a.isMultiSelect && a.fullPath) {
|
|
514
|
+
output += `*Forensic data shown for first element of selection*\n`;
|
|
515
|
+
}
|
|
516
|
+
if (a.fullPath) output += `**Full DOM Path:** ${a.fullPath}\n`;
|
|
517
|
+
if (a.cssClasses) output += `**CSS Classes:** ${a.cssClasses}\n`;
|
|
518
|
+
if (a.boundingBox) {
|
|
519
|
+
output += `**Position:** x:${Math.round(a.boundingBox.x)}, y:${Math.round(a.boundingBox.y)} (${Math.round(a.boundingBox.width)}\u00D7${Math.round(a.boundingBox.height)}px)\n`;
|
|
520
|
+
}
|
|
521
|
+
output += `**Annotation at:** ${a.x.toFixed(1)}% from left, ${Math.round(a.y)}px from top\n`;
|
|
522
|
+
if (a.selectedText) output += `**Selected text:** "${a.selectedText}"\n`;
|
|
523
|
+
if (a.nearbyText && !a.selectedText) output += `**Context:** ${a.nearbyText.slice(0, 100)}\n`;
|
|
524
|
+
if (a.computedStyles) output += `**Computed Styles:** ${a.computedStyles}\n`;
|
|
525
|
+
if (a.accessibility) output += `**Accessibility:** ${a.accessibility}\n`;
|
|
526
|
+
if (a.nearbyElements) output += `**Nearby Elements:** ${a.nearbyElements}\n`;
|
|
527
|
+
output += `**Feedback:** ${a.comment}\n\n`;
|
|
528
|
+
} else {
|
|
529
|
+
output += `### ${i + 1}. ${a.element}\n`;
|
|
530
|
+
output += `**Location:** ${a.elementPath}\n`;
|
|
531
|
+
|
|
532
|
+
if (detailLevel === 'detailed') {
|
|
533
|
+
if (a.cssClasses) output += `**Classes:** ${a.cssClasses}\n`;
|
|
534
|
+
if (a.boundingBox) {
|
|
535
|
+
output += `**Position:** ${Math.round(a.boundingBox.x)}px, ${Math.round(a.boundingBox.y)}px (${Math.round(a.boundingBox.width)}\u00D7${Math.round(a.boundingBox.height)}px)\n`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (a.selectedText) output += `**Selected text:** "${a.selectedText}"\n`;
|
|
540
|
+
if (detailLevel === 'detailed' && a.nearbyText && !a.selectedText) {
|
|
541
|
+
output += `**Context:** ${a.nearbyText.slice(0, 100)}\n`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
output += `**Feedback:** ${a.comment}\n\n`;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
return output.trim();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function escapeHtml(str) {
|
|
552
|
+
const div = document.createElement('div');
|
|
553
|
+
div.textContent = str;
|
|
554
|
+
return div.innerHTML;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getSelectedText() {
|
|
558
|
+
const selection = window.getSelection();
|
|
559
|
+
if (selection && selection.toString().trim()) {
|
|
560
|
+
return selection.toString().trim();
|
|
561
|
+
}
|
|
562
|
+
return '';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// =============================================================================
|
|
566
|
+
// Icons
|
|
567
|
+
// =============================================================================
|
|
568
|
+
|
|
569
|
+
const Icons = {
|
|
570
|
+
listSparkle: (size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><g clip-path="url(#clip0_list_sparkle)"><path d="M11.5 12L5.5 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M18.5 6.75L5.5 6.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.25 17.25L5.5 17.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M16 12.75L16.5179 13.9677C16.8078 14.6494 17.3506 15.1922 18.0323 15.4821L19.25 16L18.0323 16.5179C17.3506 16.8078 16.8078 17.3506 16.5179 18.0323L16 19.25L15.4821 18.0323C15.1922 17.3506 14.6494 16.8078 13.9677 16.5179L12.75 16L13.9677 15.4821C14.6494 15.1922 15.1922 14.6494 15.4821 13.9677L16 12.75Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></g></svg>`,
|
|
571
|
+
check: (size = 16) => `<svg width="${size}" height="${size}" viewBox="0 0 16 16" fill="none"><path d="M3 8l3.5 3.5L13 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
572
|
+
checkSmall: (size = 14) => `<svg width="${size}" height="${size}" viewBox="0 0 14 14" fill="none"><path d="M3.9375 7L6.125 9.1875L10.5 4.8125" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
573
|
+
copy: (size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M4.75 11.25C4.75 10.4216 5.42157 9.75 6.25 9.75H12.75C13.5784 9.75 14.25 10.4216 14.25 11.25V17.75C14.25 18.5784 13.5784 19.25 12.75 19.25H6.25C5.42157 19.25 4.75 18.5784 4.75 17.75V11.25Z" stroke="currentColor" stroke-width="1.5"/><path d="M17.25 14.25H17.75C18.5784 14.25 19.25 13.5784 19.25 12.75V6.25C19.25 5.42157 18.5784 4.75 17.75 4.75H11.25C10.4216 4.75 9.75 5.42157 9.75 6.25V6.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
|
574
|
+
checkCircle: (size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M12 20C7.58172 20 4 16.4182 4 12C4 7.58172 7.58172 4 12 4C16.4182 4 20 7.58172 20 12C20 16.4182 16.4182 20 12 20Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 10L11 14.25L9.25 12.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
575
|
+
eye: (size = 24, isOpen = true) => isOpen
|
|
576
|
+
? `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M3.91752 12.7539C3.65127 12.2996 3.65037 11.7515 3.9149 11.2962C4.9042 9.59346 7.72688 5.49994 12 5.49994C16.2731 5.49994 19.0958 9.59346 20.0851 11.2962C20.3496 11.7515 20.3487 12.2996 20.0825 12.7539C19.0908 14.4459 16.2694 18.4999 12 18.4999C7.73064 18.4999 4.90918 14.4459 3.91752 12.7539Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 14.8261C13.5608 14.8261 14.8261 13.5608 14.8261 12C14.8261 10.4392 13.5608 9.17392 12 9.17392C10.4392 9.17392 9.17391 10.4392 9.17391 12C9.17391 13.5608 10.4392 14.8261 12 14.8261Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
|
577
|
+
: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M18.6025 9.28503C18.9174 8.9701 19.4364 8.99481 19.7015 9.35271C20.1484 9.95606 20.4943 10.507 20.7342 10.9199C21.134 11.6086 21.1329 12.4454 20.7303 13.1328C20.2144 14.013 19.2151 15.5225 17.7723 16.8193C16.3293 18.1162 14.3852 19.2497 12.0008 19.25C11.4192 19.25 10.8638 19.1823 10.3355 19.0613C9.77966 18.934 9.63498 18.2525 10.0382 17.8493C10.2412 17.6463 10.5374 17.573 10.8188 17.6302C11.1993 17.7076 11.5935 17.75 12.0008 17.75C13.8848 17.7497 15.4867 16.8568 16.7693 15.7041C18.0522 14.5511 18.9606 13.1867 19.4363 12.375C19.5656 12.1543 19.5659 11.8943 19.4373 11.6729C19.2235 11.3049 18.921 10.8242 18.5364 10.3003C18.3085 9.98991 18.3302 9.5573 18.6025 9.28503ZM12.0008 4.75C12.5814 4.75006 13.1358 4.81803 13.6632 4.93953C14.2182 5.06741 14.362 5.74812 13.9593 6.15091C13.7558 6.35435 13.4589 6.42748 13.1771 6.36984C12.7983 6.29239 12.4061 6.25006 12.0008 6.25C10.1167 6.25 8.51415 7.15145 7.23028 8.31543C5.94678 9.47919 5.03918 10.8555 4.56426 11.6729C4.43551 11.8945 4.43582 12.1542 4.56524 12.375C4.77587 12.7343 5.07189 13.2012 5.44718 13.7105C5.67623 14.0213 5.65493 14.4552 5.38193 14.7282C5.0671 15.0431 4.54833 15.0189 4.28292 14.6614C3.84652 14.0736 3.50813 13.5369 3.27129 13.1328C2.86831 12.4451 2.86717 11.6088 3.26739 10.9199C3.78185 10.0345 4.77959 8.51239 6.22247 7.2041C7.66547 5.89584 9.61202 4.75 12.0008 4.75Z" fill="currentColor"/><path d="M5 19L19 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
|
578
|
+
pausePlay: (size = 24, isPaused = false) => isPaused
|
|
579
|
+
? `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M17.75 10.701C18.75 11.2783 18.75 12.7217 17.75 13.299L8.75 18.4952C7.75 19.0725 6.5 18.3509 6.5 17.1962L6.5 6.80384C6.5 5.64914 7.75 4.92746 8.75 5.50481L17.75 10.701Z" stroke="currentColor" stroke-width="1.5"/></svg>`
|
|
580
|
+
: `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M8 6L8 18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M16 18L16 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
|
581
|
+
gear: (size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M10.6504 5.81117C10.9939 4.39628 13.0061 4.39628 13.3496 5.81117C13.5715 6.72517 14.6187 7.15891 15.4219 6.66952C16.6652 5.91193 18.0881 7.33479 17.3305 8.57815C16.8411 9.38134 17.2748 10.4285 18.1888 10.6504C19.6037 10.9939 19.6037 13.0061 18.1888 13.3496C17.2748 13.5715 16.8411 14.6187 17.3305 15.4219C18.0881 16.6652 16.6652 18.0881 15.4219 17.3305C14.6187 16.8411 13.5715 17.2748 13.3496 18.1888C13.0061 19.6037 10.9939 19.6037 10.6504 18.1888C10.4285 17.2748 9.38135 16.8411 8.57815 17.3305C7.33479 18.0881 5.91193 16.6652 6.66952 15.4219C7.15891 14.6187 6.72517 13.5715 5.81117 13.3496C4.39628 13.0061 4.39628 10.9939 5.81117 10.6504C6.72517 10.4285 7.15891 9.38134 6.66952 8.57815C5.91193 7.33479 7.33479 5.91192 8.57815 6.66952C9.38135 7.15891 10.4285 6.72517 10.6504 5.81117Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="2.5" stroke="currentColor" stroke-width="1.5"/></svg>`,
|
|
582
|
+
trash: (size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M13.5 4C14.7426 4 15.75 5.00736 15.75 6.25V7H18.5C18.9142 7 19.25 7.33579 19.25 7.75C19.25 8.16421 18.9142 8.5 18.5 8.5H17.9678L17.6328 16.2217C17.61 16.7475 17.5912 17.1861 17.5469 17.543C17.5015 17.9087 17.4225 18.2506 17.2461 18.5723C16.9747 19.0671 16.5579 19.4671 16.0518 19.7168C15.7227 19.8791 15.3772 19.9422 15.0098 19.9717C14.6514 20.0004 14.2126 20 13.6865 20H10.3135C9.78735 20 9.34856 20.0004 8.99023 19.9717C8.62278 19.9422 8.27729 19.8791 7.94824 19.7168C7.44205 19.4671 7.02532 19.0671 6.75391 18.5723C6.57751 18.2506 6.49853 17.9087 6.45312 17.543C6.40883 17.1861 6.39005 16.7475 6.36719 16.2217L6.03223 8.5H5.5C5.08579 8.5 4.75 8.16421 4.75 7.75C4.75 7.33579 5.08579 7 5.5 7H8.25V6.25C8.25 5.00736 9.25736 4 10.5 4H13.5ZM7.86621 16.1562C7.89013 16.7063 7.90624 17.0751 7.94141 17.3584C7.97545 17.6326 8.02151 17.7644 8.06934 17.8516C8.19271 18.0763 8.38239 18.2577 8.6123 18.3711C8.70153 18.4151 8.83504 18.4545 9.11035 18.4766C9.39482 18.4994 9.76335 18.5 10.3135 18.5H13.6865C14.2367 18.5 14.6052 18.4994 14.8896 18.4766C15.165 18.4545 15.2985 18.4151 15.3877 18.3711C15.6176 18.2577 15.8073 18.0763 15.9307 17.8516C15.9785 17.7644 16.0245 17.6326 16.0586 17.3584C16.0938 17.0751 16.1099 16.7063 16.1338 16.1562L16.4668 8.5H7.5332L7.86621 16.1562ZM9.97656 10.75C10.3906 10.7371 10.7371 11.0626 10.75 11.4766L10.875 15.4766C10.8879 15.8906 10.5624 16.2371 10.1484 16.25C9.73443 16.2629 9.38794 15.9374 9.375 15.5234L9.25 11.5234C9.23706 11.1094 9.56255 10.7629 9.97656 10.75ZM14.0244 10.75C14.4384 10.7635 14.7635 11.1105 14.75 11.5244L14.6201 15.5244C14.6066 15.9384 14.2596 16.2634 13.8457 16.25C13.4317 16.2365 13.1067 15.8896 13.1201 15.4756L13.251 11.4756C13.2645 11.0617 13.6105 10.7366 14.0244 10.75ZM10.5 5.5C10.0858 5.5 9.75 5.83579 9.75 6.25V7H14.25V6.25C14.25 5.83579 13.9142 5.5 13.5 5.5H10.5Z" fill="currentColor"/></svg>`,
|
|
583
|
+
close: (size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M16.7198 6.21973C17.0127 5.92683 17.4874 5.92683 17.7803 6.21973C18.0732 6.51262 18.0732 6.9874 17.7803 7.28027L13.0606 12L17.7803 16.7197C18.0732 17.0126 18.0732 17.4874 17.7803 17.7803C17.4875 18.0731 17.0127 18.0731 16.7198 17.7803L12.0001 13.0605L7.28033 17.7803C6.98746 18.0731 6.51268 18.0731 6.21979 17.7803C5.92689 17.4874 5.92689 17.0126 6.21979 16.7197L10.9395 12L6.21979 7.28027C5.92689 6.98738 5.92689 6.51262 6.21979 6.21973C6.51268 5.92683 6.98744 5.92683 7.28033 6.21973L12.0001 10.9395L16.7198 6.21973Z" fill="currentColor"/></svg>`,
|
|
584
|
+
sun: (size = 16) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5"/><path d="M12 5V3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M12 21V19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M16.95 7.05L18.36 5.64" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M5.64 18.36L7.05 16.95" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M19 12H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M3 12H5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M16.95 16.95L18.36 18.36" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M5.64 5.64L7.05 7.05" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
|
585
|
+
moon: (size = 16) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
586
|
+
chevron: (size = 14) => `<svg width="${size}" height="${size}" viewBox="0 0 14 14" fill="none"><path d="M5.5 10.25L9 7.25L5.75 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// =============================================================================
|
|
590
|
+
// AgentationStore - Simple State Management
|
|
591
|
+
// =============================================================================
|
|
592
|
+
|
|
593
|
+
class AgentationStore {
|
|
594
|
+
constructor(initialState = {}) {
|
|
595
|
+
this.state = initialState;
|
|
596
|
+
this.listeners = [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
getState() {
|
|
600
|
+
return this.state;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
setState(updates) {
|
|
604
|
+
const prevState = this.state;
|
|
605
|
+
if (typeof updates === 'function') {
|
|
606
|
+
this.state = { ...this.state, ...updates(this.state) };
|
|
607
|
+
} else {
|
|
608
|
+
this.state = { ...this.state, ...updates };
|
|
609
|
+
}
|
|
610
|
+
this.notify(prevState);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
subscribe(callback) {
|
|
614
|
+
this.listeners.push(callback);
|
|
615
|
+
return () => {
|
|
616
|
+
this.listeners = this.listeners.filter(l => l !== callback);
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
notify(prevState) {
|
|
621
|
+
const state = this.state;
|
|
622
|
+
this.listeners.forEach(callback => callback(state, prevState));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// =============================================================================
|
|
627
|
+
// AnnotationPopup Class
|
|
628
|
+
// =============================================================================
|
|
629
|
+
|
|
630
|
+
class AnnotationPopup {
|
|
631
|
+
constructor(options) {
|
|
632
|
+
this.options = {
|
|
633
|
+
element: '',
|
|
634
|
+
timestamp: '',
|
|
635
|
+
selectedText: '',
|
|
636
|
+
placeholder: 'What should change?',
|
|
637
|
+
initialValue: '',
|
|
638
|
+
submitLabel: 'Add',
|
|
639
|
+
accentColor: BLUE,
|
|
640
|
+
lightMode: false,
|
|
641
|
+
computedStyles: null,
|
|
642
|
+
onSubmit: () => {},
|
|
643
|
+
onCancel: () => {},
|
|
644
|
+
...options
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
this.text = this.options.initialValue;
|
|
648
|
+
this.isShaking = false;
|
|
649
|
+
this.animState = 'initial';
|
|
650
|
+
this.isFocused = false;
|
|
651
|
+
this.isStylesExpanded = false;
|
|
652
|
+
this.el = null;
|
|
653
|
+
this.textareaEl = null;
|
|
654
|
+
|
|
655
|
+
this.render();
|
|
656
|
+
this.bindEvents();
|
|
657
|
+
this.animateIn();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
render() {
|
|
661
|
+
const opts = this.options;
|
|
662
|
+
const el = document.createElement('div');
|
|
663
|
+
el.className = 'agentation-popup' + (opts.lightMode ? ' agentation-popup--light' : '');
|
|
664
|
+
el.setAttribute('data-annotation-popup', 'true');
|
|
665
|
+
el.style.zIndex = '100001'; // Ensure popup is always on top
|
|
666
|
+
|
|
667
|
+
const hasStyles = opts.computedStyles && Object.keys(opts.computedStyles).length > 0;
|
|
668
|
+
|
|
669
|
+
let html = '<div class="agentation-popup__header">';
|
|
670
|
+
|
|
671
|
+
if (hasStyles) {
|
|
672
|
+
html += '<button class="agentation-popup__header-toggle" type="button">';
|
|
673
|
+
html += '<span class="agentation-popup__chevron">' + Icons.chevron(14) + '</span>';
|
|
674
|
+
html += '<span class="agentation-popup__element">' + escapeHtml(opts.element) + '</span>';
|
|
675
|
+
html += '</button>';
|
|
676
|
+
} else {
|
|
677
|
+
html += '<span class="agentation-popup__element">' + escapeHtml(opts.element) + '</span>';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (opts.timestamp) {
|
|
681
|
+
html += '<span class="agentation-popup__timestamp">' + escapeHtml(opts.timestamp) + '</span>';
|
|
682
|
+
}
|
|
683
|
+
html += '</div>';
|
|
684
|
+
|
|
685
|
+
if (hasStyles) {
|
|
686
|
+
html += '<div class="agentation-popup__styles-wrapper">';
|
|
687
|
+
html += '<div class="agentation-popup__styles-inner">';
|
|
688
|
+
html += '<div class="agentation-popup__styles-block">';
|
|
689
|
+
for (const key in opts.computedStyles) {
|
|
690
|
+
if (opts.computedStyles.hasOwnProperty(key)) {
|
|
691
|
+
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
692
|
+
html += '<div class="agentation-popup__style-line">';
|
|
693
|
+
html += '<span class="agentation-popup__style-property">' + cssKey + '</span>';
|
|
694
|
+
html += ': <span class="agentation-popup__style-value">' + escapeHtml(opts.computedStyles[key]) + '</span>;';
|
|
695
|
+
html += '</div>';
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
html += '</div></div></div>';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (opts.selectedText) {
|
|
702
|
+
html += '<div class="agentation-popup__quote">';
|
|
703
|
+
html += '“' + escapeHtml(opts.selectedText.slice(0, 80));
|
|
704
|
+
if (opts.selectedText.length > 80) html += '...';
|
|
705
|
+
html += '”</div>';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
html += '<textarea class="agentation-popup__textarea" placeholder="' + escapeHtml(opts.placeholder) + '" rows="2">' + escapeHtml(this.text) + '</textarea>';
|
|
709
|
+
|
|
710
|
+
html += '<div class="agentation-popup__actions">';
|
|
711
|
+
html += '<button class="agentation-popup__cancel" type="button">Cancel</button>';
|
|
712
|
+
html += '<button class="agentation-popup__submit" type="button" style="background-color: ' + opts.accentColor + '">' + opts.submitLabel + '</button>';
|
|
713
|
+
html += '</div>';
|
|
714
|
+
|
|
715
|
+
el.innerHTML = html;
|
|
716
|
+
document.body.appendChild(el);
|
|
717
|
+
this.el = el;
|
|
718
|
+
this.textareaEl = el.querySelector('.agentation-popup__textarea');
|
|
719
|
+
this.updateSubmitState();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
bindEvents() {
|
|
723
|
+
this.textareaEl.addEventListener('input', (e) => {
|
|
724
|
+
this.text = e.target.value;
|
|
725
|
+
this.updateSubmitState();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
this.textareaEl.addEventListener('focus', () => {
|
|
729
|
+
this.isFocused = true;
|
|
730
|
+
this.textareaEl.style.borderColor = this.options.accentColor;
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
this.textareaEl.addEventListener('blur', () => {
|
|
734
|
+
this.isFocused = false;
|
|
735
|
+
this.textareaEl.style.borderColor = '';
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
this.textareaEl.addEventListener('keydown', (e) => {
|
|
739
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
740
|
+
e.preventDefault();
|
|
741
|
+
this.handleSubmit();
|
|
742
|
+
}
|
|
743
|
+
if (e.key === 'Escape') {
|
|
744
|
+
this.handleCancel();
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const cancelBtn = this.el.querySelector('.agentation-popup__cancel');
|
|
749
|
+
cancelBtn.addEventListener('click', () => this.handleCancel());
|
|
750
|
+
|
|
751
|
+
const submitBtn = this.el.querySelector('.agentation-popup__submit');
|
|
752
|
+
submitBtn.addEventListener('click', () => this.handleSubmit());
|
|
753
|
+
|
|
754
|
+
const toggleBtn = this.el.querySelector('.agentation-popup__header-toggle');
|
|
755
|
+
if (toggleBtn) {
|
|
756
|
+
toggleBtn.addEventListener('click', () => {
|
|
757
|
+
this.isStylesExpanded = !this.isStylesExpanded;
|
|
758
|
+
const wrapper = this.el.querySelector('.agentation-popup__styles-wrapper');
|
|
759
|
+
const chevron = this.el.querySelector('.agentation-popup__chevron');
|
|
760
|
+
if (wrapper) {
|
|
761
|
+
wrapper.classList.toggle('agentation-popup__styles-wrapper--expanded', this.isStylesExpanded);
|
|
762
|
+
}
|
|
763
|
+
if (chevron) {
|
|
764
|
+
chevron.classList.toggle('agentation-popup__chevron--expanded', this.isStylesExpanded);
|
|
765
|
+
}
|
|
766
|
+
if (!this.isStylesExpanded) {
|
|
767
|
+
setTimeout(() => this.textareaEl.focus(), 0);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.el.addEventListener('click', (e) => e.stopPropagation());
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
updateSubmitState() {
|
|
776
|
+
const submitBtn = this.el.querySelector('.agentation-popup__submit');
|
|
777
|
+
if (submitBtn) {
|
|
778
|
+
submitBtn.style.opacity = this.text.trim() ? '1' : '0.4';
|
|
779
|
+
submitBtn.disabled = !this.text.trim();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
animateIn() {
|
|
784
|
+
requestAnimationFrame(() => {
|
|
785
|
+
this.el.classList.add('agentation-popup--enter');
|
|
786
|
+
this.animState = 'enter';
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
setTimeout(() => {
|
|
790
|
+
this.el.classList.remove('agentation-popup--enter');
|
|
791
|
+
this.el.classList.add('agentation-popup--entered');
|
|
792
|
+
this.animState = 'entered';
|
|
793
|
+
}, 200);
|
|
794
|
+
|
|
795
|
+
setTimeout(() => {
|
|
796
|
+
this.textareaEl.focus();
|
|
797
|
+
this.textareaEl.selectionStart = this.textareaEl.selectionEnd = this.textareaEl.value.length;
|
|
798
|
+
}, 50);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
shake() {
|
|
802
|
+
this.el.classList.add('agentation-popup--shake');
|
|
803
|
+
setTimeout(() => {
|
|
804
|
+
this.el.classList.remove('agentation-popup--shake');
|
|
805
|
+
this.textareaEl.focus();
|
|
806
|
+
}, 250);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
handleCancel() {
|
|
810
|
+
this.el.classList.remove('agentation-popup--entered');
|
|
811
|
+
this.el.classList.add('agentation-popup--exit');
|
|
812
|
+
setTimeout(() => {
|
|
813
|
+
this.destroy();
|
|
814
|
+
this.options.onCancel();
|
|
815
|
+
}, 150);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
handleSubmit() {
|
|
819
|
+
if (!this.text.trim()) return;
|
|
820
|
+
this.options.onSubmit(this.text.trim());
|
|
821
|
+
this.destroy();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
setPosition(x, y) {
|
|
825
|
+
this.el.style.left = x + 'px';
|
|
826
|
+
this.el.style.top = y + 'px';
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
destroy() {
|
|
830
|
+
if (this.el && this.el.parentNode) {
|
|
831
|
+
this.el.parentNode.removeChild(this.el);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// =============================================================================
|
|
837
|
+
// AgentationToolbar Class
|
|
838
|
+
// =============================================================================
|
|
839
|
+
|
|
840
|
+
class AgentationToolbar {
|
|
841
|
+
constructor(container, options = {}) {
|
|
842
|
+
this.container = container;
|
|
843
|
+
this.options = {
|
|
844
|
+
onAnnotationAdd: null,
|
|
845
|
+
onAnnotationDelete: null,
|
|
846
|
+
onAnnotationUpdate: null,
|
|
847
|
+
onAnnotationsClear: null,
|
|
848
|
+
onCopy: null,
|
|
849
|
+
copyToClipboard: true,
|
|
850
|
+
...options
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
this.pathname = window.location.pathname;
|
|
854
|
+
|
|
855
|
+
this.store = new AgentationStore({
|
|
856
|
+
isActive: false,
|
|
857
|
+
annotations: [],
|
|
858
|
+
showMarkers: true,
|
|
859
|
+
hoverInfo: null,
|
|
860
|
+
pendingAnnotation: null,
|
|
861
|
+
copied: false,
|
|
862
|
+
cleared: false,
|
|
863
|
+
hoveredMarkerId: null,
|
|
864
|
+
editingAnnotation: null,
|
|
865
|
+
showSettings: false,
|
|
866
|
+
showSettingsVisible: false,
|
|
867
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
868
|
+
isDarkMode: true,
|
|
869
|
+
isFrozen: false,
|
|
870
|
+
isDragging: false,
|
|
871
|
+
scrollY: 0
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// DOM references
|
|
875
|
+
this.toolbarEl = null;
|
|
876
|
+
this.overlayEl = null;
|
|
877
|
+
this.markersLayerEl = null;
|
|
878
|
+
this.settingsPanelEl = null;
|
|
879
|
+
this.hoverHighlightEl = null;
|
|
880
|
+
this.dragSelectionEl = null;
|
|
881
|
+
this.popup = null;
|
|
882
|
+
this.editPopup = null;
|
|
883
|
+
|
|
884
|
+
// Animation state
|
|
885
|
+
this.hasPlayedEntrance = false;
|
|
886
|
+
this.frozenAnimations = [];
|
|
887
|
+
|
|
888
|
+
// Drag selection state (using instance vars for performance)
|
|
889
|
+
this.mouseDownPos = null;
|
|
890
|
+
this.dragStart = null;
|
|
891
|
+
this.DRAG_THRESHOLD = 8;
|
|
892
|
+
this.justFinishedDrag = false;
|
|
893
|
+
this.highlightsContainerEl = null;
|
|
894
|
+
this.ELEMENT_UPDATE_THROTTLE = 50;
|
|
895
|
+
this.lastElementUpdate = 0;
|
|
896
|
+
|
|
897
|
+
this.init();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
init() {
|
|
901
|
+
this.loadState();
|
|
902
|
+
this.render();
|
|
903
|
+
this.bindEvents();
|
|
904
|
+
this.store.subscribe(this.onStateChange.bind(this));
|
|
905
|
+
|
|
906
|
+
// Trigger entrance animation
|
|
907
|
+
if (!this.hasPlayedEntrance) {
|
|
908
|
+
this.hasPlayedEntrance = true;
|
|
909
|
+
setTimeout(() => {
|
|
910
|
+
if (this.toolbarEl) {
|
|
911
|
+
const container = this.toolbarEl.querySelector('.agentation-toolbar__container');
|
|
912
|
+
if (container) {
|
|
913
|
+
container.classList.add('agentation-toolbar__container--entrance');
|
|
914
|
+
setTimeout(() => {
|
|
915
|
+
container.classList.remove('agentation-toolbar__container--entrance');
|
|
916
|
+
}, 750);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}, 0);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
loadState() {
|
|
924
|
+
const annotations = loadAnnotations(this.pathname);
|
|
925
|
+
const settings = loadSettings();
|
|
926
|
+
const isDarkMode = loadTheme();
|
|
927
|
+
|
|
928
|
+
this.store.setState({
|
|
929
|
+
annotations,
|
|
930
|
+
settings,
|
|
931
|
+
isDarkMode,
|
|
932
|
+
scrollY: window.scrollY
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
render() {
|
|
937
|
+
// Create toolbar
|
|
938
|
+
this.toolbarEl = document.createElement('div');
|
|
939
|
+
this.toolbarEl.className = 'agentation-toolbar';
|
|
940
|
+
this.toolbarEl.setAttribute('data-agentation', 'toolbar');
|
|
941
|
+
this.updateToolbar();
|
|
942
|
+
document.body.appendChild(this.toolbarEl);
|
|
943
|
+
|
|
944
|
+
// Create overlay (for click capture and hover highlight)
|
|
945
|
+
this.overlayEl = document.createElement('div');
|
|
946
|
+
this.overlayEl.className = 'agentation-overlay';
|
|
947
|
+
this.overlayEl.style.display = 'none';
|
|
948
|
+
this.overlayEl.setAttribute('data-agentation', 'overlay');
|
|
949
|
+
document.body.appendChild(this.overlayEl);
|
|
950
|
+
|
|
951
|
+
// Create hover highlight element
|
|
952
|
+
this.hoverHighlightEl = document.createElement('div');
|
|
953
|
+
this.hoverHighlightEl.className = 'agentation-hover-highlight';
|
|
954
|
+
this.hoverHighlightEl.style.display = 'none';
|
|
955
|
+
document.body.appendChild(this.hoverHighlightEl);
|
|
956
|
+
|
|
957
|
+
// Create markers layer
|
|
958
|
+
this.markersLayerEl = document.createElement('div');
|
|
959
|
+
this.markersLayerEl.className = 'agentation-markers-layer';
|
|
960
|
+
this.markersLayerEl.setAttribute('data-agentation', 'markers');
|
|
961
|
+
document.body.appendChild(this.markersLayerEl);
|
|
962
|
+
|
|
963
|
+
// Create drag selection element
|
|
964
|
+
this.dragSelectionEl = document.createElement('div');
|
|
965
|
+
this.dragSelectionEl.className = 'agentation-drag-selection';
|
|
966
|
+
this.dragSelectionEl.style.display = 'none';
|
|
967
|
+
this.dragSelectionEl.setAttribute('data-agentation', 'drag-selection');
|
|
968
|
+
document.body.appendChild(this.dragSelectionEl);
|
|
969
|
+
|
|
970
|
+
// Create highlights container for multi-select element highlights
|
|
971
|
+
this.highlightsContainerEl = document.createElement('div');
|
|
972
|
+
this.highlightsContainerEl.className = 'agentation-highlights-container';
|
|
973
|
+
this.highlightsContainerEl.setAttribute('data-agentation', 'highlights');
|
|
974
|
+
document.body.appendChild(this.highlightsContainerEl);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
updateToolbar() {
|
|
978
|
+
const state = this.store.getState();
|
|
979
|
+
const isExpanded = state.isActive;
|
|
980
|
+
const isDark = state.isDarkMode;
|
|
981
|
+
const annotationCount = state.annotations.length;
|
|
982
|
+
const color = state.settings.annotationColor;
|
|
983
|
+
|
|
984
|
+
// Build container classes - collapsed when not active, expanded when active
|
|
985
|
+
const containerClasses = [
|
|
986
|
+
'agentation-toolbar__container',
|
|
987
|
+
isExpanded ? 'agentation-toolbar__container--expanded' : 'agentation-toolbar__container--collapsed',
|
|
988
|
+
!isDark ? 'agentation-toolbar__container--light' : ''
|
|
989
|
+
].filter(Boolean).join(' ');
|
|
990
|
+
|
|
991
|
+
let html = `<div class="${containerClasses}">`;
|
|
992
|
+
|
|
993
|
+
// Main button (collapsed state shows icon + badge)
|
|
994
|
+
html += `<div class="agentation-toolbar__toggle ${isExpanded ? 'agentation-toolbar__toggle--hidden' : ''}" data-action="activate" style="color: ${color}">`;
|
|
995
|
+
html += Icons.listSparkle(24);
|
|
996
|
+
if (annotationCount > 0) {
|
|
997
|
+
html += `<span class="agentation-toolbar__badge" style="background-color: ${color}">${annotationCount}</span>`;
|
|
998
|
+
}
|
|
999
|
+
html += '</div>';
|
|
1000
|
+
|
|
1001
|
+
// Expanded controls
|
|
1002
|
+
html += `<div class="agentation-toolbar__controls ${isExpanded ? '' : 'agentation-toolbar__controls--hidden'}">`;
|
|
1003
|
+
|
|
1004
|
+
// Pause/Play button
|
|
1005
|
+
html += `<button class="agentation-toolbar__button ${isDark ? '' : 'agentation-toolbar__button--light'}" data-action="freeze" title="${state.isFrozen ? 'Resume animations' : 'Pause animations'}">`;
|
|
1006
|
+
html += Icons.pausePlay(24, state.isFrozen);
|
|
1007
|
+
html += '</button>';
|
|
1008
|
+
|
|
1009
|
+
// Toggle markers visibility
|
|
1010
|
+
html += `<button class="agentation-toolbar__button ${isDark ? '' : 'agentation-toolbar__button--light'}" data-action="toggle-markers" title="${state.showMarkers ? 'Hide markers' : 'Show markers'}" ${annotationCount === 0 ? 'disabled' : ''}>`;
|
|
1011
|
+
html += Icons.eye(24, state.showMarkers);
|
|
1012
|
+
html += '</button>';
|
|
1013
|
+
|
|
1014
|
+
// Copy button
|
|
1015
|
+
html += `<button class="agentation-toolbar__button ${isDark ? '' : 'agentation-toolbar__button--light'}" data-action="copy" title="Copy feedback" ${annotationCount === 0 ? 'disabled' : ''}>`;
|
|
1016
|
+
html += state.copied ? Icons.checkCircle(24) : Icons.copy(24);
|
|
1017
|
+
html += '</button>';
|
|
1018
|
+
|
|
1019
|
+
// Clear button
|
|
1020
|
+
html += `<button class="agentation-toolbar__button agentation-toolbar__button--danger ${isDark ? '' : 'agentation-toolbar__button--light'}" data-action="clear" title="Clear all" ${annotationCount === 0 ? 'disabled' : ''}>`;
|
|
1021
|
+
html += state.cleared ? Icons.check(16) : Icons.trash(24);
|
|
1022
|
+
html += '</button>';
|
|
1023
|
+
|
|
1024
|
+
// Settings button
|
|
1025
|
+
html += `<button class="agentation-toolbar__button ${isDark ? '' : 'agentation-toolbar__button--light'} ${state.showSettings ? 'agentation-toolbar__button--active' : ''}" data-action="settings" title="Settings">`;
|
|
1026
|
+
html += Icons.gear(24);
|
|
1027
|
+
html += '</button>';
|
|
1028
|
+
|
|
1029
|
+
html += '<div class="agentation-toolbar__divider"></div>';
|
|
1030
|
+
|
|
1031
|
+
// Close/Exit button
|
|
1032
|
+
html += `<button class="agentation-toolbar__button ${isDark ? '' : 'agentation-toolbar__button--light'}" data-action="deactivate" title="Exit feedback mode">`;
|
|
1033
|
+
html += Icons.close(24);
|
|
1034
|
+
html += '</button>';
|
|
1035
|
+
|
|
1036
|
+
html += '</div>'; // controls
|
|
1037
|
+
|
|
1038
|
+
// Settings Panel
|
|
1039
|
+
if (state.showSettingsVisible) {
|
|
1040
|
+
html += this.renderSettingsPanel();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
html += '</div>'; // container
|
|
1044
|
+
|
|
1045
|
+
this.toolbarEl.innerHTML = html;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
renderSettingsPanel() {
|
|
1049
|
+
const state = this.store.getState();
|
|
1050
|
+
const isDark = state.isDarkMode;
|
|
1051
|
+
const settings = state.settings;
|
|
1052
|
+
const color = settings.annotationColor;
|
|
1053
|
+
|
|
1054
|
+
let html = `<div class="agentation-settings-panel ${isDark ? '' : 'agentation-settings-panel--light'} ${state.showSettings ? 'agentation-settings-panel--enter' : 'agentation-settings-panel--exit'}">`;
|
|
1055
|
+
|
|
1056
|
+
// Header
|
|
1057
|
+
html += '<div class="agentation-settings-panel__header">';
|
|
1058
|
+
html += `<span class="agentation-settings-panel__brand"><span class="agentation-settings-panel__slash" style="color: ${color}">/</span>agentation</span>`;
|
|
1059
|
+
html += `<span class="agentation-settings-panel__version">v${VERSION}</span>`;
|
|
1060
|
+
html += `<button class="agentation-settings-panel__theme-toggle" data-action="theme" title="${isDark ? 'Switch to light mode' : 'Switch to dark mode'}">`;
|
|
1061
|
+
html += isDark ? Icons.sun(14) : Icons.moon(14);
|
|
1062
|
+
html += '</button>';
|
|
1063
|
+
html += '</div>';
|
|
1064
|
+
|
|
1065
|
+
// Output Detail section
|
|
1066
|
+
html += '<div class="agentation-settings-panel__section">';
|
|
1067
|
+
html += '<div class="agentation-settings-panel__row">';
|
|
1068
|
+
html += `<div class="agentation-settings-panel__label ${isDark ? '' : 'agentation-settings-panel__label--light'}">Output Detail</div>`;
|
|
1069
|
+
html += `<button class="agentation-settings-panel__cycle-btn ${isDark ? '' : 'agentation-settings-panel__cycle-btn--light'}" data-action="cycle-detail">`;
|
|
1070
|
+
html += `<span class="agentation-settings-panel__cycle-text">${OUTPUT_DETAIL_OPTIONS.find(o => o.value === settings.outputDetail)?.label || 'Standard'}</span>`;
|
|
1071
|
+
html += '<span class="agentation-settings-panel__cycle-dots">';
|
|
1072
|
+
OUTPUT_DETAIL_OPTIONS.forEach((opt) => {
|
|
1073
|
+
html += `<span class="agentation-settings-panel__dot ${settings.outputDetail === opt.value ? 'agentation-settings-panel__dot--active' : ''}"></span>`;
|
|
1074
|
+
});
|
|
1075
|
+
html += '</span>';
|
|
1076
|
+
html += '</button>';
|
|
1077
|
+
html += '</div>';
|
|
1078
|
+
html += '</div>';
|
|
1079
|
+
|
|
1080
|
+
// Marker Colour section
|
|
1081
|
+
html += '<div class="agentation-settings-panel__section">';
|
|
1082
|
+
html += `<div class="agentation-settings-panel__label agentation-settings-panel__label--marker ${isDark ? '' : 'agentation-settings-panel__label--light'}">Marker Colour</div>`;
|
|
1083
|
+
html += '<div class="agentation-settings-panel__colors">';
|
|
1084
|
+
COLOR_OPTIONS.forEach((c) => {
|
|
1085
|
+
const isSelected = settings.annotationColor === c.value;
|
|
1086
|
+
html += `<div class="agentation-settings-panel__color-ring ${isSelected ? 'agentation-settings-panel__color-ring--selected' : ''}" style="border-color: ${isSelected ? c.value : 'transparent'}" data-action="set-color" data-color="${c.value}" title="${c.label}">`;
|
|
1087
|
+
html += `<div class="agentation-settings-panel__color-dot" style="background-color: ${c.value}"></div>`;
|
|
1088
|
+
html += '</div>';
|
|
1089
|
+
});
|
|
1090
|
+
html += '</div>';
|
|
1091
|
+
html += '</div>';
|
|
1092
|
+
|
|
1093
|
+
// Toggle switches section
|
|
1094
|
+
html += '<div class="agentation-settings-panel__section">';
|
|
1095
|
+
|
|
1096
|
+
// Clear after output toggle
|
|
1097
|
+
html += '<label class="agentation-settings-panel__toggle-row">';
|
|
1098
|
+
html += `<input type="checkbox" class="agentation-settings-panel__checkbox" data-setting="autoClearAfterCopy" ${settings.autoClearAfterCopy ? 'checked' : ''}>`;
|
|
1099
|
+
html += `<span class="agentation-settings-panel__custom-checkbox ${settings.autoClearAfterCopy ? 'agentation-settings-panel__custom-checkbox--checked' : ''}">`;
|
|
1100
|
+
if (settings.autoClearAfterCopy) {
|
|
1101
|
+
html += Icons.checkSmall(14);
|
|
1102
|
+
}
|
|
1103
|
+
html += '</span>';
|
|
1104
|
+
html += `<span class="agentation-settings-panel__toggle-label ${isDark ? '' : 'agentation-settings-panel__toggle-label--light'}">Clear after output</span>`;
|
|
1105
|
+
html += '</label>';
|
|
1106
|
+
|
|
1107
|
+
// Block interactions toggle
|
|
1108
|
+
html += '<label class="agentation-settings-panel__toggle-row">';
|
|
1109
|
+
html += `<input type="checkbox" class="agentation-settings-panel__checkbox" data-setting="blockInteractions" ${settings.blockInteractions ? 'checked' : ''}>`;
|
|
1110
|
+
html += `<span class="agentation-settings-panel__custom-checkbox ${settings.blockInteractions ? 'agentation-settings-panel__custom-checkbox--checked' : ''}">`;
|
|
1111
|
+
if (settings.blockInteractions) {
|
|
1112
|
+
html += Icons.checkSmall(14);
|
|
1113
|
+
}
|
|
1114
|
+
html += '</span>';
|
|
1115
|
+
html += `<span class="agentation-settings-panel__toggle-label ${isDark ? '' : 'agentation-settings-panel__toggle-label--light'}">Block page interactions</span>`;
|
|
1116
|
+
html += '</label>';
|
|
1117
|
+
|
|
1118
|
+
html += '</div>';
|
|
1119
|
+
|
|
1120
|
+
html += '</div>'; // settings panel
|
|
1121
|
+
|
|
1122
|
+
return html;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
updateSettingsPanel() {
|
|
1126
|
+
// Update settings panel in place without rebuilding entire toolbar
|
|
1127
|
+
const state = this.store.getState();
|
|
1128
|
+
const settings = state.settings;
|
|
1129
|
+
const isDark = state.isDarkMode;
|
|
1130
|
+
const color = settings.annotationColor;
|
|
1131
|
+
|
|
1132
|
+
// Update output detail text and dots
|
|
1133
|
+
const cycleBtn = this.toolbarEl.querySelector('[data-action="cycle-detail"]');
|
|
1134
|
+
if (cycleBtn) {
|
|
1135
|
+
const textEl = cycleBtn.querySelector('.agentation-settings-panel__cycle-text');
|
|
1136
|
+
if (textEl) {
|
|
1137
|
+
textEl.textContent = OUTPUT_DETAIL_OPTIONS.find(o => o.value === settings.outputDetail)?.label || 'Standard';
|
|
1138
|
+
}
|
|
1139
|
+
const dots = cycleBtn.querySelectorAll('.agentation-settings-panel__dot');
|
|
1140
|
+
dots.forEach((dot, i) => {
|
|
1141
|
+
dot.classList.toggle('agentation-settings-panel__dot--active', OUTPUT_DETAIL_OPTIONS[i].value === settings.outputDetail);
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Update color selection
|
|
1146
|
+
const colorRings = this.toolbarEl.querySelectorAll('[data-action="set-color"]');
|
|
1147
|
+
colorRings.forEach(ring => {
|
|
1148
|
+
const isSelected = ring.dataset.color === settings.annotationColor;
|
|
1149
|
+
ring.classList.toggle('agentation-settings-panel__color-ring--selected', isSelected);
|
|
1150
|
+
ring.style.borderColor = isSelected ? ring.dataset.color : 'transparent';
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// Update checkboxes
|
|
1154
|
+
const checkboxes = this.toolbarEl.querySelectorAll('[data-setting]');
|
|
1155
|
+
checkboxes.forEach(checkbox => {
|
|
1156
|
+
const setting = checkbox.dataset.setting;
|
|
1157
|
+
const isChecked = settings[setting];
|
|
1158
|
+
checkbox.checked = isChecked;
|
|
1159
|
+
const customCheckbox = checkbox.nextElementSibling;
|
|
1160
|
+
if (customCheckbox) {
|
|
1161
|
+
customCheckbox.classList.toggle('agentation-settings-panel__custom-checkbox--checked', isChecked);
|
|
1162
|
+
customCheckbox.innerHTML = isChecked ? Icons.checkSmall(14) : '';
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
// Update brand slash color
|
|
1167
|
+
const slash = this.toolbarEl.querySelector('.agentation-settings-panel__slash');
|
|
1168
|
+
if (slash) {
|
|
1169
|
+
slash.style.color = color;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
bindEvents() {
|
|
1174
|
+
// Toolbar clicks (delegated)
|
|
1175
|
+
this.toolbarEl.addEventListener('click', (e) => {
|
|
1176
|
+
const actionEl = e.target.closest('[data-action]');
|
|
1177
|
+
if (!actionEl) return;
|
|
1178
|
+
|
|
1179
|
+
e.stopPropagation();
|
|
1180
|
+
const action = actionEl.dataset.action;
|
|
1181
|
+
|
|
1182
|
+
switch (action) {
|
|
1183
|
+
case 'activate':
|
|
1184
|
+
this.activate();
|
|
1185
|
+
break;
|
|
1186
|
+
case 'deactivate':
|
|
1187
|
+
this.deactivate();
|
|
1188
|
+
break;
|
|
1189
|
+
case 'freeze':
|
|
1190
|
+
this.toggleFreeze();
|
|
1191
|
+
break;
|
|
1192
|
+
case 'toggle-markers':
|
|
1193
|
+
this.toggleMarkers();
|
|
1194
|
+
break;
|
|
1195
|
+
case 'copy':
|
|
1196
|
+
this.copyToClipboard();
|
|
1197
|
+
break;
|
|
1198
|
+
case 'clear':
|
|
1199
|
+
this.clearAnnotations();
|
|
1200
|
+
break;
|
|
1201
|
+
case 'settings':
|
|
1202
|
+
this.toggleSettings();
|
|
1203
|
+
break;
|
|
1204
|
+
case 'theme':
|
|
1205
|
+
this.toggleTheme();
|
|
1206
|
+
break;
|
|
1207
|
+
case 'cycle-detail':
|
|
1208
|
+
this.cycleDetailLevel();
|
|
1209
|
+
break;
|
|
1210
|
+
case 'set-color':
|
|
1211
|
+
this.setColor(actionEl.dataset.color);
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// Settings checkbox changes
|
|
1217
|
+
this.toolbarEl.addEventListener('change', (e) => {
|
|
1218
|
+
const checkbox = e.target.closest('[data-setting]');
|
|
1219
|
+
if (checkbox) {
|
|
1220
|
+
const setting = checkbox.dataset.setting;
|
|
1221
|
+
const state = this.store.getState();
|
|
1222
|
+
const newSettings = { ...state.settings, [setting]: checkbox.checked };
|
|
1223
|
+
this.store.setState({ settings: newSettings });
|
|
1224
|
+
saveSettings(newSettings);
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// Document-level mouse events for element selection and drag-select
|
|
1229
|
+
document.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
1230
|
+
document.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
1231
|
+
document.addEventListener('mouseup', (e) => this.handleMouseUp(e));
|
|
1232
|
+
|
|
1233
|
+
// Click handler for single element selection (capture phase to intercept before element handlers)
|
|
1234
|
+
document.addEventListener('click', (e) => this.handleClick(e), true);
|
|
1235
|
+
|
|
1236
|
+
// Global escape key
|
|
1237
|
+
document.addEventListener('keydown', (e) => {
|
|
1238
|
+
if (e.key === 'Escape') {
|
|
1239
|
+
const state = this.store.getState();
|
|
1240
|
+
if (state.pendingAnnotation) {
|
|
1241
|
+
this.store.setState({ pendingAnnotation: null });
|
|
1242
|
+
if (this.popup) {
|
|
1243
|
+
this.popup.destroy();
|
|
1244
|
+
this.popup = null;
|
|
1245
|
+
}
|
|
1246
|
+
} else if (state.isActive) {
|
|
1247
|
+
this.deactivate();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
// Click outside to close settings
|
|
1253
|
+
document.addEventListener('click', (e) => {
|
|
1254
|
+
const state = this.store.getState();
|
|
1255
|
+
if (state.showSettings) {
|
|
1256
|
+
const settingsPanel = this.toolbarEl.querySelector('.agentation-settings-panel');
|
|
1257
|
+
const settingsBtn = this.toolbarEl.querySelector('[data-action="settings"]');
|
|
1258
|
+
if (settingsPanel && !settingsPanel.contains(e.target) && !settingsBtn?.contains(e.target)) {
|
|
1259
|
+
this.closeSettings();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
// Scroll tracking
|
|
1265
|
+
window.addEventListener('scroll', () => {
|
|
1266
|
+
this.store.setState({ scrollY: window.scrollY });
|
|
1267
|
+
this.updateMarkerPositions();
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
window.addEventListener('resize', () => {
|
|
1271
|
+
this.updateMarkerPositions();
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
onStateChange(state, prevState) {
|
|
1276
|
+
// Only do full toolbar rebuild when necessary (not for settings-only changes)
|
|
1277
|
+
const needsFullRebuild = !prevState ||
|
|
1278
|
+
prevState.isActive !== state.isActive ||
|
|
1279
|
+
prevState.isDarkMode !== state.isDarkMode ||
|
|
1280
|
+
prevState.annotations.length !== state.annotations.length ||
|
|
1281
|
+
prevState.showMarkers !== state.showMarkers ||
|
|
1282
|
+
prevState.copied !== state.copied ||
|
|
1283
|
+
prevState.cleared !== state.cleared ||
|
|
1284
|
+
prevState.isFrozen !== state.isFrozen ||
|
|
1285
|
+
prevState.showSettings !== state.showSettings ||
|
|
1286
|
+
prevState.showSettingsVisible !== state.showSettingsVisible;
|
|
1287
|
+
|
|
1288
|
+
if (needsFullRebuild) {
|
|
1289
|
+
this.updateToolbar();
|
|
1290
|
+
} else if (state.showSettingsVisible) {
|
|
1291
|
+
// For settings-only changes, just update the settings panel in place
|
|
1292
|
+
this.updateSettingsPanel();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
this.renderMarkers();
|
|
1296
|
+
|
|
1297
|
+
// Hide hover highlight when not active or when there's a pending annotation
|
|
1298
|
+
if (!state.isActive || state.pendingAnnotation) {
|
|
1299
|
+
this.hideHoverHighlight();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Hide drag selection when not dragging
|
|
1303
|
+
if (!state.isDragging && this.dragSelectionEl) {
|
|
1304
|
+
this.dragSelectionEl.style.display = 'none';
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
activate() {
|
|
1309
|
+
this.store.setState({ isActive: true });
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
deactivate() {
|
|
1313
|
+
this.store.setState({ isActive: false, showSettings: false, showSettingsVisible: false });
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
toggleFreeze() {
|
|
1317
|
+
const state = this.store.getState();
|
|
1318
|
+
const newFrozen = !state.isFrozen;
|
|
1319
|
+
|
|
1320
|
+
if (newFrozen) {
|
|
1321
|
+
// Pause all animations
|
|
1322
|
+
document.querySelectorAll('*').forEach(el => {
|
|
1323
|
+
const style = window.getComputedStyle(el);
|
|
1324
|
+
if (style.animationName !== 'none' || style.transitionDuration !== '0s') {
|
|
1325
|
+
el.style.animationPlayState = 'paused';
|
|
1326
|
+
this.frozenAnimations.push(el);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
} else {
|
|
1330
|
+
// Resume all animations
|
|
1331
|
+
this.frozenAnimations.forEach(el => {
|
|
1332
|
+
el.style.animationPlayState = '';
|
|
1333
|
+
});
|
|
1334
|
+
this.frozenAnimations = [];
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
this.store.setState({ isFrozen: newFrozen });
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
toggleMarkers() {
|
|
1341
|
+
const state = this.store.getState();
|
|
1342
|
+
this.store.setState({ showMarkers: !state.showMarkers });
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
toggleSettings() {
|
|
1346
|
+
const state = this.store.getState();
|
|
1347
|
+
if (state.showSettings) {
|
|
1348
|
+
this.closeSettings();
|
|
1349
|
+
} else {
|
|
1350
|
+
this.store.setState({ showSettings: true, showSettingsVisible: true });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
closeSettings() {
|
|
1355
|
+
this.store.setState({ showSettings: false });
|
|
1356
|
+
// Delay hiding for exit animation
|
|
1357
|
+
setTimeout(() => {
|
|
1358
|
+
this.store.setState({ showSettingsVisible: false });
|
|
1359
|
+
}, 200);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
toggleTheme() {
|
|
1363
|
+
const state = this.store.getState();
|
|
1364
|
+
const newTheme = !state.isDarkMode;
|
|
1365
|
+
this.store.setState({ isDarkMode: newTheme });
|
|
1366
|
+
saveTheme(newTheme);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
cycleDetailLevel() {
|
|
1370
|
+
const state = this.store.getState();
|
|
1371
|
+
const currentIndex = OUTPUT_DETAIL_OPTIONS.findIndex(o => o.value === state.settings.outputDetail);
|
|
1372
|
+
const nextIndex = (currentIndex + 1) % OUTPUT_DETAIL_OPTIONS.length;
|
|
1373
|
+
const newSettings = { ...state.settings, outputDetail: OUTPUT_DETAIL_OPTIONS[nextIndex].value };
|
|
1374
|
+
this.store.setState({ settings: newSettings });
|
|
1375
|
+
saveSettings(newSettings);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
setColor(color) {
|
|
1379
|
+
const state = this.store.getState();
|
|
1380
|
+
const newSettings = { ...state.settings, annotationColor: color };
|
|
1381
|
+
this.store.setState({ settings: newSettings });
|
|
1382
|
+
saveSettings(newSettings);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Click handler for single element selection (uses capture phase)
|
|
1386
|
+
handleClick(e) {
|
|
1387
|
+
const state = this.store.getState();
|
|
1388
|
+
if (!state.isActive) return;
|
|
1389
|
+
|
|
1390
|
+
// Skip if we just finished a drag
|
|
1391
|
+
if (this.justFinishedDrag) {
|
|
1392
|
+
this.justFinishedDrag = false;
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const target = e.target;
|
|
1397
|
+
|
|
1398
|
+
// Don't handle clicks on agentation elements
|
|
1399
|
+
if (target.closest('[data-agentation]')) return;
|
|
1400
|
+
|
|
1401
|
+
const isInteractive = target.closest('button, a, input, select, textarea, [role="button"], [onclick]');
|
|
1402
|
+
|
|
1403
|
+
// Block interactions on interactive elements when enabled
|
|
1404
|
+
if (state.settings.blockInteractions && isInteractive) {
|
|
1405
|
+
e.preventDefault();
|
|
1406
|
+
e.stopPropagation();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// If there's a pending annotation, shake the popup
|
|
1410
|
+
if (state.pendingAnnotation) {
|
|
1411
|
+
if (isInteractive && !state.settings.blockInteractions) {
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
e.preventDefault();
|
|
1415
|
+
if (this.popup) {
|
|
1416
|
+
this.popup.shake();
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// If editing, also shake
|
|
1422
|
+
if (state.editingAnnotation) {
|
|
1423
|
+
if (isInteractive && !state.settings.blockInteractions) {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
e.preventDefault();
|
|
1427
|
+
if (this.editPopup) {
|
|
1428
|
+
this.editPopup.shake();
|
|
1429
|
+
}
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
e.preventDefault();
|
|
1434
|
+
|
|
1435
|
+
// Get the element under the click
|
|
1436
|
+
const elementUnder = document.elementFromPoint(e.clientX, e.clientY);
|
|
1437
|
+
if (!elementUnder) return;
|
|
1438
|
+
|
|
1439
|
+
this.createAnnotation(elementUnder, e.clientX, e.clientY);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
handleMouseDown(e) {
|
|
1443
|
+
const state = this.store.getState();
|
|
1444
|
+
if (!state.isActive || state.pendingAnnotation) return;
|
|
1445
|
+
|
|
1446
|
+
// Don't start on agentation elements
|
|
1447
|
+
if (e.target.closest('[data-agentation]')) return;
|
|
1448
|
+
|
|
1449
|
+
// Don't start drag on text elements - allow native text selection
|
|
1450
|
+
// This matches the original React implementation
|
|
1451
|
+
const TEXT_TAGS = new Set([
|
|
1452
|
+
'P', 'SPAN', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'TD', 'TH',
|
|
1453
|
+
'LABEL', 'BLOCKQUOTE', 'FIGCAPTION', 'CAPTION', 'LEGEND', 'DT', 'DD',
|
|
1454
|
+
'PRE', 'CODE', 'EM', 'STRONG', 'B', 'I', 'U', 'S', 'A', 'TIME',
|
|
1455
|
+
'ADDRESS', 'CITE', 'Q', 'ABBR', 'DFN', 'MARK', 'SMALL', 'SUB', 'SUP',
|
|
1456
|
+
'INPUT', 'TEXTAREA', 'SELECT'
|
|
1457
|
+
]);
|
|
1458
|
+
|
|
1459
|
+
if (TEXT_TAGS.has(e.target.tagName) || e.target.isContentEditable) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
this.mouseDownPos = { x: e.clientX, y: e.clientY };
|
|
1464
|
+
this.dragStart = null;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
handleMouseMove(e) {
|
|
1468
|
+
const state = this.store.getState();
|
|
1469
|
+
if (!state.isActive || state.pendingAnnotation) return;
|
|
1470
|
+
|
|
1471
|
+
// If not in a potential drag, just do hover highlighting
|
|
1472
|
+
if (!this.mouseDownPos) {
|
|
1473
|
+
this.updateHoverHighlight(e);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Check if we've exceeded drag threshold
|
|
1478
|
+
const dx = e.clientX - this.mouseDownPos.x;
|
|
1479
|
+
const dy = e.clientY - this.mouseDownPos.y;
|
|
1480
|
+
const distance = dx * dx + dy * dy;
|
|
1481
|
+
const thresholdSq = this.DRAG_THRESHOLD * this.DRAG_THRESHOLD;
|
|
1482
|
+
|
|
1483
|
+
if (!state.isDragging && distance >= thresholdSq) {
|
|
1484
|
+
// Start dragging
|
|
1485
|
+
this.dragStart = { ...this.mouseDownPos };
|
|
1486
|
+
this.store.setState({ isDragging: true });
|
|
1487
|
+
this.hideHoverHighlight();
|
|
1488
|
+
|
|
1489
|
+
// Clear any existing text selection and prevent new selection during drag
|
|
1490
|
+
window.getSelection()?.removeAllRanges();
|
|
1491
|
+
document.body.style.userSelect = 'none';
|
|
1492
|
+
document.body.style.webkitUserSelect = 'none';
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if ((state.isDragging || distance >= thresholdSq) && this.dragStart) {
|
|
1496
|
+
// Update drag selection rectangle
|
|
1497
|
+
const left = Math.min(this.dragStart.x, e.clientX);
|
|
1498
|
+
const top = Math.min(this.dragStart.y, e.clientY);
|
|
1499
|
+
const width = Math.abs(e.clientX - this.dragStart.x);
|
|
1500
|
+
const height = Math.abs(e.clientY - this.dragStart.y);
|
|
1501
|
+
|
|
1502
|
+
this.dragSelectionEl.style.display = 'block';
|
|
1503
|
+
this.dragSelectionEl.style.transform = `translate(${left}px, ${top}px)`;
|
|
1504
|
+
this.dragSelectionEl.style.width = `${width}px`;
|
|
1505
|
+
this.dragSelectionEl.style.height = `${height}px`;
|
|
1506
|
+
|
|
1507
|
+
// Throttle element detection and highlighting
|
|
1508
|
+
const now = Date.now();
|
|
1509
|
+
if (now - this.lastElementUpdate < this.ELEMENT_UPDATE_THROTTLE) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
this.lastElementUpdate = now;
|
|
1513
|
+
|
|
1514
|
+
// Find and highlight elements during drag
|
|
1515
|
+
const right = left + width;
|
|
1516
|
+
const bottom = top + height;
|
|
1517
|
+
this.updateDragHighlights(left, top, right, bottom);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
updateDragHighlights(left, top, right, bottom) {
|
|
1522
|
+
// Find elements within the selection rectangle
|
|
1523
|
+
const matchingRects = [];
|
|
1524
|
+
const MEANINGFUL_TAGS = new Set([
|
|
1525
|
+
'BUTTON', 'A', 'INPUT', 'IMG', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
1526
|
+
'LI', 'LABEL', 'TD', 'TH', 'SECTION', 'ARTICLE', 'ASIDE', 'NAV'
|
|
1527
|
+
]);
|
|
1528
|
+
|
|
1529
|
+
// Sample points for element detection (corners, edges, center)
|
|
1530
|
+
const midX = (left + right) / 2;
|
|
1531
|
+
const midY = (top + bottom) / 2;
|
|
1532
|
+
const points = [
|
|
1533
|
+
[left, top], [right, top], [left, bottom], [right, bottom],
|
|
1534
|
+
[midX, midY], [midX, top], [midX, bottom], [left, midY], [right, midY]
|
|
1535
|
+
];
|
|
1536
|
+
|
|
1537
|
+
const candidateElements = new Set();
|
|
1538
|
+
|
|
1539
|
+
// Get elements at sample points
|
|
1540
|
+
for (const [x, y] of points) {
|
|
1541
|
+
const elements = document.elementsFromPoint(x, y);
|
|
1542
|
+
for (const el of elements) {
|
|
1543
|
+
if (el instanceof HTMLElement) candidateElements.add(el);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Also check nearby meaningful elements
|
|
1548
|
+
const nearbyElements = document.querySelectorAll(
|
|
1549
|
+
'button, a, input, img, p, h1, h2, h3, h4, h5, h6, li, label, td, th, div, span, section, article, aside, nav'
|
|
1550
|
+
);
|
|
1551
|
+
for (const el of nearbyElements) {
|
|
1552
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
1553
|
+
const rect = el.getBoundingClientRect();
|
|
1554
|
+
const centerX = rect.left + rect.width / 2;
|
|
1555
|
+
const centerY = rect.top + rect.height / 2;
|
|
1556
|
+
const centerInside = centerX >= left && centerX <= right && centerY >= top && centerY <= bottom;
|
|
1557
|
+
|
|
1558
|
+
const overlapX = Math.min(rect.right, right) - Math.max(rect.left, left);
|
|
1559
|
+
const overlapY = Math.min(rect.bottom, bottom) - Math.max(rect.top, top);
|
|
1560
|
+
const overlapArea = overlapX > 0 && overlapY > 0 ? overlapX * overlapY : 0;
|
|
1561
|
+
const elementArea = rect.width * rect.height;
|
|
1562
|
+
const overlapRatio = elementArea > 0 ? overlapArea / elementArea : 0;
|
|
1563
|
+
|
|
1564
|
+
if (centerInside || overlapRatio > 0.5) {
|
|
1565
|
+
candidateElements.add(el);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Filter to meaningful elements
|
|
1570
|
+
for (const el of candidateElements) {
|
|
1571
|
+
if (el.closest('[data-agentation]')) continue;
|
|
1572
|
+
|
|
1573
|
+
const rect = el.getBoundingClientRect();
|
|
1574
|
+
if (rect.width > window.innerWidth * 0.8 && rect.height > window.innerHeight * 0.5) continue;
|
|
1575
|
+
if (rect.width < 10 || rect.height < 10) continue;
|
|
1576
|
+
|
|
1577
|
+
// Check intersection with selection
|
|
1578
|
+
if (rect.left < right && rect.right > left && rect.top < bottom && rect.bottom > top) {
|
|
1579
|
+
const tagName = el.tagName;
|
|
1580
|
+
let shouldInclude = MEANINGFUL_TAGS.has(tagName);
|
|
1581
|
+
|
|
1582
|
+
// For divs and spans, only include if they have meaningful content
|
|
1583
|
+
if (!shouldInclude && (tagName === 'DIV' || tagName === 'SPAN')) {
|
|
1584
|
+
const hasText = el.textContent && el.textContent.trim().length > 0;
|
|
1585
|
+
const isInteractive = el.onclick !== null ||
|
|
1586
|
+
el.getAttribute('role') === 'button' ||
|
|
1587
|
+
el.getAttribute('role') === 'link';
|
|
1588
|
+
|
|
1589
|
+
if ((hasText || isInteractive) && !el.querySelector('p, h1, h2, h3, h4, h5, h6, button, a')) {
|
|
1590
|
+
shouldInclude = true;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (shouldInclude) {
|
|
1595
|
+
// Check if any existing match contains this element (filter children)
|
|
1596
|
+
let dominated = false;
|
|
1597
|
+
for (const existingRect of matchingRects) {
|
|
1598
|
+
if (existingRect.left <= rect.left && existingRect.right >= rect.right &&
|
|
1599
|
+
existingRect.top <= rect.top && existingRect.bottom >= rect.bottom) {
|
|
1600
|
+
dominated = true;
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (!dominated) {
|
|
1605
|
+
matchingRects.push(rect);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Update highlights container with matched elements
|
|
1612
|
+
const container = this.highlightsContainerEl;
|
|
1613
|
+
while (container.children.length > matchingRects.length) {
|
|
1614
|
+
container.removeChild(container.lastChild);
|
|
1615
|
+
}
|
|
1616
|
+
matchingRects.forEach((rect, i) => {
|
|
1617
|
+
let div = container.children[i];
|
|
1618
|
+
if (!div) {
|
|
1619
|
+
div = document.createElement('div');
|
|
1620
|
+
div.className = 'agentation-selected-element-highlight';
|
|
1621
|
+
container.appendChild(div);
|
|
1622
|
+
}
|
|
1623
|
+
div.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
|
1624
|
+
div.style.width = `${rect.width}px`;
|
|
1625
|
+
div.style.height = `${rect.height}px`;
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
handleMouseUp(e) {
|
|
1630
|
+
const state = this.store.getState();
|
|
1631
|
+
if (!state.isActive) return;
|
|
1632
|
+
|
|
1633
|
+
const wasDragging = state.isDragging;
|
|
1634
|
+
const dragStart = this.dragStart;
|
|
1635
|
+
|
|
1636
|
+
if (wasDragging && dragStart) {
|
|
1637
|
+
// Set flag to prevent click handler from firing
|
|
1638
|
+
this.justFinishedDrag = true;
|
|
1639
|
+
|
|
1640
|
+
// Finish drag selection - find all elements in the selection rectangle
|
|
1641
|
+
const left = Math.min(dragStart.x, e.clientX);
|
|
1642
|
+
const top = Math.min(dragStart.y, e.clientY);
|
|
1643
|
+
const right = Math.max(dragStart.x, e.clientX);
|
|
1644
|
+
const bottom = Math.max(dragStart.y, e.clientY);
|
|
1645
|
+
|
|
1646
|
+
// Only process if drag area is meaningful size
|
|
1647
|
+
if ((right - left) > 20 && (bottom - top) > 20) {
|
|
1648
|
+
const selectedElements = this.getElementsInRect(left, top, right, bottom);
|
|
1649
|
+
if (selectedElements.length > 0) {
|
|
1650
|
+
this.createMultiSelectAnnotation(selectedElements, left, top, right, bottom);
|
|
1651
|
+
} else {
|
|
1652
|
+
// No elements, but allow annotation on empty area
|
|
1653
|
+
this.createAreaAnnotation(left, top, right, bottom, e.clientX, e.clientY);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
this.dragSelectionEl.style.display = 'none';
|
|
1658
|
+
this.store.setState({ isDragging: false });
|
|
1659
|
+
|
|
1660
|
+
// Clear highlights container
|
|
1661
|
+
this.highlightsContainerEl.innerHTML = '';
|
|
1662
|
+
|
|
1663
|
+
// Restore text selection ability
|
|
1664
|
+
document.body.style.userSelect = '';
|
|
1665
|
+
document.body.style.webkitUserSelect = '';
|
|
1666
|
+
} else if (wasDragging) {
|
|
1667
|
+
// Was dragging but no dragStart - just set the flag
|
|
1668
|
+
this.justFinishedDrag = true;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Note: Single clicks are now handled by handleClick, not here
|
|
1672
|
+
this.mouseDownPos = null;
|
|
1673
|
+
this.dragStart = null;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
createAreaAnnotation(left, top, right, bottom, clientX, clientY) {
|
|
1677
|
+
const x = (clientX / window.innerWidth) * 100;
|
|
1678
|
+
const y = clientY + window.scrollY;
|
|
1679
|
+
const width = right - left;
|
|
1680
|
+
const height = bottom - top;
|
|
1681
|
+
|
|
1682
|
+
const pendingAnnotation = {
|
|
1683
|
+
x,
|
|
1684
|
+
y,
|
|
1685
|
+
clientY,
|
|
1686
|
+
element: 'Area selection',
|
|
1687
|
+
elementPath: `region at (${Math.round(left)}, ${Math.round(top)})`,
|
|
1688
|
+
boundingBox: {
|
|
1689
|
+
x: left,
|
|
1690
|
+
y: top + window.scrollY,
|
|
1691
|
+
width,
|
|
1692
|
+
height
|
|
1693
|
+
},
|
|
1694
|
+
isMultiSelect: true
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
this.store.setState({ pendingAnnotation, hoverInfo: null });
|
|
1698
|
+
this.showPopup(pendingAnnotation);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
updateHoverHighlight(e) {
|
|
1702
|
+
const state = this.store.getState();
|
|
1703
|
+
if (state.pendingAnnotation || state.isDragging) return;
|
|
1704
|
+
|
|
1705
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
1706
|
+
|
|
1707
|
+
if (!target || target.closest('[data-agentation]') || target === document.body || target === document.documentElement) {
|
|
1708
|
+
this.hideHoverHighlight();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const rect = target.getBoundingClientRect();
|
|
1713
|
+
const color = state.settings.annotationColor;
|
|
1714
|
+
|
|
1715
|
+
this.hoverHighlightEl.style.display = 'block';
|
|
1716
|
+
this.hoverHighlightEl.style.pointerEvents = 'none';
|
|
1717
|
+
this.hoverHighlightEl.style.left = rect.left + 'px';
|
|
1718
|
+
this.hoverHighlightEl.style.top = rect.top + 'px';
|
|
1719
|
+
this.hoverHighlightEl.style.width = rect.width + 'px';
|
|
1720
|
+
this.hoverHighlightEl.style.height = rect.height + 'px';
|
|
1721
|
+
this.hoverHighlightEl.style.borderColor = hexToRgba(color, 0.5);
|
|
1722
|
+
this.hoverHighlightEl.style.backgroundColor = hexToRgba(color, 0.04);
|
|
1723
|
+
|
|
1724
|
+
if (state.hoverInfo?.element !== target) {
|
|
1725
|
+
this.store.setState({ hoverInfo: { element: target, rect } });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
hideHoverHighlight() {
|
|
1730
|
+
this.hoverHighlightEl.style.display = 'none';
|
|
1731
|
+
// Only update state if hoverInfo is not already null (prevents infinite loop)
|
|
1732
|
+
if (this.store.getState().hoverInfo !== null) {
|
|
1733
|
+
this.store.setState({ hoverInfo: null });
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
getElementsInRect(left, top, right, bottom) {
|
|
1738
|
+
const elements = [];
|
|
1739
|
+
const allElements = document.querySelectorAll('*');
|
|
1740
|
+
|
|
1741
|
+
for (const el of allElements) {
|
|
1742
|
+
// Skip agentation elements and non-visible elements
|
|
1743
|
+
if (el.closest('[data-agentation]')) continue;
|
|
1744
|
+
if (el === document.body || el === document.documentElement) continue;
|
|
1745
|
+
|
|
1746
|
+
const rect = el.getBoundingClientRect();
|
|
1747
|
+
|
|
1748
|
+
// Skip elements with no size
|
|
1749
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
1750
|
+
|
|
1751
|
+
// Check if element intersects with selection rectangle
|
|
1752
|
+
const intersects = !(rect.right < left || rect.left > right || rect.bottom < top || rect.top > bottom);
|
|
1753
|
+
|
|
1754
|
+
if (intersects) {
|
|
1755
|
+
// Only add leaf elements or meaningful containers
|
|
1756
|
+
const isLeaf = el.children.length === 0;
|
|
1757
|
+
const hasText = el.textContent?.trim().length > 0;
|
|
1758
|
+
const isInteractive = ['button', 'a', 'input', 'select', 'textarea'].includes(el.tagName.toLowerCase());
|
|
1759
|
+
|
|
1760
|
+
if (isLeaf || isInteractive || (hasText && rect.width < 500 && rect.height < 200)) {
|
|
1761
|
+
elements.push(el);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Dedupe - remove elements that are children of other selected elements
|
|
1767
|
+
return elements.filter(el => !elements.some(other => other !== el && other.contains(el)));
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
createMultiSelectAnnotation(elements, left, top, right, bottom) {
|
|
1771
|
+
const state = this.store.getState();
|
|
1772
|
+
const centerX = (left + right) / 2;
|
|
1773
|
+
const centerY = (top + bottom) / 2;
|
|
1774
|
+
|
|
1775
|
+
// Build description of selected elements
|
|
1776
|
+
const elementNames = elements.slice(0, 5).map(el => identifyElement(el).name);
|
|
1777
|
+
const elementDescription = elements.length > 5
|
|
1778
|
+
? `${elementNames.join(', ')} + ${elements.length - 5} more`
|
|
1779
|
+
: elementNames.join(', ');
|
|
1780
|
+
|
|
1781
|
+
const pending = {
|
|
1782
|
+
id: generateId(),
|
|
1783
|
+
element: `${elements.length} elements: ${elementDescription}`,
|
|
1784
|
+
elementPath: elements[0] ? getElementPath(elements[0]) : '',
|
|
1785
|
+
x: (centerX / window.innerWidth) * 100,
|
|
1786
|
+
y: centerY + window.scrollY,
|
|
1787
|
+
timestamp: Date.now(),
|
|
1788
|
+
comment: '',
|
|
1789
|
+
selectedText: '',
|
|
1790
|
+
boundingBox: { x: left, y: top, width: right - left, height: bottom - top },
|
|
1791
|
+
cssClasses: '',
|
|
1792
|
+
nearbyText: '',
|
|
1793
|
+
fullPath: elements[0] ? getFullElementPath(elements[0]) : '',
|
|
1794
|
+
computedStyles: null,
|
|
1795
|
+
accessibility: '',
|
|
1796
|
+
nearbyElements: '',
|
|
1797
|
+
isMultiSelect: true,
|
|
1798
|
+
selectedElements: elements.length
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
this.store.setState({ pendingAnnotation: pending });
|
|
1802
|
+
|
|
1803
|
+
let popupY = centerY + 20;
|
|
1804
|
+
if (popupY + 200 > window.innerHeight) {
|
|
1805
|
+
popupY = centerY - 220;
|
|
1806
|
+
}
|
|
1807
|
+
if (popupY < 10) popupY = 10;
|
|
1808
|
+
|
|
1809
|
+
let popupX = centerX;
|
|
1810
|
+
const popupHalfWidth = 140;
|
|
1811
|
+
if (popupX - popupHalfWidth < 10) {
|
|
1812
|
+
popupX = popupHalfWidth + 10;
|
|
1813
|
+
} else if (popupX + popupHalfWidth > window.innerWidth - 10) {
|
|
1814
|
+
popupX = window.innerWidth - popupHalfWidth - 10;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
this.popup = new AnnotationPopup({
|
|
1818
|
+
element: pending.element,
|
|
1819
|
+
selectedText: '',
|
|
1820
|
+
accentColor: GREEN,
|
|
1821
|
+
lightMode: !state.isDarkMode,
|
|
1822
|
+
computedStyles: null,
|
|
1823
|
+
onSubmit: (comment) => {
|
|
1824
|
+
this.addAnnotation(comment);
|
|
1825
|
+
this.popup = null;
|
|
1826
|
+
},
|
|
1827
|
+
onCancel: () => {
|
|
1828
|
+
this.store.setState({ pendingAnnotation: null });
|
|
1829
|
+
this.popup = null;
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
this.popup.setPosition(popupX, popupY);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
createAnnotation(target, clientX, clientY) {
|
|
1837
|
+
const state = this.store.getState();
|
|
1838
|
+
const rect = target.getBoundingClientRect();
|
|
1839
|
+
const { name, path } = identifyElement(target);
|
|
1840
|
+
const selectedText = getSelectedText();
|
|
1841
|
+
|
|
1842
|
+
const centerX = rect.left + rect.width / 2;
|
|
1843
|
+
const centerY = rect.top + rect.height / 2;
|
|
1844
|
+
|
|
1845
|
+
const pending = {
|
|
1846
|
+
id: generateId(),
|
|
1847
|
+
element: name,
|
|
1848
|
+
elementPath: path,
|
|
1849
|
+
x: (clientX / window.innerWidth) * 100,
|
|
1850
|
+
y: clientY + window.scrollY,
|
|
1851
|
+
timestamp: Date.now(),
|
|
1852
|
+
comment: '',
|
|
1853
|
+
selectedText: selectedText,
|
|
1854
|
+
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
1855
|
+
cssClasses: getElementClasses(target),
|
|
1856
|
+
nearbyText: getNearbyText(target),
|
|
1857
|
+
fullPath: getFullElementPath(target),
|
|
1858
|
+
computedStyles: state.settings.outputDetail === 'forensic' ? getForensicComputedStyles(target) : getDetailedComputedStyles(target),
|
|
1859
|
+
accessibility: getAccessibilityInfo(target),
|
|
1860
|
+
nearbyElements: getNearbyElements(target)
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
this.store.setState({ pendingAnnotation: pending });
|
|
1864
|
+
|
|
1865
|
+
// Calculate popup position - ensure it stays within viewport
|
|
1866
|
+
let popupX = centerX;
|
|
1867
|
+
let popupY = centerY + 20;
|
|
1868
|
+
|
|
1869
|
+
// Keep X within bounds (popup is 280px wide, centered)
|
|
1870
|
+
const popupHalfWidth = 140;
|
|
1871
|
+
if (popupX - popupHalfWidth < 10) {
|
|
1872
|
+
popupX = popupHalfWidth + 10;
|
|
1873
|
+
} else if (popupX + popupHalfWidth > window.innerWidth - 10) {
|
|
1874
|
+
popupX = window.innerWidth - popupHalfWidth - 10;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Keep Y within bounds
|
|
1878
|
+
if (popupY + 200 > window.innerHeight) {
|
|
1879
|
+
popupY = centerY - 220;
|
|
1880
|
+
}
|
|
1881
|
+
if (popupY < 10) {
|
|
1882
|
+
popupY = 10;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
this.popup = new AnnotationPopup({
|
|
1886
|
+
element: pending.element,
|
|
1887
|
+
selectedText: selectedText,
|
|
1888
|
+
accentColor: state.settings.annotationColor,
|
|
1889
|
+
lightMode: !state.isDarkMode,
|
|
1890
|
+
computedStyles: pending.computedStyles,
|
|
1891
|
+
onSubmit: (comment) => {
|
|
1892
|
+
this.addAnnotation(comment);
|
|
1893
|
+
this.popup = null;
|
|
1894
|
+
},
|
|
1895
|
+
onCancel: () => {
|
|
1896
|
+
this.store.setState({ pendingAnnotation: null });
|
|
1897
|
+
this.popup = null;
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
this.popup.setPosition(popupX, popupY);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
addAnnotation(comment) {
|
|
1905
|
+
const state = this.store.getState();
|
|
1906
|
+
const pending = state.pendingAnnotation;
|
|
1907
|
+
if (!pending) return;
|
|
1908
|
+
|
|
1909
|
+
const annotation = { ...pending, comment };
|
|
1910
|
+
const annotations = [...state.annotations, annotation];
|
|
1911
|
+
|
|
1912
|
+
this.store.setState({
|
|
1913
|
+
annotations,
|
|
1914
|
+
pendingAnnotation: null
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
saveAnnotations(this.pathname, annotations);
|
|
1918
|
+
|
|
1919
|
+
if (this.options.onAnnotationAdd) {
|
|
1920
|
+
this.options.onAnnotationAdd(annotation);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
renderMarkers() {
|
|
1925
|
+
const state = this.store.getState();
|
|
1926
|
+
if (!state.showMarkers || !state.isActive) {
|
|
1927
|
+
this.markersLayerEl.innerHTML = '';
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
const color = state.settings.annotationColor;
|
|
1932
|
+
let html = '';
|
|
1933
|
+
|
|
1934
|
+
state.annotations.forEach((annotation, index) => {
|
|
1935
|
+
const left = annotation.x;
|
|
1936
|
+
const top = annotation.y - state.scrollY;
|
|
1937
|
+
|
|
1938
|
+
html += `<div class="agentation-marker" data-id="${annotation.id}" style="left: ${left}%; top: ${top}px; background-color: ${color}">`;
|
|
1939
|
+
html += `<span class="agentation-marker__number">${index + 1}</span>`;
|
|
1940
|
+
html += '</div>';
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
this.markersLayerEl.innerHTML = html;
|
|
1944
|
+
|
|
1945
|
+
// Bind marker events
|
|
1946
|
+
this.markersLayerEl.querySelectorAll('.agentation-marker').forEach(marker => {
|
|
1947
|
+
marker.addEventListener('click', (e) => {
|
|
1948
|
+
e.stopPropagation();
|
|
1949
|
+
const id = marker.dataset.id;
|
|
1950
|
+
if (e.shiftKey) {
|
|
1951
|
+
this.deleteAnnotation(id);
|
|
1952
|
+
} else {
|
|
1953
|
+
this.editAnnotation(id);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
marker.addEventListener('contextmenu', (e) => {
|
|
1958
|
+
e.preventDefault();
|
|
1959
|
+
const id = marker.dataset.id;
|
|
1960
|
+
this.editAnnotation(id);
|
|
1961
|
+
});
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
updateMarkerPositions() {
|
|
1966
|
+
const state = this.store.getState();
|
|
1967
|
+
const markers = this.markersLayerEl.querySelectorAll('.agentation-marker');
|
|
1968
|
+
|
|
1969
|
+
markers.forEach(marker => {
|
|
1970
|
+
const id = marker.dataset.id;
|
|
1971
|
+
const annotation = state.annotations.find(a => a.id === id);
|
|
1972
|
+
if (annotation) {
|
|
1973
|
+
marker.style.top = (annotation.y - state.scrollY) + 'px';
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
editAnnotation(id) {
|
|
1979
|
+
const state = this.store.getState();
|
|
1980
|
+
const annotation = state.annotations.find(a => a.id === id);
|
|
1981
|
+
if (!annotation) return;
|
|
1982
|
+
|
|
1983
|
+
const markerEl = this.markersLayerEl.querySelector(`[data-id="${id}"]`);
|
|
1984
|
+
if (!markerEl) return;
|
|
1985
|
+
|
|
1986
|
+
const rect = markerEl.getBoundingClientRect();
|
|
1987
|
+
|
|
1988
|
+
this.editPopup = new AnnotationPopup({
|
|
1989
|
+
element: annotation.element,
|
|
1990
|
+
initialValue: annotation.comment,
|
|
1991
|
+
submitLabel: 'Save',
|
|
1992
|
+
accentColor: state.settings.annotationColor,
|
|
1993
|
+
lightMode: !state.isDarkMode,
|
|
1994
|
+
onSubmit: (comment) => {
|
|
1995
|
+
const annotations = state.annotations.map(a =>
|
|
1996
|
+
a.id === id ? { ...a, comment } : a
|
|
1997
|
+
);
|
|
1998
|
+
this.store.setState({ annotations });
|
|
1999
|
+
saveAnnotations(this.pathname, annotations);
|
|
2000
|
+
this.editPopup = null;
|
|
2001
|
+
|
|
2002
|
+
if (this.options.onAnnotationUpdate) {
|
|
2003
|
+
this.options.onAnnotationUpdate({ ...annotation, comment });
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
onCancel: () => {
|
|
2007
|
+
this.editPopup = null;
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
this.editPopup.setPosition(rect.left, rect.bottom + 10);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
deleteAnnotation(id) {
|
|
2015
|
+
const state = this.store.getState();
|
|
2016
|
+
const deleted = state.annotations.find(a => a.id === id);
|
|
2017
|
+
const annotations = state.annotations.filter(a => a.id !== id);
|
|
2018
|
+
this.store.setState({ annotations });
|
|
2019
|
+
saveAnnotations(this.pathname, annotations);
|
|
2020
|
+
|
|
2021
|
+
if (this.options.onAnnotationDelete && deleted) {
|
|
2022
|
+
this.options.onAnnotationDelete(deleted);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
copyToClipboard() {
|
|
2027
|
+
const state = this.store.getState();
|
|
2028
|
+
const output = generateOutput(state.annotations, this.pathname, state.settings.outputDetail);
|
|
2029
|
+
|
|
2030
|
+
if (this.options.copyToClipboard && navigator.clipboard) {
|
|
2031
|
+
navigator.clipboard.writeText(output).then(() => {
|
|
2032
|
+
this.store.setState({ copied: true });
|
|
2033
|
+
setTimeout(() => this.store.setState({ copied: false }), 2000);
|
|
2034
|
+
|
|
2035
|
+
// Auto-clear if setting enabled
|
|
2036
|
+
if (state.settings.autoClearAfterCopy) {
|
|
2037
|
+
setTimeout(() => this.clearAnnotations(), 500);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
if (this.options.onCopy) {
|
|
2043
|
+
this.options.onCopy(output);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
clearAnnotations() {
|
|
2048
|
+
const state = this.store.getState();
|
|
2049
|
+
if (state.annotations.length === 0) return;
|
|
2050
|
+
|
|
2051
|
+
const cleared = [...state.annotations];
|
|
2052
|
+
this.store.setState({ annotations: [], cleared: true });
|
|
2053
|
+
saveAnnotations(this.pathname, []);
|
|
2054
|
+
|
|
2055
|
+
setTimeout(() => this.store.setState({ cleared: false }), 2000);
|
|
2056
|
+
|
|
2057
|
+
if (this.options.onAnnotationsClear) {
|
|
2058
|
+
this.options.onAnnotationsClear(cleared);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
destroy() {
|
|
2063
|
+
if (this.toolbarEl) this.toolbarEl.remove();
|
|
2064
|
+
if (this.overlayEl) this.overlayEl.remove();
|
|
2065
|
+
if (this.markersLayerEl) this.markersLayerEl.remove();
|
|
2066
|
+
if (this.hoverHighlightEl) this.hoverHighlightEl.remove();
|
|
2067
|
+
if (this.dragSelectionEl) this.dragSelectionEl.remove();
|
|
2068
|
+
if (this.popup) this.popup.destroy();
|
|
2069
|
+
if (this.editPopup) this.editPopup.destroy();
|
|
2070
|
+
|
|
2071
|
+
// Restore any frozen animations
|
|
2072
|
+
this.frozenAnimations.forEach(el => {
|
|
2073
|
+
el.style.animationPlayState = '';
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// =============================================================================
|
|
2079
|
+
// Auto-initialization
|
|
2080
|
+
// =============================================================================
|
|
2081
|
+
|
|
2082
|
+
function init() {
|
|
2083
|
+
const root = document.getElementById('agentation-root');
|
|
2084
|
+
if (root) {
|
|
2085
|
+
let options = {};
|
|
2086
|
+
try {
|
|
2087
|
+
options = JSON.parse(root.dataset.options || '{}');
|
|
2088
|
+
} catch (e) {}
|
|
2089
|
+
|
|
2090
|
+
window.agentation = new AgentationToolbar(root, options);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// Auto-init on DOMContentLoaded or immediately if DOM is ready
|
|
2095
|
+
if (document.readyState === 'loading') {
|
|
2096
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
2097
|
+
} else {
|
|
2098
|
+
init();
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// =============================================================================
|
|
2102
|
+
// Exports
|
|
2103
|
+
// =============================================================================
|
|
2104
|
+
|
|
2105
|
+
export {
|
|
2106
|
+
AgentationToolbar,
|
|
2107
|
+
AgentationStore,
|
|
2108
|
+
AnnotationPopup,
|
|
2109
|
+
loadAnnotations,
|
|
2110
|
+
saveAnnotations,
|
|
2111
|
+
getStorageKey,
|
|
2112
|
+
identifyElement,
|
|
2113
|
+
getElementPath,
|
|
2114
|
+
getNearbyText,
|
|
2115
|
+
getElementClasses,
|
|
2116
|
+
getDetailedComputedStyles,
|
|
2117
|
+
getForensicComputedStyles,
|
|
2118
|
+
getAccessibilityInfo,
|
|
2119
|
+
getFullElementPath,
|
|
2120
|
+
getNearbyElements,
|
|
2121
|
+
isElementFixed,
|
|
2122
|
+
hexToRgba,
|
|
2123
|
+
generateId,
|
|
2124
|
+
generateOutput,
|
|
2125
|
+
DEFAULT_SETTINGS,
|
|
2126
|
+
OUTPUT_DETAIL_OPTIONS,
|
|
2127
|
+
COLOR_OPTIONS,
|
|
2128
|
+
Icons
|
|
2129
|
+
};
|