@qontinui/ui-bridge 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.d.mts +312 -155
- package/dist/ai/index.d.ts +312 -155
- package/dist/ai/index.js +2363 -67
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/index.mjs +2328 -68
- package/dist/ai/index.mjs.map +1 -1
- package/dist/annotations/index.d.mts +218 -0
- package/dist/annotations/index.d.ts +218 -0
- package/dist/annotations/index.js +246 -0
- package/dist/annotations/index.js.map +1 -0
- package/dist/annotations/index.mjs +241 -0
- package/dist/annotations/index.mjs.map +1 -0
- package/dist/assertions-BSR3afVr.d.ts +161 -0
- package/dist/assertions-CTw1hfOx.d.mts +161 -0
- package/dist/babel-plugin/index.js +23 -34
- package/dist/babel-plugin/index.js.map +1 -1
- package/dist/babel-plugin/index.mjs +23 -34
- package/dist/babel-plugin/index.mjs.map +1 -1
- package/dist/browser-capture-Bms60T6f.d.mts +47 -0
- package/dist/browser-capture-CsTU29mb.d.ts +47 -0
- package/dist/control/index.d.mts +26 -7
- package/dist/control/index.d.ts +26 -7
- package/dist/control/index.js +276 -48
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +276 -48
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.d.mts +5 -3
- package/dist/debug/index.d.ts +5 -3
- package/dist/debug/index.js +925 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +924 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.mts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +4720 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4656 -174
- package/dist/index.mjs.map +1 -1
- package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
- package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
- package/dist/native/control/index.js +2 -7
- package/dist/native/control/index.js.map +1 -1
- package/dist/native/control/index.mjs +2 -7
- package/dist/native/control/index.mjs.map +1 -1
- package/dist/native/core/index.js.map +1 -1
- package/dist/native/core/index.mjs.map +1 -1
- package/dist/native/debug/index.js +23 -66
- package/dist/native/debug/index.js.map +1 -1
- package/dist/native/debug/index.mjs +23 -66
- package/dist/native/debug/index.mjs.map +1 -1
- package/dist/native/index.js +89 -131
- package/dist/native/index.js.map +1 -1
- package/dist/native/index.mjs +89 -131
- package/dist/native/index.mjs.map +1 -1
- package/dist/native/react/index.js +28 -52
- package/dist/native/react/index.js.map +1 -1
- package/dist/native/react/index.mjs +28 -52
- package/dist/native/react/index.mjs.map +1 -1
- package/dist/native/server/index.js +38 -13
- package/dist/native/server/index.js.map +1 -1
- package/dist/native/server/index.mjs +38 -13
- package/dist/native/server/index.mjs.map +1 -1
- package/dist/react/index.d.mts +107 -8
- package/dist/react/index.d.ts +107 -8
- package/dist/react/index.js +2194 -84
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +2194 -85
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
- package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
- package/dist/render-log/index.d.mts +1 -1
- package/dist/render-log/index.d.ts +1 -1
- package/dist/server/express.d.mts +5 -4
- package/dist/server/express.d.ts +5 -4
- package/dist/server/express.js +104 -2
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.mjs +104 -2
- package/dist/server/express.mjs.map +1 -1
- package/dist/server/handlers.d.mts +36 -5
- package/dist/server/handlers.d.ts +36 -5
- package/dist/server/handlers.js +3129 -224
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/handlers.mjs +3129 -224
- package/dist/server/handlers.mjs.map +1 -1
- package/dist/server/index.d.mts +7 -5
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.js +3215 -183
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +3215 -183
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/nextjs.d.mts +6 -4
- package/dist/server/nextjs.d.ts +6 -4
- package/dist/server/nextjs.js +106 -3
- package/dist/server/nextjs.js.map +1 -1
- package/dist/server/nextjs.mjs +106 -3
- package/dist/server/nextjs.mjs.map +1 -1
- package/dist/server/standalone.d.mts +6 -5
- package/dist/server/standalone.d.ts +6 -5
- package/dist/server/standalone.js +131 -5
- package/dist/server/standalone.js.map +1 -1
- package/dist/server/standalone.mjs +131 -5
- package/dist/server/standalone.mjs.map +1 -1
- package/dist/specs/index.d.mts +365 -0
- package/dist/specs/index.d.ts +365 -0
- package/dist/specs/index.js +2809 -0
- package/dist/specs/index.js.map +1 -0
- package/dist/specs/index.mjs +2786 -0
- package/dist/specs/index.mjs.map +1 -0
- package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
- package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
- package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
- package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
- package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
- package/dist/types-CebMQj76.d.ts +1275 -0
- package/dist/types-D_ypYl3T.d.mts +1275 -0
- package/dist/types-UBtp7R0u.d.mts +132 -0
- package/dist/types-UBtp7R0u.d.ts +132 -0
- package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
- package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
- package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
- package/package.json +18 -2
- package/dist/types-B5Q0GVo0.d.mts +0 -646
- package/dist/types-DfPqwU-i.d.ts +0 -646
package/dist/server/handlers.mjs
CHANGED
|
@@ -421,17 +421,72 @@ function generateDescription(input) {
|
|
|
421
421
|
}
|
|
422
422
|
parts.push(`"${name}"`);
|
|
423
423
|
}
|
|
424
|
-
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
424
|
+
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
425
|
+
input.elementType || "element"
|
|
426
|
+
];
|
|
425
427
|
parts.push(typeWords[0]);
|
|
426
428
|
if (input.inputType && input.inputType !== "text") {
|
|
427
429
|
parts.push(`(${input.inputType})`);
|
|
428
430
|
}
|
|
429
431
|
return parts.join(" ");
|
|
430
432
|
}
|
|
433
|
+
var CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
434
|
+
"heading",
|
|
435
|
+
"paragraph",
|
|
436
|
+
"list-item",
|
|
437
|
+
"table-cell",
|
|
438
|
+
"table-header",
|
|
439
|
+
"label",
|
|
440
|
+
"caption",
|
|
441
|
+
"blockquote",
|
|
442
|
+
"code-block",
|
|
443
|
+
"badge",
|
|
444
|
+
"status-message",
|
|
445
|
+
"metric-value",
|
|
446
|
+
"description-text",
|
|
447
|
+
"nav-text",
|
|
448
|
+
"content-generic"
|
|
449
|
+
]);
|
|
431
450
|
function generatePurpose(input) {
|
|
432
451
|
const text = (input.textContent || input.ariaLabel || input.title || "").toLowerCase();
|
|
433
452
|
const type = input.elementType?.toLowerCase() || "";
|
|
434
453
|
const inputType = input.inputType?.toLowerCase() || "";
|
|
454
|
+
if (CONTENT_TYPES.has(type)) {
|
|
455
|
+
switch (type) {
|
|
456
|
+
case "heading":
|
|
457
|
+
return "Section heading";
|
|
458
|
+
case "paragraph":
|
|
459
|
+
return "Body text content";
|
|
460
|
+
case "list-item":
|
|
461
|
+
return "List item";
|
|
462
|
+
case "table-cell":
|
|
463
|
+
return "Table data cell";
|
|
464
|
+
case "table-header":
|
|
465
|
+
return "Table column header";
|
|
466
|
+
case "label":
|
|
467
|
+
return "Field label or definition term";
|
|
468
|
+
case "caption":
|
|
469
|
+
return "Figure or table caption";
|
|
470
|
+
case "blockquote":
|
|
471
|
+
return "Quoted content";
|
|
472
|
+
case "code-block":
|
|
473
|
+
return "Code or preformatted text";
|
|
474
|
+
case "badge":
|
|
475
|
+
return "Status badge or tag";
|
|
476
|
+
case "status-message":
|
|
477
|
+
return "Dynamic status indicator";
|
|
478
|
+
case "metric-value":
|
|
479
|
+
return "Metric or statistic value";
|
|
480
|
+
case "description-text":
|
|
481
|
+
return "Description or definition";
|
|
482
|
+
case "nav-text":
|
|
483
|
+
return "Navigation label";
|
|
484
|
+
case "content-generic":
|
|
485
|
+
return "Text content";
|
|
486
|
+
default:
|
|
487
|
+
return "Static content";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
435
490
|
if (type === "button" || inputType === "submit") {
|
|
436
491
|
if (text.match(/submit|send|save|confirm|ok|done|finish|apply/)) {
|
|
437
492
|
return "Submits the form";
|
|
@@ -496,6 +551,10 @@ function generateSuggestedActions(input) {
|
|
|
496
551
|
const inputType = input.inputType?.toLowerCase() || "";
|
|
497
552
|
const text = (input.textContent || input.ariaLabel || "").toLowerCase();
|
|
498
553
|
const actions = [];
|
|
554
|
+
if (CONTENT_TYPES.has(type)) {
|
|
555
|
+
actions.push("read text content", "verify text matches expected");
|
|
556
|
+
return actions;
|
|
557
|
+
}
|
|
499
558
|
switch (type) {
|
|
500
559
|
case "button":
|
|
501
560
|
actions.push(`click "${text || "this button"}"`);
|
|
@@ -543,6 +602,241 @@ function areSynonyms(word1, word2) {
|
|
|
543
602
|
return synonyms1.includes(w2) || synonyms2.includes(w1);
|
|
544
603
|
}
|
|
545
604
|
|
|
605
|
+
// src/annotations/types.ts
|
|
606
|
+
var ANNOTATION_CONFIG_VERSION = "1.0.0";
|
|
607
|
+
|
|
608
|
+
// src/annotations/store.ts
|
|
609
|
+
var AnnotationStore = class {
|
|
610
|
+
constructor() {
|
|
611
|
+
this.store = /* @__PURE__ */ new Map();
|
|
612
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Get an annotation by element ID.
|
|
616
|
+
*/
|
|
617
|
+
get(elementId) {
|
|
618
|
+
return this.store.get(elementId);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Get all annotations as a record.
|
|
622
|
+
*/
|
|
623
|
+
getAll() {
|
|
624
|
+
const result = {};
|
|
625
|
+
for (const [id, annotation] of this.store) {
|
|
626
|
+
result[id] = annotation;
|
|
627
|
+
}
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Set an annotation for an element. Auto-sets `updatedAt`.
|
|
632
|
+
*/
|
|
633
|
+
set(elementId, annotation) {
|
|
634
|
+
const updated = {
|
|
635
|
+
...annotation,
|
|
636
|
+
updatedAt: Date.now()
|
|
637
|
+
};
|
|
638
|
+
this.store.set(elementId, updated);
|
|
639
|
+
this.emit({
|
|
640
|
+
type: "annotation:set",
|
|
641
|
+
elementId,
|
|
642
|
+
annotation: updated,
|
|
643
|
+
timestamp: Date.now()
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Delete an annotation by element ID.
|
|
648
|
+
*
|
|
649
|
+
* @returns true if the annotation existed and was deleted
|
|
650
|
+
*/
|
|
651
|
+
delete(elementId) {
|
|
652
|
+
const existed = this.store.delete(elementId);
|
|
653
|
+
if (existed) {
|
|
654
|
+
this.emit({
|
|
655
|
+
type: "annotation:deleted",
|
|
656
|
+
elementId,
|
|
657
|
+
timestamp: Date.now()
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return existed;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Check if an annotation exists for an element.
|
|
664
|
+
*/
|
|
665
|
+
has(elementId) {
|
|
666
|
+
return this.store.has(elementId);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Get the number of stored annotations.
|
|
670
|
+
*/
|
|
671
|
+
get count() {
|
|
672
|
+
return this.store.size;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Clear all annotations.
|
|
676
|
+
*/
|
|
677
|
+
clear() {
|
|
678
|
+
this.store.clear();
|
|
679
|
+
this.emit({
|
|
680
|
+
type: "annotation:cleared",
|
|
681
|
+
timestamp: Date.now()
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Import annotations from a config object.
|
|
686
|
+
*
|
|
687
|
+
* Merges with existing annotations (new values overwrite per element ID).
|
|
688
|
+
*
|
|
689
|
+
* @returns Number of annotations imported
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* const config: AnnotationConfig = {
|
|
694
|
+
* version: '1.0.0',
|
|
695
|
+
* annotations: {
|
|
696
|
+
* 'btn-1': { description: 'Submit button', tags: ['form'] },
|
|
697
|
+
* 'input-1': { description: 'Name field' },
|
|
698
|
+
* },
|
|
699
|
+
* };
|
|
700
|
+
* const count = store.importConfig(config); // 2
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
importConfig(config) {
|
|
704
|
+
let count = 0;
|
|
705
|
+
for (const [id, annotation] of Object.entries(config.annotations)) {
|
|
706
|
+
this.store.set(id, {
|
|
707
|
+
...annotation,
|
|
708
|
+
updatedAt: annotation.updatedAt ?? Date.now()
|
|
709
|
+
});
|
|
710
|
+
count++;
|
|
711
|
+
}
|
|
712
|
+
this.emit({
|
|
713
|
+
type: "annotation:imported",
|
|
714
|
+
count,
|
|
715
|
+
timestamp: Date.now()
|
|
716
|
+
});
|
|
717
|
+
return count;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Export all annotations as a config object.
|
|
721
|
+
*
|
|
722
|
+
* The returned object can be serialized to JSON and saved to a file,
|
|
723
|
+
* then later re-imported with {@link importConfig}.
|
|
724
|
+
*
|
|
725
|
+
* @param metadata - Optional metadata to include (appName, description, etc.)
|
|
726
|
+
* @returns AnnotationConfig with all current annotations
|
|
727
|
+
*
|
|
728
|
+
* @example
|
|
729
|
+
* ```ts
|
|
730
|
+
* const config = store.exportConfig({ appName: 'MyApp' });
|
|
731
|
+
* // config.version === '1.0.0'
|
|
732
|
+
* // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
|
|
733
|
+
* // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
|
|
734
|
+
*
|
|
735
|
+
* // Save to file
|
|
736
|
+
* fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
|
|
737
|
+
* ```
|
|
738
|
+
*/
|
|
739
|
+
exportConfig(metadata) {
|
|
740
|
+
return {
|
|
741
|
+
version: ANNOTATION_CONFIG_VERSION,
|
|
742
|
+
annotations: this.getAll(),
|
|
743
|
+
metadata: {
|
|
744
|
+
...metadata,
|
|
745
|
+
exportedAt: Date.now()
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Compute annotation coverage against a set of known element IDs.
|
|
751
|
+
*
|
|
752
|
+
* Compares the store's annotations against the provided list of element IDs
|
|
753
|
+
* to determine what percentage of elements have been annotated.
|
|
754
|
+
*
|
|
755
|
+
* @param allElementIds - Array of all known element IDs in the UI
|
|
756
|
+
* @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
761
|
+
* store.set('input-1', { description: 'Name' });
|
|
762
|
+
*
|
|
763
|
+
* const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
|
|
764
|
+
* // coverage.totalElements === 4
|
|
765
|
+
* // coverage.annotatedElements === 2
|
|
766
|
+
* // coverage.coveragePercent === 50
|
|
767
|
+
* // coverage.annotatedIds === ['btn-1', 'input-1']
|
|
768
|
+
* // coverage.unannotatedIds === ['input-2', 'link-1']
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
getCoverage(allElementIds) {
|
|
772
|
+
const annotatedIds = [];
|
|
773
|
+
const unannotatedIds = [];
|
|
774
|
+
for (const id of allElementIds) {
|
|
775
|
+
if (this.store.has(id)) {
|
|
776
|
+
annotatedIds.push(id);
|
|
777
|
+
} else {
|
|
778
|
+
unannotatedIds.push(id);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const total = allElementIds.length;
|
|
782
|
+
return {
|
|
783
|
+
totalElements: total,
|
|
784
|
+
annotatedElements: annotatedIds.length,
|
|
785
|
+
coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
|
|
786
|
+
annotatedIds,
|
|
787
|
+
unannotatedIds,
|
|
788
|
+
timestamp: Date.now()
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Subscribe to annotation events.
|
|
793
|
+
*
|
|
794
|
+
* The listener is called whenever annotations are set, deleted, imported,
|
|
795
|
+
* or cleared. Returns an unsubscribe function to stop listening.
|
|
796
|
+
*
|
|
797
|
+
* @param listener - Callback function receiving {@link AnnotationEvent} objects
|
|
798
|
+
* @returns Unsubscribe function - call it to remove the listener
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* const unsubscribe = store.on((event) => {
|
|
803
|
+
* if (event.type === 'annotation:set') {
|
|
804
|
+
* console.log(`Element ${event.elementId} annotated:`, event.annotation);
|
|
805
|
+
* }
|
|
806
|
+
* });
|
|
807
|
+
*
|
|
808
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
809
|
+
* // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
|
|
810
|
+
*
|
|
811
|
+
* unsubscribe(); // Stop listening
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
814
|
+
on(listener) {
|
|
815
|
+
this.listeners.add(listener);
|
|
816
|
+
return () => {
|
|
817
|
+
this.listeners.delete(listener);
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Emit an event to all listeners.
|
|
822
|
+
*/
|
|
823
|
+
emit(event) {
|
|
824
|
+
for (const listener of this.listeners) {
|
|
825
|
+
try {
|
|
826
|
+
listener(event);
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
var globalStore = null;
|
|
833
|
+
function getGlobalAnnotationStore() {
|
|
834
|
+
if (!globalStore) {
|
|
835
|
+
globalStore = new AnnotationStore();
|
|
836
|
+
}
|
|
837
|
+
return globalStore;
|
|
838
|
+
}
|
|
839
|
+
|
|
546
840
|
// src/ai/search-engine.ts
|
|
547
841
|
var DEFAULT_SEARCH_CONFIG = {
|
|
548
842
|
fuzzyThreshold: 0.7,
|
|
@@ -585,17 +879,40 @@ var SearchEngine = class {
|
|
|
585
879
|
if ("getState" in element && typeof element.getState === "function") {
|
|
586
880
|
state = getState ? getState(element) : element.getState();
|
|
587
881
|
textContent = state.textContent || void 0;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
882
|
+
try {
|
|
883
|
+
tagName = element.element.tagName.toLowerCase();
|
|
884
|
+
} catch {
|
|
885
|
+
tagName = element.type || "unknown";
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
role = element.element.getAttribute("role") || void 0;
|
|
889
|
+
ariaLabel = element.element.getAttribute("aria-label") || void 0;
|
|
890
|
+
placeholder = element.element.getAttribute("placeholder") || void 0;
|
|
891
|
+
title = element.element.getAttribute("title") || void 0;
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
if (!ariaLabel && element.label) {
|
|
895
|
+
ariaLabel = element.label;
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
if (element.element.id) {
|
|
899
|
+
const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
|
|
900
|
+
labelText = labelEl?.textContent?.trim() || void 0;
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
if (!labelText && element.label) {
|
|
905
|
+
labelText = element.label;
|
|
906
|
+
}
|
|
907
|
+
if (!textContent && element.label) {
|
|
908
|
+
textContent = element.label;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
|
|
912
|
+
value = element.element.value || void 0;
|
|
913
|
+
}
|
|
914
|
+
} catch {
|
|
915
|
+
value = state.value || void 0;
|
|
599
916
|
}
|
|
600
917
|
} else {
|
|
601
918
|
const discovered = element;
|
|
@@ -604,8 +921,11 @@ var SearchEngine = class {
|
|
|
604
921
|
tagName = discovered.tagName;
|
|
605
922
|
role = discovered.role || void 0;
|
|
606
923
|
ariaLabel = discovered.accessibleName || void 0;
|
|
924
|
+
if (!labelText && element.label) {
|
|
925
|
+
labelText = element.label;
|
|
926
|
+
}
|
|
607
927
|
}
|
|
608
|
-
|
|
928
|
+
let aliases = generateAliases({
|
|
609
929
|
textContent,
|
|
610
930
|
ariaLabel,
|
|
611
931
|
placeholder,
|
|
@@ -615,7 +935,14 @@ var SearchEngine = class {
|
|
|
615
935
|
labelText,
|
|
616
936
|
value
|
|
617
937
|
});
|
|
618
|
-
|
|
938
|
+
if ("aliases" in element && Array.isArray(element.aliases) && element.aliases.length > 0) {
|
|
939
|
+
const aliasSet = /* @__PURE__ */ new Set([
|
|
940
|
+
...aliases,
|
|
941
|
+
...element.aliases.map((a) => a.toLowerCase())
|
|
942
|
+
]);
|
|
943
|
+
aliases = [...aliasSet];
|
|
944
|
+
}
|
|
945
|
+
let description = generateDescription({
|
|
619
946
|
textContent,
|
|
620
947
|
ariaLabel,
|
|
621
948
|
placeholder,
|
|
@@ -624,6 +951,22 @@ var SearchEngine = class {
|
|
|
624
951
|
id: element.id,
|
|
625
952
|
labelText
|
|
626
953
|
});
|
|
954
|
+
if (!description && "description" in element && element.description) {
|
|
955
|
+
description = element.description;
|
|
956
|
+
}
|
|
957
|
+
const annotation = getGlobalAnnotationStore().get(element.id);
|
|
958
|
+
if (annotation) {
|
|
959
|
+
if (annotation.description) {
|
|
960
|
+
description = annotation.description;
|
|
961
|
+
}
|
|
962
|
+
if (annotation.tags && annotation.tags.length > 0) {
|
|
963
|
+
const tagSet = /* @__PURE__ */ new Set([...aliases, ...annotation.tags.map((t) => t.toLowerCase())]);
|
|
964
|
+
aliases = [...tagSet];
|
|
965
|
+
}
|
|
966
|
+
if (annotation.notes) {
|
|
967
|
+
aliases.push(annotation.notes.toLowerCase());
|
|
968
|
+
}
|
|
969
|
+
}
|
|
627
970
|
return {
|
|
628
971
|
id: element.id,
|
|
629
972
|
element,
|
|
@@ -726,7 +1069,12 @@ var SearchEngine = class {
|
|
|
726
1069
|
threshold: criteria.fuzzyThreshold ?? this.config.fuzzyThreshold
|
|
727
1070
|
};
|
|
728
1071
|
if (criteria.text) {
|
|
729
|
-
const textScore = this.scoreTextMatch(
|
|
1072
|
+
const textScore = this.scoreTextMatch(
|
|
1073
|
+
searchable,
|
|
1074
|
+
criteria.text,
|
|
1075
|
+
criteria.fuzzy !== false,
|
|
1076
|
+
fuzzyConfig.threshold
|
|
1077
|
+
);
|
|
730
1078
|
scores.text = textScore.score;
|
|
731
1079
|
if (textScore.score > 0) {
|
|
732
1080
|
matchReasons.push(...textScore.reasons);
|
|
@@ -734,8 +1082,37 @@ var SearchEngine = class {
|
|
|
734
1082
|
weightedScore += textScore.score * this.config.textWeight;
|
|
735
1083
|
totalWeight += this.config.textWeight;
|
|
736
1084
|
}
|
|
1085
|
+
if (criteria.textContent && !criteria.text) {
|
|
1086
|
+
const alternatives = criteria.textContent.includes("|") ? criteria.textContent.split("|").map((s) => s.trim()).filter(Boolean) : [criteria.textContent];
|
|
1087
|
+
let bestScore = 0;
|
|
1088
|
+
let bestReasons = [];
|
|
1089
|
+
for (const alt of alternatives) {
|
|
1090
|
+
const exactScore = this.scoreTextMatch(
|
|
1091
|
+
searchable,
|
|
1092
|
+
alt,
|
|
1093
|
+
criteria.fuzzy !== false,
|
|
1094
|
+
fuzzyConfig.threshold
|
|
1095
|
+
);
|
|
1096
|
+
const containsScore = this.scoreContainsMatch(searchable, alt, criteria.fuzzy !== false);
|
|
1097
|
+
const altBest = Math.max(exactScore.score, containsScore.score);
|
|
1098
|
+
if (altBest > bestScore) {
|
|
1099
|
+
bestScore = altBest;
|
|
1100
|
+
bestReasons = exactScore.score >= containsScore.score ? exactScore.reasons : containsScore.reasons;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
scores.text = bestScore;
|
|
1104
|
+
if (bestScore > 0) {
|
|
1105
|
+
matchReasons.push(...bestReasons);
|
|
1106
|
+
}
|
|
1107
|
+
weightedScore += bestScore * this.config.textWeight;
|
|
1108
|
+
totalWeight += this.config.textWeight;
|
|
1109
|
+
}
|
|
737
1110
|
if (criteria.textContains) {
|
|
738
|
-
const containsScore = this.scoreContainsMatch(
|
|
1111
|
+
const containsScore = this.scoreContainsMatch(
|
|
1112
|
+
searchable,
|
|
1113
|
+
criteria.textContains,
|
|
1114
|
+
criteria.fuzzy !== false
|
|
1115
|
+
);
|
|
739
1116
|
scores.text = Math.max(scores.text || 0, containsScore.score);
|
|
740
1117
|
if (containsScore.score > 0 && containsScore.reasons.length > 0) {
|
|
741
1118
|
matchReasons.push(...containsScore.reasons);
|
|
@@ -784,7 +1161,11 @@ var SearchEngine = class {
|
|
|
784
1161
|
totalWeight += this.config.spatialWeight;
|
|
785
1162
|
}
|
|
786
1163
|
if (criteria.placeholder && searchable.placeholder) {
|
|
787
|
-
const placeholderResult = fuzzyMatch(
|
|
1164
|
+
const placeholderResult = fuzzyMatch(
|
|
1165
|
+
searchable.placeholder,
|
|
1166
|
+
criteria.placeholder,
|
|
1167
|
+
fuzzyConfig
|
|
1168
|
+
);
|
|
788
1169
|
if (placeholderResult.isMatch) {
|
|
789
1170
|
matchReasons.push(`placeholder matches`);
|
|
790
1171
|
weightedScore += placeholderResult.similarity * this.config.textWeight;
|
|
@@ -829,11 +1210,9 @@ var SearchEngine = class {
|
|
|
829
1210
|
scoreTextMatch(searchable, text, fuzzy, threshold) {
|
|
830
1211
|
const reasons = [];
|
|
831
1212
|
let maxScore = 0;
|
|
832
|
-
const textsToMatch = [
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
searchable.value
|
|
836
|
-
].filter(Boolean);
|
|
1213
|
+
const textsToMatch = [searchable.textContent, searchable.labelText, searchable.value].filter(
|
|
1214
|
+
Boolean
|
|
1215
|
+
);
|
|
837
1216
|
for (const targetText of textsToMatch) {
|
|
838
1217
|
if (targetText.toLowerCase() === text.toLowerCase()) {
|
|
839
1218
|
maxScore = Math.max(maxScore, 1);
|
|
@@ -929,7 +1308,9 @@ var SearchEngine = class {
|
|
|
929
1308
|
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
930
1309
|
};
|
|
931
1310
|
const inferredRoles = tagRoleMap[normalizedRole] || [];
|
|
932
|
-
if (inferredRoles.some(
|
|
1311
|
+
if (inferredRoles.some(
|
|
1312
|
+
(r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole
|
|
1313
|
+
)) {
|
|
933
1314
|
return { score: 0.8, reasons: [`inferred role: ${role}`] };
|
|
934
1315
|
}
|
|
935
1316
|
return { score: 0, reasons };
|
|
@@ -1116,11 +1497,14 @@ function generatePageSummary(elements, pageContext, config = {}) {
|
|
|
1116
1497
|
if (finalConfig.includeElementCounts) {
|
|
1117
1498
|
const counts = countElementTypes(elements);
|
|
1118
1499
|
const countParts = [];
|
|
1119
|
-
if (counts.button > 0)
|
|
1500
|
+
if (counts.button > 0)
|
|
1501
|
+
countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
|
|
1120
1502
|
if (counts.input > 0) countParts.push(`${counts.input} input${counts.input > 1 ? "s" : ""}`);
|
|
1121
1503
|
if (counts.link > 0) countParts.push(`${counts.link} link${counts.link > 1 ? "s" : ""}`);
|
|
1122
|
-
if (counts.select > 0)
|
|
1123
|
-
|
|
1504
|
+
if (counts.select > 0)
|
|
1505
|
+
countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
|
|
1506
|
+
if (counts.checkbox > 0)
|
|
1507
|
+
countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
|
|
1124
1508
|
if (countParts.length > 0) {
|
|
1125
1509
|
lines.push(`Contains: ${countParts.join(", ")}`);
|
|
1126
1510
|
}
|
|
@@ -1163,7 +1547,9 @@ function generateFormSummary(form, verbosity) {
|
|
|
1163
1547
|
if (verbosity === "brief") {
|
|
1164
1548
|
const fieldCount = form.fields.length;
|
|
1165
1549
|
const filledCount = form.fields.filter((f) => f.value).length;
|
|
1166
|
-
lines.push(
|
|
1550
|
+
lines.push(
|
|
1551
|
+
` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`
|
|
1552
|
+
);
|
|
1167
1553
|
} else {
|
|
1168
1554
|
for (const field of form.fields) {
|
|
1169
1555
|
let fieldLine = ` - ${field.label || field.id}`;
|
|
@@ -1198,7 +1584,9 @@ function generateDiffSummary(appeared, disappeared, modified) {
|
|
|
1198
1584
|
if (modified.length > 0) {
|
|
1199
1585
|
lines.push("Changed:");
|
|
1200
1586
|
for (const mod of modified.slice(0, 5)) {
|
|
1201
|
-
lines.push(
|
|
1587
|
+
lines.push(
|
|
1588
|
+
` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`
|
|
1589
|
+
);
|
|
1202
1590
|
}
|
|
1203
1591
|
if (modified.length > 5) {
|
|
1204
1592
|
lines.push(` ... and ${modified.length - 5} more changes`);
|
|
@@ -1629,7 +2017,9 @@ function parseNLInstruction(instruction) {
|
|
|
1629
2017
|
}
|
|
1630
2018
|
}
|
|
1631
2019
|
if (pattern.action === "assert") {
|
|
1632
|
-
const assertMatch = trimmed.match(
|
|
2020
|
+
const assertMatch = trimmed.match(
|
|
2021
|
+
/(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i
|
|
2022
|
+
);
|
|
1633
2023
|
if (assertMatch) {
|
|
1634
2024
|
parsed.assertionType = ASSERTION_TYPE_MAP[assertMatch[1].toLowerCase()];
|
|
1635
2025
|
}
|
|
@@ -2390,7 +2780,9 @@ var NLActionExecutor = class {
|
|
|
2390
2780
|
switch (errorCode) {
|
|
2391
2781
|
case "PARSE_ERROR":
|
|
2392
2782
|
suggestions.push('Try using a simpler phrase like "click Submit button"');
|
|
2393
|
-
suggestions.push(
|
|
2783
|
+
suggestions.push(
|
|
2784
|
+
'Ensure the instruction follows patterns like "click X" or "type Y into X"'
|
|
2785
|
+
);
|
|
2394
2786
|
break;
|
|
2395
2787
|
case "ELEMENT_NOT_FOUND":
|
|
2396
2788
|
if (alternatives.length > 0) {
|
|
@@ -2458,9 +2850,15 @@ var AssertionExecutor = class {
|
|
|
2458
2850
|
async assert(request) {
|
|
2459
2851
|
const startTime = performance.now();
|
|
2460
2852
|
const timeout = request.timeout ?? this.config.defaultTimeout;
|
|
2461
|
-
const
|
|
2853
|
+
const searchResult = this.findElementDetailed(request.target, request.fuzzy !== false);
|
|
2854
|
+
const element = searchResult?.element ?? null;
|
|
2855
|
+
const searchDetails = searchResult ? {
|
|
2856
|
+
confidence: searchResult.confidence,
|
|
2857
|
+
matchReasons: searchResult.matchReasons,
|
|
2858
|
+
candidateCount: this.elements.length
|
|
2859
|
+
} : void 0;
|
|
2462
2860
|
if (!element && request.type !== "notExists") {
|
|
2463
|
-
|
|
2861
|
+
const result2 = this.createResult(
|
|
2464
2862
|
false,
|
|
2465
2863
|
typeof request.target === "string" ? request.target : JSON.stringify(request.target),
|
|
2466
2864
|
"element not found",
|
|
@@ -2470,8 +2868,16 @@ var AssertionExecutor = class {
|
|
|
2470
2868
|
this.config.includeSuggestions ? "Check if the element exists and is properly labeled" : void 0,
|
|
2471
2869
|
startTime
|
|
2472
2870
|
);
|
|
2871
|
+
if (searchDetails) {
|
|
2872
|
+
result2.searchDetails = searchDetails;
|
|
2873
|
+
}
|
|
2874
|
+
return result2;
|
|
2473
2875
|
}
|
|
2474
|
-
|
|
2876
|
+
const result = await this.executeAssertion(request, element, timeout, startTime);
|
|
2877
|
+
if (searchDetails) {
|
|
2878
|
+
result.searchDetails = searchDetails;
|
|
2879
|
+
}
|
|
2880
|
+
return result;
|
|
2475
2881
|
}
|
|
2476
2882
|
/**
|
|
2477
2883
|
* Execute multiple assertions
|
|
@@ -2576,16 +2982,26 @@ var AssertionExecutor = class {
|
|
|
2576
2982
|
return this.assert({ target, type: "count", expected: expectedCount, timeout });
|
|
2577
2983
|
}
|
|
2578
2984
|
/**
|
|
2579
|
-
* Find element by target
|
|
2985
|
+
* Find element by target with full search metadata.
|
|
2986
|
+
* Returns the SearchResult (including confidence, matchReasons, scores)
|
|
2987
|
+
* or null if no match above the fuzzy threshold.
|
|
2580
2988
|
*/
|
|
2581
|
-
|
|
2989
|
+
findElementDetailed(target, fuzzy = true) {
|
|
2582
2990
|
const criteria = typeof target === "string" ? { text: target, fuzzy } : { ...target, fuzzy };
|
|
2583
|
-
const searchResult = this.searchEngine.findBest(criteria);
|
|
2991
|
+
const searchResult = this.searchEngine.findBest(criteria, this.elements);
|
|
2584
2992
|
if (searchResult && searchResult.confidence >= this.config.fuzzyThreshold) {
|
|
2585
|
-
return searchResult
|
|
2993
|
+
return searchResult;
|
|
2586
2994
|
}
|
|
2587
2995
|
return null;
|
|
2588
2996
|
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Find element by target (string or criteria).
|
|
2999
|
+
* Public for use by condition evaluation in SpecExecutor.
|
|
3000
|
+
*/
|
|
3001
|
+
async findElement(target, fuzzy = true) {
|
|
3002
|
+
const result = this.findElementDetailed(target, fuzzy);
|
|
3003
|
+
return result?.element ?? null;
|
|
3004
|
+
}
|
|
2589
3005
|
/**
|
|
2590
3006
|
* Execute the actual assertion
|
|
2591
3007
|
*/
|
|
@@ -2594,19 +3010,55 @@ var AssertionExecutor = class {
|
|
|
2594
3010
|
const elementDescription = element?.description || targetStr;
|
|
2595
3011
|
switch (request.type) {
|
|
2596
3012
|
case "visible":
|
|
2597
|
-
return this.assertVisibility(
|
|
3013
|
+
return this.assertVisibility(
|
|
3014
|
+
element,
|
|
3015
|
+
true,
|
|
3016
|
+
elementDescription,
|
|
3017
|
+
request.message,
|
|
3018
|
+
startTime
|
|
3019
|
+
);
|
|
2598
3020
|
case "hidden":
|
|
2599
|
-
return this.assertVisibility(
|
|
3021
|
+
return this.assertVisibility(
|
|
3022
|
+
element,
|
|
3023
|
+
false,
|
|
3024
|
+
elementDescription,
|
|
3025
|
+
request.message,
|
|
3026
|
+
startTime
|
|
3027
|
+
);
|
|
2600
3028
|
case "enabled":
|
|
2601
|
-
return this.assertEnabledState(
|
|
3029
|
+
return this.assertEnabledState(
|
|
3030
|
+
element,
|
|
3031
|
+
true,
|
|
3032
|
+
elementDescription,
|
|
3033
|
+
request.message,
|
|
3034
|
+
startTime
|
|
3035
|
+
);
|
|
2602
3036
|
case "disabled":
|
|
2603
|
-
return this.assertEnabledState(
|
|
3037
|
+
return this.assertEnabledState(
|
|
3038
|
+
element,
|
|
3039
|
+
false,
|
|
3040
|
+
elementDescription,
|
|
3041
|
+
request.message,
|
|
3042
|
+
startTime
|
|
3043
|
+
);
|
|
2604
3044
|
case "focused":
|
|
2605
3045
|
return this.assertFocused(element, elementDescription, request.message, startTime);
|
|
2606
3046
|
case "checked":
|
|
2607
|
-
return this.assertCheckedState(
|
|
3047
|
+
return this.assertCheckedState(
|
|
3048
|
+
element,
|
|
3049
|
+
true,
|
|
3050
|
+
elementDescription,
|
|
3051
|
+
request.message,
|
|
3052
|
+
startTime
|
|
3053
|
+
);
|
|
2608
3054
|
case "unchecked":
|
|
2609
|
-
return this.assertCheckedState(
|
|
3055
|
+
return this.assertCheckedState(
|
|
3056
|
+
element,
|
|
3057
|
+
false,
|
|
3058
|
+
elementDescription,
|
|
3059
|
+
request.message,
|
|
3060
|
+
startTime
|
|
3061
|
+
);
|
|
2610
3062
|
case "hasText":
|
|
2611
3063
|
return this.assertTextMatch(
|
|
2612
3064
|
element,
|
|
@@ -2941,7 +3393,8 @@ var DEFAULT_SNAPSHOT_CONFIG = {
|
|
|
2941
3393
|
detectModals: true,
|
|
2942
3394
|
inferPageType: true,
|
|
2943
3395
|
generateDescriptions: true,
|
|
2944
|
-
maxElements: 500
|
|
3396
|
+
maxElements: 500,
|
|
3397
|
+
useAnnotations: true
|
|
2945
3398
|
};
|
|
2946
3399
|
var SemanticSnapshotManager = class {
|
|
2947
3400
|
constructor(config = {}) {
|
|
@@ -3014,26 +3467,52 @@ var SemanticSnapshotManager = class {
|
|
|
3014
3467
|
* Convert a single element to AI element
|
|
3015
3468
|
*/
|
|
3016
3469
|
convertElement(element) {
|
|
3470
|
+
const isContent = element.category === "content";
|
|
3017
3471
|
const aliases = generateAliases({
|
|
3018
3472
|
textContent: element.state.textContent,
|
|
3019
3473
|
elementType: element.type,
|
|
3020
3474
|
id: element.id,
|
|
3021
3475
|
labelText: element.label
|
|
3022
3476
|
});
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3477
|
+
let description;
|
|
3478
|
+
if (isContent && element.contentMetadata) {
|
|
3479
|
+
description = this.generateContentDescription(element);
|
|
3480
|
+
} else if (this.config.generateDescriptions) {
|
|
3481
|
+
description = generateDescription({
|
|
3482
|
+
textContent: element.state.textContent,
|
|
3483
|
+
elementType: element.type,
|
|
3484
|
+
id: element.id,
|
|
3485
|
+
labelText: element.label
|
|
3486
|
+
});
|
|
3487
|
+
} else {
|
|
3488
|
+
description = element.label || element.id;
|
|
3489
|
+
}
|
|
3490
|
+
const purpose = isContent ? generatePurpose({ textContent: element.state.textContent, elementType: element.type }) : generatePurpose({ textContent: element.state.textContent, elementType: element.type });
|
|
3491
|
+
const suggestedActions = isContent ? generateSuggestedActions({
|
|
3030
3492
|
textContent: element.state.textContent,
|
|
3031
3493
|
elementType: element.type
|
|
3032
|
-
})
|
|
3033
|
-
const suggestedActions = generateSuggestedActions({
|
|
3494
|
+
}) : generateSuggestedActions({
|
|
3034
3495
|
textContent: element.state.textContent,
|
|
3035
3496
|
elementType: element.type
|
|
3036
3497
|
});
|
|
3498
|
+
let finalDescription = description;
|
|
3499
|
+
let finalPurpose = purpose;
|
|
3500
|
+
let finalAliases = aliases;
|
|
3501
|
+
if (this.config.useAnnotations) {
|
|
3502
|
+
const annotation = getGlobalAnnotationStore().get(element.id);
|
|
3503
|
+
if (annotation) {
|
|
3504
|
+
if (annotation.description) {
|
|
3505
|
+
finalDescription = annotation.description;
|
|
3506
|
+
}
|
|
3507
|
+
if (annotation.purpose) {
|
|
3508
|
+
finalPurpose = annotation.purpose;
|
|
3509
|
+
}
|
|
3510
|
+
if (annotation.tags && annotation.tags.length > 0) {
|
|
3511
|
+
const tagSet = /* @__PURE__ */ new Set([...finalAliases, ...annotation.tags.map((t) => t.toLowerCase())]);
|
|
3512
|
+
finalAliases = [...tagSet];
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3037
3516
|
return {
|
|
3038
3517
|
id: element.id,
|
|
3039
3518
|
type: element.type,
|
|
@@ -3044,13 +3523,56 @@ var SemanticSnapshotManager = class {
|
|
|
3044
3523
|
actions: element.actions,
|
|
3045
3524
|
state: element.state,
|
|
3046
3525
|
registered: true,
|
|
3047
|
-
description,
|
|
3048
|
-
aliases,
|
|
3049
|
-
purpose,
|
|
3526
|
+
description: finalDescription,
|
|
3527
|
+
aliases: finalAliases,
|
|
3528
|
+
purpose: finalPurpose,
|
|
3050
3529
|
suggestedActions,
|
|
3051
|
-
semanticType: this.inferSemanticType(element)
|
|
3530
|
+
semanticType: this.inferSemanticType(element),
|
|
3531
|
+
category: element.category,
|
|
3532
|
+
contentMetadata: element.contentMetadata
|
|
3052
3533
|
};
|
|
3053
3534
|
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Generate a content-specific description
|
|
3537
|
+
*/
|
|
3538
|
+
generateContentDescription(element) {
|
|
3539
|
+
const meta = element.contentMetadata;
|
|
3540
|
+
const text = element.state.textContent?.trim() || "";
|
|
3541
|
+
const truncatedText = text.length > 60 ? text.substring(0, 57) + "..." : text;
|
|
3542
|
+
if (!meta) return `"${truncatedText}"`;
|
|
3543
|
+
switch (meta.contentRole) {
|
|
3544
|
+
case "heading":
|
|
3545
|
+
return `Level ${meta.headingLevel || "?"} heading: '${truncatedText}'`;
|
|
3546
|
+
case "table-cell":
|
|
3547
|
+
return `Table cell${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
|
|
3548
|
+
case "table-header":
|
|
3549
|
+
return `Table header${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
|
|
3550
|
+
case "status":
|
|
3551
|
+
return `Status message: '${truncatedText}'`;
|
|
3552
|
+
case "badge":
|
|
3553
|
+
return `Badge: '${truncatedText}'`;
|
|
3554
|
+
case "metric":
|
|
3555
|
+
return `Metric value: '${truncatedText}'`;
|
|
3556
|
+
case "body-text":
|
|
3557
|
+
return `Text: '${truncatedText}'`;
|
|
3558
|
+
case "list-item":
|
|
3559
|
+
return `List item: '${truncatedText}'`;
|
|
3560
|
+
case "quote":
|
|
3561
|
+
return `Blockquote: '${truncatedText}'`;
|
|
3562
|
+
case "code":
|
|
3563
|
+
return `Code block: '${truncatedText}'`;
|
|
3564
|
+
case "caption":
|
|
3565
|
+
return `Caption: '${truncatedText}'`;
|
|
3566
|
+
case "label":
|
|
3567
|
+
return `Label: '${truncatedText}'`;
|
|
3568
|
+
case "description":
|
|
3569
|
+
return `Description: '${truncatedText}'`;
|
|
3570
|
+
case "navigation":
|
|
3571
|
+
return `Navigation text: '${truncatedText}'`;
|
|
3572
|
+
default:
|
|
3573
|
+
return `Content: '${truncatedText}'`;
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3054
3576
|
/**
|
|
3055
3577
|
* Build full page context
|
|
3056
3578
|
*/
|
|
@@ -3154,9 +3676,7 @@ var SemanticSnapshotManager = class {
|
|
|
3154
3676
|
*/
|
|
3155
3677
|
detectModals(elements) {
|
|
3156
3678
|
const modals = [];
|
|
3157
|
-
const dialogElements = elements.filter(
|
|
3158
|
-
(el) => el.type === "dialog" && el.state.visible
|
|
3159
|
-
);
|
|
3679
|
+
const dialogElements = elements.filter((el) => el.type === "dialog" && el.state.visible);
|
|
3160
3680
|
for (const dialog of dialogElements) {
|
|
3161
3681
|
const closeButton = elements.find(
|
|
3162
3682
|
(el) => el.type === "button" && el.state.visible && (el.semanticType === "cancel-button" || el.state.textContent?.toLowerCase().match(/close|cancel|x|dismiss/))
|
|
@@ -3207,9 +3727,7 @@ var SemanticSnapshotManager = class {
|
|
|
3207
3727
|
* Infer form purpose from fields
|
|
3208
3728
|
*/
|
|
3209
3729
|
inferFormPurpose(fields) {
|
|
3210
|
-
const labels = fields.map(
|
|
3211
|
-
(f) => (f.accessibleName || f.label || "").toLowerCase()
|
|
3212
|
-
);
|
|
3730
|
+
const labels = fields.map((f) => (f.accessibleName || f.label || "").toLowerCase());
|
|
3213
3731
|
const allLabels = labels.join(" ");
|
|
3214
3732
|
if (allLabels.includes("email") && allLabels.includes("password")) {
|
|
3215
3733
|
if (allLabels.includes("confirm") || allLabels.includes("name")) {
|
|
@@ -3263,6 +3781,13 @@ var SemanticSnapshotManager = class {
|
|
|
3263
3781
|
* Infer semantic type
|
|
3264
3782
|
*/
|
|
3265
3783
|
inferSemanticType(element) {
|
|
3784
|
+
if (element.category === "content" && element.contentMetadata) {
|
|
3785
|
+
const role = element.contentMetadata.contentRole;
|
|
3786
|
+
if (role === "heading" && element.contentMetadata.headingLevel) {
|
|
3787
|
+
return `heading-${element.contentMetadata.headingLevel}`;
|
|
3788
|
+
}
|
|
3789
|
+
return role;
|
|
3790
|
+
}
|
|
3266
3791
|
const text = (element.state.textContent || element.label || "").toLowerCase();
|
|
3267
3792
|
const type = element.type.toLowerCase();
|
|
3268
3793
|
if (type === "button") {
|
|
@@ -3344,6 +3869,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
|
|
|
3344
3869
|
const probableTrigger = detectTrigger(appeared, disappeared, limitedModifications);
|
|
3345
3870
|
const suggestedActions = finalConfig.generateSuggestions ? generateSuggestedActionsFromDiff(appeared, disappeared, limitedModifications, probableTrigger) : void 0;
|
|
3346
3871
|
const pageChanges = detectPageChanges(fromSnapshot, toSnapshot);
|
|
3872
|
+
const contentChanges = detectContentChanges(fromElements, toElements);
|
|
3347
3873
|
const summary = generateDiffSummary(
|
|
3348
3874
|
appeared.map((e) => e.description),
|
|
3349
3875
|
disappeared.map((e) => e.description),
|
|
@@ -3358,6 +3884,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
|
|
|
3358
3884
|
disappeared,
|
|
3359
3885
|
modified: limitedModifications
|
|
3360
3886
|
},
|
|
3887
|
+
contentChanges: contentChanges || void 0,
|
|
3361
3888
|
probableTrigger,
|
|
3362
3889
|
suggestedActions,
|
|
3363
3890
|
pageChanges,
|
|
@@ -3492,9 +4019,7 @@ function generateSuggestedActionsFromDiff(appeared, disappeared, modified, trigg
|
|
|
3492
4019
|
suggestions.push("Fix the validation errors before submitting");
|
|
3493
4020
|
}
|
|
3494
4021
|
if (trigger === "Modal opened") {
|
|
3495
|
-
const modal = appeared.find(
|
|
3496
|
-
(e) => e.type === "dialog" || e.semanticType?.includes("dialog")
|
|
3497
|
-
);
|
|
4022
|
+
const modal = appeared.find((e) => e.type === "dialog" || e.semanticType?.includes("dialog"));
|
|
3498
4023
|
if (modal) {
|
|
3499
4024
|
suggestions.push(`Interact with the "${modal.description}" dialog`);
|
|
3500
4025
|
}
|
|
@@ -3557,76 +4082,1869 @@ var SemanticDiffManager = class {
|
|
|
3557
4082
|
return this.lastSnapshot;
|
|
3558
4083
|
}
|
|
3559
4084
|
};
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
data,
|
|
3566
|
-
timestamp: Date.now()
|
|
3567
|
-
};
|
|
4085
|
+
var METRIC_CONTENT_TYPES = /* @__PURE__ */ new Set(["metric-value"]);
|
|
4086
|
+
var STATUS_CONTENT_TYPES = /* @__PURE__ */ new Set(["status-message", "badge"]);
|
|
4087
|
+
var HEADING_CONTENT_TYPES = /* @__PURE__ */ new Set(["heading"]);
|
|
4088
|
+
function isContentElement(element) {
|
|
4089
|
+
return element.category === "content" || element.contentMetadata !== void 0;
|
|
3568
4090
|
}
|
|
3569
|
-
function
|
|
4091
|
+
function getContentType(element) {
|
|
4092
|
+
if (element.contentMetadata?.contentRole) {
|
|
4093
|
+
return element.contentMetadata.contentRole;
|
|
4094
|
+
}
|
|
4095
|
+
return element.type;
|
|
4096
|
+
}
|
|
4097
|
+
function detectContentChanges(fromElements, toElements) {
|
|
4098
|
+
const textChanges = [];
|
|
4099
|
+
const metricChanges = [];
|
|
4100
|
+
const statusChanges = [];
|
|
4101
|
+
for (const [id, toElement] of toElements) {
|
|
4102
|
+
const fromElement = fromElements.get(id);
|
|
4103
|
+
if (fromElement) {
|
|
4104
|
+
if (isContentElement(toElement) || isContentElement(fromElement)) {
|
|
4105
|
+
const fromText = (fromElement.state.textContent || "").trim();
|
|
4106
|
+
const toText = (toElement.state.textContent || "").trim();
|
|
4107
|
+
if (fromText !== toText) {
|
|
4108
|
+
const contentType = getContentType(toElement);
|
|
4109
|
+
const label = toElement.description || toElement.accessibleName || id;
|
|
4110
|
+
if (METRIC_CONTENT_TYPES.has(contentType) || contentType === "metric") {
|
|
4111
|
+
const parsed = parseMetricChange(fromText, toText, id, label);
|
|
4112
|
+
if (parsed) {
|
|
4113
|
+
metricChanges.push(parsed);
|
|
4114
|
+
}
|
|
4115
|
+
} else if (STATUS_CONTENT_TYPES.has(contentType) || contentType === "status") {
|
|
4116
|
+
statusChanges.push({
|
|
4117
|
+
elementId: id,
|
|
4118
|
+
label,
|
|
4119
|
+
oldStatus: fromText,
|
|
4120
|
+
newStatus: toText,
|
|
4121
|
+
direction: classifyStatusDirection(fromText, toText)
|
|
4122
|
+
});
|
|
4123
|
+
} else {
|
|
4124
|
+
textChanges.push({
|
|
4125
|
+
elementId: id,
|
|
4126
|
+
contentType,
|
|
4127
|
+
oldText: fromText,
|
|
4128
|
+
newText: toText,
|
|
4129
|
+
changeType: "modified"
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
} else {
|
|
4135
|
+
if (isContentElement(toElement)) {
|
|
4136
|
+
const toText = (toElement.state.textContent || "").trim();
|
|
4137
|
+
if (toText) {
|
|
4138
|
+
textChanges.push({
|
|
4139
|
+
elementId: id,
|
|
4140
|
+
contentType: getContentType(toElement),
|
|
4141
|
+
oldText: "",
|
|
4142
|
+
newText: toText,
|
|
4143
|
+
changeType: "added"
|
|
4144
|
+
});
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
for (const [id, fromElement] of fromElements) {
|
|
4150
|
+
if (!toElements.has(id) && isContentElement(fromElement)) {
|
|
4151
|
+
const fromText = (fromElement.state.textContent || "").trim();
|
|
4152
|
+
if (fromText) {
|
|
4153
|
+
textChanges.push({
|
|
4154
|
+
elementId: id,
|
|
4155
|
+
contentType: getContentType(fromElement),
|
|
4156
|
+
oldText: fromText,
|
|
4157
|
+
newText: "",
|
|
4158
|
+
changeType: "removed"
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
if (textChanges.length === 0 && metricChanges.length === 0 && statusChanges.length === 0) {
|
|
4164
|
+
return null;
|
|
4165
|
+
}
|
|
3570
4166
|
return {
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
4167
|
+
textChanges,
|
|
4168
|
+
metricChanges,
|
|
4169
|
+
statusChanges,
|
|
4170
|
+
summary: generateContentChangeSummary(textChanges, metricChanges, statusChanges)
|
|
3575
4171
|
};
|
|
3576
4172
|
}
|
|
3577
|
-
function
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
case "ACTION_TIMEOUT":
|
|
3604
|
-
return [
|
|
3605
|
-
{ suggestion: "Increase the timeout duration", confidence: 0.8, retryable: true },
|
|
3606
|
-
{ suggestion: "Check if the condition can ever be met", confidence: 0.7, retryable: false },
|
|
3607
|
-
{ suggestion: "Verify the page is responding", command: "check page status", confidence: 0.6, retryable: true }
|
|
3608
|
-
];
|
|
3609
|
-
case "LOW_CONFIDENCE":
|
|
3610
|
-
return [
|
|
3611
|
-
{ suggestion: "Use the exact text shown on the element", confidence: 0.9, retryable: false },
|
|
3612
|
-
{ suggestion: "Try a different description that more closely matches the element", confidence: 0.8, retryable: false },
|
|
3613
|
-
{ suggestion: "Lower the confidence threshold if the match is correct", confidence: 0.7, retryable: true }
|
|
3614
|
-
];
|
|
3615
|
-
case "AMBIGUOUS_MATCH":
|
|
3616
|
-
return [
|
|
3617
|
-
{ suggestion: "Be more specific about which element you mean", confidence: 0.9, retryable: false },
|
|
3618
|
-
{ suggestion: "Include the section or form name in the description", confidence: 0.8, retryable: false },
|
|
3619
|
-
{ suggestion: "Use the element ID directly", confidence: 0.7, retryable: false }
|
|
3620
|
-
];
|
|
3621
|
-
default:
|
|
3622
|
-
return [
|
|
3623
|
-
{ suggestion: "Try a different approach or check the page state", confidence: 0.5, retryable: false }
|
|
3624
|
-
];
|
|
4173
|
+
function parseNumericValue(text) {
|
|
4174
|
+
const trimmed = text.trim();
|
|
4175
|
+
if (!trimmed) return null;
|
|
4176
|
+
let working = trimmed;
|
|
4177
|
+
let negate = false;
|
|
4178
|
+
if (working.startsWith("(") && working.endsWith(")")) {
|
|
4179
|
+
working = working.slice(1, -1).trim();
|
|
4180
|
+
negate = true;
|
|
4181
|
+
}
|
|
4182
|
+
if (working.startsWith("-")) {
|
|
4183
|
+
negate = !negate;
|
|
4184
|
+
working = working.slice(1).trim();
|
|
4185
|
+
}
|
|
4186
|
+
if (working.startsWith("+")) {
|
|
4187
|
+
working = working.slice(1).trim();
|
|
4188
|
+
}
|
|
4189
|
+
working = working.replace(/^[£€¥₹$]/, "").trim();
|
|
4190
|
+
const isPercent = working.endsWith("%");
|
|
4191
|
+
if (isPercent) {
|
|
4192
|
+
working = working.slice(0, -1).trim();
|
|
4193
|
+
}
|
|
4194
|
+
working = working.replace(/\s*(ms|s|m|h|d|hrs?|mins?|secs?|days?)$/i, "").trim();
|
|
4195
|
+
working = working.replace(/,/g, "");
|
|
4196
|
+
const num = Number(working);
|
|
4197
|
+
if (isNaN(num) || !isFinite(num) || working === "") {
|
|
4198
|
+
return null;
|
|
3625
4199
|
}
|
|
4200
|
+
return negate ? -num : num;
|
|
3626
4201
|
}
|
|
3627
|
-
function
|
|
3628
|
-
const
|
|
3629
|
-
|
|
4202
|
+
function parseMetricChange(fromText, toText, elementId, label) {
|
|
4203
|
+
const fromNum = parseNumericValue(fromText);
|
|
4204
|
+
const toNum = parseNumericValue(toText);
|
|
4205
|
+
let numericDelta;
|
|
4206
|
+
let percentChange;
|
|
4207
|
+
let significant = false;
|
|
4208
|
+
if (fromNum !== null && toNum !== null) {
|
|
4209
|
+
numericDelta = toNum - fromNum;
|
|
4210
|
+
if (fromNum !== 0) {
|
|
4211
|
+
percentChange = (toNum - fromNum) / Math.abs(fromNum) * 100;
|
|
4212
|
+
}
|
|
4213
|
+
if (percentChange !== void 0 && Math.abs(percentChange) > 10) {
|
|
4214
|
+
significant = true;
|
|
4215
|
+
}
|
|
4216
|
+
if (fromNum > 0 && toNum < 0) significant = true;
|
|
4217
|
+
if (fromNum < 0 && toNum > 0) significant = true;
|
|
4218
|
+
if (fromNum === 0 && toNum !== 0) significant = true;
|
|
4219
|
+
if (fromNum !== 0 && toNum === 0) significant = true;
|
|
4220
|
+
} else {
|
|
4221
|
+
significant = fromText !== toText;
|
|
4222
|
+
}
|
|
4223
|
+
return {
|
|
4224
|
+
elementId,
|
|
4225
|
+
label,
|
|
4226
|
+
oldValue: fromText,
|
|
4227
|
+
newValue: toText,
|
|
4228
|
+
numericDelta,
|
|
4229
|
+
percentChange: percentChange !== void 0 ? Math.round(percentChange * 100) / 100 : void 0,
|
|
4230
|
+
significant
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
var STATUS_PROGRESSIONS = [
|
|
4234
|
+
[
|
|
4235
|
+
"failed",
|
|
4236
|
+
"error",
|
|
4237
|
+
"pending",
|
|
4238
|
+
"queued",
|
|
4239
|
+
"running",
|
|
4240
|
+
"in progress",
|
|
4241
|
+
"completed",
|
|
4242
|
+
"success",
|
|
4243
|
+
"done"
|
|
4244
|
+
],
|
|
4245
|
+
["disconnected", "connecting", "connected"],
|
|
4246
|
+
["unhealthy", "degraded", "healthy"],
|
|
4247
|
+
["offline", "online"],
|
|
4248
|
+
["inactive", "active"],
|
|
4249
|
+
["disabled", "enabled"],
|
|
4250
|
+
["down", "up"],
|
|
4251
|
+
["stopped", "starting", "started", "running"],
|
|
4252
|
+
["closed", "open"],
|
|
4253
|
+
["blocked", "unblocked"],
|
|
4254
|
+
["rejected", "pending", "approved"],
|
|
4255
|
+
["critical", "warning", "info", "ok"],
|
|
4256
|
+
["red", "yellow", "green"]
|
|
4257
|
+
];
|
|
4258
|
+
function classifyStatusDirection(oldStatus, newStatus) {
|
|
4259
|
+
const oldLower = oldStatus.toLowerCase().trim();
|
|
4260
|
+
const newLower = newStatus.toLowerCase().trim();
|
|
4261
|
+
for (const progression of STATUS_PROGRESSIONS) {
|
|
4262
|
+
let oldIndex = -1;
|
|
4263
|
+
let newIndex = -1;
|
|
4264
|
+
for (let i = 0; i < progression.length; i++) {
|
|
4265
|
+
if (oldLower.includes(progression[i])) oldIndex = i;
|
|
4266
|
+
if (newLower.includes(progression[i])) newIndex = i;
|
|
4267
|
+
}
|
|
4268
|
+
if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
|
|
4269
|
+
return newIndex > oldIndex ? "improved" : "degraded";
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
return "neutral";
|
|
4273
|
+
}
|
|
4274
|
+
function generateContentChangeSummary(textChanges, metricChanges, statusChanges) {
|
|
4275
|
+
const parts = [];
|
|
4276
|
+
const modified = textChanges.filter((t) => t.changeType === "modified").length;
|
|
4277
|
+
const added = textChanges.filter((t) => t.changeType === "added").length;
|
|
4278
|
+
const removed = textChanges.filter((t) => t.changeType === "removed").length;
|
|
4279
|
+
const headingChanges = textChanges.filter(
|
|
4280
|
+
(t) => HEADING_CONTENT_TYPES.has(t.contentType) || t.contentType === "heading"
|
|
4281
|
+
);
|
|
4282
|
+
if (headingChanges.length > 0) {
|
|
4283
|
+
parts.push(`${headingChanges.length} heading${headingChanges.length > 1 ? "s" : ""} changed`);
|
|
4284
|
+
}
|
|
4285
|
+
if (metricChanges.length > 0) {
|
|
4286
|
+
const significantMetrics = metricChanges.filter((m) => m.significant);
|
|
4287
|
+
if (significantMetrics.length > 0) {
|
|
4288
|
+
parts.push(
|
|
4289
|
+
`${significantMetrics.length} metric${significantMetrics.length > 1 ? "s" : ""} changed significantly`
|
|
4290
|
+
);
|
|
4291
|
+
} else {
|
|
4292
|
+
parts.push(`${metricChanges.length} metric${metricChanges.length > 1 ? "s" : ""} changed`);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
if (statusChanges.length > 0) {
|
|
4296
|
+
const degraded = statusChanges.filter((s) => s.direction === "degraded");
|
|
4297
|
+
const improved = statusChanges.filter((s) => s.direction === "improved");
|
|
4298
|
+
if (degraded.length > 0) {
|
|
4299
|
+
parts.push(`${degraded.length} status${degraded.length > 1 ? "es" : ""} degraded`);
|
|
4300
|
+
}
|
|
4301
|
+
if (improved.length > 0) {
|
|
4302
|
+
parts.push(`${improved.length} status${improved.length > 1 ? "es" : ""} improved`);
|
|
4303
|
+
}
|
|
4304
|
+
const neutral = statusChanges.length - degraded.length - improved.length;
|
|
4305
|
+
if (neutral > 0 && degraded.length === 0 && improved.length === 0) {
|
|
4306
|
+
parts.push(`${neutral} status${neutral > 1 ? "es" : ""} changed`);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
const otherModified = modified - headingChanges.filter((h) => h.changeType === "modified").length;
|
|
4310
|
+
if (otherModified > 0) {
|
|
4311
|
+
parts.push(`${otherModified} text${otherModified > 1 ? " values" : " value"} modified`);
|
|
4312
|
+
}
|
|
4313
|
+
if (added > 0) {
|
|
4314
|
+
parts.push(`${added} content${added > 1 ? " elements" : " element"} added`);
|
|
4315
|
+
}
|
|
4316
|
+
if (removed > 0) {
|
|
4317
|
+
parts.push(`${removed} content${removed > 1 ? " elements" : " element"} removed`);
|
|
4318
|
+
}
|
|
4319
|
+
if (parts.length === 0) {
|
|
4320
|
+
return "No content changes";
|
|
4321
|
+
}
|
|
4322
|
+
return parts.join(", ");
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
// src/ai/data-extraction.ts
|
|
4326
|
+
var DEFAULT_DATA_EXTRACTION_CONFIG = {
|
|
4327
|
+
minConfidence: 0.3,
|
|
4328
|
+
normalizeWhitespace: true
|
|
4329
|
+
};
|
|
4330
|
+
function classifyDataType(value) {
|
|
4331
|
+
const trimmed = value.trim();
|
|
4332
|
+
if (!trimmed) return { type: "unknown", confidence: 0 };
|
|
4333
|
+
if (/^(true|false|yes|no|on|off)$/i.test(trimmed)) {
|
|
4334
|
+
return { type: "boolean", confidence: 0.95 };
|
|
4335
|
+
}
|
|
4336
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
|
4337
|
+
return { type: "email", confidence: 0.95 };
|
|
4338
|
+
}
|
|
4339
|
+
if (/^https?:\/\/\S+/.test(trimmed)) {
|
|
4340
|
+
return { type: "url", confidence: 0.95 };
|
|
4341
|
+
}
|
|
4342
|
+
if (/^[+]?[\d\s\-().]{7,20}$/.test(trimmed) && /\d{3,}/.test(trimmed)) {
|
|
4343
|
+
return { type: "phone", confidence: 0.7 };
|
|
4344
|
+
}
|
|
4345
|
+
if (/^[£$€¥₹][\s]?[\d,.]+$/.test(trimmed) || /^[\d,.]+[\s]?[£$€¥₹]$/.test(trimmed)) {
|
|
4346
|
+
return { type: "currency", confidence: 0.9 };
|
|
4347
|
+
}
|
|
4348
|
+
if (/^[\d,.]+\s?%$/.test(trimmed)) {
|
|
4349
|
+
return { type: "percentage", confidence: 0.95 };
|
|
4350
|
+
}
|
|
4351
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed) || /^\d{1,2}\/\d{1,2}\/\d{2,4}$/.test(trimmed) || /^\d{1,2}\.\d{1,2}\.\d{2,4}$/.test(trimmed) || /^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) {
|
|
4352
|
+
return { type: "date", confidence: 0.85 };
|
|
4353
|
+
}
|
|
4354
|
+
if (/^-?[\d,]+\.?\d*$/.test(trimmed) && trimmed !== "") {
|
|
4355
|
+
return { type: "number", confidence: 0.9 };
|
|
4356
|
+
}
|
|
4357
|
+
return { type: "text", confidence: 0.5 };
|
|
4358
|
+
}
|
|
4359
|
+
function normalizeValue(value, dataType) {
|
|
4360
|
+
const trimmed = value.trim();
|
|
4361
|
+
switch (dataType) {
|
|
4362
|
+
case "number":
|
|
4363
|
+
case "currency":
|
|
4364
|
+
case "percentage": {
|
|
4365
|
+
const numeric = trimmed.replace(/[^0-9.-]/g, "");
|
|
4366
|
+
const parsed = parseFloat(numeric);
|
|
4367
|
+
return isNaN(parsed) ? trimmed.toLowerCase() : parsed.toString();
|
|
4368
|
+
}
|
|
4369
|
+
case "date": {
|
|
4370
|
+
const d = new Date(trimmed);
|
|
4371
|
+
return isNaN(d.getTime()) ? trimmed.toLowerCase() : d.toISOString().split("T")[0];
|
|
4372
|
+
}
|
|
4373
|
+
case "boolean":
|
|
4374
|
+
return /^(true|yes|on)$/i.test(trimmed) ? "true" : "false";
|
|
4375
|
+
case "email":
|
|
4376
|
+
return trimmed.toLowerCase();
|
|
4377
|
+
case "url":
|
|
4378
|
+
return trimmed.replace(/\/+$/, "").toLowerCase();
|
|
4379
|
+
case "phone":
|
|
4380
|
+
return trimmed.replace(/[^\d+]/g, "");
|
|
4381
|
+
default:
|
|
4382
|
+
return trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
function extractElementValue(element) {
|
|
4386
|
+
const state = element.state;
|
|
4387
|
+
if (state?.value !== void 0 && state.value !== "") {
|
|
4388
|
+
return String(state.value);
|
|
4389
|
+
}
|
|
4390
|
+
if (state?.textContent !== void 0 && state.textContent !== "") {
|
|
4391
|
+
return String(state.textContent);
|
|
4392
|
+
}
|
|
4393
|
+
return "";
|
|
4394
|
+
}
|
|
4395
|
+
function extractLabel(element) {
|
|
4396
|
+
return element.accessibleName || element.labelText || element.label || element.description || element.id;
|
|
4397
|
+
}
|
|
4398
|
+
function extractPageData(elements, config = DEFAULT_DATA_EXTRACTION_CONFIG) {
|
|
4399
|
+
const values = {};
|
|
4400
|
+
let extractedCount = 0;
|
|
4401
|
+
for (const element of elements) {
|
|
4402
|
+
const rawValue = extractElementValue(element);
|
|
4403
|
+
if (!rawValue) continue;
|
|
4404
|
+
const label = extractLabel(element);
|
|
4405
|
+
const { type: dataType, confidence } = classifyDataType(rawValue);
|
|
4406
|
+
if (confidence < config.minConfidence) continue;
|
|
4407
|
+
const normalizedValue = normalizeValue(rawValue, dataType);
|
|
4408
|
+
values[label] = {
|
|
4409
|
+
elementId: element.id,
|
|
4410
|
+
label,
|
|
4411
|
+
rawValue: config.normalizeWhitespace ? rawValue.replace(/\s+/g, " ").trim() : rawValue,
|
|
4412
|
+
normalizedValue,
|
|
4413
|
+
dataType,
|
|
4414
|
+
confidence
|
|
4415
|
+
};
|
|
4416
|
+
extractedCount++;
|
|
4417
|
+
}
|
|
4418
|
+
return {
|
|
4419
|
+
values,
|
|
4420
|
+
scannedCount: elements.length,
|
|
4421
|
+
extractedCount
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
// src/ai/region-segmentation.ts
|
|
4426
|
+
var DEFAULT_REGION_SEGMENTATION_CONFIG = {
|
|
4427
|
+
minRegionElements: 1,
|
|
4428
|
+
headerFraction: 0.12,
|
|
4429
|
+
footerFraction: 0.9,
|
|
4430
|
+
sidebarFraction: 0.2
|
|
4431
|
+
};
|
|
4432
|
+
function toBounded(el) {
|
|
4433
|
+
const rect = el.state?.rect;
|
|
4434
|
+
if (!rect) return null;
|
|
4435
|
+
return {
|
|
4436
|
+
element: el,
|
|
4437
|
+
x: rect.x ?? 0,
|
|
4438
|
+
y: rect.y ?? 0,
|
|
4439
|
+
width: rect.width ?? 0,
|
|
4440
|
+
height: rect.height ?? 0
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
function classifyRegionType(el, relativeY, relativeX, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
|
|
4444
|
+
const role = (el.role || "").toLowerCase();
|
|
4445
|
+
const semanticType = (el.semanticType || "").toLowerCase();
|
|
4446
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
4447
|
+
if (role === "navigation" || role === "nav" || tag === "nav") {
|
|
4448
|
+
return { type: "navigation", confidence: 0.95 };
|
|
4449
|
+
}
|
|
4450
|
+
if (role === "banner" || tag === "header") {
|
|
4451
|
+
return { type: "header", confidence: 0.95 };
|
|
4452
|
+
}
|
|
4453
|
+
if (role === "contentinfo" || tag === "footer") {
|
|
4454
|
+
return { type: "footer", confidence: 0.95 };
|
|
4455
|
+
}
|
|
4456
|
+
if (role === "main" || tag === "main") {
|
|
4457
|
+
return { type: "main-content", confidence: 0.95 };
|
|
4458
|
+
}
|
|
4459
|
+
if (role === "complementary" || tag === "aside") {
|
|
4460
|
+
return { type: "sidebar", confidence: 0.9 };
|
|
4461
|
+
}
|
|
4462
|
+
if (role === "form" || tag === "form") {
|
|
4463
|
+
return { type: "form", confidence: 0.9 };
|
|
4464
|
+
}
|
|
4465
|
+
if (role === "table" || tag === "table") {
|
|
4466
|
+
return { type: "table", confidence: 0.9 };
|
|
4467
|
+
}
|
|
4468
|
+
if (role === "dialog" || role === "alertdialog") {
|
|
4469
|
+
return { type: "modal", confidence: 0.95 };
|
|
4470
|
+
}
|
|
4471
|
+
if (role === "toolbar") {
|
|
4472
|
+
return { type: "toolbar", confidence: 0.9 };
|
|
4473
|
+
}
|
|
4474
|
+
if (semanticType.includes("card")) {
|
|
4475
|
+
return { type: "card", confidence: 0.8 };
|
|
4476
|
+
}
|
|
4477
|
+
if (relativeY < config.headerFraction) {
|
|
4478
|
+
return { type: "header", confidence: 0.6 };
|
|
4479
|
+
}
|
|
4480
|
+
if (relativeY > config.footerFraction) {
|
|
4481
|
+
return { type: "footer", confidence: 0.6 };
|
|
4482
|
+
}
|
|
4483
|
+
if (relativeX < config.sidebarFraction) {
|
|
4484
|
+
return { type: "sidebar", confidence: 0.5 };
|
|
4485
|
+
}
|
|
4486
|
+
return { type: "main-content", confidence: 0.3 };
|
|
4487
|
+
}
|
|
4488
|
+
function segmentPageRegions(elements, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
|
|
4489
|
+
const bounded = elements.map(toBounded).filter((b) => b !== null);
|
|
4490
|
+
if (bounded.length === 0) {
|
|
4491
|
+
return { regions: [], assignedCount: 0, unassignedIds: elements.map((e) => e.id) };
|
|
4492
|
+
}
|
|
4493
|
+
let maxX = 0;
|
|
4494
|
+
let maxY = 0;
|
|
4495
|
+
for (const b of bounded) {
|
|
4496
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
4497
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
4498
|
+
}
|
|
4499
|
+
if (maxX === 0) maxX = 1;
|
|
4500
|
+
if (maxY === 0) maxY = 1;
|
|
4501
|
+
const regionGroups = /* @__PURE__ */ new Map();
|
|
4502
|
+
const unassignedIds = [];
|
|
4503
|
+
for (const b of bounded) {
|
|
4504
|
+
const relativeX = b.x / maxX;
|
|
4505
|
+
const relativeY = b.y / maxY;
|
|
4506
|
+
const { type, confidence } = classifyRegionType(b.element, relativeY, relativeX, config);
|
|
4507
|
+
if (!regionGroups.has(type)) {
|
|
4508
|
+
regionGroups.set(type, { elements: [], confidences: [] });
|
|
4509
|
+
}
|
|
4510
|
+
regionGroups.get(type).elements.push(b);
|
|
4511
|
+
regionGroups.get(type).confidences.push(confidence);
|
|
4512
|
+
}
|
|
4513
|
+
const regions = [];
|
|
4514
|
+
let assignedCount = 0;
|
|
4515
|
+
for (const [type, group] of regionGroups) {
|
|
4516
|
+
if (group.elements.length < config.minRegionElements) {
|
|
4517
|
+
for (const b of group.elements) unassignedIds.push(b.element.id);
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
let minX = Infinity, minY = Infinity, maxRX = 0, maxRY = 0;
|
|
4521
|
+
const elementIds = [];
|
|
4522
|
+
for (const b of group.elements) {
|
|
4523
|
+
minX = Math.min(minX, b.x);
|
|
4524
|
+
minY = Math.min(minY, b.y);
|
|
4525
|
+
maxRX = Math.max(maxRX, b.x + b.width);
|
|
4526
|
+
maxRY = Math.max(maxRY, b.y + b.height);
|
|
4527
|
+
elementIds.push(b.element.id);
|
|
4528
|
+
}
|
|
4529
|
+
const avgConfidence = group.confidences.reduce((a, b) => a + b, 0) / group.confidences.length;
|
|
4530
|
+
regions.push({
|
|
4531
|
+
type,
|
|
4532
|
+
bounds: { x: minX, y: minY, width: maxRX - minX, height: maxRY - minY },
|
|
4533
|
+
elementIds,
|
|
4534
|
+
label: type.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
4535
|
+
confidence: Math.round(avgConfidence * 100) / 100
|
|
4536
|
+
});
|
|
4537
|
+
assignedCount += elementIds.length;
|
|
4538
|
+
}
|
|
4539
|
+
return { regions, assignedCount, unassignedIds };
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
// src/ai/table-extraction.ts
|
|
4543
|
+
var DEFAULT_TABLE_EXTRACTION_CONFIG = {
|
|
4544
|
+
minTableColumns: 2,
|
|
4545
|
+
minTableRows: 2,
|
|
4546
|
+
minListItems: 2,
|
|
4547
|
+
columnTolerance: 20,
|
|
4548
|
+
rowTolerance: 10
|
|
4549
|
+
};
|
|
4550
|
+
function getElementBounds(el) {
|
|
4551
|
+
const rect = el.state?.rect;
|
|
4552
|
+
if (!rect || rect.width === 0) return null;
|
|
4553
|
+
const text = el.state?.textContent ?? el.state?.value ?? "";
|
|
4554
|
+
if (!text) return null;
|
|
4555
|
+
return {
|
|
4556
|
+
element: el,
|
|
4557
|
+
x: rect.x ?? 0,
|
|
4558
|
+
y: rect.y ?? 0,
|
|
4559
|
+
width: rect.width ?? 0,
|
|
4560
|
+
height: rect.height ?? 0,
|
|
4561
|
+
text: text.trim()
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
function clusterPositions(values, tolerance) {
|
|
4565
|
+
if (values.length === 0) return [];
|
|
4566
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4567
|
+
const clusters = [sorted[0]];
|
|
4568
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
4569
|
+
if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
|
|
4570
|
+
clusters.push(sorted[i]);
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
return clusters;
|
|
4574
|
+
}
|
|
4575
|
+
function assignToCluster(value, clusters, tolerance) {
|
|
4576
|
+
let best = 0;
|
|
4577
|
+
let bestDist = Math.abs(value - clusters[0]);
|
|
4578
|
+
for (let i = 1; i < clusters.length; i++) {
|
|
4579
|
+
const dist = Math.abs(value - clusters[i]);
|
|
4580
|
+
if (dist < bestDist) {
|
|
4581
|
+
bestDist = dist;
|
|
4582
|
+
best = i;
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
return bestDist <= tolerance ? best : -1;
|
|
4586
|
+
}
|
|
4587
|
+
function detectTable(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
|
|
4588
|
+
const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
|
|
4589
|
+
if (withBounds.length < config.minTableColumns * config.minTableRows) return null;
|
|
4590
|
+
const xPositions = withBounds.map((b) => b.x);
|
|
4591
|
+
const yPositions = withBounds.map((b) => b.y);
|
|
4592
|
+
const columnClusters = clusterPositions(xPositions, config.columnTolerance);
|
|
4593
|
+
const rowClusters = clusterPositions(yPositions, config.rowTolerance);
|
|
4594
|
+
if (columnClusters.length < config.minTableColumns || rowClusters.length < config.minTableRows) {
|
|
4595
|
+
return null;
|
|
4596
|
+
}
|
|
4597
|
+
const grid = Array.from(
|
|
4598
|
+
{ length: rowClusters.length },
|
|
4599
|
+
() => Array(columnClusters.length).fill(null)
|
|
4600
|
+
);
|
|
4601
|
+
for (const b of withBounds) {
|
|
4602
|
+
const col = assignToCluster(b.x, columnClusters, config.columnTolerance);
|
|
4603
|
+
const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
|
|
4604
|
+
if (col >= 0 && row >= 0 && grid[row][col] === null) {
|
|
4605
|
+
grid[row][col] = b.text;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
const headers = grid[0].map((h) => h ?? "");
|
|
4609
|
+
const columns = headers.map((header, index) => {
|
|
4610
|
+
const bodyCells = grid.slice(1).map((r) => r[index]).filter((c) => c !== null);
|
|
4611
|
+
const types = bodyCells.map((c) => classifyDataType(c).type);
|
|
4612
|
+
const mostCommon = mode(types) ?? "text";
|
|
4613
|
+
return { header, index, dataType: mostCommon };
|
|
4614
|
+
});
|
|
4615
|
+
const rows = grid.slice(1).map((row) => row.map((cell) => cell ?? ""));
|
|
4616
|
+
return {
|
|
4617
|
+
label: headers[0] || "Table",
|
|
4618
|
+
columns,
|
|
4619
|
+
rows
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
function detectList(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
|
|
4623
|
+
const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
|
|
4624
|
+
if (withBounds.length < config.minListItems) return null;
|
|
4625
|
+
const sorted = [...withBounds].sort((a, b) => a.y - b.y);
|
|
4626
|
+
const yPositions = sorted.map((b) => b.y);
|
|
4627
|
+
const rowClusters = clusterPositions(yPositions, config.rowTolerance);
|
|
4628
|
+
if (rowClusters.length < config.minListItems) return null;
|
|
4629
|
+
const rowGroups = /* @__PURE__ */ new Map();
|
|
4630
|
+
for (const b of sorted) {
|
|
4631
|
+
const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
|
|
4632
|
+
if (row >= 0) {
|
|
4633
|
+
if (!rowGroups.has(row)) rowGroups.set(row, []);
|
|
4634
|
+
rowGroups.get(row).push(b);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
const items = [];
|
|
4638
|
+
const fieldLabels = [];
|
|
4639
|
+
let fieldLabelsInitialized = false;
|
|
4640
|
+
for (const [, rowElements] of [...rowGroups.entries()].sort(([a], [b]) => a - b)) {
|
|
4641
|
+
const sortedRow = [...rowElements].sort((a, b) => a.x - b.x);
|
|
4642
|
+
const item = {};
|
|
4643
|
+
for (let i = 0; i < sortedRow.length; i++) {
|
|
4644
|
+
const label = `field_${i}`;
|
|
4645
|
+
if (!fieldLabelsInitialized) fieldLabels.push(label);
|
|
4646
|
+
item[label] = sortedRow[i].text;
|
|
4647
|
+
}
|
|
4648
|
+
fieldLabelsInitialized = true;
|
|
4649
|
+
items.push(item);
|
|
4650
|
+
}
|
|
4651
|
+
if (items.length < config.minListItems) return null;
|
|
4652
|
+
const fields = fieldLabels.map((label) => {
|
|
4653
|
+
const values = items.map((item) => item[label]).filter(Boolean);
|
|
4654
|
+
const types = values.map((v) => classifyDataType(v).type);
|
|
4655
|
+
return { label, dataType: mode(types) ?? "text" };
|
|
4656
|
+
});
|
|
4657
|
+
return {
|
|
4658
|
+
label: "List",
|
|
4659
|
+
fields,
|
|
4660
|
+
items
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
function extractStructuredData(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
|
|
4664
|
+
const tables = [];
|
|
4665
|
+
const lists = [];
|
|
4666
|
+
const table = detectTable(elements, config);
|
|
4667
|
+
if (table) {
|
|
4668
|
+
tables.push(table);
|
|
4669
|
+
}
|
|
4670
|
+
const listCandidates = elements.filter((el) => {
|
|
4671
|
+
const role = el.role || el.type;
|
|
4672
|
+
return ["listitem", "row", "option", "link", "button"].includes(role);
|
|
4673
|
+
});
|
|
4674
|
+
if (listCandidates.length >= config.minListItems) {
|
|
4675
|
+
const list = detectList(listCandidates, config);
|
|
4676
|
+
if (list) {
|
|
4677
|
+
lists.push(list);
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return { tables, lists };
|
|
4681
|
+
}
|
|
4682
|
+
function mode(arr) {
|
|
4683
|
+
if (arr.length === 0) return void 0;
|
|
4684
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4685
|
+
let best = arr[0];
|
|
4686
|
+
let bestCount = 0;
|
|
4687
|
+
for (const v of arr) {
|
|
4688
|
+
const c = (counts.get(v) ?? 0) + 1;
|
|
4689
|
+
counts.set(v, c);
|
|
4690
|
+
if (c > bestCount) {
|
|
4691
|
+
bestCount = c;
|
|
4692
|
+
best = v;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
return best;
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// src/ai/format-analysis.ts
|
|
4699
|
+
var DEFAULT_FORMAT_ANALYSIS_CONFIG = {
|
|
4700
|
+
lenientFormatting: true
|
|
4701
|
+
};
|
|
4702
|
+
function detectFormatPattern(value, dataType) {
|
|
4703
|
+
const trimmed = value.trim();
|
|
4704
|
+
switch (dataType) {
|
|
4705
|
+
case "currency": {
|
|
4706
|
+
const hasLeadingSymbol = /^[£$€¥₹]/.test(trimmed);
|
|
4707
|
+
const hasTrailingSymbol = /[£$€¥₹]$/.test(trimmed);
|
|
4708
|
+
const usesCommaThousands = /\d{1,3}(,\d{3})+/.test(trimmed);
|
|
4709
|
+
const usesPeriodThousands = /\d{1,3}(\.\d{3})+,/.test(trimmed);
|
|
4710
|
+
let pattern = hasLeadingSymbol ? "$" : "";
|
|
4711
|
+
if (usesCommaThousands) pattern += "#,###";
|
|
4712
|
+
else if (usesPeriodThousands) pattern += "#.###";
|
|
4713
|
+
else pattern += "#";
|
|
4714
|
+
if (/\.\d{2}$/.test(trimmed)) pattern += ".##";
|
|
4715
|
+
else if (/,\d{2}$/.test(trimmed)) pattern += ",##";
|
|
4716
|
+
if (hasTrailingSymbol) pattern += "$";
|
|
4717
|
+
return pattern;
|
|
4718
|
+
}
|
|
4719
|
+
case "date": {
|
|
4720
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return "YYYY-MM-DD";
|
|
4721
|
+
if (/^\d{2}\/\d{2}\/\d{4}$/.test(trimmed)) return "MM/DD/YYYY";
|
|
4722
|
+
if (/^\d{2}\.\d{2}\.\d{4}$/.test(trimmed)) return "DD.MM.YYYY";
|
|
4723
|
+
if (/^\d{1,2}\/\d{1,2}\/\d{2}$/.test(trimmed)) return "M/D/YY";
|
|
4724
|
+
if (/^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) return "Month DD, YYYY";
|
|
4725
|
+
return "date";
|
|
4726
|
+
}
|
|
4727
|
+
case "percentage":
|
|
4728
|
+
return /\s%$/.test(trimmed) ? "#.## %" : "#.##%";
|
|
4729
|
+
case "number": {
|
|
4730
|
+
const hasCommas = /,/.test(trimmed);
|
|
4731
|
+
const decimalPlaces = trimmed.includes(".") ? trimmed.split(".")[1]?.length || 0 : 0;
|
|
4732
|
+
return (hasCommas ? "#,###" : "#") + (decimalPlaces > 0 ? "." + "#".repeat(decimalPlaces) : "");
|
|
4733
|
+
}
|
|
4734
|
+
case "phone": {
|
|
4735
|
+
if (/^\(\d{3}\)\s?\d{3}-\d{4}$/.test(trimmed)) return "(###) ###-####";
|
|
4736
|
+
if (/^\d{3}-\d{3}-\d{4}$/.test(trimmed)) return "###-###-####";
|
|
4737
|
+
if (/^\+\d/.test(trimmed)) return "+# ###...";
|
|
4738
|
+
return "phone";
|
|
4739
|
+
}
|
|
4740
|
+
default:
|
|
4741
|
+
return dataType;
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
function analyzeFormat(elementId, label, rawValue) {
|
|
4745
|
+
const { type: dataType } = classifyDataType(rawValue);
|
|
4746
|
+
const pattern = detectFormatPattern(rawValue, dataType);
|
|
4747
|
+
return {
|
|
4748
|
+
elementId,
|
|
4749
|
+
label,
|
|
4750
|
+
dataType,
|
|
4751
|
+
pattern,
|
|
4752
|
+
example: rawValue.trim()
|
|
4753
|
+
};
|
|
4754
|
+
}
|
|
4755
|
+
function analyzePageFormats(elements) {
|
|
4756
|
+
const descriptors = [];
|
|
4757
|
+
for (const el of elements) {
|
|
4758
|
+
const rawValue = el.state?.value ?? el.state?.textContent ?? "";
|
|
4759
|
+
if (!rawValue) continue;
|
|
4760
|
+
const label = el.accessibleName || el.labelText || el.label || el.description || el.id;
|
|
4761
|
+
descriptors.push(analyzeFormat(el.id, label, rawValue));
|
|
4762
|
+
}
|
|
4763
|
+
return descriptors;
|
|
4764
|
+
}
|
|
4765
|
+
function compareFormats(sourceFormats, targetFormats, config = DEFAULT_FORMAT_ANALYSIS_CONFIG) {
|
|
4766
|
+
const mismatches = [];
|
|
4767
|
+
const targetByLabel = /* @__PURE__ */ new Map();
|
|
4768
|
+
for (const t of targetFormats) {
|
|
4769
|
+
targetByLabel.set(t.label.toLowerCase(), t);
|
|
4770
|
+
}
|
|
4771
|
+
for (const source of sourceFormats) {
|
|
4772
|
+
const target = targetByLabel.get(source.label.toLowerCase());
|
|
4773
|
+
if (!target) continue;
|
|
4774
|
+
if (source.dataType !== target.dataType) {
|
|
4775
|
+
mismatches.push({
|
|
4776
|
+
label: source.label,
|
|
4777
|
+
sourceFormat: source,
|
|
4778
|
+
targetFormat: target,
|
|
4779
|
+
severity: "error",
|
|
4780
|
+
description: `Data type mismatch: source is ${source.dataType}, target is ${target.dataType}`
|
|
4781
|
+
});
|
|
4782
|
+
continue;
|
|
4783
|
+
}
|
|
4784
|
+
if (source.pattern !== target.pattern) {
|
|
4785
|
+
const severity = config.lenientFormatting ? "warning" : "error";
|
|
4786
|
+
mismatches.push({
|
|
4787
|
+
label: source.label,
|
|
4788
|
+
sourceFormat: source,
|
|
4789
|
+
targetFormat: target,
|
|
4790
|
+
severity,
|
|
4791
|
+
description: `Format differs: source uses "${source.pattern}", target uses "${target.pattern}"`
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
return mismatches;
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
// src/ai/cross-app-diff.ts
|
|
4799
|
+
var DEFAULT_CROSS_APP_DIFF_CONFIG = {
|
|
4800
|
+
matchThreshold: 0.5,
|
|
4801
|
+
accessibleNameWeight: 1,
|
|
4802
|
+
textWeight: 0.95,
|
|
4803
|
+
rolePositionWeight: 0.7
|
|
4804
|
+
};
|
|
4805
|
+
function getElementText(el) {
|
|
4806
|
+
return el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "";
|
|
4807
|
+
}
|
|
4808
|
+
function getRole(el) {
|
|
4809
|
+
return (el.role || el.type || "").toLowerCase();
|
|
4810
|
+
}
|
|
4811
|
+
function getCenter(el) {
|
|
4812
|
+
const rect = el.state?.rect;
|
|
4813
|
+
if (!rect) return null;
|
|
4814
|
+
return {
|
|
4815
|
+
x: rect.x + rect.width / 2,
|
|
4816
|
+
y: rect.y + rect.height / 2
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
function computeMatchScore(source, target, config) {
|
|
4820
|
+
let bestScore = 0;
|
|
4821
|
+
let bestStrategy = "none";
|
|
4822
|
+
const srcName = (source.accessibleName || "").trim();
|
|
4823
|
+
const tgtName = (target.accessibleName || "").trim();
|
|
4824
|
+
if (srcName && tgtName && srcName.toLowerCase() === tgtName.toLowerCase()) {
|
|
4825
|
+
return { score: config.accessibleNameWeight, strategy: "accessible-name-exact" };
|
|
4826
|
+
}
|
|
4827
|
+
const srcText = getElementText(source);
|
|
4828
|
+
const tgtText = getElementText(target);
|
|
4829
|
+
if (srcText && tgtText && srcText.toLowerCase() === tgtText.toLowerCase()) {
|
|
4830
|
+
const score = config.textWeight;
|
|
4831
|
+
if (score > bestScore) {
|
|
4832
|
+
bestScore = score;
|
|
4833
|
+
bestStrategy = "text-exact";
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
if (srcText && tgtText) {
|
|
4837
|
+
const srcNorm = normalizeString(srcText);
|
|
4838
|
+
const tgtNorm = normalizeString(tgtText);
|
|
4839
|
+
const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
|
|
4840
|
+
const score = similarity * 0.85;
|
|
4841
|
+
if (score > bestScore) {
|
|
4842
|
+
bestScore = score;
|
|
4843
|
+
bestStrategy = "text-fuzzy";
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
const srcRole = getRole(source);
|
|
4847
|
+
const tgtRole = getRole(target);
|
|
4848
|
+
if (srcRole && srcRole === tgtRole) {
|
|
4849
|
+
const srcCenter = getCenter(source);
|
|
4850
|
+
const tgtCenter = getCenter(target);
|
|
4851
|
+
if (srcCenter && tgtCenter) {
|
|
4852
|
+
const dx = Math.abs(srcCenter.x - tgtCenter.x) / 1920;
|
|
4853
|
+
const dy = Math.abs(srcCenter.y - tgtCenter.y) / 1080;
|
|
4854
|
+
const posSimilarity = 1 - Math.min(1, Math.sqrt(dx * dx + dy * dy));
|
|
4855
|
+
const score = config.rolePositionWeight * posSimilarity;
|
|
4856
|
+
if (score > bestScore) {
|
|
4857
|
+
bestScore = score;
|
|
4858
|
+
bestStrategy = "role-position";
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
const srcVal = source.state?.value ?? source.state?.textContent ?? "";
|
|
4863
|
+
const tgtVal = target.state?.value ?? target.state?.textContent ?? "";
|
|
4864
|
+
if (srcVal && tgtVal) {
|
|
4865
|
+
const srcType = classifyDataType(srcVal).type;
|
|
4866
|
+
const tgtType = classifyDataType(tgtVal).type;
|
|
4867
|
+
const srcNorm = normalizeValue(srcVal, srcType);
|
|
4868
|
+
const tgtNorm = normalizeValue(tgtVal, tgtType);
|
|
4869
|
+
if (srcNorm === tgtNorm && srcNorm !== "") {
|
|
4870
|
+
const score = 0.6;
|
|
4871
|
+
if (score > bestScore) {
|
|
4872
|
+
bestScore = score;
|
|
4873
|
+
bestStrategy = "data-overlap";
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
return { score: bestScore, strategy: bestStrategy };
|
|
4878
|
+
}
|
|
4879
|
+
function matchElements(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
|
|
4880
|
+
const candidates = [];
|
|
4881
|
+
for (let si = 0; si < sourceElements.length; si++) {
|
|
4882
|
+
for (let ti = 0; ti < targetElements.length; ti++) {
|
|
4883
|
+
const { score, strategy } = computeMatchScore(sourceElements[si], targetElements[ti], config);
|
|
4884
|
+
if (score >= config.matchThreshold) {
|
|
4885
|
+
candidates.push({ sourceIdx: si, targetIdx: ti, score, strategy });
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
4890
|
+
const usedSource = /* @__PURE__ */ new Set();
|
|
4891
|
+
const usedTarget = /* @__PURE__ */ new Set();
|
|
4892
|
+
const pairs = [];
|
|
4893
|
+
for (const c of candidates) {
|
|
4894
|
+
if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
|
|
4895
|
+
usedSource.add(c.sourceIdx);
|
|
4896
|
+
usedTarget.add(c.targetIdx);
|
|
4897
|
+
const src = sourceElements[c.sourceIdx];
|
|
4898
|
+
const tgt = targetElements[c.targetIdx];
|
|
4899
|
+
pairs.push({
|
|
4900
|
+
sourceId: src.id,
|
|
4901
|
+
targetId: tgt.id,
|
|
4902
|
+
sourceLabel: getElementText(src) || src.id,
|
|
4903
|
+
targetLabel: getElementText(tgt) || tgt.id,
|
|
4904
|
+
confidence: Math.round(c.score * 100) / 100,
|
|
4905
|
+
matchStrategy: c.strategy
|
|
4906
|
+
});
|
|
4907
|
+
}
|
|
4908
|
+
return pairs;
|
|
4909
|
+
}
|
|
4910
|
+
function computeCrossAppDiff(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
|
|
4911
|
+
const matchedPairs = matchElements(sourceElements, targetElements, config);
|
|
4912
|
+
const matchedSourceIds = new Set(matchedPairs.map((p) => p.sourceId));
|
|
4913
|
+
const matchedTargetIds = new Set(matchedPairs.map((p) => p.targetId));
|
|
4914
|
+
const unmatchedSourceIds = sourceElements.filter((e) => !matchedSourceIds.has(e.id)).map((e) => e.id);
|
|
4915
|
+
const unmatchedTargetIds = targetElements.filter((e) => !matchedTargetIds.has(e.id)).map((e) => e.id);
|
|
4916
|
+
const sourceData = extractPageData(sourceElements);
|
|
4917
|
+
const targetData = extractPageData(targetElements);
|
|
4918
|
+
const dataComparisons = [];
|
|
4919
|
+
for (const pair of matchedPairs) {
|
|
4920
|
+
const srcEntry = Object.values(sourceData.values).find((v) => v.elementId === pair.sourceId);
|
|
4921
|
+
const tgtEntry = Object.values(targetData.values).find((v) => v.elementId === pair.targetId);
|
|
4922
|
+
if (srcEntry && tgtEntry) {
|
|
4923
|
+
dataComparisons.push({
|
|
4924
|
+
label: pair.sourceLabel,
|
|
4925
|
+
sourceValue: srcEntry.rawValue,
|
|
4926
|
+
targetValue: tgtEntry.rawValue,
|
|
4927
|
+
valuesMatch: srcEntry.normalizedValue === tgtEntry.normalizedValue,
|
|
4928
|
+
formatsMatch: srcEntry.dataType === tgtEntry.dataType
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
const sourceFormats = analyzePageFormats(sourceElements);
|
|
4933
|
+
const targetFormats = analyzePageFormats(targetElements);
|
|
4934
|
+
const formatMismatches = compareFormats(sourceFormats, targetFormats);
|
|
4935
|
+
return {
|
|
4936
|
+
matchedPairs,
|
|
4937
|
+
unmatchedSourceIds,
|
|
4938
|
+
unmatchedTargetIds,
|
|
4939
|
+
dataComparisons,
|
|
4940
|
+
formatMismatches
|
|
4941
|
+
};
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
// src/ai/action-parity.ts
|
|
4945
|
+
var DEFAULT_ACTION_PARITY_CONFIG = {
|
|
4946
|
+
ignoreActions: []
|
|
4947
|
+
};
|
|
4948
|
+
function getActions(el, ignoreActions) {
|
|
4949
|
+
const actions = el.actions || el.suggestedActions || [];
|
|
4950
|
+
const ignoreSet = new Set(ignoreActions.map((a) => a.toLowerCase()));
|
|
4951
|
+
return actions.map(
|
|
4952
|
+
(a) => typeof a === "string" ? a : a.action || a.name || ""
|
|
4953
|
+
).filter((a) => a && !ignoreSet.has(a.toLowerCase()));
|
|
4954
|
+
}
|
|
4955
|
+
function analyzeActionParity(matchedPairs, sourceElements, targetElements, config = DEFAULT_ACTION_PARITY_CONFIG) {
|
|
4956
|
+
const sourceById = new Map(sourceElements.map((e) => [e.id, e]));
|
|
4957
|
+
const targetById = new Map(targetElements.map((e) => [e.id, e]));
|
|
4958
|
+
const results = [];
|
|
4959
|
+
for (const pair of matchedPairs) {
|
|
4960
|
+
const src = sourceById.get(pair.sourceId);
|
|
4961
|
+
const tgt = targetById.get(pair.targetId);
|
|
4962
|
+
if (!src || !tgt) continue;
|
|
4963
|
+
const sourceActions = getActions(src, config.ignoreActions);
|
|
4964
|
+
const targetActions = getActions(tgt, config.ignoreActions);
|
|
4965
|
+
const sourceSet = new Set(sourceActions.map((a) => a.toLowerCase()));
|
|
4966
|
+
const targetSet = new Set(targetActions.map((a) => a.toLowerCase()));
|
|
4967
|
+
const missingInTarget = sourceActions.filter((a) => !targetSet.has(a.toLowerCase()));
|
|
4968
|
+
const missingInSource = targetActions.filter((a) => !sourceSet.has(a.toLowerCase()));
|
|
4969
|
+
results.push({
|
|
4970
|
+
pair,
|
|
4971
|
+
sourceActions,
|
|
4972
|
+
targetActions,
|
|
4973
|
+
missingInTarget,
|
|
4974
|
+
missingInSource
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4977
|
+
return results;
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
// src/ai/navigation-map.ts
|
|
4981
|
+
var DEFAULT_NAVIGATION_MAP_CONFIG = {
|
|
4982
|
+
labelMatchThreshold: 0.8
|
|
4983
|
+
};
|
|
4984
|
+
function isNavigationElement(el) {
|
|
4985
|
+
const role = (el.role || "").toLowerCase();
|
|
4986
|
+
const type = (el.type || "").toLowerCase();
|
|
4987
|
+
const semanticType = (el.semanticType || "").toLowerCase();
|
|
4988
|
+
if (["link", "menuitem", "tab"].includes(role)) return true;
|
|
4989
|
+
if (["link", "menuitem"].includes(type)) return true;
|
|
4990
|
+
if (semanticType.includes("nav") || semanticType.includes("menu") || semanticType.includes("tab")) {
|
|
4991
|
+
return true;
|
|
4992
|
+
}
|
|
4993
|
+
const context = (el.parentContext || "").toLowerCase();
|
|
4994
|
+
if (context.includes("nav") || context.includes("menu") || context.includes("sidebar")) {
|
|
4995
|
+
if (role === "button" || type === "button" || role === "link" || type === "link") {
|
|
4996
|
+
return true;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
return false;
|
|
5000
|
+
}
|
|
5001
|
+
function getNavLabel(el) {
|
|
5002
|
+
return el.accessibleName || el.labelText || el.label || el.description || el.id;
|
|
5003
|
+
}
|
|
5004
|
+
function getHref(el) {
|
|
5005
|
+
const state = el.state;
|
|
5006
|
+
return state?.href || void 0;
|
|
5007
|
+
}
|
|
5008
|
+
function hrefsMatch(a, b) {
|
|
5009
|
+
if (!a || !b) return false;
|
|
5010
|
+
const normalize = (h) => h.replace(/^https?:\/\//, "").replace(/localhost:\d+/, "").replace(/\/+$/, "").toLowerCase();
|
|
5011
|
+
return normalize(a) === normalize(b);
|
|
5012
|
+
}
|
|
5013
|
+
function buildNavigationMap(sourceElements, targetElements, config = DEFAULT_NAVIGATION_MAP_CONFIG) {
|
|
5014
|
+
const sourceNav = sourceElements.filter(isNavigationElement);
|
|
5015
|
+
const targetNav = targetElements.filter(isNavigationElement);
|
|
5016
|
+
const pairs = [];
|
|
5017
|
+
const matchedTargetIds = /* @__PURE__ */ new Set();
|
|
5018
|
+
for (const src of sourceNav) {
|
|
5019
|
+
const srcLabel = getNavLabel(src);
|
|
5020
|
+
const srcNorm = normalizeString(srcLabel);
|
|
5021
|
+
let bestTarget = null;
|
|
5022
|
+
let bestScore = 0;
|
|
5023
|
+
for (const tgt of targetNav) {
|
|
5024
|
+
if (matchedTargetIds.has(tgt.id)) continue;
|
|
5025
|
+
const tgtLabel = getNavLabel(tgt);
|
|
5026
|
+
const tgtNorm = normalizeString(tgtLabel);
|
|
5027
|
+
if (srcNorm === tgtNorm) {
|
|
5028
|
+
bestTarget = tgt;
|
|
5029
|
+
bestScore = 1;
|
|
5030
|
+
break;
|
|
5031
|
+
}
|
|
5032
|
+
const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
|
|
5033
|
+
if (similarity > bestScore && similarity >= config.labelMatchThreshold) {
|
|
5034
|
+
bestScore = similarity;
|
|
5035
|
+
bestTarget = tgt;
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
if (bestTarget) {
|
|
5039
|
+
matchedTargetIds.add(bestTarget.id);
|
|
5040
|
+
const srcHref = getHref(src);
|
|
5041
|
+
const tgtHref = getHref(bestTarget);
|
|
5042
|
+
pairs.push({
|
|
5043
|
+
sourceId: src.id,
|
|
5044
|
+
targetId: bestTarget.id,
|
|
5045
|
+
label: srcLabel,
|
|
5046
|
+
sourceHref: srcHref,
|
|
5047
|
+
targetHref: tgtHref,
|
|
5048
|
+
destinationMatch: hrefsMatch(srcHref, tgtHref)
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
const sourceOnly = sourceNav.filter((s) => !pairs.some((p) => p.sourceId === s.id)).map((s) => s.id);
|
|
5053
|
+
const targetOnly = targetNav.filter((t) => !matchedTargetIds.has(t.id)).map((t) => t.id);
|
|
5054
|
+
return { pairs, sourceOnly, targetOnly };
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
// src/ai/component-comparison.ts
|
|
5058
|
+
var DEFAULT_COMPONENT_COMPARISON_CONFIG = {
|
|
5059
|
+
nameMatchThreshold: 0.75
|
|
5060
|
+
};
|
|
5061
|
+
function computeComponentMatchScore(source, target) {
|
|
5062
|
+
if (source.name.toLowerCase() === target.name.toLowerCase()) return 1;
|
|
5063
|
+
let score = 0;
|
|
5064
|
+
if (source.type === target.type) {
|
|
5065
|
+
score += 0.3;
|
|
5066
|
+
}
|
|
5067
|
+
const nameSimilarity = jaroWinklerSimilarity(
|
|
5068
|
+
normalizeString(source.name),
|
|
5069
|
+
normalizeString(target.name)
|
|
5070
|
+
);
|
|
5071
|
+
score += nameSimilarity * 0.7;
|
|
5072
|
+
return score;
|
|
5073
|
+
}
|
|
5074
|
+
function compareComponents(sourceComponents, targetComponents, config = DEFAULT_COMPONENT_COMPARISON_CONFIG) {
|
|
5075
|
+
const candidates = [];
|
|
5076
|
+
for (let si = 0; si < sourceComponents.length; si++) {
|
|
5077
|
+
for (let ti = 0; ti < targetComponents.length; ti++) {
|
|
5078
|
+
const score = computeComponentMatchScore(sourceComponents[si], targetComponents[ti]);
|
|
5079
|
+
if (score >= config.nameMatchThreshold) {
|
|
5080
|
+
candidates.push({ sourceIdx: si, targetIdx: ti, score });
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
5085
|
+
const usedSource = /* @__PURE__ */ new Set();
|
|
5086
|
+
const usedTarget = /* @__PURE__ */ new Set();
|
|
5087
|
+
const matches = [];
|
|
5088
|
+
for (const c of candidates) {
|
|
5089
|
+
if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
|
|
5090
|
+
usedSource.add(c.sourceIdx);
|
|
5091
|
+
usedTarget.add(c.targetIdx);
|
|
5092
|
+
const src = sourceComponents[c.sourceIdx];
|
|
5093
|
+
const tgt = targetComponents[c.targetIdx];
|
|
5094
|
+
const srcKeys = new Set(src.stateKeys);
|
|
5095
|
+
const tgtKeys = new Set(tgt.stateKeys);
|
|
5096
|
+
const missingKeys = src.stateKeys.filter((k) => !tgtKeys.has(k));
|
|
5097
|
+
const extraKeys = tgt.stateKeys.filter((k) => !srcKeys.has(k));
|
|
5098
|
+
const srcActions = new Set(src.actions.map((a) => a.toLowerCase()));
|
|
5099
|
+
const tgtActions = new Set(tgt.actions.map((a) => a.toLowerCase()));
|
|
5100
|
+
const missingActions = src.actions.filter((a) => !tgtActions.has(a.toLowerCase()));
|
|
5101
|
+
const extraActions = tgt.actions.filter((a) => !srcActions.has(a.toLowerCase()));
|
|
5102
|
+
matches.push({
|
|
5103
|
+
source: src,
|
|
5104
|
+
target: tgt,
|
|
5105
|
+
confidence: Math.round(c.score * 100) / 100,
|
|
5106
|
+
stateKeyDiff: { missing: missingKeys, extra: extraKeys },
|
|
5107
|
+
actionDiff: { missing: missingActions, extra: extraActions }
|
|
5108
|
+
});
|
|
5109
|
+
}
|
|
5110
|
+
const sourceOnly = sourceComponents.filter((_, i) => !usedSource.has(i));
|
|
5111
|
+
const targetOnly = targetComponents.filter((_, i) => !usedTarget.has(i));
|
|
5112
|
+
return { matches, sourceOnly, targetOnly };
|
|
5113
|
+
}
|
|
5114
|
+
|
|
5115
|
+
// src/ai/layout-comparison.ts
|
|
5116
|
+
var DEFAULT_LAYOUT_COMPARISON_CONFIG = {
|
|
5117
|
+
gridTolerance: 20
|
|
5118
|
+
};
|
|
5119
|
+
function getRect(el) {
|
|
5120
|
+
const rect = el.state?.rect;
|
|
5121
|
+
if (!rect || !rect.width) return null;
|
|
5122
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
5123
|
+
}
|
|
5124
|
+
function clusterValues(values, tolerance) {
|
|
5125
|
+
if (values.length === 0) return [];
|
|
5126
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5127
|
+
const clusters = [sorted[0]];
|
|
5128
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
5129
|
+
if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
|
|
5130
|
+
clusters.push(sorted[i]);
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
return clusters;
|
|
5134
|
+
}
|
|
5135
|
+
function detectGridStructure(elements, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
|
|
5136
|
+
const rects = elements.map(getRect).filter((r) => r !== null);
|
|
5137
|
+
const xPositions = rects.map((r) => r.x);
|
|
5138
|
+
const yPositions = rects.map((r) => r.y);
|
|
5139
|
+
const columns = clusterValues(xPositions, config.gridTolerance);
|
|
5140
|
+
const rows = clusterValues(yPositions, config.gridTolerance);
|
|
5141
|
+
return {
|
|
5142
|
+
columns,
|
|
5143
|
+
rows,
|
|
5144
|
+
columnCount: columns.length,
|
|
5145
|
+
rowCount: rows.length
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
function computeMaxDepth(elements) {
|
|
5149
|
+
let maxDepth = 0;
|
|
5150
|
+
for (const el of elements) {
|
|
5151
|
+
const context = el.parentContext || "";
|
|
5152
|
+
const depth = context ? context.split(">").length : 1;
|
|
5153
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
5154
|
+
}
|
|
5155
|
+
return maxDepth;
|
|
5156
|
+
}
|
|
5157
|
+
function compareLayouts(sourceElements, targetElements, sourceRegions, targetRegions, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
|
|
5158
|
+
const sourceGrid = detectGridStructure(sourceElements, config);
|
|
5159
|
+
const targetGrid = detectGridStructure(targetElements, config);
|
|
5160
|
+
const gridDiff = {
|
|
5161
|
+
sourceGrid,
|
|
5162
|
+
targetGrid,
|
|
5163
|
+
columnDiff: sourceGrid.columnCount - targetGrid.columnCount,
|
|
5164
|
+
rowDiff: sourceGrid.rowCount - targetGrid.rowCount
|
|
5165
|
+
};
|
|
5166
|
+
const sourceDepth = computeMaxDepth(sourceElements);
|
|
5167
|
+
const targetDepth = computeMaxDepth(targetElements);
|
|
5168
|
+
const hierarchyDiff = {
|
|
5169
|
+
sourceDepth,
|
|
5170
|
+
targetDepth,
|
|
5171
|
+
depthDiff: sourceDepth - targetDepth
|
|
5172
|
+
};
|
|
5173
|
+
const sourceRegionCount = sourceRegions?.regions.length || 1;
|
|
5174
|
+
const targetRegionCount = targetRegions?.regions.length || 1;
|
|
5175
|
+
const sourceDensity = sourceElements.length / sourceRegionCount;
|
|
5176
|
+
const targetDensity = targetElements.length / targetRegionCount;
|
|
5177
|
+
const density = {
|
|
5178
|
+
sourceDensity: Math.round(sourceDensity * 100) / 100,
|
|
5179
|
+
targetDensity: Math.round(targetDensity * 100) / 100,
|
|
5180
|
+
ratio: targetDensity > 0 ? Math.round(sourceDensity / targetDensity * 100) / 100 : 0
|
|
5181
|
+
};
|
|
5182
|
+
const gridSimilarity = sourceGrid.columnCount === 0 && targetGrid.columnCount === 0 ? 1 : 1 - Math.abs(gridDiff.columnDiff) / Math.max(sourceGrid.columnCount, targetGrid.columnCount, 1);
|
|
5183
|
+
const hierarchySimilarity = sourceDepth === 0 && targetDepth === 0 ? 1 : 1 - Math.abs(hierarchyDiff.depthDiff) / Math.max(sourceDepth, targetDepth, 1);
|
|
5184
|
+
const densitySimilarity = density.ratio > 0 ? Math.min(density.ratio, 1 / density.ratio) : 0;
|
|
5185
|
+
const similarity = Math.round((gridSimilarity * 0.4 + hierarchySimilarity * 0.3 + densitySimilarity * 0.3) * 100) / 100;
|
|
5186
|
+
return {
|
|
5187
|
+
gridDiff,
|
|
5188
|
+
hierarchyDiff,
|
|
5189
|
+
density,
|
|
5190
|
+
similarity
|
|
5191
|
+
};
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
// src/ai/content-comparison.ts
|
|
5195
|
+
var DEFAULT_CONTENT_COMPARISON_CONFIG = {
|
|
5196
|
+
labelMatchThreshold: 0.8,
|
|
5197
|
+
headingMatchThreshold: 0.75,
|
|
5198
|
+
maxCellDifferences: 50
|
|
5199
|
+
};
|
|
5200
|
+
function getElementText2(el) {
|
|
5201
|
+
return (el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "").trim();
|
|
5202
|
+
}
|
|
5203
|
+
function getContentRole(el) {
|
|
5204
|
+
if (el.contentMetadata?.contentRole) {
|
|
5205
|
+
return el.contentMetadata.contentRole;
|
|
5206
|
+
}
|
|
5207
|
+
const t = (el.type || "").toLowerCase();
|
|
5208
|
+
if (t === "heading" || t.startsWith("h") && /^h[1-6]$/.test(t)) return "heading";
|
|
5209
|
+
if (t === "metric-value" || t === "metric") return "metric";
|
|
5210
|
+
if (t === "status-message" || t === "status") return "status";
|
|
5211
|
+
if (t === "label") return "label";
|
|
5212
|
+
if (t === "badge") return "badge";
|
|
5213
|
+
if (t === "table-cell") return "table-cell";
|
|
5214
|
+
if (t === "table-header") return "table-header";
|
|
5215
|
+
if (t === "caption") return "caption";
|
|
5216
|
+
return null;
|
|
5217
|
+
}
|
|
5218
|
+
function getHeadingLevel(el) {
|
|
5219
|
+
if (el.contentMetadata?.headingLevel) {
|
|
5220
|
+
return el.contentMetadata.headingLevel;
|
|
5221
|
+
}
|
|
5222
|
+
const tag = (el.tagName || el.type || "").toLowerCase();
|
|
5223
|
+
const match = /^h([1-6])$/.exec(tag);
|
|
5224
|
+
if (match) return parseInt(match[1], 10);
|
|
5225
|
+
return void 0;
|
|
5226
|
+
}
|
|
5227
|
+
function isContentElement2(el) {
|
|
5228
|
+
if (el.category === "content") return true;
|
|
5229
|
+
if (el.contentMetadata) return true;
|
|
5230
|
+
const role = getContentRole(el);
|
|
5231
|
+
return role !== null;
|
|
5232
|
+
}
|
|
5233
|
+
function normalizeText(text) {
|
|
5234
|
+
return normalizeString(text, { caseSensitive: false, ignoreWhitespace: true });
|
|
5235
|
+
}
|
|
5236
|
+
function parseMetricText(el) {
|
|
5237
|
+
const text = getElementText2(el);
|
|
5238
|
+
const colonMatch = text.match(/^(.+?):\s*(.+)$/);
|
|
5239
|
+
if (colonMatch) {
|
|
5240
|
+
return { label: colonMatch[1].trim(), value: colonMatch[2].trim() };
|
|
5241
|
+
}
|
|
5242
|
+
const dashMatch = text.match(/^(.+?)\s*[-]\s*(.+)$/);
|
|
5243
|
+
if (dashMatch) {
|
|
5244
|
+
return { label: dashMatch[1].trim(), value: dashMatch[2].trim() };
|
|
5245
|
+
}
|
|
5246
|
+
const elLabel = el.accessibleName || el.labelText || el.label || el.id;
|
|
5247
|
+
return { label: elLabel, value: text };
|
|
5248
|
+
}
|
|
5249
|
+
function filterHeadings(elements) {
|
|
5250
|
+
return elements.filter((el) => getContentRole(el) === "heading");
|
|
5251
|
+
}
|
|
5252
|
+
function filterMetrics(elements) {
|
|
5253
|
+
return elements.filter((el) => getContentRole(el) === "metric");
|
|
5254
|
+
}
|
|
5255
|
+
function filterStatuses(elements) {
|
|
5256
|
+
return elements.filter((el) => {
|
|
5257
|
+
const role = getContentRole(el);
|
|
5258
|
+
return role === "status" || role === "badge";
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
function filterLabels(elements) {
|
|
5262
|
+
return elements.filter((el) => {
|
|
5263
|
+
const role = getContentRole(el);
|
|
5264
|
+
return role === "label" || role === "caption";
|
|
5265
|
+
});
|
|
5266
|
+
}
|
|
5267
|
+
function matchTexts(sourceTexts, targetTexts, threshold) {
|
|
5268
|
+
const candidates = [];
|
|
5269
|
+
for (let si = 0; si < sourceTexts.length; si++) {
|
|
5270
|
+
const sNorm = normalizeText(sourceTexts[si]);
|
|
5271
|
+
if (!sNorm) continue;
|
|
5272
|
+
for (let ti = 0; ti < targetTexts.length; ti++) {
|
|
5273
|
+
const tNorm = normalizeText(targetTexts[ti]);
|
|
5274
|
+
if (!tNorm) continue;
|
|
5275
|
+
const score = sNorm === tNorm ? 1 : jaroWinklerSimilarity(sNorm, tNorm);
|
|
5276
|
+
if (score >= threshold) {
|
|
5277
|
+
candidates.push({ sourceIdx: si, targetIdx: ti, score });
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
5282
|
+
const usedSource = /* @__PURE__ */ new Set();
|
|
5283
|
+
const usedTarget = /* @__PURE__ */ new Set();
|
|
5284
|
+
const matched = [];
|
|
5285
|
+
for (const c of candidates) {
|
|
5286
|
+
if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
|
|
5287
|
+
usedSource.add(c.sourceIdx);
|
|
5288
|
+
usedTarget.add(c.targetIdx);
|
|
5289
|
+
matched.push(c);
|
|
5290
|
+
}
|
|
5291
|
+
const unmatchedSource = sourceTexts.map((_, i) => i).filter((i) => !usedSource.has(i));
|
|
5292
|
+
const unmatchedTarget = targetTexts.map((_, i) => i).filter((i) => !usedTarget.has(i));
|
|
5293
|
+
return { matched, unmatchedSource, unmatchedTarget };
|
|
5294
|
+
}
|
|
5295
|
+
function compareHeadings(sourceElements, targetElements, config) {
|
|
5296
|
+
const srcHeadings = filterHeadings(sourceElements);
|
|
5297
|
+
const tgtHeadings = filterHeadings(targetElements);
|
|
5298
|
+
const srcTexts = srcHeadings.map(getElementText2);
|
|
5299
|
+
const tgtTexts = tgtHeadings.map(getElementText2);
|
|
5300
|
+
const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
|
|
5301
|
+
srcTexts,
|
|
5302
|
+
tgtTexts,
|
|
5303
|
+
config.headingMatchThreshold
|
|
5304
|
+
);
|
|
5305
|
+
const headingMatched = [];
|
|
5306
|
+
const headingChanged = [];
|
|
5307
|
+
for (const m of matched) {
|
|
5308
|
+
const srcText = srcTexts[m.sourceIdx];
|
|
5309
|
+
const tgtText = tgtTexts[m.targetIdx];
|
|
5310
|
+
const srcLevel = getHeadingLevel(srcHeadings[m.sourceIdx]);
|
|
5311
|
+
const tgtLevel = getHeadingLevel(tgtHeadings[m.targetIdx]);
|
|
5312
|
+
if (normalizeText(srcText) === normalizeText(tgtText)) {
|
|
5313
|
+
headingMatched.push({
|
|
5314
|
+
source: srcText,
|
|
5315
|
+
target: tgtText,
|
|
5316
|
+
level: srcLevel
|
|
5317
|
+
});
|
|
5318
|
+
} else {
|
|
5319
|
+
headingChanged.push({
|
|
5320
|
+
source: srcText,
|
|
5321
|
+
target: tgtText,
|
|
5322
|
+
level: srcLevel ?? tgtLevel
|
|
5323
|
+
});
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
return {
|
|
5327
|
+
matched: headingMatched,
|
|
5328
|
+
sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
|
|
5329
|
+
targetOnly: unmatchedTarget.map((i) => tgtTexts[i]),
|
|
5330
|
+
changed: headingChanged
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5333
|
+
function compareMetrics(sourceElements, targetElements, config) {
|
|
5334
|
+
const srcMetrics = filterMetrics(sourceElements);
|
|
5335
|
+
const tgtMetrics = filterMetrics(targetElements);
|
|
5336
|
+
const srcParsed = srcMetrics.map(parseMetricText);
|
|
5337
|
+
const tgtParsed = tgtMetrics.map(parseMetricText);
|
|
5338
|
+
const srcLabels = srcParsed.map((p) => p.label);
|
|
5339
|
+
const tgtLabels = tgtParsed.map((p) => p.label);
|
|
5340
|
+
const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
|
|
5341
|
+
srcLabels,
|
|
5342
|
+
tgtLabels,
|
|
5343
|
+
config.labelMatchThreshold
|
|
5344
|
+
);
|
|
5345
|
+
const metricMatched = [];
|
|
5346
|
+
const metricChanged = [];
|
|
5347
|
+
for (const m of matched) {
|
|
5348
|
+
const src = srcParsed[m.sourceIdx];
|
|
5349
|
+
const tgt = tgtParsed[m.targetIdx];
|
|
5350
|
+
if (normalizeText(src.value) === normalizeText(tgt.value)) {
|
|
5351
|
+
metricMatched.push({
|
|
5352
|
+
label: src.label,
|
|
5353
|
+
sourceValue: src.value,
|
|
5354
|
+
targetValue: tgt.value
|
|
5355
|
+
});
|
|
5356
|
+
} else {
|
|
5357
|
+
metricChanged.push({
|
|
5358
|
+
label: src.label,
|
|
5359
|
+
sourceValue: src.value,
|
|
5360
|
+
targetValue: tgt.value
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
return {
|
|
5365
|
+
matched: metricMatched,
|
|
5366
|
+
changed: metricChanged,
|
|
5367
|
+
sourceOnly: unmatchedSource.map((i) => srcParsed[i].label),
|
|
5368
|
+
targetOnly: unmatchedTarget.map((i) => tgtParsed[i].label)
|
|
5369
|
+
};
|
|
5370
|
+
}
|
|
5371
|
+
function compareStatuses(sourceElements, targetElements, config) {
|
|
5372
|
+
const srcStatuses = filterStatuses(sourceElements);
|
|
5373
|
+
const tgtStatuses = filterStatuses(targetElements);
|
|
5374
|
+
const srcParsed = srcStatuses.map(parseMetricText);
|
|
5375
|
+
const tgtParsed = tgtStatuses.map(parseMetricText);
|
|
5376
|
+
const srcLabels = srcParsed.map((p) => p.label);
|
|
5377
|
+
const tgtLabels = tgtParsed.map((p) => p.label);
|
|
5378
|
+
const { matched } = matchTexts(srcLabels, tgtLabels, config.labelMatchThreshold);
|
|
5379
|
+
const statusMatched = [];
|
|
5380
|
+
const statusChanged = [];
|
|
5381
|
+
for (const m of matched) {
|
|
5382
|
+
const src = srcParsed[m.sourceIdx];
|
|
5383
|
+
const tgt = tgtParsed[m.targetIdx];
|
|
5384
|
+
if (normalizeText(src.value) === normalizeText(tgt.value)) {
|
|
5385
|
+
statusMatched.push({
|
|
5386
|
+
label: src.label,
|
|
5387
|
+
sourceStatus: src.value,
|
|
5388
|
+
targetStatus: tgt.value
|
|
5389
|
+
});
|
|
5390
|
+
} else {
|
|
5391
|
+
statusChanged.push({
|
|
5392
|
+
label: src.label,
|
|
5393
|
+
sourceStatus: src.value,
|
|
5394
|
+
targetStatus: tgt.value
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
return {
|
|
5399
|
+
matched: statusMatched,
|
|
5400
|
+
changed: statusChanged
|
|
5401
|
+
};
|
|
5402
|
+
}
|
|
5403
|
+
function compareLabels(sourceElements, targetElements, config) {
|
|
5404
|
+
const srcLabels = filterLabels(sourceElements);
|
|
5405
|
+
const tgtLabels = filterLabels(targetElements);
|
|
5406
|
+
const srcTexts = srcLabels.map(getElementText2);
|
|
5407
|
+
const tgtTexts = tgtLabels.map(getElementText2);
|
|
5408
|
+
const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
|
|
5409
|
+
srcTexts,
|
|
5410
|
+
tgtTexts,
|
|
5411
|
+
config.labelMatchThreshold
|
|
5412
|
+
);
|
|
5413
|
+
return {
|
|
5414
|
+
matched: matched.map((m) => srcTexts[m.sourceIdx]),
|
|
5415
|
+
sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
|
|
5416
|
+
targetOnly: unmatchedTarget.map((i) => tgtTexts[i])
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
function compareTables(sourceElements, targetElements, config) {
|
|
5420
|
+
const srcData = extractStructuredData(sourceElements);
|
|
5421
|
+
const tgtData = extractStructuredData(targetElements);
|
|
5422
|
+
const srcTables = srcData.tables;
|
|
5423
|
+
const tgtTables = tgtData.tables;
|
|
5424
|
+
if (srcTables.length === 0 || tgtTables.length === 0) {
|
|
5425
|
+
return [];
|
|
5426
|
+
}
|
|
5427
|
+
const srcTableLabels = srcTables.map((t) => t.label || "");
|
|
5428
|
+
const tgtTableLabels = tgtTables.map((t) => t.label || "");
|
|
5429
|
+
const { matched } = matchTexts(srcTableLabels, tgtTableLabels, config.labelMatchThreshold);
|
|
5430
|
+
const tablePairs = [];
|
|
5431
|
+
if (matched.length > 0) {
|
|
5432
|
+
for (const m of matched) {
|
|
5433
|
+
tablePairs.push({ srcIdx: m.sourceIdx, tgtIdx: m.targetIdx });
|
|
5434
|
+
}
|
|
5435
|
+
} else if (srcTables.length === 1 && tgtTables.length === 1) {
|
|
5436
|
+
tablePairs.push({ srcIdx: 0, tgtIdx: 0 });
|
|
5437
|
+
}
|
|
5438
|
+
const comparisons = [];
|
|
5439
|
+
for (const pair of tablePairs) {
|
|
5440
|
+
const srcTable = srcTables[pair.srcIdx];
|
|
5441
|
+
const tgtTable = tgtTables[pair.tgtIdx];
|
|
5442
|
+
const srcHeaders = srcTable.columns.map((c) => c.header);
|
|
5443
|
+
const tgtHeaders = tgtTable.columns.map((c) => c.header);
|
|
5444
|
+
const srcHeaderSet = new Set(srcHeaders.map(normalizeText));
|
|
5445
|
+
const tgtHeaderSet = new Set(tgtHeaders.map(normalizeText));
|
|
5446
|
+
const sourceOnlyColumns = srcHeaders.filter((h) => !tgtHeaderSet.has(normalizeText(h)));
|
|
5447
|
+
const targetOnlyColumns = tgtHeaders.filter((h) => !srcHeaderSet.has(normalizeText(h)));
|
|
5448
|
+
const columnsMatch = sourceOnlyColumns.length === 0 && targetOnlyColumns.length === 0;
|
|
5449
|
+
const cellDifferences = [];
|
|
5450
|
+
const commonHeaders = srcHeaders.filter((h) => tgtHeaderSet.has(normalizeText(h)));
|
|
5451
|
+
const minRows = Math.min(srcTable.rows.length, tgtTable.rows.length);
|
|
5452
|
+
for (let row = 0; row < minRows; row++) {
|
|
5453
|
+
if (cellDifferences.length >= config.maxCellDifferences) break;
|
|
5454
|
+
for (const header of commonHeaders) {
|
|
5455
|
+
const srcColIdx = srcHeaders.indexOf(header);
|
|
5456
|
+
const tgtColIdx = tgtHeaders.findIndex((h) => normalizeText(h) === normalizeText(header));
|
|
5457
|
+
if (srcColIdx < 0 || tgtColIdx < 0) continue;
|
|
5458
|
+
const srcValue = srcTable.rows[row]?.[srcColIdx] ?? "";
|
|
5459
|
+
const tgtValue = tgtTable.rows[row]?.[tgtColIdx] ?? "";
|
|
5460
|
+
if (normalizeText(srcValue) !== normalizeText(tgtValue)) {
|
|
5461
|
+
cellDifferences.push({
|
|
5462
|
+
row,
|
|
5463
|
+
column: header,
|
|
5464
|
+
sourceValue: srcValue,
|
|
5465
|
+
targetValue: tgtValue
|
|
5466
|
+
});
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
comparisons.push({
|
|
5471
|
+
sourceLabel: srcTable.label,
|
|
5472
|
+
targetLabel: tgtTable.label,
|
|
5473
|
+
columnsMatch,
|
|
5474
|
+
sourceOnlyColumns,
|
|
5475
|
+
targetOnlyColumns,
|
|
5476
|
+
sourceRowCount: srcTable.rows.length,
|
|
5477
|
+
targetRowCount: tgtTable.rows.length,
|
|
5478
|
+
cellDifferences
|
|
5479
|
+
});
|
|
5480
|
+
}
|
|
5481
|
+
return comparisons;
|
|
5482
|
+
}
|
|
5483
|
+
function compareHeadingHierarchy(sourceElements, targetElements) {
|
|
5484
|
+
const srcHeadings = filterHeadings(sourceElements);
|
|
5485
|
+
const tgtHeadings = filterHeadings(targetElements);
|
|
5486
|
+
const srcByLevel = /* @__PURE__ */ new Map();
|
|
5487
|
+
const tgtByLevel = /* @__PURE__ */ new Map();
|
|
5488
|
+
for (const el of srcHeadings) {
|
|
5489
|
+
const level = getHeadingLevel(el) ?? 0;
|
|
5490
|
+
srcByLevel.set(level, (srcByLevel.get(level) ?? 0) + 1);
|
|
5491
|
+
}
|
|
5492
|
+
for (const el of tgtHeadings) {
|
|
5493
|
+
const level = getHeadingLevel(el) ?? 0;
|
|
5494
|
+
tgtByLevel.set(level, (tgtByLevel.get(level) ?? 0) + 1);
|
|
5495
|
+
}
|
|
5496
|
+
const allLevels = /* @__PURE__ */ new Set([...srcByLevel.keys(), ...tgtByLevel.keys()]);
|
|
5497
|
+
const result = [];
|
|
5498
|
+
for (const level of [...allLevels].sort()) {
|
|
5499
|
+
result.push({
|
|
5500
|
+
level,
|
|
5501
|
+
sourceCount: srcByLevel.get(level) ?? 0,
|
|
5502
|
+
targetCount: tgtByLevel.get(level) ?? 0
|
|
5503
|
+
});
|
|
5504
|
+
}
|
|
5505
|
+
return result;
|
|
5506
|
+
}
|
|
5507
|
+
function compareContent(sourceElements, targetElements, config = DEFAULT_CONTENT_COMPARISON_CONFIG) {
|
|
5508
|
+
const srcContent = sourceElements.filter(isContentElement2);
|
|
5509
|
+
const tgtContent = targetElements.filter(isContentElement2);
|
|
5510
|
+
const headings = compareHeadings(srcContent, tgtContent, config);
|
|
5511
|
+
const metrics = compareMetrics(srcContent, tgtContent, config);
|
|
5512
|
+
const statuses = compareStatuses(srcContent, tgtContent, config);
|
|
5513
|
+
const labels = compareLabels(srcContent, tgtContent, config);
|
|
5514
|
+
const tables = compareTables(sourceElements, targetElements, config);
|
|
5515
|
+
const headingHierarchy = compareHeadingHierarchy(srcContent, tgtContent);
|
|
5516
|
+
const contentParity = calculateContentParity(headings, metrics, statuses, labels, tables);
|
|
5517
|
+
return {
|
|
5518
|
+
headings,
|
|
5519
|
+
metrics,
|
|
5520
|
+
statuses,
|
|
5521
|
+
labels,
|
|
5522
|
+
tables,
|
|
5523
|
+
headingHierarchy,
|
|
5524
|
+
contentParity
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
function calculateContentParity(headings, metrics, statuses, labels, tables) {
|
|
5528
|
+
const scores = [];
|
|
5529
|
+
const totalHeadings = headings.matched.length + headings.changed.length + headings.sourceOnly.length + headings.targetOnly.length;
|
|
5530
|
+
if (totalHeadings > 0) {
|
|
5531
|
+
scores.push(headings.matched.length / totalHeadings);
|
|
5532
|
+
}
|
|
5533
|
+
const totalMetrics = metrics.matched.length + metrics.changed.length + metrics.sourceOnly.length + metrics.targetOnly.length;
|
|
5534
|
+
if (totalMetrics > 0) {
|
|
5535
|
+
const metricScore = (metrics.matched.length + metrics.changed.length * 0.5) / totalMetrics;
|
|
5536
|
+
scores.push(metricScore);
|
|
5537
|
+
}
|
|
5538
|
+
const totalStatuses = statuses.matched.length + statuses.changed.length;
|
|
5539
|
+
if (totalStatuses > 0) {
|
|
5540
|
+
scores.push(statuses.matched.length / totalStatuses);
|
|
5541
|
+
}
|
|
5542
|
+
const totalLabels = labels.matched.length + labels.sourceOnly.length + labels.targetOnly.length;
|
|
5543
|
+
if (totalLabels > 0) {
|
|
5544
|
+
scores.push(labels.matched.length / totalLabels);
|
|
5545
|
+
}
|
|
5546
|
+
if (tables.length > 0) {
|
|
5547
|
+
let tableScore = 0;
|
|
5548
|
+
for (const table of tables) {
|
|
5549
|
+
let tScore = table.columnsMatch ? 0.5 : 0;
|
|
5550
|
+
if (table.sourceRowCount > 0) {
|
|
5551
|
+
const rowRatio = Math.min(
|
|
5552
|
+
table.targetRowCount / table.sourceRowCount,
|
|
5553
|
+
table.sourceRowCount / table.targetRowCount
|
|
5554
|
+
);
|
|
5555
|
+
tScore += rowRatio * 0.3;
|
|
5556
|
+
} else {
|
|
5557
|
+
tScore += 0.3;
|
|
5558
|
+
}
|
|
5559
|
+
const totalCells = Math.max(table.sourceRowCount, 1) * Math.max(
|
|
5560
|
+
table.sourceOnlyColumns.length + table.targetOnlyColumns.length + (table.columnsMatch ? 1 : 0),
|
|
5561
|
+
1
|
|
5562
|
+
);
|
|
5563
|
+
const diffRatio = totalCells > 0 ? 1 - Math.min(table.cellDifferences.length / totalCells, 1) : 1;
|
|
5564
|
+
tScore += diffRatio * 0.2;
|
|
5565
|
+
tableScore += tScore;
|
|
5566
|
+
}
|
|
5567
|
+
scores.push(tableScore / tables.length);
|
|
5568
|
+
}
|
|
5569
|
+
if (scores.length === 0) return 1;
|
|
5570
|
+
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 100) / 100;
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
// src/ai/comparison-report.ts
|
|
5574
|
+
var DEFAULT_COMPARISON_REPORT_CONFIG = {
|
|
5575
|
+
includeComponents: false
|
|
5576
|
+
};
|
|
5577
|
+
function generateComparisonReport(source, target, options) {
|
|
5578
|
+
const startTime = Date.now();
|
|
5579
|
+
const config = { ...DEFAULT_COMPARISON_REPORT_CONFIG, ...options?.config };
|
|
5580
|
+
const srcElements = source.elements;
|
|
5581
|
+
const tgtElements = target.elements;
|
|
5582
|
+
const diff = computeCrossAppDiff(srcElements, tgtElements);
|
|
5583
|
+
const navigation = buildNavigationMap(srcElements, tgtElements);
|
|
5584
|
+
const sourceRegions = segmentPageRegions(srcElements);
|
|
5585
|
+
const targetRegions = segmentPageRegions(tgtElements);
|
|
5586
|
+
const layout = compareLayouts(srcElements, tgtElements, sourceRegions, targetRegions);
|
|
5587
|
+
const actionParityResults = analyzeActionParity(diff.matchedPairs, srcElements, tgtElements);
|
|
5588
|
+
const componentComparison = config.includeComponents && options?.sourceComponents && options?.targetComponents ? compareComponents(options.sourceComponents, options.targetComponents) : null;
|
|
5589
|
+
const contentComparison = compareContent(srcElements, tgtElements);
|
|
5590
|
+
const sourceData = extractPageData(srcElements);
|
|
5591
|
+
extractPageData(tgtElements);
|
|
5592
|
+
const sourceFieldCount = Object.keys(sourceData.values).length;
|
|
5593
|
+
const matchedDataCount = diff.dataComparisons.length;
|
|
5594
|
+
const dataCompleteness = sourceFieldCount > 0 ? Math.round(matchedDataCount / sourceFieldCount * 100) / 100 : 1;
|
|
5595
|
+
const formatMatchCount = diff.dataComparisons.filter((c) => c.formatsMatch).length;
|
|
5596
|
+
const formatAlignment = matchedDataCount > 0 ? Math.round(formatMatchCount / matchedDataCount * 100) / 100 : 1;
|
|
5597
|
+
const presentationAlignment = layout.similarity;
|
|
5598
|
+
const totalNavItems = navigation.pairs.length + navigation.sourceOnly.length;
|
|
5599
|
+
const navigationParity = totalNavItems > 0 ? Math.round(navigation.pairs.length / totalNavItems * 100) / 100 : 1;
|
|
5600
|
+
const totalActionChecks = actionParityResults.length;
|
|
5601
|
+
const fullParityCount = actionParityResults.filter((r) => r.missingInTarget.length === 0).length;
|
|
5602
|
+
const actionParity = totalActionChecks > 0 ? Math.round(fullParityCount / totalActionChecks * 100) / 100 : 1;
|
|
5603
|
+
const contentParity = contentComparison.contentParity;
|
|
5604
|
+
const overallScore = Math.round(
|
|
5605
|
+
(dataCompleteness * 0.2 + formatAlignment * 0.1 + presentationAlignment * 0.15 + navigationParity * 0.15 + actionParity * 0.15 + contentParity * 0.25) * 100
|
|
5606
|
+
) / 100;
|
|
5607
|
+
const issues = [];
|
|
5608
|
+
for (const srcId of diff.unmatchedSourceIds) {
|
|
5609
|
+
const srcVal = Object.values(sourceData.values).find((v) => v.elementId === srcId);
|
|
5610
|
+
if (srcVal) {
|
|
5611
|
+
issues.push({
|
|
5612
|
+
severity: "warning",
|
|
5613
|
+
category: "missing-data",
|
|
5614
|
+
description: `Data field "${srcVal.label}" (${srcVal.dataType}) exists in source but has no match in target`,
|
|
5615
|
+
sourceElementId: srcId
|
|
5616
|
+
});
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
for (const comp of diff.dataComparisons) {
|
|
5620
|
+
if (!comp.valuesMatch) {
|
|
5621
|
+
issues.push({
|
|
5622
|
+
severity: "error",
|
|
5623
|
+
category: "value-mismatch",
|
|
5624
|
+
description: `Value mismatch for "${comp.label}": source="${comp.sourceValue}", target="${comp.targetValue}"`
|
|
5625
|
+
});
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
for (const fm of diff.formatMismatches) {
|
|
5629
|
+
issues.push({
|
|
5630
|
+
severity: fm.severity,
|
|
5631
|
+
category: "format-mismatch",
|
|
5632
|
+
description: fm.description
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
for (const ap of actionParityResults) {
|
|
5636
|
+
for (const action of ap.missingInTarget) {
|
|
5637
|
+
issues.push({
|
|
5638
|
+
severity: "warning",
|
|
5639
|
+
category: "missing-action",
|
|
5640
|
+
description: `Action "${action}" available on source element "${ap.pair.sourceLabel}" is missing in target`,
|
|
5641
|
+
sourceElementId: ap.pair.sourceId,
|
|
5642
|
+
targetElementId: ap.pair.targetId
|
|
5643
|
+
});
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
for (const srcId of navigation.sourceOnly) {
|
|
5647
|
+
issues.push({
|
|
5648
|
+
severity: "warning",
|
|
5649
|
+
category: "navigation-gap",
|
|
5650
|
+
description: `Navigation item "${srcId}" in source has no match in target`,
|
|
5651
|
+
sourceElementId: srcId
|
|
5652
|
+
});
|
|
5653
|
+
}
|
|
5654
|
+
if (layout.similarity < 0.5) {
|
|
5655
|
+
issues.push({
|
|
5656
|
+
severity: "warning",
|
|
5657
|
+
category: "layout-difference",
|
|
5658
|
+
description: `Layout similarity is low (${layout.similarity}). Grid: ${layout.gridDiff.sourceGrid.columnCount} cols vs ${layout.gridDiff.targetGrid.columnCount} cols`
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
if (componentComparison) {
|
|
5662
|
+
for (const src of componentComparison.sourceOnly) {
|
|
5663
|
+
issues.push({
|
|
5664
|
+
severity: "info",
|
|
5665
|
+
category: "component-mismatch",
|
|
5666
|
+
description: `Component "${src.name}" (${src.type}) exists in source but not target`
|
|
5667
|
+
});
|
|
5668
|
+
}
|
|
5669
|
+
for (const match of componentComparison.matches) {
|
|
5670
|
+
if (match.stateKeyDiff.missing.length > 0) {
|
|
5671
|
+
issues.push({
|
|
5672
|
+
severity: "warning",
|
|
5673
|
+
category: "component-mismatch",
|
|
5674
|
+
description: `Component "${match.source.name}": state keys missing in target: ${match.stateKeyDiff.missing.join(", ")}`
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
for (const heading of contentComparison.headings.sourceOnly) {
|
|
5680
|
+
issues.push({
|
|
5681
|
+
severity: "warning",
|
|
5682
|
+
category: "content-difference",
|
|
5683
|
+
description: `Heading "${heading}" exists in source but not in target`
|
|
5684
|
+
});
|
|
5685
|
+
}
|
|
5686
|
+
for (const heading of contentComparison.headings.targetOnly) {
|
|
5687
|
+
issues.push({
|
|
5688
|
+
severity: "info",
|
|
5689
|
+
category: "content-difference",
|
|
5690
|
+
description: `Heading "${heading}" exists in target but not in source`
|
|
5691
|
+
});
|
|
5692
|
+
}
|
|
5693
|
+
for (const change of contentComparison.headings.changed) {
|
|
5694
|
+
issues.push({
|
|
5695
|
+
severity: "warning",
|
|
5696
|
+
category: "content-difference",
|
|
5697
|
+
description: `Heading changed: "${change.source}" -> "${change.target}"`
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
for (const change of contentComparison.metrics.changed) {
|
|
5701
|
+
issues.push({
|
|
5702
|
+
severity: "warning",
|
|
5703
|
+
category: "content-difference",
|
|
5704
|
+
description: `Metric "${change.label}" value differs: "${change.sourceValue}" vs "${change.targetValue}"`
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
for (const label of contentComparison.metrics.sourceOnly) {
|
|
5708
|
+
issues.push({
|
|
5709
|
+
severity: "warning",
|
|
5710
|
+
category: "content-difference",
|
|
5711
|
+
description: `Metric "${label}" exists in source but not in target`
|
|
5712
|
+
});
|
|
5713
|
+
}
|
|
5714
|
+
for (const change of contentComparison.statuses.changed) {
|
|
5715
|
+
issues.push({
|
|
5716
|
+
severity: "warning",
|
|
5717
|
+
category: "content-difference",
|
|
5718
|
+
description: `Status "${change.label}" differs: "${change.sourceStatus}" vs "${change.targetStatus}"`
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
5721
|
+
for (const table of contentComparison.tables) {
|
|
5722
|
+
if (!table.columnsMatch) {
|
|
5723
|
+
issues.push({
|
|
5724
|
+
severity: "warning",
|
|
5725
|
+
category: "content-difference",
|
|
5726
|
+
description: `Table "${table.sourceLabel}" column mismatch: source-only=[${table.sourceOnlyColumns.join(", ")}], target-only=[${table.targetOnlyColumns.join(", ")}]`
|
|
5727
|
+
});
|
|
5728
|
+
}
|
|
5729
|
+
if (table.sourceRowCount !== table.targetRowCount) {
|
|
5730
|
+
issues.push({
|
|
5731
|
+
severity: "info",
|
|
5732
|
+
category: "content-difference",
|
|
5733
|
+
description: `Table "${table.sourceLabel}" row count differs: ${table.sourceRowCount} vs ${table.targetRowCount}`
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
if (table.cellDifferences.length > 0) {
|
|
5737
|
+
issues.push({
|
|
5738
|
+
severity: "warning",
|
|
5739
|
+
category: "content-difference",
|
|
5740
|
+
description: `Table "${table.sourceLabel}" has ${table.cellDifferences.length} cell value difference(s)`
|
|
5741
|
+
});
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
5745
|
+
issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
5746
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
5747
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
5748
|
+
const infoCount = issues.filter((i) => i.severity === "info").length;
|
|
5749
|
+
const summaryLines = [
|
|
5750
|
+
`Cross-app comparison: ${source.page.url} vs ${target.page.url}`,
|
|
5751
|
+
`Overall score: ${(overallScore * 100).toFixed(0)}%`,
|
|
5752
|
+
`Matched elements: ${diff.matchedPairs.length}`,
|
|
5753
|
+
`Unmatched: ${diff.unmatchedSourceIds.length} source, ${diff.unmatchedTargetIds.length} target`,
|
|
5754
|
+
`Navigation: ${navigation.pairs.length} matched, ${navigation.sourceOnly.length} source-only, ${navigation.targetOnly.length} target-only`
|
|
5755
|
+
];
|
|
5756
|
+
if (componentComparison) {
|
|
5757
|
+
summaryLines.push(
|
|
5758
|
+
`Components: ${componentComparison.matches.length} matched, ${componentComparison.sourceOnly.length} source-only, ${componentComparison.targetOnly.length} target-only`
|
|
5759
|
+
);
|
|
5760
|
+
}
|
|
5761
|
+
const hMatched = contentComparison.headings.matched.length;
|
|
5762
|
+
const hChanged = contentComparison.headings.changed.length;
|
|
5763
|
+
const hSrcOnly = contentComparison.headings.sourceOnly.length;
|
|
5764
|
+
const hTgtOnly = contentComparison.headings.targetOnly.length;
|
|
5765
|
+
const mMatched = contentComparison.metrics.matched.length;
|
|
5766
|
+
const mChanged = contentComparison.metrics.changed.length;
|
|
5767
|
+
const sMatched = contentComparison.statuses.matched.length;
|
|
5768
|
+
const sChanged = contentComparison.statuses.changed.length;
|
|
5769
|
+
const totalContent = hMatched + hChanged + hSrcOnly + hTgtOnly + mMatched + mChanged + sMatched + sChanged;
|
|
5770
|
+
if (totalContent > 0) {
|
|
5771
|
+
summaryLines.push(
|
|
5772
|
+
`Content: headings=${hMatched} matched/${hChanged} changed/${hSrcOnly + hTgtOnly} unmatched, metrics=${mMatched} matched/${mChanged} changed, statuses=${sMatched} matched/${sChanged} changed, parity=${(contentParity * 100).toFixed(0)}%`
|
|
5773
|
+
);
|
|
5774
|
+
}
|
|
5775
|
+
summaryLines.push(`Issues: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
|
|
5776
|
+
const summary = summaryLines.join("\n");
|
|
5777
|
+
const report = {
|
|
5778
|
+
sourceUrl: source.page.url,
|
|
5779
|
+
targetUrl: target.page.url,
|
|
5780
|
+
timestamp: Date.now(),
|
|
5781
|
+
durationMs: Date.now() - startTime,
|
|
5782
|
+
scores: {
|
|
5783
|
+
dataCompleteness,
|
|
5784
|
+
formatAlignment,
|
|
5785
|
+
presentationAlignment,
|
|
5786
|
+
navigationParity,
|
|
5787
|
+
actionParity,
|
|
5788
|
+
overallScore
|
|
5789
|
+
},
|
|
5790
|
+
diff,
|
|
5791
|
+
navigation,
|
|
5792
|
+
layout,
|
|
5793
|
+
contentComparison,
|
|
5794
|
+
issues,
|
|
5795
|
+
summary
|
|
5796
|
+
};
|
|
5797
|
+
if (componentComparison) {
|
|
5798
|
+
report.components = componentComparison;
|
|
5799
|
+
}
|
|
5800
|
+
return report;
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
// src/server/handlers.ts
|
|
5804
|
+
function success(data) {
|
|
5805
|
+
return {
|
|
5806
|
+
success: true,
|
|
5807
|
+
data,
|
|
5808
|
+
timestamp: Date.now()
|
|
5809
|
+
};
|
|
5810
|
+
}
|
|
5811
|
+
function error(message, code) {
|
|
5812
|
+
return {
|
|
5813
|
+
success: false,
|
|
5814
|
+
error: message,
|
|
5815
|
+
code,
|
|
5816
|
+
timestamp: Date.now()
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5819
|
+
function getRecoverySuggestions(errorCode) {
|
|
5820
|
+
switch (errorCode) {
|
|
5821
|
+
case "ELEMENT_NOT_FOUND":
|
|
5822
|
+
return [
|
|
5823
|
+
{
|
|
5824
|
+
suggestion: "Wait for the page to fully load",
|
|
5825
|
+
command: "wait for page to load",
|
|
5826
|
+
confidence: 0.7,
|
|
5827
|
+
retryable: true
|
|
5828
|
+
},
|
|
5829
|
+
{
|
|
5830
|
+
suggestion: "Use a different description for the element",
|
|
5831
|
+
confidence: 0.8,
|
|
5832
|
+
retryable: false
|
|
5833
|
+
},
|
|
5834
|
+
{
|
|
5835
|
+
suggestion: "Scroll the page to reveal the element",
|
|
5836
|
+
command: "scroll down",
|
|
5837
|
+
confidence: 0.6,
|
|
5838
|
+
retryable: true
|
|
5839
|
+
}
|
|
5840
|
+
];
|
|
5841
|
+
case "ELEMENT_NOT_VISIBLE":
|
|
5842
|
+
return [
|
|
5843
|
+
{
|
|
5844
|
+
suggestion: "Scroll to make the element visible",
|
|
5845
|
+
command: "scroll to element",
|
|
5846
|
+
confidence: 0.9,
|
|
5847
|
+
retryable: true
|
|
5848
|
+
},
|
|
5849
|
+
{
|
|
5850
|
+
suggestion: "Wait for any loading overlays to disappear",
|
|
5851
|
+
confidence: 0.7,
|
|
5852
|
+
retryable: true
|
|
5853
|
+
},
|
|
5854
|
+
{
|
|
5855
|
+
suggestion: "Close any blocking modals or popups",
|
|
5856
|
+
command: "click close button",
|
|
5857
|
+
confidence: 0.8,
|
|
5858
|
+
retryable: true
|
|
5859
|
+
}
|
|
5860
|
+
];
|
|
5861
|
+
case "ELEMENT_NOT_ENABLED":
|
|
5862
|
+
return [
|
|
5863
|
+
{ suggestion: "Fill in required fields first", confidence: 0.8, retryable: false },
|
|
5864
|
+
{
|
|
5865
|
+
suggestion: "Complete prerequisite steps in the form",
|
|
5866
|
+
confidence: 0.7,
|
|
5867
|
+
retryable: false
|
|
5868
|
+
},
|
|
5869
|
+
{
|
|
5870
|
+
suggestion: "Wait for the element to become enabled",
|
|
5871
|
+
command: "wait for element to be enabled",
|
|
5872
|
+
confidence: 0.6,
|
|
5873
|
+
retryable: true
|
|
5874
|
+
}
|
|
5875
|
+
];
|
|
5876
|
+
case "ELEMENT_NOT_INTERACTABLE":
|
|
5877
|
+
return [
|
|
5878
|
+
{
|
|
5879
|
+
suggestion: "Close any modal or popup blocking the element",
|
|
5880
|
+
command: "click close button",
|
|
5881
|
+
confidence: 0.9,
|
|
5882
|
+
retryable: true
|
|
5883
|
+
},
|
|
5884
|
+
{ suggestion: "Wait for animations to complete", confidence: 0.7, retryable: true },
|
|
5885
|
+
{
|
|
5886
|
+
suggestion: "Scroll the element into the viewport",
|
|
5887
|
+
command: "scroll to element",
|
|
5888
|
+
confidence: 0.8,
|
|
5889
|
+
retryable: true
|
|
5890
|
+
}
|
|
5891
|
+
];
|
|
5892
|
+
case "ACTION_TIMEOUT":
|
|
5893
|
+
return [
|
|
5894
|
+
{ suggestion: "Increase the timeout duration", confidence: 0.8, retryable: true },
|
|
5895
|
+
{ suggestion: "Check if the condition can ever be met", confidence: 0.7, retryable: false },
|
|
5896
|
+
{
|
|
5897
|
+
suggestion: "Verify the page is responding",
|
|
5898
|
+
command: "check page status",
|
|
5899
|
+
confidence: 0.6,
|
|
5900
|
+
retryable: true
|
|
5901
|
+
}
|
|
5902
|
+
];
|
|
5903
|
+
case "LOW_CONFIDENCE":
|
|
5904
|
+
return [
|
|
5905
|
+
{
|
|
5906
|
+
suggestion: "Use the exact text shown on the element",
|
|
5907
|
+
confidence: 0.9,
|
|
5908
|
+
retryable: false
|
|
5909
|
+
},
|
|
5910
|
+
{
|
|
5911
|
+
suggestion: "Try a different description that more closely matches the element",
|
|
5912
|
+
confidence: 0.8,
|
|
5913
|
+
retryable: false
|
|
5914
|
+
},
|
|
5915
|
+
{
|
|
5916
|
+
suggestion: "Lower the confidence threshold if the match is correct",
|
|
5917
|
+
confidence: 0.7,
|
|
5918
|
+
retryable: true
|
|
5919
|
+
}
|
|
5920
|
+
];
|
|
5921
|
+
case "AMBIGUOUS_MATCH":
|
|
5922
|
+
return [
|
|
5923
|
+
{
|
|
5924
|
+
suggestion: "Be more specific about which element you mean",
|
|
5925
|
+
confidence: 0.9,
|
|
5926
|
+
retryable: false
|
|
5927
|
+
},
|
|
5928
|
+
{
|
|
5929
|
+
suggestion: "Include the section or form name in the description",
|
|
5930
|
+
confidence: 0.8,
|
|
5931
|
+
retryable: false
|
|
5932
|
+
},
|
|
5933
|
+
{ suggestion: "Use the element ID directly", confidence: 0.7, retryable: false }
|
|
5934
|
+
];
|
|
5935
|
+
default:
|
|
5936
|
+
return [
|
|
5937
|
+
{
|
|
5938
|
+
suggestion: "Try a different approach or check the page state",
|
|
5939
|
+
confidence: 0.5,
|
|
5940
|
+
retryable: false
|
|
5941
|
+
}
|
|
5942
|
+
];
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
function createFailureDetails(errorCode, message, options = {}) {
|
|
5946
|
+
const retryableErrors = [
|
|
5947
|
+
"ELEMENT_NOT_VISIBLE",
|
|
3630
5948
|
"ACTION_TIMEOUT",
|
|
3631
5949
|
"LOW_CONFIDENCE",
|
|
3632
5950
|
"NETWORK_ERROR",
|
|
@@ -3649,6 +5967,9 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3649
5967
|
const assertionExecutor = new AssertionExecutor();
|
|
3650
5968
|
const snapshotManager = new SemanticSnapshotManager();
|
|
3651
5969
|
const diffManager = new SemanticDiffManager();
|
|
5970
|
+
const intentRegistry = /* @__PURE__ */ new Map();
|
|
5971
|
+
const consoleCapture = config.consoleCapture ?? null;
|
|
5972
|
+
const annotationStore = config.annotationStore ?? getGlobalAnnotationStore();
|
|
3652
5973
|
function refreshElements() {
|
|
3653
5974
|
const elements = registry.getAllElements();
|
|
3654
5975
|
searchEngine.updateElements(elements);
|
|
@@ -3715,10 +6036,14 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3715
6036
|
try {
|
|
3716
6037
|
const element = registry.getElement(id);
|
|
3717
6038
|
if (!element) {
|
|
3718
|
-
const failureDetails = createFailureDetails(
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
6039
|
+
const failureDetails = createFailureDetails(
|
|
6040
|
+
"ELEMENT_NOT_FOUND",
|
|
6041
|
+
`Element not found: ${id}`,
|
|
6042
|
+
{
|
|
6043
|
+
elementId: id,
|
|
6044
|
+
selectorsTried: [id]
|
|
6045
|
+
}
|
|
6046
|
+
);
|
|
3722
6047
|
return {
|
|
3723
6048
|
success: false,
|
|
3724
6049
|
error: `Element not found: ${id}`,
|
|
@@ -3748,11 +6073,15 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3748
6073
|
try {
|
|
3749
6074
|
const element = registry.getElement(id);
|
|
3750
6075
|
if (!element) {
|
|
3751
|
-
const failureDetails = createFailureDetails(
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
6076
|
+
const failureDetails = createFailureDetails(
|
|
6077
|
+
"ELEMENT_NOT_FOUND",
|
|
6078
|
+
`Element not found: ${id}`,
|
|
6079
|
+
{
|
|
6080
|
+
elementId: id,
|
|
6081
|
+
selectorsTried: [id],
|
|
6082
|
+
durationMs: Date.now() - startTime
|
|
6083
|
+
}
|
|
6084
|
+
);
|
|
3756
6085
|
return {
|
|
3757
6086
|
success: false,
|
|
3758
6087
|
error: `Element not found: ${id}`,
|
|
@@ -3787,10 +6116,14 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3787
6116
|
} else if (errorMsg.includes("blocked") || errorMsg.includes("interactable")) {
|
|
3788
6117
|
errorCode = "ELEMENT_NOT_INTERACTABLE";
|
|
3789
6118
|
}
|
|
3790
|
-
const failureDetails = createFailureDetails(
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
6119
|
+
const failureDetails = createFailureDetails(
|
|
6120
|
+
errorCode,
|
|
6121
|
+
actionResult.error || "Action failed",
|
|
6122
|
+
{
|
|
6123
|
+
elementId: id,
|
|
6124
|
+
durationMs: Date.now() - startTime
|
|
6125
|
+
}
|
|
6126
|
+
);
|
|
3794
6127
|
return success({
|
|
3795
6128
|
...actionResult,
|
|
3796
6129
|
failureDetails
|
|
@@ -3889,7 +6222,12 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3889
6222
|
try {
|
|
3890
6223
|
const findRequest = request;
|
|
3891
6224
|
const elements = registry.findElements?.(findRequest) ?? registry.getAllElements();
|
|
3892
|
-
return success({
|
|
6225
|
+
return success({
|
|
6226
|
+
elements,
|
|
6227
|
+
timestamp: Date.now(),
|
|
6228
|
+
total: elements.length,
|
|
6229
|
+
durationMs: 0
|
|
6230
|
+
});
|
|
3893
6231
|
} catch (err) {
|
|
3894
6232
|
return error(err.message, "FIND_ERROR");
|
|
3895
6233
|
}
|
|
@@ -3898,7 +6236,12 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3898
6236
|
try {
|
|
3899
6237
|
const findRequest = request;
|
|
3900
6238
|
const elements = registry.findElements?.(findRequest) ?? registry.getAllElements();
|
|
3901
|
-
return success({
|
|
6239
|
+
return success({
|
|
6240
|
+
elements,
|
|
6241
|
+
timestamp: Date.now(),
|
|
6242
|
+
total: elements.length,
|
|
6243
|
+
durationMs: 0
|
|
6244
|
+
});
|
|
3902
6245
|
} catch (err) {
|
|
3903
6246
|
return error(err.message, "DISCOVER_ERROR");
|
|
3904
6247
|
}
|
|
@@ -3995,6 +6338,28 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
3995
6338
|
return error(err.message, "ELEMENT_TREE_ERROR");
|
|
3996
6339
|
}
|
|
3997
6340
|
},
|
|
6341
|
+
getConsoleErrors: async (params) => {
|
|
6342
|
+
try {
|
|
6343
|
+
if (!consoleCapture) {
|
|
6344
|
+
return success({ errors: [], count: 0 });
|
|
6345
|
+
}
|
|
6346
|
+
const errors = params?.since ? consoleCapture.getConsoleSince(params.since) : consoleCapture.getConsoleRecent(params?.limit ?? 50);
|
|
6347
|
+
return success({ errors, count: errors.length });
|
|
6348
|
+
} catch (err) {
|
|
6349
|
+
return error(err.message, "CONSOLE_ERRORS_ERROR");
|
|
6350
|
+
}
|
|
6351
|
+
},
|
|
6352
|
+
clearConsoleErrors: async () => {
|
|
6353
|
+
try {
|
|
6354
|
+
if (!consoleCapture) {
|
|
6355
|
+
return success({ cleared: false });
|
|
6356
|
+
}
|
|
6357
|
+
consoleCapture.clear();
|
|
6358
|
+
return success({ cleared: true });
|
|
6359
|
+
} catch (err) {
|
|
6360
|
+
return error(err.message, "CONSOLE_CLEAR_ERROR");
|
|
6361
|
+
}
|
|
6362
|
+
},
|
|
3998
6363
|
// =========================================================================
|
|
3999
6364
|
// AI-Native Handlers
|
|
4000
6365
|
// =========================================================================
|
|
@@ -4013,106 +6378,219 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
4013
6378
|
const response = await nlExecutor.execute(request);
|
|
4014
6379
|
return success(response);
|
|
4015
6380
|
} catch (err) {
|
|
4016
|
-
return error(err.message, "AI_EXECUTE_ERROR");
|
|
6381
|
+
return error(err.message, "AI_EXECUTE_ERROR");
|
|
6382
|
+
}
|
|
6383
|
+
},
|
|
6384
|
+
aiAssert: async (request) => {
|
|
6385
|
+
try {
|
|
6386
|
+
refreshElements();
|
|
6387
|
+
const result = await assertionExecutor.assert(request);
|
|
6388
|
+
return success(result);
|
|
6389
|
+
} catch (err) {
|
|
6390
|
+
return error(err.message, "AI_ASSERT_ERROR");
|
|
6391
|
+
}
|
|
6392
|
+
},
|
|
6393
|
+
aiAssertBatch: async (request) => {
|
|
6394
|
+
try {
|
|
6395
|
+
refreshElements();
|
|
6396
|
+
const result = await assertionExecutor.assertBatch(request);
|
|
6397
|
+
return success(result);
|
|
6398
|
+
} catch (err) {
|
|
6399
|
+
return error(err.message, "AI_ASSERT_BATCH_ERROR");
|
|
6400
|
+
}
|
|
6401
|
+
},
|
|
6402
|
+
getSemanticSnapshot: async () => {
|
|
6403
|
+
try {
|
|
6404
|
+
const controlSnapshot = registry.createSnapshot();
|
|
6405
|
+
const snapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
6406
|
+
return success(snapshot);
|
|
6407
|
+
} catch (err) {
|
|
6408
|
+
return error(err.message, "SEMANTIC_SNAPSHOT_ERROR");
|
|
6409
|
+
}
|
|
6410
|
+
},
|
|
6411
|
+
getSemanticDiff: async (_since) => {
|
|
6412
|
+
try {
|
|
6413
|
+
const controlSnapshot = registry.createSnapshot();
|
|
6414
|
+
const currentSnapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
6415
|
+
const diff = diffManager.update(currentSnapshot);
|
|
6416
|
+
return success(diff);
|
|
6417
|
+
} catch (err) {
|
|
6418
|
+
return error(err.message, "SEMANTIC_DIFF_ERROR");
|
|
6419
|
+
}
|
|
6420
|
+
},
|
|
6421
|
+
getPageSummary: async () => {
|
|
6422
|
+
try {
|
|
6423
|
+
const snapshot = registry.createSnapshot();
|
|
6424
|
+
const elements = snapshot.elements.map((el) => ({
|
|
6425
|
+
...el,
|
|
6426
|
+
description: el.label || el.id,
|
|
6427
|
+
aliases: [],
|
|
6428
|
+
suggestedActions: [],
|
|
6429
|
+
tagName: el.type,
|
|
6430
|
+
accessibleName: el.label,
|
|
6431
|
+
registered: true
|
|
6432
|
+
}));
|
|
6433
|
+
const summary = generatePageSummary(elements);
|
|
6434
|
+
return success(summary);
|
|
6435
|
+
} catch (err) {
|
|
6436
|
+
return error(err.message, "PAGE_SUMMARY_ERROR");
|
|
6437
|
+
}
|
|
6438
|
+
},
|
|
6439
|
+
// =========================================================================
|
|
6440
|
+
// Semantic Search Handler (Embedding-based)
|
|
6441
|
+
// =========================================================================
|
|
6442
|
+
// =========================================================================
|
|
6443
|
+
// Page Navigation Handlers
|
|
6444
|
+
// =========================================================================
|
|
6445
|
+
pageRefresh: async () => {
|
|
6446
|
+
try {
|
|
6447
|
+
window.location.reload();
|
|
6448
|
+
return success({ success: true, url: window.location.href, timestamp: Date.now() });
|
|
6449
|
+
} catch (err) {
|
|
6450
|
+
return error(err.message, "PAGE_REFRESH_ERROR");
|
|
6451
|
+
}
|
|
6452
|
+
},
|
|
6453
|
+
pageNavigate: async (request) => {
|
|
6454
|
+
try {
|
|
6455
|
+
if (!request.url) {
|
|
6456
|
+
return error("URL is required", "INVALID_REQUEST");
|
|
6457
|
+
}
|
|
6458
|
+
window.location.href = request.url;
|
|
6459
|
+
return success({ success: true, url: request.url, timestamp: Date.now() });
|
|
6460
|
+
} catch (err) {
|
|
6461
|
+
return error(err.message, "PAGE_NAVIGATE_ERROR");
|
|
6462
|
+
}
|
|
6463
|
+
},
|
|
6464
|
+
pageGoBack: async () => {
|
|
6465
|
+
try {
|
|
6466
|
+
window.history.back();
|
|
6467
|
+
return success({ success: true, url: window.location.href, timestamp: Date.now() });
|
|
6468
|
+
} catch (err) {
|
|
6469
|
+
return error(err.message, "PAGE_GO_BACK_ERROR");
|
|
6470
|
+
}
|
|
6471
|
+
},
|
|
6472
|
+
pageGoForward: async () => {
|
|
6473
|
+
try {
|
|
6474
|
+
window.history.forward();
|
|
6475
|
+
return success({ success: true, url: window.location.href, timestamp: Date.now() });
|
|
6476
|
+
} catch (err) {
|
|
6477
|
+
return error(err.message, "PAGE_GO_FORWARD_ERROR");
|
|
6478
|
+
}
|
|
6479
|
+
},
|
|
6480
|
+
// =========================================================================
|
|
6481
|
+
// Annotation Handlers
|
|
6482
|
+
//
|
|
6483
|
+
// REST API endpoints for managing element annotations:
|
|
6484
|
+
// GET /annotations - List all annotations
|
|
6485
|
+
// GET /annotations/export - Export all annotations as AnnotationConfig
|
|
6486
|
+
// GET /annotations/coverage - Get annotation coverage statistics
|
|
6487
|
+
// GET /annotations/:id - Get annotation for a specific element
|
|
6488
|
+
// PUT /annotations/:id - Create or update an annotation
|
|
6489
|
+
// DELETE /annotations/:id - Delete an annotation
|
|
6490
|
+
// POST /annotations/import - Import annotations from AnnotationConfig
|
|
6491
|
+
// =========================================================================
|
|
6492
|
+
getAnnotations: async () => {
|
|
6493
|
+
try {
|
|
6494
|
+
return success(annotationStore.getAll());
|
|
6495
|
+
} catch (err) {
|
|
6496
|
+
return error(err.message, "ANNOTATIONS_ERROR");
|
|
6497
|
+
}
|
|
6498
|
+
},
|
|
6499
|
+
getAnnotation: async (id) => {
|
|
6500
|
+
try {
|
|
6501
|
+
const annotation = annotationStore.get(id);
|
|
6502
|
+
if (!annotation) {
|
|
6503
|
+
return error(`Annotation not found: ${id}`, "NOT_FOUND");
|
|
6504
|
+
}
|
|
6505
|
+
return success(annotation);
|
|
6506
|
+
} catch (err) {
|
|
6507
|
+
return error(err.message, "ANNOTATION_ERROR");
|
|
4017
6508
|
}
|
|
4018
6509
|
},
|
|
4019
|
-
|
|
6510
|
+
setAnnotation: async (id, annotation) => {
|
|
4020
6511
|
try {
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
return success(result);
|
|
6512
|
+
annotationStore.set(id, annotation);
|
|
6513
|
+
return success(annotationStore.get(id));
|
|
4024
6514
|
} catch (err) {
|
|
4025
|
-
return error(err.message, "
|
|
6515
|
+
return error(err.message, "ANNOTATION_SET_ERROR");
|
|
4026
6516
|
}
|
|
4027
6517
|
},
|
|
4028
|
-
|
|
6518
|
+
deleteAnnotation: async (id) => {
|
|
4029
6519
|
try {
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
6520
|
+
const existed = annotationStore.delete(id);
|
|
6521
|
+
if (!existed) {
|
|
6522
|
+
return error(`Annotation not found: ${id}`, "NOT_FOUND");
|
|
6523
|
+
}
|
|
6524
|
+
return success(void 0);
|
|
4033
6525
|
} catch (err) {
|
|
4034
|
-
return error(err.message, "
|
|
6526
|
+
return error(err.message, "ANNOTATION_DELETE_ERROR");
|
|
4035
6527
|
}
|
|
4036
6528
|
},
|
|
4037
|
-
|
|
6529
|
+
importAnnotations: async (config2) => {
|
|
4038
6530
|
try {
|
|
4039
|
-
const
|
|
4040
|
-
|
|
4041
|
-
return success(snapshot);
|
|
6531
|
+
const count = annotationStore.importConfig(config2);
|
|
6532
|
+
return success({ count });
|
|
4042
6533
|
} catch (err) {
|
|
4043
|
-
return error(err.message, "
|
|
6534
|
+
return error(err.message, "ANNOTATION_IMPORT_ERROR");
|
|
4044
6535
|
}
|
|
4045
6536
|
},
|
|
4046
|
-
|
|
6537
|
+
exportAnnotations: async () => {
|
|
4047
6538
|
try {
|
|
4048
|
-
|
|
4049
|
-
const currentSnapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
4050
|
-
const diff = diffManager.update(currentSnapshot);
|
|
4051
|
-
return success(diff);
|
|
6539
|
+
return success(annotationStore.exportConfig());
|
|
4052
6540
|
} catch (err) {
|
|
4053
|
-
return error(err.message, "
|
|
6541
|
+
return error(err.message, "ANNOTATION_EXPORT_ERROR");
|
|
4054
6542
|
}
|
|
4055
6543
|
},
|
|
4056
|
-
|
|
6544
|
+
getAnnotationCoverage: async () => {
|
|
4057
6545
|
try {
|
|
4058
|
-
const
|
|
4059
|
-
const
|
|
4060
|
-
|
|
4061
|
-
description: el.label || el.id,
|
|
4062
|
-
aliases: [],
|
|
4063
|
-
suggestedActions: [],
|
|
4064
|
-
tagName: el.type,
|
|
4065
|
-
accessibleName: el.label,
|
|
4066
|
-
registered: true
|
|
4067
|
-
}));
|
|
4068
|
-
const summary = generatePageSummary(elements);
|
|
4069
|
-
return success(summary);
|
|
6546
|
+
const allElements = registry.getAllElements();
|
|
6547
|
+
const allIds = allElements.map((el) => el.id);
|
|
6548
|
+
return success(annotationStore.getCoverage(allIds));
|
|
4070
6549
|
} catch (err) {
|
|
4071
|
-
return error(err.message, "
|
|
6550
|
+
return error(err.message, "ANNOTATION_COVERAGE_ERROR");
|
|
4072
6551
|
}
|
|
4073
6552
|
},
|
|
4074
|
-
// =========================================================================
|
|
4075
|
-
// Semantic Search Handler (Embedding-based)
|
|
4076
|
-
// =========================================================================
|
|
4077
6553
|
aiSemanticSearch: async (criteria) => {
|
|
4078
6554
|
const startTime = performance.now();
|
|
4079
6555
|
try {
|
|
4080
6556
|
refreshElements();
|
|
4081
6557
|
const allElements = registry.getAllElements();
|
|
4082
|
-
const aiElements = allElements.map(
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
6558
|
+
const aiElements = allElements.map(
|
|
6559
|
+
(el) => {
|
|
6560
|
+
const textParts = [];
|
|
6561
|
+
const state = "getState" in el ? el.getState() : el.state;
|
|
6562
|
+
const textContent = state?.textContent || "";
|
|
6563
|
+
const label = el.label || "";
|
|
6564
|
+
const accessibleName = el.accessibleName || "";
|
|
6565
|
+
const placeholder = el.placeholder || "";
|
|
6566
|
+
const title = el.title || "";
|
|
6567
|
+
if (label) textParts.push(label);
|
|
6568
|
+
if (accessibleName && accessibleName !== label) textParts.push(accessibleName);
|
|
6569
|
+
if (textContent && textContent !== label && textContent !== accessibleName) {
|
|
6570
|
+
textParts.push(textContent);
|
|
6571
|
+
}
|
|
6572
|
+
if (placeholder) textParts.push(`placeholder: ${placeholder}`);
|
|
6573
|
+
if (title) textParts.push(title);
|
|
6574
|
+
const combinedText = textParts.join(" ").trim() || el.id;
|
|
6575
|
+
return {
|
|
6576
|
+
element: {
|
|
6577
|
+
id: el.id,
|
|
6578
|
+
type: el.type,
|
|
6579
|
+
label: el.label,
|
|
6580
|
+
tagName: el.tagName || el.type,
|
|
6581
|
+
role: el.role,
|
|
6582
|
+
accessibleName: el.accessibleName,
|
|
6583
|
+
actions: el.actions || [],
|
|
6584
|
+
state: state || {},
|
|
6585
|
+
registered: true,
|
|
6586
|
+
description: label || el.id,
|
|
6587
|
+
aliases: [],
|
|
6588
|
+
suggestedActions: []
|
|
6589
|
+
},
|
|
6590
|
+
text: combinedText
|
|
6591
|
+
};
|
|
4094
6592
|
}
|
|
4095
|
-
|
|
4096
|
-
if (title) textParts.push(title);
|
|
4097
|
-
const combinedText = textParts.join(" ").trim() || el.id;
|
|
4098
|
-
return {
|
|
4099
|
-
element: {
|
|
4100
|
-
id: el.id,
|
|
4101
|
-
type: el.type,
|
|
4102
|
-
label: el.label,
|
|
4103
|
-
tagName: el.tagName || el.type,
|
|
4104
|
-
role: el.role,
|
|
4105
|
-
accessibleName: el.accessibleName,
|
|
4106
|
-
actions: el.actions || [],
|
|
4107
|
-
state: state || {},
|
|
4108
|
-
registered: true,
|
|
4109
|
-
description: label || el.id,
|
|
4110
|
-
aliases: [],
|
|
4111
|
-
suggestedActions: []
|
|
4112
|
-
},
|
|
4113
|
-
text: combinedText
|
|
4114
|
-
};
|
|
4115
|
-
});
|
|
6593
|
+
);
|
|
4116
6594
|
let filteredElements = aiElements;
|
|
4117
6595
|
if (criteria.type) {
|
|
4118
6596
|
filteredElements = filteredElements.filter(
|
|
@@ -4177,6 +6655,398 @@ function createHandlers(registry, actionExecutor, config = {}) {
|
|
|
4177
6655
|
} catch (err) {
|
|
4178
6656
|
return error(err.message, "AI_SEMANTIC_SEARCH_ERROR");
|
|
4179
6657
|
}
|
|
6658
|
+
},
|
|
6659
|
+
// =========================================================================
|
|
6660
|
+
// State Management Handlers
|
|
6661
|
+
// =========================================================================
|
|
6662
|
+
getStates: async () => {
|
|
6663
|
+
try {
|
|
6664
|
+
const states = registry.getStates?.() ?? [];
|
|
6665
|
+
return success(states);
|
|
6666
|
+
} catch (err) {
|
|
6667
|
+
return error(err.message, "STATES_ERROR");
|
|
6668
|
+
}
|
|
6669
|
+
},
|
|
6670
|
+
getState: async (id) => {
|
|
6671
|
+
try {
|
|
6672
|
+
const state = registry.getState?.(id);
|
|
6673
|
+
if (!state) {
|
|
6674
|
+
return error(`State not found: ${id}`, "NOT_FOUND");
|
|
6675
|
+
}
|
|
6676
|
+
return success(state);
|
|
6677
|
+
} catch (err) {
|
|
6678
|
+
return error(err.message, "STATE_ERROR");
|
|
6679
|
+
}
|
|
6680
|
+
},
|
|
6681
|
+
getActiveStates: async () => {
|
|
6682
|
+
try {
|
|
6683
|
+
const states = registry.getActiveStates?.() ?? [];
|
|
6684
|
+
return success(states);
|
|
6685
|
+
} catch (err) {
|
|
6686
|
+
return error(err.message, "ACTIVE_STATES_ERROR");
|
|
6687
|
+
}
|
|
6688
|
+
},
|
|
6689
|
+
activateState: async (id) => {
|
|
6690
|
+
try {
|
|
6691
|
+
if (!registry.activateState) {
|
|
6692
|
+
return error("State management not available", "NOT_IMPLEMENTED");
|
|
6693
|
+
}
|
|
6694
|
+
registry.activateState(id);
|
|
6695
|
+
return success(void 0);
|
|
6696
|
+
} catch (err) {
|
|
6697
|
+
return error(err.message, "ACTIVATE_STATE_ERROR");
|
|
6698
|
+
}
|
|
6699
|
+
},
|
|
6700
|
+
deactivateState: async (id) => {
|
|
6701
|
+
try {
|
|
6702
|
+
if (!registry.deactivateState) {
|
|
6703
|
+
return error("State management not available", "NOT_IMPLEMENTED");
|
|
6704
|
+
}
|
|
6705
|
+
registry.deactivateState(id);
|
|
6706
|
+
return success(void 0);
|
|
6707
|
+
} catch (err) {
|
|
6708
|
+
return error(err.message, "DEACTIVATE_STATE_ERROR");
|
|
6709
|
+
}
|
|
6710
|
+
},
|
|
6711
|
+
getStateGroups: async () => {
|
|
6712
|
+
try {
|
|
6713
|
+
const groups = registry.getStateGroups?.() ?? [];
|
|
6714
|
+
return success(groups);
|
|
6715
|
+
} catch (err) {
|
|
6716
|
+
return error(err.message, "STATE_GROUPS_ERROR");
|
|
6717
|
+
}
|
|
6718
|
+
},
|
|
6719
|
+
activateStateGroup: async (id) => {
|
|
6720
|
+
try {
|
|
6721
|
+
if (!registry.activateStateGroup) {
|
|
6722
|
+
return error("State group management not available", "NOT_IMPLEMENTED");
|
|
6723
|
+
}
|
|
6724
|
+
registry.activateStateGroup(id);
|
|
6725
|
+
return success(void 0);
|
|
6726
|
+
} catch (err) {
|
|
6727
|
+
return error(err.message, "ACTIVATE_STATE_GROUP_ERROR");
|
|
6728
|
+
}
|
|
6729
|
+
},
|
|
6730
|
+
deactivateStateGroup: async (id) => {
|
|
6731
|
+
try {
|
|
6732
|
+
if (!registry.deactivateStateGroup) {
|
|
6733
|
+
return error("State group management not available", "NOT_IMPLEMENTED");
|
|
6734
|
+
}
|
|
6735
|
+
registry.deactivateStateGroup(id);
|
|
6736
|
+
return success(void 0);
|
|
6737
|
+
} catch (err) {
|
|
6738
|
+
return error(err.message, "DEACTIVATE_STATE_GROUP_ERROR");
|
|
6739
|
+
}
|
|
6740
|
+
},
|
|
6741
|
+
getTransitions: async () => {
|
|
6742
|
+
try {
|
|
6743
|
+
const transitions = registry.getTransitions?.() ?? [];
|
|
6744
|
+
return success(transitions);
|
|
6745
|
+
} catch (err) {
|
|
6746
|
+
return error(err.message, "TRANSITIONS_ERROR");
|
|
6747
|
+
}
|
|
6748
|
+
},
|
|
6749
|
+
canExecuteTransition: async (id) => {
|
|
6750
|
+
try {
|
|
6751
|
+
if (!registry.canExecuteTransition) {
|
|
6752
|
+
return error("Transition management not available", "NOT_IMPLEMENTED");
|
|
6753
|
+
}
|
|
6754
|
+
const result = registry.canExecuteTransition(id);
|
|
6755
|
+
return success(result);
|
|
6756
|
+
} catch (err) {
|
|
6757
|
+
return error(err.message, "CAN_EXECUTE_TRANSITION_ERROR");
|
|
6758
|
+
}
|
|
6759
|
+
},
|
|
6760
|
+
executeTransition: async (id) => {
|
|
6761
|
+
try {
|
|
6762
|
+
if (!registry.executeTransition) {
|
|
6763
|
+
return error("Transition execution not available", "NOT_IMPLEMENTED");
|
|
6764
|
+
}
|
|
6765
|
+
const result = await registry.executeTransition(id);
|
|
6766
|
+
return success(result);
|
|
6767
|
+
} catch (err) {
|
|
6768
|
+
return error(err.message, "EXECUTE_TRANSITION_ERROR");
|
|
6769
|
+
}
|
|
6770
|
+
},
|
|
6771
|
+
findPath: async (request) => {
|
|
6772
|
+
try {
|
|
6773
|
+
if (!registry.findPath) {
|
|
6774
|
+
return error("Pathfinding not available", "NOT_IMPLEMENTED");
|
|
6775
|
+
}
|
|
6776
|
+
const result = registry.findPath(request.targetStates);
|
|
6777
|
+
return success(result);
|
|
6778
|
+
} catch (err) {
|
|
6779
|
+
return error(err.message, "FIND_PATH_ERROR");
|
|
6780
|
+
}
|
|
6781
|
+
},
|
|
6782
|
+
navigateTo: async (request) => {
|
|
6783
|
+
try {
|
|
6784
|
+
if (!registry.navigateTo) {
|
|
6785
|
+
return error("Navigation not available", "NOT_IMPLEMENTED");
|
|
6786
|
+
}
|
|
6787
|
+
const result = await registry.navigateTo(request.targetStates);
|
|
6788
|
+
return success(result);
|
|
6789
|
+
} catch (err) {
|
|
6790
|
+
return error(err.message, "NAVIGATE_TO_ERROR");
|
|
6791
|
+
}
|
|
6792
|
+
},
|
|
6793
|
+
getStateSnapshot: async () => {
|
|
6794
|
+
try {
|
|
6795
|
+
if (!registry.getStateSnapshot) {
|
|
6796
|
+
const snapshot = {
|
|
6797
|
+
timestamp: Date.now(),
|
|
6798
|
+
activeStates: (registry.getActiveStates?.() ?? []).map((s) => s.id),
|
|
6799
|
+
states: registry.getStates?.() ?? [],
|
|
6800
|
+
groups: registry.getStateGroups?.() ?? [],
|
|
6801
|
+
transitions: registry.getTransitions?.() ?? []
|
|
6802
|
+
};
|
|
6803
|
+
return success(snapshot);
|
|
6804
|
+
}
|
|
6805
|
+
return success(registry.getStateSnapshot());
|
|
6806
|
+
} catch (err) {
|
|
6807
|
+
return error(err.message, "STATE_SNAPSHOT_ERROR");
|
|
6808
|
+
}
|
|
6809
|
+
},
|
|
6810
|
+
// =========================================================================
|
|
6811
|
+
// Intent Handlers
|
|
6812
|
+
// =========================================================================
|
|
6813
|
+
executeIntent: async (request) => {
|
|
6814
|
+
const startTime = Date.now();
|
|
6815
|
+
try {
|
|
6816
|
+
refreshElements();
|
|
6817
|
+
const intent = intentRegistry.get(request.intentId);
|
|
6818
|
+
if (!intent) {
|
|
6819
|
+
return error(`Intent not found: ${request.intentId}`, "NOT_FOUND");
|
|
6820
|
+
}
|
|
6821
|
+
const nlResponse = await nlExecutor.execute({
|
|
6822
|
+
instruction: intent.description,
|
|
6823
|
+
context: `Executing intent: ${intent.name}`
|
|
6824
|
+
});
|
|
6825
|
+
return success({
|
|
6826
|
+
success: nlResponse.success,
|
|
6827
|
+
intentId: request.intentId,
|
|
6828
|
+
result: nlResponse,
|
|
6829
|
+
error: nlResponse.error,
|
|
6830
|
+
durationMs: Date.now() - startTime
|
|
6831
|
+
});
|
|
6832
|
+
} catch (err) {
|
|
6833
|
+
return error(err.message, "EXECUTE_INTENT_ERROR");
|
|
6834
|
+
}
|
|
6835
|
+
},
|
|
6836
|
+
findIntent: async (request) => {
|
|
6837
|
+
try {
|
|
6838
|
+
const query = request.query.toLowerCase();
|
|
6839
|
+
const results = [];
|
|
6840
|
+
for (const intent of intentRegistry.values()) {
|
|
6841
|
+
let confidence = 0;
|
|
6842
|
+
const nameLower = intent.name.toLowerCase();
|
|
6843
|
+
const descLower = intent.description.toLowerCase();
|
|
6844
|
+
if (nameLower === query) {
|
|
6845
|
+
confidence = 1;
|
|
6846
|
+
} else if (nameLower.includes(query) || query.includes(nameLower)) {
|
|
6847
|
+
confidence = 0.8;
|
|
6848
|
+
} else if (descLower.includes(query)) {
|
|
6849
|
+
confidence = 0.6;
|
|
6850
|
+
} else if (intent.tags?.some((t) => t.toLowerCase().includes(query))) {
|
|
6851
|
+
confidence = 0.5;
|
|
6852
|
+
}
|
|
6853
|
+
if (confidence > 0) {
|
|
6854
|
+
results.push({ intent, confidence });
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
6858
|
+
return success({ intents: results });
|
|
6859
|
+
} catch (err) {
|
|
6860
|
+
return error(err.message, "FIND_INTENT_ERROR");
|
|
6861
|
+
}
|
|
6862
|
+
},
|
|
6863
|
+
listIntents: async () => {
|
|
6864
|
+
try {
|
|
6865
|
+
return success(Array.from(intentRegistry.values()));
|
|
6866
|
+
} catch (err) {
|
|
6867
|
+
return error(err.message, "LIST_INTENTS_ERROR");
|
|
6868
|
+
}
|
|
6869
|
+
},
|
|
6870
|
+
registerIntent: async (intent) => {
|
|
6871
|
+
try {
|
|
6872
|
+
intentRegistry.set(intent.id, intent);
|
|
6873
|
+
return success(intent);
|
|
6874
|
+
} catch (err) {
|
|
6875
|
+
return error(err.message, "REGISTER_INTENT_ERROR");
|
|
6876
|
+
}
|
|
6877
|
+
},
|
|
6878
|
+
executeIntentFromQuery: async (request) => {
|
|
6879
|
+
const startTime = Date.now();
|
|
6880
|
+
try {
|
|
6881
|
+
refreshElements();
|
|
6882
|
+
const query = request.query.toLowerCase();
|
|
6883
|
+
let bestIntent = null;
|
|
6884
|
+
let bestConfidence = 0;
|
|
6885
|
+
for (const intent of intentRegistry.values()) {
|
|
6886
|
+
let confidence = 0;
|
|
6887
|
+
const nameLower = intent.name.toLowerCase();
|
|
6888
|
+
const descLower = intent.description.toLowerCase();
|
|
6889
|
+
if (nameLower === query) {
|
|
6890
|
+
confidence = 1;
|
|
6891
|
+
} else if (nameLower.includes(query) || query.includes(nameLower)) {
|
|
6892
|
+
confidence = 0.8;
|
|
6893
|
+
} else if (descLower.includes(query)) {
|
|
6894
|
+
confidence = 0.6;
|
|
6895
|
+
}
|
|
6896
|
+
if (confidence > bestConfidence) {
|
|
6897
|
+
bestConfidence = confidence;
|
|
6898
|
+
bestIntent = intent;
|
|
6899
|
+
}
|
|
6900
|
+
}
|
|
6901
|
+
if (!bestIntent) {
|
|
6902
|
+
return success({
|
|
6903
|
+
success: false,
|
|
6904
|
+
intentId: "",
|
|
6905
|
+
error: `No intent found matching query: ${request.query}`,
|
|
6906
|
+
durationMs: Date.now() - startTime
|
|
6907
|
+
});
|
|
6908
|
+
}
|
|
6909
|
+
const nlResponse = await nlExecutor.execute({
|
|
6910
|
+
instruction: bestIntent.description,
|
|
6911
|
+
context: `Executing intent from query: ${request.query}`
|
|
6912
|
+
});
|
|
6913
|
+
return success({
|
|
6914
|
+
success: nlResponse.success,
|
|
6915
|
+
intentId: bestIntent.id,
|
|
6916
|
+
result: nlResponse,
|
|
6917
|
+
error: nlResponse.error,
|
|
6918
|
+
durationMs: Date.now() - startTime
|
|
6919
|
+
});
|
|
6920
|
+
} catch (err) {
|
|
6921
|
+
return error(err.message, "EXECUTE_INTENT_FROM_QUERY_ERROR");
|
|
6922
|
+
}
|
|
6923
|
+
},
|
|
6924
|
+
// =========================================================================
|
|
6925
|
+
// Recovery Handler
|
|
6926
|
+
// =========================================================================
|
|
6927
|
+
attemptRecovery: async (request) => {
|
|
6928
|
+
const startTime = Date.now();
|
|
6929
|
+
try {
|
|
6930
|
+
refreshElements();
|
|
6931
|
+
const strategiesAttempted = [];
|
|
6932
|
+
let lastResult;
|
|
6933
|
+
const suggestions = request.failure.suggestedActions ?? [];
|
|
6934
|
+
for (let i = 0; i < Math.min(suggestions.length, request.maxRetries); i++) {
|
|
6935
|
+
const suggestion = suggestions[i];
|
|
6936
|
+
strategiesAttempted.push(suggestion.suggestion || `strategy-${i}`);
|
|
6937
|
+
const instruction = suggestion.command || request.instruction;
|
|
6938
|
+
try {
|
|
6939
|
+
const result = await nlExecutor.execute({
|
|
6940
|
+
instruction,
|
|
6941
|
+
context: `Recovery attempt ${i + 1}: ${suggestion.suggestion}`
|
|
6942
|
+
});
|
|
6943
|
+
lastResult = result;
|
|
6944
|
+
if (result.success) {
|
|
6945
|
+
return success({
|
|
6946
|
+
recovered: true,
|
|
6947
|
+
strategiesAttempted,
|
|
6948
|
+
finalResult: result,
|
|
6949
|
+
durationMs: Date.now() - startTime
|
|
6950
|
+
});
|
|
6951
|
+
}
|
|
6952
|
+
} catch {
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
if (strategiesAttempted.length === 0 || !lastResult?.success) {
|
|
6956
|
+
strategiesAttempted.push("direct-instruction");
|
|
6957
|
+
try {
|
|
6958
|
+
const result = await nlExecutor.execute({
|
|
6959
|
+
instruction: request.instruction,
|
|
6960
|
+
context: "Recovery: direct instruction attempt"
|
|
6961
|
+
});
|
|
6962
|
+
lastResult = result;
|
|
6963
|
+
if (result.success) {
|
|
6964
|
+
return success({
|
|
6965
|
+
recovered: true,
|
|
6966
|
+
strategiesAttempted,
|
|
6967
|
+
finalResult: result,
|
|
6968
|
+
durationMs: Date.now() - startTime
|
|
6969
|
+
});
|
|
6970
|
+
}
|
|
6971
|
+
} catch {
|
|
6972
|
+
}
|
|
6973
|
+
}
|
|
6974
|
+
return success({
|
|
6975
|
+
recovered: false,
|
|
6976
|
+
strategiesAttempted,
|
|
6977
|
+
finalResult: lastResult,
|
|
6978
|
+
error: "All recovery strategies exhausted",
|
|
6979
|
+
durationMs: Date.now() - startTime
|
|
6980
|
+
});
|
|
6981
|
+
} catch (err) {
|
|
6982
|
+
return error(err.message, "RECOVERY_ERROR");
|
|
6983
|
+
}
|
|
6984
|
+
},
|
|
6985
|
+
// =========================================================================
|
|
6986
|
+
// Cross-App Analysis Handlers
|
|
6987
|
+
// =========================================================================
|
|
6988
|
+
analyzePageData: async () => {
|
|
6989
|
+
try {
|
|
6990
|
+
const controlSnapshot = registry.createSnapshot();
|
|
6991
|
+
const snapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
6992
|
+
const result = extractPageData(snapshot.elements);
|
|
6993
|
+
return success(result);
|
|
6994
|
+
} catch (err) {
|
|
6995
|
+
return error(err.message, "ANALYZE_DATA_ERROR");
|
|
6996
|
+
}
|
|
6997
|
+
},
|
|
6998
|
+
analyzePageRegions: async () => {
|
|
6999
|
+
try {
|
|
7000
|
+
const controlSnapshot = registry.createSnapshot();
|
|
7001
|
+
const snapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
7002
|
+
const result = segmentPageRegions(snapshot.elements);
|
|
7003
|
+
return success(result);
|
|
7004
|
+
} catch (err) {
|
|
7005
|
+
return error(err.message, "ANALYZE_REGIONS_ERROR");
|
|
7006
|
+
}
|
|
7007
|
+
},
|
|
7008
|
+
analyzeStructuredData: async () => {
|
|
7009
|
+
try {
|
|
7010
|
+
const controlSnapshot = registry.createSnapshot();
|
|
7011
|
+
const snapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
7012
|
+
const result = extractStructuredData(snapshot.elements);
|
|
7013
|
+
return success(result);
|
|
7014
|
+
} catch (err) {
|
|
7015
|
+
return error(err.message, "ANALYZE_STRUCTURED_DATA_ERROR");
|
|
7016
|
+
}
|
|
7017
|
+
},
|
|
7018
|
+
crossAppCompare: async (request) => {
|
|
7019
|
+
try {
|
|
7020
|
+
const hasComponents = request.sourceComponents && request.targetComponents;
|
|
7021
|
+
const report = generateComparisonReport(
|
|
7022
|
+
request.sourceSnapshot,
|
|
7023
|
+
request.targetSnapshot,
|
|
7024
|
+
hasComponents ? {
|
|
7025
|
+
config: { includeComponents: true },
|
|
7026
|
+
sourceComponents: request.sourceComponents,
|
|
7027
|
+
targetComponents: request.targetComponents
|
|
7028
|
+
} : void 0
|
|
7029
|
+
);
|
|
7030
|
+
return success(report);
|
|
7031
|
+
} catch (err) {
|
|
7032
|
+
return error(err.message, "CROSS_APP_COMPARE_ERROR");
|
|
7033
|
+
}
|
|
7034
|
+
},
|
|
7035
|
+
// =========================================================================
|
|
7036
|
+
// Performance Diagnostics Handlers
|
|
7037
|
+
// =========================================================================
|
|
7038
|
+
getPerformanceEntries: async () => {
|
|
7039
|
+
return {
|
|
7040
|
+
success: true,
|
|
7041
|
+
data: { navigation: null, resources: [], paint: [] },
|
|
7042
|
+
timestamp: Date.now()
|
|
7043
|
+
};
|
|
7044
|
+
},
|
|
7045
|
+
clearPerformanceEntries: async () => {
|
|
7046
|
+
return { success: true, data: { cleared: true }, timestamp: Date.now() };
|
|
7047
|
+
},
|
|
7048
|
+
getBrowserEvents: async (_params) => {
|
|
7049
|
+
return { success: true, data: { events: [], count: 0 }, timestamp: Date.now() };
|
|
4180
7050
|
}
|
|
4181
7051
|
};
|
|
4182
7052
|
}
|
|
@@ -4200,7 +7070,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4200
7070
|
const response = searchEngine.search(criteria);
|
|
4201
7071
|
return { success: true, data: response, timestamp: Date.now() };
|
|
4202
7072
|
} catch (err) {
|
|
4203
|
-
return {
|
|
7073
|
+
return {
|
|
7074
|
+
success: false,
|
|
7075
|
+
error: err.message,
|
|
7076
|
+
code: "AI_SEARCH_ERROR",
|
|
7077
|
+
timestamp: Date.now()
|
|
7078
|
+
};
|
|
4204
7079
|
}
|
|
4205
7080
|
},
|
|
4206
7081
|
aiExecute: async (request) => {
|
|
@@ -4209,7 +7084,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4209
7084
|
const response = await nlExecutor.execute(request);
|
|
4210
7085
|
return { success: true, data: response, timestamp: Date.now() };
|
|
4211
7086
|
} catch (err) {
|
|
4212
|
-
return {
|
|
7087
|
+
return {
|
|
7088
|
+
success: false,
|
|
7089
|
+
error: err.message,
|
|
7090
|
+
code: "AI_EXECUTE_ERROR",
|
|
7091
|
+
timestamp: Date.now()
|
|
7092
|
+
};
|
|
4213
7093
|
}
|
|
4214
7094
|
},
|
|
4215
7095
|
aiAssert: async (request) => {
|
|
@@ -4218,7 +7098,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4218
7098
|
const result = await assertionExecutor.assert(request);
|
|
4219
7099
|
return { success: true, data: result, timestamp: Date.now() };
|
|
4220
7100
|
} catch (err) {
|
|
4221
|
-
return {
|
|
7101
|
+
return {
|
|
7102
|
+
success: false,
|
|
7103
|
+
error: err.message,
|
|
7104
|
+
code: "AI_ASSERT_ERROR",
|
|
7105
|
+
timestamp: Date.now()
|
|
7106
|
+
};
|
|
4222
7107
|
}
|
|
4223
7108
|
},
|
|
4224
7109
|
aiAssertBatch: async (request) => {
|
|
@@ -4227,7 +7112,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4227
7112
|
const result = await assertionExecutor.assertBatch(request);
|
|
4228
7113
|
return { success: true, data: result, timestamp: Date.now() };
|
|
4229
7114
|
} catch (err) {
|
|
4230
|
-
return {
|
|
7115
|
+
return {
|
|
7116
|
+
success: false,
|
|
7117
|
+
error: err.message,
|
|
7118
|
+
code: "AI_ASSERT_BATCH_ERROR",
|
|
7119
|
+
timestamp: Date.now()
|
|
7120
|
+
};
|
|
4231
7121
|
}
|
|
4232
7122
|
},
|
|
4233
7123
|
getSemanticSnapshot: async () => {
|
|
@@ -4236,7 +7126,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4236
7126
|
const snapshot = snapshotManager.createSnapshot(controlSnapshot);
|
|
4237
7127
|
return { success: true, data: snapshot, timestamp: Date.now() };
|
|
4238
7128
|
} catch (err) {
|
|
4239
|
-
return {
|
|
7129
|
+
return {
|
|
7130
|
+
success: false,
|
|
7131
|
+
error: err.message,
|
|
7132
|
+
code: "SEMANTIC_SNAPSHOT_ERROR",
|
|
7133
|
+
timestamp: Date.now()
|
|
7134
|
+
};
|
|
4240
7135
|
}
|
|
4241
7136
|
},
|
|
4242
7137
|
getSemanticDiff: async (_since) => {
|
|
@@ -4246,7 +7141,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4246
7141
|
const diff = diffManager.update(currentSnapshot);
|
|
4247
7142
|
return { success: true, data: diff, timestamp: Date.now() };
|
|
4248
7143
|
} catch (err) {
|
|
4249
|
-
return {
|
|
7144
|
+
return {
|
|
7145
|
+
success: false,
|
|
7146
|
+
error: err.message,
|
|
7147
|
+
code: "SEMANTIC_DIFF_ERROR",
|
|
7148
|
+
timestamp: Date.now()
|
|
7149
|
+
};
|
|
4250
7150
|
}
|
|
4251
7151
|
},
|
|
4252
7152
|
getPageSummary: async () => {
|
|
@@ -4264,7 +7164,12 @@ function createAIHandlers(registry, actionExecutor) {
|
|
|
4264
7164
|
const summary = generatePageSummary(elements);
|
|
4265
7165
|
return { success: true, data: summary, timestamp: Date.now() };
|
|
4266
7166
|
} catch (err) {
|
|
4267
|
-
return {
|
|
7167
|
+
return {
|
|
7168
|
+
success: false,
|
|
7169
|
+
error: err.message,
|
|
7170
|
+
code: "PAGE_SUMMARY_ERROR",
|
|
7171
|
+
timestamp: Date.now()
|
|
7172
|
+
};
|
|
4268
7173
|
}
|
|
4269
7174
|
}
|
|
4270
7175
|
};
|