@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/ai/index.mjs
CHANGED
|
@@ -449,17 +449,72 @@ function generateDescription(input) {
|
|
|
449
449
|
}
|
|
450
450
|
parts.push(`"${name}"`);
|
|
451
451
|
}
|
|
452
|
-
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
452
|
+
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
453
|
+
input.elementType || "element"
|
|
454
|
+
];
|
|
453
455
|
parts.push(typeWords[0]);
|
|
454
456
|
if (input.inputType && input.inputType !== "text") {
|
|
455
457
|
parts.push(`(${input.inputType})`);
|
|
456
458
|
}
|
|
457
459
|
return parts.join(" ");
|
|
458
460
|
}
|
|
461
|
+
var CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
462
|
+
"heading",
|
|
463
|
+
"paragraph",
|
|
464
|
+
"list-item",
|
|
465
|
+
"table-cell",
|
|
466
|
+
"table-header",
|
|
467
|
+
"label",
|
|
468
|
+
"caption",
|
|
469
|
+
"blockquote",
|
|
470
|
+
"code-block",
|
|
471
|
+
"badge",
|
|
472
|
+
"status-message",
|
|
473
|
+
"metric-value",
|
|
474
|
+
"description-text",
|
|
475
|
+
"nav-text",
|
|
476
|
+
"content-generic"
|
|
477
|
+
]);
|
|
459
478
|
function generatePurpose(input) {
|
|
460
479
|
const text = (input.textContent || input.ariaLabel || input.title || "").toLowerCase();
|
|
461
480
|
const type = input.elementType?.toLowerCase() || "";
|
|
462
481
|
const inputType = input.inputType?.toLowerCase() || "";
|
|
482
|
+
if (CONTENT_TYPES.has(type)) {
|
|
483
|
+
switch (type) {
|
|
484
|
+
case "heading":
|
|
485
|
+
return "Section heading";
|
|
486
|
+
case "paragraph":
|
|
487
|
+
return "Body text content";
|
|
488
|
+
case "list-item":
|
|
489
|
+
return "List item";
|
|
490
|
+
case "table-cell":
|
|
491
|
+
return "Table data cell";
|
|
492
|
+
case "table-header":
|
|
493
|
+
return "Table column header";
|
|
494
|
+
case "label":
|
|
495
|
+
return "Field label or definition term";
|
|
496
|
+
case "caption":
|
|
497
|
+
return "Figure or table caption";
|
|
498
|
+
case "blockquote":
|
|
499
|
+
return "Quoted content";
|
|
500
|
+
case "code-block":
|
|
501
|
+
return "Code or preformatted text";
|
|
502
|
+
case "badge":
|
|
503
|
+
return "Status badge or tag";
|
|
504
|
+
case "status-message":
|
|
505
|
+
return "Dynamic status indicator";
|
|
506
|
+
case "metric-value":
|
|
507
|
+
return "Metric or statistic value";
|
|
508
|
+
case "description-text":
|
|
509
|
+
return "Description or definition";
|
|
510
|
+
case "nav-text":
|
|
511
|
+
return "Navigation label";
|
|
512
|
+
case "content-generic":
|
|
513
|
+
return "Text content";
|
|
514
|
+
default:
|
|
515
|
+
return "Static content";
|
|
516
|
+
}
|
|
517
|
+
}
|
|
463
518
|
if (type === "button" || inputType === "submit") {
|
|
464
519
|
if (text.match(/submit|send|save|confirm|ok|done|finish|apply/)) {
|
|
465
520
|
return "Submits the form";
|
|
@@ -524,6 +579,10 @@ function generateSuggestedActions(input) {
|
|
|
524
579
|
const inputType = input.inputType?.toLowerCase() || "";
|
|
525
580
|
const text = (input.textContent || input.ariaLabel || "").toLowerCase();
|
|
526
581
|
const actions = [];
|
|
582
|
+
if (CONTENT_TYPES.has(type)) {
|
|
583
|
+
actions.push("read text content", "verify text matches expected");
|
|
584
|
+
return actions;
|
|
585
|
+
}
|
|
527
586
|
switch (type) {
|
|
528
587
|
case "button":
|
|
529
588
|
actions.push(`click "${text || "this button"}"`);
|
|
@@ -575,6 +634,241 @@ function areSynonyms(word1, word2) {
|
|
|
575
634
|
return synonyms1.includes(w2) || synonyms2.includes(w1);
|
|
576
635
|
}
|
|
577
636
|
|
|
637
|
+
// src/annotations/types.ts
|
|
638
|
+
var ANNOTATION_CONFIG_VERSION = "1.0.0";
|
|
639
|
+
|
|
640
|
+
// src/annotations/store.ts
|
|
641
|
+
var AnnotationStore = class {
|
|
642
|
+
constructor() {
|
|
643
|
+
this.store = /* @__PURE__ */ new Map();
|
|
644
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get an annotation by element ID.
|
|
648
|
+
*/
|
|
649
|
+
get(elementId) {
|
|
650
|
+
return this.store.get(elementId);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get all annotations as a record.
|
|
654
|
+
*/
|
|
655
|
+
getAll() {
|
|
656
|
+
const result = {};
|
|
657
|
+
for (const [id, annotation] of this.store) {
|
|
658
|
+
result[id] = annotation;
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Set an annotation for an element. Auto-sets `updatedAt`.
|
|
664
|
+
*/
|
|
665
|
+
set(elementId, annotation) {
|
|
666
|
+
const updated = {
|
|
667
|
+
...annotation,
|
|
668
|
+
updatedAt: Date.now()
|
|
669
|
+
};
|
|
670
|
+
this.store.set(elementId, updated);
|
|
671
|
+
this.emit({
|
|
672
|
+
type: "annotation:set",
|
|
673
|
+
elementId,
|
|
674
|
+
annotation: updated,
|
|
675
|
+
timestamp: Date.now()
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Delete an annotation by element ID.
|
|
680
|
+
*
|
|
681
|
+
* @returns true if the annotation existed and was deleted
|
|
682
|
+
*/
|
|
683
|
+
delete(elementId) {
|
|
684
|
+
const existed = this.store.delete(elementId);
|
|
685
|
+
if (existed) {
|
|
686
|
+
this.emit({
|
|
687
|
+
type: "annotation:deleted",
|
|
688
|
+
elementId,
|
|
689
|
+
timestamp: Date.now()
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
return existed;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Check if an annotation exists for an element.
|
|
696
|
+
*/
|
|
697
|
+
has(elementId) {
|
|
698
|
+
return this.store.has(elementId);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Get the number of stored annotations.
|
|
702
|
+
*/
|
|
703
|
+
get count() {
|
|
704
|
+
return this.store.size;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Clear all annotations.
|
|
708
|
+
*/
|
|
709
|
+
clear() {
|
|
710
|
+
this.store.clear();
|
|
711
|
+
this.emit({
|
|
712
|
+
type: "annotation:cleared",
|
|
713
|
+
timestamp: Date.now()
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Import annotations from a config object.
|
|
718
|
+
*
|
|
719
|
+
* Merges with existing annotations (new values overwrite per element ID).
|
|
720
|
+
*
|
|
721
|
+
* @returns Number of annotations imported
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```ts
|
|
725
|
+
* const config: AnnotationConfig = {
|
|
726
|
+
* version: '1.0.0',
|
|
727
|
+
* annotations: {
|
|
728
|
+
* 'btn-1': { description: 'Submit button', tags: ['form'] },
|
|
729
|
+
* 'input-1': { description: 'Name field' },
|
|
730
|
+
* },
|
|
731
|
+
* };
|
|
732
|
+
* const count = store.importConfig(config); // 2
|
|
733
|
+
* ```
|
|
734
|
+
*/
|
|
735
|
+
importConfig(config) {
|
|
736
|
+
let count = 0;
|
|
737
|
+
for (const [id, annotation] of Object.entries(config.annotations)) {
|
|
738
|
+
this.store.set(id, {
|
|
739
|
+
...annotation,
|
|
740
|
+
updatedAt: annotation.updatedAt ?? Date.now()
|
|
741
|
+
});
|
|
742
|
+
count++;
|
|
743
|
+
}
|
|
744
|
+
this.emit({
|
|
745
|
+
type: "annotation:imported",
|
|
746
|
+
count,
|
|
747
|
+
timestamp: Date.now()
|
|
748
|
+
});
|
|
749
|
+
return count;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Export all annotations as a config object.
|
|
753
|
+
*
|
|
754
|
+
* The returned object can be serialized to JSON and saved to a file,
|
|
755
|
+
* then later re-imported with {@link importConfig}.
|
|
756
|
+
*
|
|
757
|
+
* @param metadata - Optional metadata to include (appName, description, etc.)
|
|
758
|
+
* @returns AnnotationConfig with all current annotations
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* ```ts
|
|
762
|
+
* const config = store.exportConfig({ appName: 'MyApp' });
|
|
763
|
+
* // config.version === '1.0.0'
|
|
764
|
+
* // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
|
|
765
|
+
* // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
|
|
766
|
+
*
|
|
767
|
+
* // Save to file
|
|
768
|
+
* fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
exportConfig(metadata) {
|
|
772
|
+
return {
|
|
773
|
+
version: ANNOTATION_CONFIG_VERSION,
|
|
774
|
+
annotations: this.getAll(),
|
|
775
|
+
metadata: {
|
|
776
|
+
...metadata,
|
|
777
|
+
exportedAt: Date.now()
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Compute annotation coverage against a set of known element IDs.
|
|
783
|
+
*
|
|
784
|
+
* Compares the store's annotations against the provided list of element IDs
|
|
785
|
+
* to determine what percentage of elements have been annotated.
|
|
786
|
+
*
|
|
787
|
+
* @param allElementIds - Array of all known element IDs in the UI
|
|
788
|
+
* @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* ```ts
|
|
792
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
793
|
+
* store.set('input-1', { description: 'Name' });
|
|
794
|
+
*
|
|
795
|
+
* const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
|
|
796
|
+
* // coverage.totalElements === 4
|
|
797
|
+
* // coverage.annotatedElements === 2
|
|
798
|
+
* // coverage.coveragePercent === 50
|
|
799
|
+
* // coverage.annotatedIds === ['btn-1', 'input-1']
|
|
800
|
+
* // coverage.unannotatedIds === ['input-2', 'link-1']
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
803
|
+
getCoverage(allElementIds) {
|
|
804
|
+
const annotatedIds = [];
|
|
805
|
+
const unannotatedIds = [];
|
|
806
|
+
for (const id of allElementIds) {
|
|
807
|
+
if (this.store.has(id)) {
|
|
808
|
+
annotatedIds.push(id);
|
|
809
|
+
} else {
|
|
810
|
+
unannotatedIds.push(id);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const total = allElementIds.length;
|
|
814
|
+
return {
|
|
815
|
+
totalElements: total,
|
|
816
|
+
annotatedElements: annotatedIds.length,
|
|
817
|
+
coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
|
|
818
|
+
annotatedIds,
|
|
819
|
+
unannotatedIds,
|
|
820
|
+
timestamp: Date.now()
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Subscribe to annotation events.
|
|
825
|
+
*
|
|
826
|
+
* The listener is called whenever annotations are set, deleted, imported,
|
|
827
|
+
* or cleared. Returns an unsubscribe function to stop listening.
|
|
828
|
+
*
|
|
829
|
+
* @param listener - Callback function receiving {@link AnnotationEvent} objects
|
|
830
|
+
* @returns Unsubscribe function - call it to remove the listener
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```ts
|
|
834
|
+
* const unsubscribe = store.on((event) => {
|
|
835
|
+
* if (event.type === 'annotation:set') {
|
|
836
|
+
* console.log(`Element ${event.elementId} annotated:`, event.annotation);
|
|
837
|
+
* }
|
|
838
|
+
* });
|
|
839
|
+
*
|
|
840
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
841
|
+
* // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
|
|
842
|
+
*
|
|
843
|
+
* unsubscribe(); // Stop listening
|
|
844
|
+
* ```
|
|
845
|
+
*/
|
|
846
|
+
on(listener) {
|
|
847
|
+
this.listeners.add(listener);
|
|
848
|
+
return () => {
|
|
849
|
+
this.listeners.delete(listener);
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Emit an event to all listeners.
|
|
854
|
+
*/
|
|
855
|
+
emit(event) {
|
|
856
|
+
for (const listener of this.listeners) {
|
|
857
|
+
try {
|
|
858
|
+
listener(event);
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
var globalStore = null;
|
|
865
|
+
function getGlobalAnnotationStore() {
|
|
866
|
+
if (!globalStore) {
|
|
867
|
+
globalStore = new AnnotationStore();
|
|
868
|
+
}
|
|
869
|
+
return globalStore;
|
|
870
|
+
}
|
|
871
|
+
|
|
578
872
|
// src/ai/search-engine.ts
|
|
579
873
|
var DEFAULT_SEARCH_CONFIG = {
|
|
580
874
|
fuzzyThreshold: 0.7,
|
|
@@ -617,17 +911,40 @@ var SearchEngine = class {
|
|
|
617
911
|
if ("getState" in element && typeof element.getState === "function") {
|
|
618
912
|
state = getState ? getState(element) : element.getState();
|
|
619
913
|
textContent = state.textContent || void 0;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
914
|
+
try {
|
|
915
|
+
tagName = element.element.tagName.toLowerCase();
|
|
916
|
+
} catch {
|
|
917
|
+
tagName = element.type || "unknown";
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
role = element.element.getAttribute("role") || void 0;
|
|
921
|
+
ariaLabel = element.element.getAttribute("aria-label") || void 0;
|
|
922
|
+
placeholder = element.element.getAttribute("placeholder") || void 0;
|
|
923
|
+
title = element.element.getAttribute("title") || void 0;
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
if (!ariaLabel && element.label) {
|
|
927
|
+
ariaLabel = element.label;
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
if (element.element.id) {
|
|
931
|
+
const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
|
|
932
|
+
labelText = labelEl?.textContent?.trim() || void 0;
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
if (!labelText && element.label) {
|
|
937
|
+
labelText = element.label;
|
|
938
|
+
}
|
|
939
|
+
if (!textContent && element.label) {
|
|
940
|
+
textContent = element.label;
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
|
|
944
|
+
value = element.element.value || void 0;
|
|
945
|
+
}
|
|
946
|
+
} catch {
|
|
947
|
+
value = state.value || void 0;
|
|
631
948
|
}
|
|
632
949
|
} else {
|
|
633
950
|
const discovered = element;
|
|
@@ -636,8 +953,11 @@ var SearchEngine = class {
|
|
|
636
953
|
tagName = discovered.tagName;
|
|
637
954
|
role = discovered.role || void 0;
|
|
638
955
|
ariaLabel = discovered.accessibleName || void 0;
|
|
956
|
+
if (!labelText && element.label) {
|
|
957
|
+
labelText = element.label;
|
|
958
|
+
}
|
|
639
959
|
}
|
|
640
|
-
|
|
960
|
+
let aliases = generateAliases({
|
|
641
961
|
textContent,
|
|
642
962
|
ariaLabel,
|
|
643
963
|
placeholder,
|
|
@@ -647,7 +967,14 @@ var SearchEngine = class {
|
|
|
647
967
|
labelText,
|
|
648
968
|
value
|
|
649
969
|
});
|
|
650
|
-
|
|
970
|
+
if ("aliases" in element && Array.isArray(element.aliases) && element.aliases.length > 0) {
|
|
971
|
+
const aliasSet = /* @__PURE__ */ new Set([
|
|
972
|
+
...aliases,
|
|
973
|
+
...element.aliases.map((a) => a.toLowerCase())
|
|
974
|
+
]);
|
|
975
|
+
aliases = [...aliasSet];
|
|
976
|
+
}
|
|
977
|
+
let description = generateDescription({
|
|
651
978
|
textContent,
|
|
652
979
|
ariaLabel,
|
|
653
980
|
placeholder,
|
|
@@ -656,6 +983,22 @@ var SearchEngine = class {
|
|
|
656
983
|
id: element.id,
|
|
657
984
|
labelText
|
|
658
985
|
});
|
|
986
|
+
if (!description && "description" in element && element.description) {
|
|
987
|
+
description = element.description;
|
|
988
|
+
}
|
|
989
|
+
const annotation = getGlobalAnnotationStore().get(element.id);
|
|
990
|
+
if (annotation) {
|
|
991
|
+
if (annotation.description) {
|
|
992
|
+
description = annotation.description;
|
|
993
|
+
}
|
|
994
|
+
if (annotation.tags && annotation.tags.length > 0) {
|
|
995
|
+
const tagSet = /* @__PURE__ */ new Set([...aliases, ...annotation.tags.map((t) => t.toLowerCase())]);
|
|
996
|
+
aliases = [...tagSet];
|
|
997
|
+
}
|
|
998
|
+
if (annotation.notes) {
|
|
999
|
+
aliases.push(annotation.notes.toLowerCase());
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
659
1002
|
return {
|
|
660
1003
|
id: element.id,
|
|
661
1004
|
element,
|
|
@@ -758,7 +1101,12 @@ var SearchEngine = class {
|
|
|
758
1101
|
threshold: criteria.fuzzyThreshold ?? this.config.fuzzyThreshold
|
|
759
1102
|
};
|
|
760
1103
|
if (criteria.text) {
|
|
761
|
-
const textScore = this.scoreTextMatch(
|
|
1104
|
+
const textScore = this.scoreTextMatch(
|
|
1105
|
+
searchable,
|
|
1106
|
+
criteria.text,
|
|
1107
|
+
criteria.fuzzy !== false,
|
|
1108
|
+
fuzzyConfig.threshold
|
|
1109
|
+
);
|
|
762
1110
|
scores.text = textScore.score;
|
|
763
1111
|
if (textScore.score > 0) {
|
|
764
1112
|
matchReasons.push(...textScore.reasons);
|
|
@@ -766,8 +1114,37 @@ var SearchEngine = class {
|
|
|
766
1114
|
weightedScore += textScore.score * this.config.textWeight;
|
|
767
1115
|
totalWeight += this.config.textWeight;
|
|
768
1116
|
}
|
|
1117
|
+
if (criteria.textContent && !criteria.text) {
|
|
1118
|
+
const alternatives = criteria.textContent.includes("|") ? criteria.textContent.split("|").map((s) => s.trim()).filter(Boolean) : [criteria.textContent];
|
|
1119
|
+
let bestScore = 0;
|
|
1120
|
+
let bestReasons = [];
|
|
1121
|
+
for (const alt of alternatives) {
|
|
1122
|
+
const exactScore = this.scoreTextMatch(
|
|
1123
|
+
searchable,
|
|
1124
|
+
alt,
|
|
1125
|
+
criteria.fuzzy !== false,
|
|
1126
|
+
fuzzyConfig.threshold
|
|
1127
|
+
);
|
|
1128
|
+
const containsScore = this.scoreContainsMatch(searchable, alt, criteria.fuzzy !== false);
|
|
1129
|
+
const altBest = Math.max(exactScore.score, containsScore.score);
|
|
1130
|
+
if (altBest > bestScore) {
|
|
1131
|
+
bestScore = altBest;
|
|
1132
|
+
bestReasons = exactScore.score >= containsScore.score ? exactScore.reasons : containsScore.reasons;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
scores.text = bestScore;
|
|
1136
|
+
if (bestScore > 0) {
|
|
1137
|
+
matchReasons.push(...bestReasons);
|
|
1138
|
+
}
|
|
1139
|
+
weightedScore += bestScore * this.config.textWeight;
|
|
1140
|
+
totalWeight += this.config.textWeight;
|
|
1141
|
+
}
|
|
769
1142
|
if (criteria.textContains) {
|
|
770
|
-
const containsScore = this.scoreContainsMatch(
|
|
1143
|
+
const containsScore = this.scoreContainsMatch(
|
|
1144
|
+
searchable,
|
|
1145
|
+
criteria.textContains,
|
|
1146
|
+
criteria.fuzzy !== false
|
|
1147
|
+
);
|
|
771
1148
|
scores.text = Math.max(scores.text || 0, containsScore.score);
|
|
772
1149
|
if (containsScore.score > 0 && containsScore.reasons.length > 0) {
|
|
773
1150
|
matchReasons.push(...containsScore.reasons);
|
|
@@ -816,7 +1193,11 @@ var SearchEngine = class {
|
|
|
816
1193
|
totalWeight += this.config.spatialWeight;
|
|
817
1194
|
}
|
|
818
1195
|
if (criteria.placeholder && searchable.placeholder) {
|
|
819
|
-
const placeholderResult = fuzzyMatch(
|
|
1196
|
+
const placeholderResult = fuzzyMatch(
|
|
1197
|
+
searchable.placeholder,
|
|
1198
|
+
criteria.placeholder,
|
|
1199
|
+
fuzzyConfig
|
|
1200
|
+
);
|
|
820
1201
|
if (placeholderResult.isMatch) {
|
|
821
1202
|
matchReasons.push(`placeholder matches`);
|
|
822
1203
|
weightedScore += placeholderResult.similarity * this.config.textWeight;
|
|
@@ -861,11 +1242,9 @@ var SearchEngine = class {
|
|
|
861
1242
|
scoreTextMatch(searchable, text, fuzzy, threshold) {
|
|
862
1243
|
const reasons = [];
|
|
863
1244
|
let maxScore = 0;
|
|
864
|
-
const textsToMatch = [
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
searchable.value
|
|
868
|
-
].filter(Boolean);
|
|
1245
|
+
const textsToMatch = [searchable.textContent, searchable.labelText, searchable.value].filter(
|
|
1246
|
+
Boolean
|
|
1247
|
+
);
|
|
869
1248
|
for (const targetText of textsToMatch) {
|
|
870
1249
|
if (targetText.toLowerCase() === text.toLowerCase()) {
|
|
871
1250
|
maxScore = Math.max(maxScore, 1);
|
|
@@ -961,7 +1340,9 @@ var SearchEngine = class {
|
|
|
961
1340
|
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
962
1341
|
};
|
|
963
1342
|
const inferredRoles = tagRoleMap[normalizedRole] || [];
|
|
964
|
-
if (inferredRoles.some(
|
|
1343
|
+
if (inferredRoles.some(
|
|
1344
|
+
(r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole
|
|
1345
|
+
)) {
|
|
965
1346
|
return { score: 0.8, reasons: [`inferred role: ${role}`] };
|
|
966
1347
|
}
|
|
967
1348
|
return { score: 0, reasons };
|
|
@@ -1151,11 +1532,14 @@ function generatePageSummary(elements, pageContext, config = {}) {
|
|
|
1151
1532
|
if (finalConfig.includeElementCounts) {
|
|
1152
1533
|
const counts = countElementTypes(elements);
|
|
1153
1534
|
const countParts = [];
|
|
1154
|
-
if (counts.button > 0)
|
|
1535
|
+
if (counts.button > 0)
|
|
1536
|
+
countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
|
|
1155
1537
|
if (counts.input > 0) countParts.push(`${counts.input} input${counts.input > 1 ? "s" : ""}`);
|
|
1156
1538
|
if (counts.link > 0) countParts.push(`${counts.link} link${counts.link > 1 ? "s" : ""}`);
|
|
1157
|
-
if (counts.select > 0)
|
|
1158
|
-
|
|
1539
|
+
if (counts.select > 0)
|
|
1540
|
+
countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
|
|
1541
|
+
if (counts.checkbox > 0)
|
|
1542
|
+
countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
|
|
1159
1543
|
if (countParts.length > 0) {
|
|
1160
1544
|
lines.push(`Contains: ${countParts.join(", ")}`);
|
|
1161
1545
|
}
|
|
@@ -1219,7 +1603,9 @@ function generateFormSummary(form, verbosity) {
|
|
|
1219
1603
|
if (verbosity === "brief") {
|
|
1220
1604
|
const fieldCount = form.fields.length;
|
|
1221
1605
|
const filledCount = form.fields.filter((f) => f.value).length;
|
|
1222
|
-
lines.push(
|
|
1606
|
+
lines.push(
|
|
1607
|
+
` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`
|
|
1608
|
+
);
|
|
1223
1609
|
} else {
|
|
1224
1610
|
for (const field of form.fields) {
|
|
1225
1611
|
let fieldLine = ` - ${field.label || field.id}`;
|
|
@@ -1315,7 +1701,9 @@ function generateDiffSummary(appeared, disappeared, modified) {
|
|
|
1315
1701
|
if (modified.length > 0) {
|
|
1316
1702
|
lines.push("Changed:");
|
|
1317
1703
|
for (const mod of modified.slice(0, 5)) {
|
|
1318
|
-
lines.push(
|
|
1704
|
+
lines.push(
|
|
1705
|
+
` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`
|
|
1706
|
+
);
|
|
1319
1707
|
}
|
|
1320
1708
|
if (modified.length > 5) {
|
|
1321
1709
|
lines.push(` ... and ${modified.length - 5} more changes`);
|
|
@@ -1765,7 +2153,9 @@ function parseNLInstruction(instruction) {
|
|
|
1765
2153
|
}
|
|
1766
2154
|
}
|
|
1767
2155
|
if (pattern.action === "assert") {
|
|
1768
|
-
const assertMatch = trimmed.match(
|
|
2156
|
+
const assertMatch = trimmed.match(
|
|
2157
|
+
/(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i
|
|
2158
|
+
);
|
|
1769
2159
|
if (assertMatch) {
|
|
1770
2160
|
parsed.assertionType = ASSERTION_TYPE_MAP[assertMatch[1].toLowerCase()];
|
|
1771
2161
|
}
|
|
@@ -2280,7 +2670,9 @@ function formatErrorContext(context) {
|
|
|
2280
2670
|
lines.push("");
|
|
2281
2671
|
if (context.searchResults.nearestMatch) {
|
|
2282
2672
|
const match = context.searchResults.nearestMatch;
|
|
2283
|
-
lines.push(
|
|
2673
|
+
lines.push(
|
|
2674
|
+
`Nearest match: "${match.element.description}" (${(match.confidence * 100).toFixed(0)}% confidence)`
|
|
2675
|
+
);
|
|
2284
2676
|
lines.push(`Why not used: ${match.whyNotSelected}`);
|
|
2285
2677
|
lines.push("");
|
|
2286
2678
|
}
|
|
@@ -2627,7 +3019,9 @@ var NLActionExecutor = class {
|
|
|
2627
3019
|
switch (errorCode) {
|
|
2628
3020
|
case "PARSE_ERROR":
|
|
2629
3021
|
suggestions.push('Try using a simpler phrase like "click Submit button"');
|
|
2630
|
-
suggestions.push(
|
|
3022
|
+
suggestions.push(
|
|
3023
|
+
'Ensure the instruction follows patterns like "click X" or "type Y into X"'
|
|
3024
|
+
);
|
|
2631
3025
|
break;
|
|
2632
3026
|
case "ELEMENT_NOT_FOUND":
|
|
2633
3027
|
if (alternatives.length > 0) {
|
|
@@ -2698,9 +3092,15 @@ var AssertionExecutor = class {
|
|
|
2698
3092
|
async assert(request) {
|
|
2699
3093
|
const startTime = performance.now();
|
|
2700
3094
|
const timeout = request.timeout ?? this.config.defaultTimeout;
|
|
2701
|
-
const
|
|
3095
|
+
const searchResult = this.findElementDetailed(request.target, request.fuzzy !== false);
|
|
3096
|
+
const element = searchResult?.element ?? null;
|
|
3097
|
+
const searchDetails = searchResult ? {
|
|
3098
|
+
confidence: searchResult.confidence,
|
|
3099
|
+
matchReasons: searchResult.matchReasons,
|
|
3100
|
+
candidateCount: this.elements.length
|
|
3101
|
+
} : void 0;
|
|
2702
3102
|
if (!element && request.type !== "notExists") {
|
|
2703
|
-
|
|
3103
|
+
const result2 = this.createResult(
|
|
2704
3104
|
false,
|
|
2705
3105
|
typeof request.target === "string" ? request.target : JSON.stringify(request.target),
|
|
2706
3106
|
"element not found",
|
|
@@ -2710,8 +3110,16 @@ var AssertionExecutor = class {
|
|
|
2710
3110
|
this.config.includeSuggestions ? "Check if the element exists and is properly labeled" : void 0,
|
|
2711
3111
|
startTime
|
|
2712
3112
|
);
|
|
3113
|
+
if (searchDetails) {
|
|
3114
|
+
result2.searchDetails = searchDetails;
|
|
3115
|
+
}
|
|
3116
|
+
return result2;
|
|
2713
3117
|
}
|
|
2714
|
-
|
|
3118
|
+
const result = await this.executeAssertion(request, element, timeout, startTime);
|
|
3119
|
+
if (searchDetails) {
|
|
3120
|
+
result.searchDetails = searchDetails;
|
|
3121
|
+
}
|
|
3122
|
+
return result;
|
|
2715
3123
|
}
|
|
2716
3124
|
/**
|
|
2717
3125
|
* Execute multiple assertions
|
|
@@ -2816,16 +3224,26 @@ var AssertionExecutor = class {
|
|
|
2816
3224
|
return this.assert({ target, type: "count", expected: expectedCount, timeout });
|
|
2817
3225
|
}
|
|
2818
3226
|
/**
|
|
2819
|
-
* Find element by target
|
|
3227
|
+
* Find element by target with full search metadata.
|
|
3228
|
+
* Returns the SearchResult (including confidence, matchReasons, scores)
|
|
3229
|
+
* or null if no match above the fuzzy threshold.
|
|
2820
3230
|
*/
|
|
2821
|
-
|
|
3231
|
+
findElementDetailed(target, fuzzy = true) {
|
|
2822
3232
|
const criteria = typeof target === "string" ? { text: target, fuzzy } : { ...target, fuzzy };
|
|
2823
|
-
const searchResult = this.searchEngine.findBest(criteria);
|
|
3233
|
+
const searchResult = this.searchEngine.findBest(criteria, this.elements);
|
|
2824
3234
|
if (searchResult && searchResult.confidence >= this.config.fuzzyThreshold) {
|
|
2825
|
-
return searchResult
|
|
3235
|
+
return searchResult;
|
|
2826
3236
|
}
|
|
2827
3237
|
return null;
|
|
2828
3238
|
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Find element by target (string or criteria).
|
|
3241
|
+
* Public for use by condition evaluation in SpecExecutor.
|
|
3242
|
+
*/
|
|
3243
|
+
async findElement(target, fuzzy = true) {
|
|
3244
|
+
const result = this.findElementDetailed(target, fuzzy);
|
|
3245
|
+
return result?.element ?? null;
|
|
3246
|
+
}
|
|
2829
3247
|
/**
|
|
2830
3248
|
* Execute the actual assertion
|
|
2831
3249
|
*/
|
|
@@ -2834,19 +3252,55 @@ var AssertionExecutor = class {
|
|
|
2834
3252
|
const elementDescription = element?.description || targetStr;
|
|
2835
3253
|
switch (request.type) {
|
|
2836
3254
|
case "visible":
|
|
2837
|
-
return this.assertVisibility(
|
|
3255
|
+
return this.assertVisibility(
|
|
3256
|
+
element,
|
|
3257
|
+
true,
|
|
3258
|
+
elementDescription,
|
|
3259
|
+
request.message,
|
|
3260
|
+
startTime
|
|
3261
|
+
);
|
|
2838
3262
|
case "hidden":
|
|
2839
|
-
return this.assertVisibility(
|
|
3263
|
+
return this.assertVisibility(
|
|
3264
|
+
element,
|
|
3265
|
+
false,
|
|
3266
|
+
elementDescription,
|
|
3267
|
+
request.message,
|
|
3268
|
+
startTime
|
|
3269
|
+
);
|
|
2840
3270
|
case "enabled":
|
|
2841
|
-
return this.assertEnabledState(
|
|
3271
|
+
return this.assertEnabledState(
|
|
3272
|
+
element,
|
|
3273
|
+
true,
|
|
3274
|
+
elementDescription,
|
|
3275
|
+
request.message,
|
|
3276
|
+
startTime
|
|
3277
|
+
);
|
|
2842
3278
|
case "disabled":
|
|
2843
|
-
return this.assertEnabledState(
|
|
3279
|
+
return this.assertEnabledState(
|
|
3280
|
+
element,
|
|
3281
|
+
false,
|
|
3282
|
+
elementDescription,
|
|
3283
|
+
request.message,
|
|
3284
|
+
startTime
|
|
3285
|
+
);
|
|
2844
3286
|
case "focused":
|
|
2845
3287
|
return this.assertFocused(element, elementDescription, request.message, startTime);
|
|
2846
3288
|
case "checked":
|
|
2847
|
-
return this.assertCheckedState(
|
|
3289
|
+
return this.assertCheckedState(
|
|
3290
|
+
element,
|
|
3291
|
+
true,
|
|
3292
|
+
elementDescription,
|
|
3293
|
+
request.message,
|
|
3294
|
+
startTime
|
|
3295
|
+
);
|
|
2848
3296
|
case "unchecked":
|
|
2849
|
-
return this.assertCheckedState(
|
|
3297
|
+
return this.assertCheckedState(
|
|
3298
|
+
element,
|
|
3299
|
+
false,
|
|
3300
|
+
elementDescription,
|
|
3301
|
+
request.message,
|
|
3302
|
+
startTime
|
|
3303
|
+
);
|
|
2850
3304
|
case "hasText":
|
|
2851
3305
|
return this.assertTextMatch(
|
|
2852
3306
|
element,
|
|
@@ -3184,7 +3638,8 @@ var DEFAULT_SNAPSHOT_CONFIG = {
|
|
|
3184
3638
|
detectModals: true,
|
|
3185
3639
|
inferPageType: true,
|
|
3186
3640
|
generateDescriptions: true,
|
|
3187
|
-
maxElements: 500
|
|
3641
|
+
maxElements: 500,
|
|
3642
|
+
useAnnotations: true
|
|
3188
3643
|
};
|
|
3189
3644
|
var SemanticSnapshotManager = class {
|
|
3190
3645
|
constructor(config = {}) {
|
|
@@ -3257,26 +3712,52 @@ var SemanticSnapshotManager = class {
|
|
|
3257
3712
|
* Convert a single element to AI element
|
|
3258
3713
|
*/
|
|
3259
3714
|
convertElement(element) {
|
|
3715
|
+
const isContent = element.category === "content";
|
|
3260
3716
|
const aliases = generateAliases({
|
|
3261
3717
|
textContent: element.state.textContent,
|
|
3262
3718
|
elementType: element.type,
|
|
3263
3719
|
id: element.id,
|
|
3264
3720
|
labelText: element.label
|
|
3265
3721
|
});
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3722
|
+
let description;
|
|
3723
|
+
if (isContent && element.contentMetadata) {
|
|
3724
|
+
description = this.generateContentDescription(element);
|
|
3725
|
+
} else if (this.config.generateDescriptions) {
|
|
3726
|
+
description = generateDescription({
|
|
3727
|
+
textContent: element.state.textContent,
|
|
3728
|
+
elementType: element.type,
|
|
3729
|
+
id: element.id,
|
|
3730
|
+
labelText: element.label
|
|
3731
|
+
});
|
|
3732
|
+
} else {
|
|
3733
|
+
description = element.label || element.id;
|
|
3734
|
+
}
|
|
3735
|
+
const purpose = isContent ? generatePurpose({ textContent: element.state.textContent, elementType: element.type }) : generatePurpose({ textContent: element.state.textContent, elementType: element.type });
|
|
3736
|
+
const suggestedActions = isContent ? generateSuggestedActions({
|
|
3273
3737
|
textContent: element.state.textContent,
|
|
3274
3738
|
elementType: element.type
|
|
3275
|
-
})
|
|
3276
|
-
const suggestedActions = generateSuggestedActions({
|
|
3739
|
+
}) : generateSuggestedActions({
|
|
3277
3740
|
textContent: element.state.textContent,
|
|
3278
3741
|
elementType: element.type
|
|
3279
3742
|
});
|
|
3743
|
+
let finalDescription = description;
|
|
3744
|
+
let finalPurpose = purpose;
|
|
3745
|
+
let finalAliases = aliases;
|
|
3746
|
+
if (this.config.useAnnotations) {
|
|
3747
|
+
const annotation = getGlobalAnnotationStore().get(element.id);
|
|
3748
|
+
if (annotation) {
|
|
3749
|
+
if (annotation.description) {
|
|
3750
|
+
finalDescription = annotation.description;
|
|
3751
|
+
}
|
|
3752
|
+
if (annotation.purpose) {
|
|
3753
|
+
finalPurpose = annotation.purpose;
|
|
3754
|
+
}
|
|
3755
|
+
if (annotation.tags && annotation.tags.length > 0) {
|
|
3756
|
+
const tagSet = /* @__PURE__ */ new Set([...finalAliases, ...annotation.tags.map((t) => t.toLowerCase())]);
|
|
3757
|
+
finalAliases = [...tagSet];
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3280
3761
|
return {
|
|
3281
3762
|
id: element.id,
|
|
3282
3763
|
type: element.type,
|
|
@@ -3287,13 +3768,56 @@ var SemanticSnapshotManager = class {
|
|
|
3287
3768
|
actions: element.actions,
|
|
3288
3769
|
state: element.state,
|
|
3289
3770
|
registered: true,
|
|
3290
|
-
description,
|
|
3291
|
-
aliases,
|
|
3292
|
-
purpose,
|
|
3771
|
+
description: finalDescription,
|
|
3772
|
+
aliases: finalAliases,
|
|
3773
|
+
purpose: finalPurpose,
|
|
3293
3774
|
suggestedActions,
|
|
3294
|
-
semanticType: this.inferSemanticType(element)
|
|
3775
|
+
semanticType: this.inferSemanticType(element),
|
|
3776
|
+
category: element.category,
|
|
3777
|
+
contentMetadata: element.contentMetadata
|
|
3295
3778
|
};
|
|
3296
3779
|
}
|
|
3780
|
+
/**
|
|
3781
|
+
* Generate a content-specific description
|
|
3782
|
+
*/
|
|
3783
|
+
generateContentDescription(element) {
|
|
3784
|
+
const meta = element.contentMetadata;
|
|
3785
|
+
const text = element.state.textContent?.trim() || "";
|
|
3786
|
+
const truncatedText = text.length > 60 ? text.substring(0, 57) + "..." : text;
|
|
3787
|
+
if (!meta) return `"${truncatedText}"`;
|
|
3788
|
+
switch (meta.contentRole) {
|
|
3789
|
+
case "heading":
|
|
3790
|
+
return `Level ${meta.headingLevel || "?"} heading: '${truncatedText}'`;
|
|
3791
|
+
case "table-cell":
|
|
3792
|
+
return `Table cell${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
|
|
3793
|
+
case "table-header":
|
|
3794
|
+
return `Table header${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
|
|
3795
|
+
case "status":
|
|
3796
|
+
return `Status message: '${truncatedText}'`;
|
|
3797
|
+
case "badge":
|
|
3798
|
+
return `Badge: '${truncatedText}'`;
|
|
3799
|
+
case "metric":
|
|
3800
|
+
return `Metric value: '${truncatedText}'`;
|
|
3801
|
+
case "body-text":
|
|
3802
|
+
return `Text: '${truncatedText}'`;
|
|
3803
|
+
case "list-item":
|
|
3804
|
+
return `List item: '${truncatedText}'`;
|
|
3805
|
+
case "quote":
|
|
3806
|
+
return `Blockquote: '${truncatedText}'`;
|
|
3807
|
+
case "code":
|
|
3808
|
+
return `Code block: '${truncatedText}'`;
|
|
3809
|
+
case "caption":
|
|
3810
|
+
return `Caption: '${truncatedText}'`;
|
|
3811
|
+
case "label":
|
|
3812
|
+
return `Label: '${truncatedText}'`;
|
|
3813
|
+
case "description":
|
|
3814
|
+
return `Description: '${truncatedText}'`;
|
|
3815
|
+
case "navigation":
|
|
3816
|
+
return `Navigation text: '${truncatedText}'`;
|
|
3817
|
+
default:
|
|
3818
|
+
return `Content: '${truncatedText}'`;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3297
3821
|
/**
|
|
3298
3822
|
* Build full page context
|
|
3299
3823
|
*/
|
|
@@ -3397,9 +3921,7 @@ var SemanticSnapshotManager = class {
|
|
|
3397
3921
|
*/
|
|
3398
3922
|
detectModals(elements) {
|
|
3399
3923
|
const modals = [];
|
|
3400
|
-
const dialogElements = elements.filter(
|
|
3401
|
-
(el) => el.type === "dialog" && el.state.visible
|
|
3402
|
-
);
|
|
3924
|
+
const dialogElements = elements.filter((el) => el.type === "dialog" && el.state.visible);
|
|
3403
3925
|
for (const dialog of dialogElements) {
|
|
3404
3926
|
const closeButton = elements.find(
|
|
3405
3927
|
(el) => el.type === "button" && el.state.visible && (el.semanticType === "cancel-button" || el.state.textContent?.toLowerCase().match(/close|cancel|x|dismiss/))
|
|
@@ -3450,9 +3972,7 @@ var SemanticSnapshotManager = class {
|
|
|
3450
3972
|
* Infer form purpose from fields
|
|
3451
3973
|
*/
|
|
3452
3974
|
inferFormPurpose(fields) {
|
|
3453
|
-
const labels = fields.map(
|
|
3454
|
-
(f) => (f.accessibleName || f.label || "").toLowerCase()
|
|
3455
|
-
);
|
|
3975
|
+
const labels = fields.map((f) => (f.accessibleName || f.label || "").toLowerCase());
|
|
3456
3976
|
const allLabels = labels.join(" ");
|
|
3457
3977
|
if (allLabels.includes("email") && allLabels.includes("password")) {
|
|
3458
3978
|
if (allLabels.includes("confirm") || allLabels.includes("name")) {
|
|
@@ -3506,6 +4026,13 @@ var SemanticSnapshotManager = class {
|
|
|
3506
4026
|
* Infer semantic type
|
|
3507
4027
|
*/
|
|
3508
4028
|
inferSemanticType(element) {
|
|
4029
|
+
if (element.category === "content" && element.contentMetadata) {
|
|
4030
|
+
const role = element.contentMetadata.contentRole;
|
|
4031
|
+
if (role === "heading" && element.contentMetadata.headingLevel) {
|
|
4032
|
+
return `heading-${element.contentMetadata.headingLevel}`;
|
|
4033
|
+
}
|
|
4034
|
+
return role;
|
|
4035
|
+
}
|
|
3509
4036
|
const text = (element.state.textContent || element.label || "").toLowerCase();
|
|
3510
4037
|
const type = element.type.toLowerCase();
|
|
3511
4038
|
if (type === "button") {
|
|
@@ -3590,6 +4117,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
|
|
|
3590
4117
|
const probableTrigger = detectTrigger(appeared, disappeared, limitedModifications);
|
|
3591
4118
|
const suggestedActions = finalConfig.generateSuggestions ? generateSuggestedActionsFromDiff(appeared, disappeared, limitedModifications, probableTrigger) : void 0;
|
|
3592
4119
|
const pageChanges = detectPageChanges(fromSnapshot, toSnapshot);
|
|
4120
|
+
const contentChanges = detectContentChanges(fromElements, toElements);
|
|
3593
4121
|
const summary = generateDiffSummary(
|
|
3594
4122
|
appeared.map((e) => e.description),
|
|
3595
4123
|
disappeared.map((e) => e.description),
|
|
@@ -3604,6 +4132,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
|
|
|
3604
4132
|
disappeared,
|
|
3605
4133
|
modified: limitedModifications
|
|
3606
4134
|
},
|
|
4135
|
+
contentChanges: contentChanges || void 0,
|
|
3607
4136
|
probableTrigger,
|
|
3608
4137
|
suggestedActions,
|
|
3609
4138
|
pageChanges,
|
|
@@ -3738,9 +4267,7 @@ function generateSuggestedActionsFromDiff(appeared, disappeared, modified, trigg
|
|
|
3738
4267
|
suggestions.push("Fix the validation errors before submitting");
|
|
3739
4268
|
}
|
|
3740
4269
|
if (trigger === "Modal opened") {
|
|
3741
|
-
const modal = appeared.find(
|
|
3742
|
-
(e) => e.type === "dialog" || e.semanticType?.includes("dialog")
|
|
3743
|
-
);
|
|
4270
|
+
const modal = appeared.find((e) => e.type === "dialog" || e.semanticType?.includes("dialog"));
|
|
3744
4271
|
if (modal) {
|
|
3745
4272
|
suggestions.push(`Interact with the "${modal.description}" dialog`);
|
|
3746
4273
|
}
|
|
@@ -3811,6 +4338,12 @@ function hasSignificantChanges(diff) {
|
|
|
3811
4338
|
if (diff.changes.disappeared.length > 0) return true;
|
|
3812
4339
|
if (diff.changes.modified.some((m) => m.significant)) return true;
|
|
3813
4340
|
if (diff.pageChanges?.urlChanged) return true;
|
|
4341
|
+
if (diff.contentChanges) {
|
|
4342
|
+
const cc = diff.contentChanges;
|
|
4343
|
+
if (cc.textChanges.length > 0) return true;
|
|
4344
|
+
if (cc.metricChanges.some((m) => m.significant)) return true;
|
|
4345
|
+
if (cc.statusChanges.length > 0) return true;
|
|
4346
|
+
}
|
|
3814
4347
|
return false;
|
|
3815
4348
|
}
|
|
3816
4349
|
function describeDiff(diff) {
|
|
@@ -3828,12 +4361,1739 @@ function describeDiff(diff) {
|
|
|
3828
4361
|
if (diff.pageChanges?.urlChanged) {
|
|
3829
4362
|
parts.push("URL changed");
|
|
3830
4363
|
}
|
|
4364
|
+
if (diff.contentChanges) {
|
|
4365
|
+
parts.push(diff.contentChanges.summary);
|
|
4366
|
+
}
|
|
3831
4367
|
if (parts.length === 0) {
|
|
3832
4368
|
return "No significant changes";
|
|
3833
4369
|
}
|
|
3834
4370
|
return parts.join(", ");
|
|
3835
4371
|
}
|
|
4372
|
+
var METRIC_CONTENT_TYPES = /* @__PURE__ */ new Set(["metric-value"]);
|
|
4373
|
+
var STATUS_CONTENT_TYPES = /* @__PURE__ */ new Set(["status-message", "badge"]);
|
|
4374
|
+
var HEADING_CONTENT_TYPES = /* @__PURE__ */ new Set(["heading"]);
|
|
4375
|
+
function isContentElement(element) {
|
|
4376
|
+
return element.category === "content" || element.contentMetadata !== void 0;
|
|
4377
|
+
}
|
|
4378
|
+
function getContentType(element) {
|
|
4379
|
+
if (element.contentMetadata?.contentRole) {
|
|
4380
|
+
return element.contentMetadata.contentRole;
|
|
4381
|
+
}
|
|
4382
|
+
return element.type;
|
|
4383
|
+
}
|
|
4384
|
+
function detectContentChanges(fromElements, toElements) {
|
|
4385
|
+
const textChanges = [];
|
|
4386
|
+
const metricChanges = [];
|
|
4387
|
+
const statusChanges = [];
|
|
4388
|
+
for (const [id, toElement] of toElements) {
|
|
4389
|
+
const fromElement = fromElements.get(id);
|
|
4390
|
+
if (fromElement) {
|
|
4391
|
+
if (isContentElement(toElement) || isContentElement(fromElement)) {
|
|
4392
|
+
const fromText = (fromElement.state.textContent || "").trim();
|
|
4393
|
+
const toText = (toElement.state.textContent || "").trim();
|
|
4394
|
+
if (fromText !== toText) {
|
|
4395
|
+
const contentType = getContentType(toElement);
|
|
4396
|
+
const label = toElement.description || toElement.accessibleName || id;
|
|
4397
|
+
if (METRIC_CONTENT_TYPES.has(contentType) || contentType === "metric") {
|
|
4398
|
+
const parsed = parseMetricChange(fromText, toText, id, label);
|
|
4399
|
+
if (parsed) {
|
|
4400
|
+
metricChanges.push(parsed);
|
|
4401
|
+
}
|
|
4402
|
+
} else if (STATUS_CONTENT_TYPES.has(contentType) || contentType === "status") {
|
|
4403
|
+
statusChanges.push({
|
|
4404
|
+
elementId: id,
|
|
4405
|
+
label,
|
|
4406
|
+
oldStatus: fromText,
|
|
4407
|
+
newStatus: toText,
|
|
4408
|
+
direction: classifyStatusDirection(fromText, toText)
|
|
4409
|
+
});
|
|
4410
|
+
} else {
|
|
4411
|
+
textChanges.push({
|
|
4412
|
+
elementId: id,
|
|
4413
|
+
contentType,
|
|
4414
|
+
oldText: fromText,
|
|
4415
|
+
newText: toText,
|
|
4416
|
+
changeType: "modified"
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
} else {
|
|
4422
|
+
if (isContentElement(toElement)) {
|
|
4423
|
+
const toText = (toElement.state.textContent || "").trim();
|
|
4424
|
+
if (toText) {
|
|
4425
|
+
textChanges.push({
|
|
4426
|
+
elementId: id,
|
|
4427
|
+
contentType: getContentType(toElement),
|
|
4428
|
+
oldText: "",
|
|
4429
|
+
newText: toText,
|
|
4430
|
+
changeType: "added"
|
|
4431
|
+
});
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
for (const [id, fromElement] of fromElements) {
|
|
4437
|
+
if (!toElements.has(id) && isContentElement(fromElement)) {
|
|
4438
|
+
const fromText = (fromElement.state.textContent || "").trim();
|
|
4439
|
+
if (fromText) {
|
|
4440
|
+
textChanges.push({
|
|
4441
|
+
elementId: id,
|
|
4442
|
+
contentType: getContentType(fromElement),
|
|
4443
|
+
oldText: fromText,
|
|
4444
|
+
newText: "",
|
|
4445
|
+
changeType: "removed"
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
if (textChanges.length === 0 && metricChanges.length === 0 && statusChanges.length === 0) {
|
|
4451
|
+
return null;
|
|
4452
|
+
}
|
|
4453
|
+
return {
|
|
4454
|
+
textChanges,
|
|
4455
|
+
metricChanges,
|
|
4456
|
+
statusChanges,
|
|
4457
|
+
summary: generateContentChangeSummary(textChanges, metricChanges, statusChanges)
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
function parseNumericValue(text) {
|
|
4461
|
+
const trimmed = text.trim();
|
|
4462
|
+
if (!trimmed) return null;
|
|
4463
|
+
let working = trimmed;
|
|
4464
|
+
let negate = false;
|
|
4465
|
+
if (working.startsWith("(") && working.endsWith(")")) {
|
|
4466
|
+
working = working.slice(1, -1).trim();
|
|
4467
|
+
negate = true;
|
|
4468
|
+
}
|
|
4469
|
+
if (working.startsWith("-")) {
|
|
4470
|
+
negate = !negate;
|
|
4471
|
+
working = working.slice(1).trim();
|
|
4472
|
+
}
|
|
4473
|
+
if (working.startsWith("+")) {
|
|
4474
|
+
working = working.slice(1).trim();
|
|
4475
|
+
}
|
|
4476
|
+
working = working.replace(/^[£€¥₹$]/, "").trim();
|
|
4477
|
+
const isPercent = working.endsWith("%");
|
|
4478
|
+
if (isPercent) {
|
|
4479
|
+
working = working.slice(0, -1).trim();
|
|
4480
|
+
}
|
|
4481
|
+
working = working.replace(/\s*(ms|s|m|h|d|hrs?|mins?|secs?|days?)$/i, "").trim();
|
|
4482
|
+
working = working.replace(/,/g, "");
|
|
4483
|
+
const num = Number(working);
|
|
4484
|
+
if (isNaN(num) || !isFinite(num) || working === "") {
|
|
4485
|
+
return null;
|
|
4486
|
+
}
|
|
4487
|
+
return negate ? -num : num;
|
|
4488
|
+
}
|
|
4489
|
+
function parseMetricChange(fromText, toText, elementId, label) {
|
|
4490
|
+
const fromNum = parseNumericValue(fromText);
|
|
4491
|
+
const toNum = parseNumericValue(toText);
|
|
4492
|
+
let numericDelta;
|
|
4493
|
+
let percentChange;
|
|
4494
|
+
let significant = false;
|
|
4495
|
+
if (fromNum !== null && toNum !== null) {
|
|
4496
|
+
numericDelta = toNum - fromNum;
|
|
4497
|
+
if (fromNum !== 0) {
|
|
4498
|
+
percentChange = (toNum - fromNum) / Math.abs(fromNum) * 100;
|
|
4499
|
+
}
|
|
4500
|
+
if (percentChange !== void 0 && Math.abs(percentChange) > 10) {
|
|
4501
|
+
significant = true;
|
|
4502
|
+
}
|
|
4503
|
+
if (fromNum > 0 && toNum < 0) significant = true;
|
|
4504
|
+
if (fromNum < 0 && toNum > 0) significant = true;
|
|
4505
|
+
if (fromNum === 0 && toNum !== 0) significant = true;
|
|
4506
|
+
if (fromNum !== 0 && toNum === 0) significant = true;
|
|
4507
|
+
} else {
|
|
4508
|
+
significant = fromText !== toText;
|
|
4509
|
+
}
|
|
4510
|
+
return {
|
|
4511
|
+
elementId,
|
|
4512
|
+
label,
|
|
4513
|
+
oldValue: fromText,
|
|
4514
|
+
newValue: toText,
|
|
4515
|
+
numericDelta,
|
|
4516
|
+
percentChange: percentChange !== void 0 ? Math.round(percentChange * 100) / 100 : void 0,
|
|
4517
|
+
significant
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4520
|
+
var STATUS_PROGRESSIONS = [
|
|
4521
|
+
[
|
|
4522
|
+
"failed",
|
|
4523
|
+
"error",
|
|
4524
|
+
"pending",
|
|
4525
|
+
"queued",
|
|
4526
|
+
"running",
|
|
4527
|
+
"in progress",
|
|
4528
|
+
"completed",
|
|
4529
|
+
"success",
|
|
4530
|
+
"done"
|
|
4531
|
+
],
|
|
4532
|
+
["disconnected", "connecting", "connected"],
|
|
4533
|
+
["unhealthy", "degraded", "healthy"],
|
|
4534
|
+
["offline", "online"],
|
|
4535
|
+
["inactive", "active"],
|
|
4536
|
+
["disabled", "enabled"],
|
|
4537
|
+
["down", "up"],
|
|
4538
|
+
["stopped", "starting", "started", "running"],
|
|
4539
|
+
["closed", "open"],
|
|
4540
|
+
["blocked", "unblocked"],
|
|
4541
|
+
["rejected", "pending", "approved"],
|
|
4542
|
+
["critical", "warning", "info", "ok"],
|
|
4543
|
+
["red", "yellow", "green"]
|
|
4544
|
+
];
|
|
4545
|
+
function classifyStatusDirection(oldStatus, newStatus) {
|
|
4546
|
+
const oldLower = oldStatus.toLowerCase().trim();
|
|
4547
|
+
const newLower = newStatus.toLowerCase().trim();
|
|
4548
|
+
for (const progression of STATUS_PROGRESSIONS) {
|
|
4549
|
+
let oldIndex = -1;
|
|
4550
|
+
let newIndex = -1;
|
|
4551
|
+
for (let i = 0; i < progression.length; i++) {
|
|
4552
|
+
if (oldLower.includes(progression[i])) oldIndex = i;
|
|
4553
|
+
if (newLower.includes(progression[i])) newIndex = i;
|
|
4554
|
+
}
|
|
4555
|
+
if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
|
|
4556
|
+
return newIndex > oldIndex ? "improved" : "degraded";
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
return "neutral";
|
|
4560
|
+
}
|
|
4561
|
+
function generateContentChangeSummary(textChanges, metricChanges, statusChanges) {
|
|
4562
|
+
const parts = [];
|
|
4563
|
+
const modified = textChanges.filter((t) => t.changeType === "modified").length;
|
|
4564
|
+
const added = textChanges.filter((t) => t.changeType === "added").length;
|
|
4565
|
+
const removed = textChanges.filter((t) => t.changeType === "removed").length;
|
|
4566
|
+
const headingChanges = textChanges.filter(
|
|
4567
|
+
(t) => HEADING_CONTENT_TYPES.has(t.contentType) || t.contentType === "heading"
|
|
4568
|
+
);
|
|
4569
|
+
if (headingChanges.length > 0) {
|
|
4570
|
+
parts.push(`${headingChanges.length} heading${headingChanges.length > 1 ? "s" : ""} changed`);
|
|
4571
|
+
}
|
|
4572
|
+
if (metricChanges.length > 0) {
|
|
4573
|
+
const significantMetrics = metricChanges.filter((m) => m.significant);
|
|
4574
|
+
if (significantMetrics.length > 0) {
|
|
4575
|
+
parts.push(
|
|
4576
|
+
`${significantMetrics.length} metric${significantMetrics.length > 1 ? "s" : ""} changed significantly`
|
|
4577
|
+
);
|
|
4578
|
+
} else {
|
|
4579
|
+
parts.push(`${metricChanges.length} metric${metricChanges.length > 1 ? "s" : ""} changed`);
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
if (statusChanges.length > 0) {
|
|
4583
|
+
const degraded = statusChanges.filter((s) => s.direction === "degraded");
|
|
4584
|
+
const improved = statusChanges.filter((s) => s.direction === "improved");
|
|
4585
|
+
if (degraded.length > 0) {
|
|
4586
|
+
parts.push(`${degraded.length} status${degraded.length > 1 ? "es" : ""} degraded`);
|
|
4587
|
+
}
|
|
4588
|
+
if (improved.length > 0) {
|
|
4589
|
+
parts.push(`${improved.length} status${improved.length > 1 ? "es" : ""} improved`);
|
|
4590
|
+
}
|
|
4591
|
+
const neutral = statusChanges.length - degraded.length - improved.length;
|
|
4592
|
+
if (neutral > 0 && degraded.length === 0 && improved.length === 0) {
|
|
4593
|
+
parts.push(`${neutral} status${neutral > 1 ? "es" : ""} changed`);
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
const otherModified = modified - headingChanges.filter((h) => h.changeType === "modified").length;
|
|
4597
|
+
if (otherModified > 0) {
|
|
4598
|
+
parts.push(`${otherModified} text${otherModified > 1 ? " values" : " value"} modified`);
|
|
4599
|
+
}
|
|
4600
|
+
if (added > 0) {
|
|
4601
|
+
parts.push(`${added} content${added > 1 ? " elements" : " element"} added`);
|
|
4602
|
+
}
|
|
4603
|
+
if (removed > 0) {
|
|
4604
|
+
parts.push(`${removed} content${removed > 1 ? " elements" : " element"} removed`);
|
|
4605
|
+
}
|
|
4606
|
+
if (parts.length === 0) {
|
|
4607
|
+
return "No content changes";
|
|
4608
|
+
}
|
|
4609
|
+
return parts.join(", ");
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
// src/ai/data-extraction.ts
|
|
4613
|
+
var DEFAULT_DATA_EXTRACTION_CONFIG = {
|
|
4614
|
+
minConfidence: 0.3,
|
|
4615
|
+
normalizeWhitespace: true
|
|
4616
|
+
};
|
|
4617
|
+
function classifyDataType(value) {
|
|
4618
|
+
const trimmed = value.trim();
|
|
4619
|
+
if (!trimmed) return { type: "unknown", confidence: 0 };
|
|
4620
|
+
if (/^(true|false|yes|no|on|off)$/i.test(trimmed)) {
|
|
4621
|
+
return { type: "boolean", confidence: 0.95 };
|
|
4622
|
+
}
|
|
4623
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
|
4624
|
+
return { type: "email", confidence: 0.95 };
|
|
4625
|
+
}
|
|
4626
|
+
if (/^https?:\/\/\S+/.test(trimmed)) {
|
|
4627
|
+
return { type: "url", confidence: 0.95 };
|
|
4628
|
+
}
|
|
4629
|
+
if (/^[+]?[\d\s\-().]{7,20}$/.test(trimmed) && /\d{3,}/.test(trimmed)) {
|
|
4630
|
+
return { type: "phone", confidence: 0.7 };
|
|
4631
|
+
}
|
|
4632
|
+
if (/^[£$€¥₹][\s]?[\d,.]+$/.test(trimmed) || /^[\d,.]+[\s]?[£$€¥₹]$/.test(trimmed)) {
|
|
4633
|
+
return { type: "currency", confidence: 0.9 };
|
|
4634
|
+
}
|
|
4635
|
+
if (/^[\d,.]+\s?%$/.test(trimmed)) {
|
|
4636
|
+
return { type: "percentage", confidence: 0.95 };
|
|
4637
|
+
}
|
|
4638
|
+
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)) {
|
|
4639
|
+
return { type: "date", confidence: 0.85 };
|
|
4640
|
+
}
|
|
4641
|
+
if (/^-?[\d,]+\.?\d*$/.test(trimmed) && trimmed !== "") {
|
|
4642
|
+
return { type: "number", confidence: 0.9 };
|
|
4643
|
+
}
|
|
4644
|
+
return { type: "text", confidence: 0.5 };
|
|
4645
|
+
}
|
|
4646
|
+
function normalizeValue(value, dataType) {
|
|
4647
|
+
const trimmed = value.trim();
|
|
4648
|
+
switch (dataType) {
|
|
4649
|
+
case "number":
|
|
4650
|
+
case "currency":
|
|
4651
|
+
case "percentage": {
|
|
4652
|
+
const numeric = trimmed.replace(/[^0-9.-]/g, "");
|
|
4653
|
+
const parsed = parseFloat(numeric);
|
|
4654
|
+
return isNaN(parsed) ? trimmed.toLowerCase() : parsed.toString();
|
|
4655
|
+
}
|
|
4656
|
+
case "date": {
|
|
4657
|
+
const d = new Date(trimmed);
|
|
4658
|
+
return isNaN(d.getTime()) ? trimmed.toLowerCase() : d.toISOString().split("T")[0];
|
|
4659
|
+
}
|
|
4660
|
+
case "boolean":
|
|
4661
|
+
return /^(true|yes|on)$/i.test(trimmed) ? "true" : "false";
|
|
4662
|
+
case "email":
|
|
4663
|
+
return trimmed.toLowerCase();
|
|
4664
|
+
case "url":
|
|
4665
|
+
return trimmed.replace(/\/+$/, "").toLowerCase();
|
|
4666
|
+
case "phone":
|
|
4667
|
+
return trimmed.replace(/[^\d+]/g, "");
|
|
4668
|
+
default:
|
|
4669
|
+
return trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
function extractElementValue(element) {
|
|
4673
|
+
const state = element.state;
|
|
4674
|
+
if (state?.value !== void 0 && state.value !== "") {
|
|
4675
|
+
return String(state.value);
|
|
4676
|
+
}
|
|
4677
|
+
if (state?.textContent !== void 0 && state.textContent !== "") {
|
|
4678
|
+
return String(state.textContent);
|
|
4679
|
+
}
|
|
4680
|
+
return "";
|
|
4681
|
+
}
|
|
4682
|
+
function extractLabel(element) {
|
|
4683
|
+
return element.accessibleName || element.labelText || element.label || element.description || element.id;
|
|
4684
|
+
}
|
|
4685
|
+
function extractPageData(elements, config = DEFAULT_DATA_EXTRACTION_CONFIG) {
|
|
4686
|
+
const values = {};
|
|
4687
|
+
let extractedCount = 0;
|
|
4688
|
+
for (const element of elements) {
|
|
4689
|
+
const rawValue = extractElementValue(element);
|
|
4690
|
+
if (!rawValue) continue;
|
|
4691
|
+
const label = extractLabel(element);
|
|
4692
|
+
const { type: dataType, confidence } = classifyDataType(rawValue);
|
|
4693
|
+
if (confidence < config.minConfidence) continue;
|
|
4694
|
+
const normalizedValue = normalizeValue(rawValue, dataType);
|
|
4695
|
+
values[label] = {
|
|
4696
|
+
elementId: element.id,
|
|
4697
|
+
label,
|
|
4698
|
+
rawValue: config.normalizeWhitespace ? rawValue.replace(/\s+/g, " ").trim() : rawValue,
|
|
4699
|
+
normalizedValue,
|
|
4700
|
+
dataType,
|
|
4701
|
+
confidence
|
|
4702
|
+
};
|
|
4703
|
+
extractedCount++;
|
|
4704
|
+
}
|
|
4705
|
+
return {
|
|
4706
|
+
values,
|
|
4707
|
+
scannedCount: elements.length,
|
|
4708
|
+
extractedCount
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
// src/ai/region-segmentation.ts
|
|
4713
|
+
var DEFAULT_REGION_SEGMENTATION_CONFIG = {
|
|
4714
|
+
minRegionElements: 1,
|
|
4715
|
+
headerFraction: 0.12,
|
|
4716
|
+
footerFraction: 0.9,
|
|
4717
|
+
sidebarFraction: 0.2
|
|
4718
|
+
};
|
|
4719
|
+
function toBounded(el) {
|
|
4720
|
+
const rect = el.state?.rect;
|
|
4721
|
+
if (!rect) return null;
|
|
4722
|
+
return {
|
|
4723
|
+
element: el,
|
|
4724
|
+
x: rect.x ?? 0,
|
|
4725
|
+
y: rect.y ?? 0,
|
|
4726
|
+
width: rect.width ?? 0,
|
|
4727
|
+
height: rect.height ?? 0
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
function classifyRegionType(el, relativeY, relativeX, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
|
|
4731
|
+
const role = (el.role || "").toLowerCase();
|
|
4732
|
+
const semanticType = (el.semanticType || "").toLowerCase();
|
|
4733
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
4734
|
+
if (role === "navigation" || role === "nav" || tag === "nav") {
|
|
4735
|
+
return { type: "navigation", confidence: 0.95 };
|
|
4736
|
+
}
|
|
4737
|
+
if (role === "banner" || tag === "header") {
|
|
4738
|
+
return { type: "header", confidence: 0.95 };
|
|
4739
|
+
}
|
|
4740
|
+
if (role === "contentinfo" || tag === "footer") {
|
|
4741
|
+
return { type: "footer", confidence: 0.95 };
|
|
4742
|
+
}
|
|
4743
|
+
if (role === "main" || tag === "main") {
|
|
4744
|
+
return { type: "main-content", confidence: 0.95 };
|
|
4745
|
+
}
|
|
4746
|
+
if (role === "complementary" || tag === "aside") {
|
|
4747
|
+
return { type: "sidebar", confidence: 0.9 };
|
|
4748
|
+
}
|
|
4749
|
+
if (role === "form" || tag === "form") {
|
|
4750
|
+
return { type: "form", confidence: 0.9 };
|
|
4751
|
+
}
|
|
4752
|
+
if (role === "table" || tag === "table") {
|
|
4753
|
+
return { type: "table", confidence: 0.9 };
|
|
4754
|
+
}
|
|
4755
|
+
if (role === "dialog" || role === "alertdialog") {
|
|
4756
|
+
return { type: "modal", confidence: 0.95 };
|
|
4757
|
+
}
|
|
4758
|
+
if (role === "toolbar") {
|
|
4759
|
+
return { type: "toolbar", confidence: 0.9 };
|
|
4760
|
+
}
|
|
4761
|
+
if (semanticType.includes("card")) {
|
|
4762
|
+
return { type: "card", confidence: 0.8 };
|
|
4763
|
+
}
|
|
4764
|
+
if (relativeY < config.headerFraction) {
|
|
4765
|
+
return { type: "header", confidence: 0.6 };
|
|
4766
|
+
}
|
|
4767
|
+
if (relativeY > config.footerFraction) {
|
|
4768
|
+
return { type: "footer", confidence: 0.6 };
|
|
4769
|
+
}
|
|
4770
|
+
if (relativeX < config.sidebarFraction) {
|
|
4771
|
+
return { type: "sidebar", confidence: 0.5 };
|
|
4772
|
+
}
|
|
4773
|
+
return { type: "main-content", confidence: 0.3 };
|
|
4774
|
+
}
|
|
4775
|
+
function segmentPageRegions(elements, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
|
|
4776
|
+
const bounded = elements.map(toBounded).filter((b) => b !== null);
|
|
4777
|
+
if (bounded.length === 0) {
|
|
4778
|
+
return { regions: [], assignedCount: 0, unassignedIds: elements.map((e) => e.id) };
|
|
4779
|
+
}
|
|
4780
|
+
let maxX = 0;
|
|
4781
|
+
let maxY = 0;
|
|
4782
|
+
for (const b of bounded) {
|
|
4783
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
4784
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
4785
|
+
}
|
|
4786
|
+
if (maxX === 0) maxX = 1;
|
|
4787
|
+
if (maxY === 0) maxY = 1;
|
|
4788
|
+
const regionGroups = /* @__PURE__ */ new Map();
|
|
4789
|
+
const unassignedIds = [];
|
|
4790
|
+
for (const b of bounded) {
|
|
4791
|
+
const relativeX = b.x / maxX;
|
|
4792
|
+
const relativeY = b.y / maxY;
|
|
4793
|
+
const { type, confidence } = classifyRegionType(b.element, relativeY, relativeX, config);
|
|
4794
|
+
if (!regionGroups.has(type)) {
|
|
4795
|
+
regionGroups.set(type, { elements: [], confidences: [] });
|
|
4796
|
+
}
|
|
4797
|
+
regionGroups.get(type).elements.push(b);
|
|
4798
|
+
regionGroups.get(type).confidences.push(confidence);
|
|
4799
|
+
}
|
|
4800
|
+
const regions = [];
|
|
4801
|
+
let assignedCount = 0;
|
|
4802
|
+
for (const [type, group] of regionGroups) {
|
|
4803
|
+
if (group.elements.length < config.minRegionElements) {
|
|
4804
|
+
for (const b of group.elements) unassignedIds.push(b.element.id);
|
|
4805
|
+
continue;
|
|
4806
|
+
}
|
|
4807
|
+
let minX = Infinity, minY = Infinity, maxRX = 0, maxRY = 0;
|
|
4808
|
+
const elementIds = [];
|
|
4809
|
+
for (const b of group.elements) {
|
|
4810
|
+
minX = Math.min(minX, b.x);
|
|
4811
|
+
minY = Math.min(minY, b.y);
|
|
4812
|
+
maxRX = Math.max(maxRX, b.x + b.width);
|
|
4813
|
+
maxRY = Math.max(maxRY, b.y + b.height);
|
|
4814
|
+
elementIds.push(b.element.id);
|
|
4815
|
+
}
|
|
4816
|
+
const avgConfidence = group.confidences.reduce((a, b) => a + b, 0) / group.confidences.length;
|
|
4817
|
+
regions.push({
|
|
4818
|
+
type,
|
|
4819
|
+
bounds: { x: minX, y: minY, width: maxRX - minX, height: maxRY - minY },
|
|
4820
|
+
elementIds,
|
|
4821
|
+
label: type.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
4822
|
+
confidence: Math.round(avgConfidence * 100) / 100
|
|
4823
|
+
});
|
|
4824
|
+
assignedCount += elementIds.length;
|
|
4825
|
+
}
|
|
4826
|
+
return { regions, assignedCount, unassignedIds };
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
// src/ai/table-extraction.ts
|
|
4830
|
+
var DEFAULT_TABLE_EXTRACTION_CONFIG = {
|
|
4831
|
+
minTableColumns: 2,
|
|
4832
|
+
minTableRows: 2,
|
|
4833
|
+
minListItems: 2,
|
|
4834
|
+
columnTolerance: 20,
|
|
4835
|
+
rowTolerance: 10
|
|
4836
|
+
};
|
|
4837
|
+
function getElementBounds(el) {
|
|
4838
|
+
const rect = el.state?.rect;
|
|
4839
|
+
if (!rect || rect.width === 0) return null;
|
|
4840
|
+
const text = el.state?.textContent ?? el.state?.value ?? "";
|
|
4841
|
+
if (!text) return null;
|
|
4842
|
+
return {
|
|
4843
|
+
element: el,
|
|
4844
|
+
x: rect.x ?? 0,
|
|
4845
|
+
y: rect.y ?? 0,
|
|
4846
|
+
width: rect.width ?? 0,
|
|
4847
|
+
height: rect.height ?? 0,
|
|
4848
|
+
text: text.trim()
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
function clusterPositions(values, tolerance) {
|
|
4852
|
+
if (values.length === 0) return [];
|
|
4853
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4854
|
+
const clusters = [sorted[0]];
|
|
4855
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
4856
|
+
if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
|
|
4857
|
+
clusters.push(sorted[i]);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
return clusters;
|
|
4861
|
+
}
|
|
4862
|
+
function assignToCluster(value, clusters, tolerance) {
|
|
4863
|
+
let best = 0;
|
|
4864
|
+
let bestDist = Math.abs(value - clusters[0]);
|
|
4865
|
+
for (let i = 1; i < clusters.length; i++) {
|
|
4866
|
+
const dist = Math.abs(value - clusters[i]);
|
|
4867
|
+
if (dist < bestDist) {
|
|
4868
|
+
bestDist = dist;
|
|
4869
|
+
best = i;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
return bestDist <= tolerance ? best : -1;
|
|
4873
|
+
}
|
|
4874
|
+
function detectTable(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
|
|
4875
|
+
const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
|
|
4876
|
+
if (withBounds.length < config.minTableColumns * config.minTableRows) return null;
|
|
4877
|
+
const xPositions = withBounds.map((b) => b.x);
|
|
4878
|
+
const yPositions = withBounds.map((b) => b.y);
|
|
4879
|
+
const columnClusters = clusterPositions(xPositions, config.columnTolerance);
|
|
4880
|
+
const rowClusters = clusterPositions(yPositions, config.rowTolerance);
|
|
4881
|
+
if (columnClusters.length < config.minTableColumns || rowClusters.length < config.minTableRows) {
|
|
4882
|
+
return null;
|
|
4883
|
+
}
|
|
4884
|
+
const grid = Array.from(
|
|
4885
|
+
{ length: rowClusters.length },
|
|
4886
|
+
() => Array(columnClusters.length).fill(null)
|
|
4887
|
+
);
|
|
4888
|
+
for (const b of withBounds) {
|
|
4889
|
+
const col = assignToCluster(b.x, columnClusters, config.columnTolerance);
|
|
4890
|
+
const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
|
|
4891
|
+
if (col >= 0 && row >= 0 && grid[row][col] === null) {
|
|
4892
|
+
grid[row][col] = b.text;
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
const headers = grid[0].map((h) => h ?? "");
|
|
4896
|
+
const columns = headers.map((header, index) => {
|
|
4897
|
+
const bodyCells = grid.slice(1).map((r) => r[index]).filter((c) => c !== null);
|
|
4898
|
+
const types = bodyCells.map((c) => classifyDataType(c).type);
|
|
4899
|
+
const mostCommon = mode(types) ?? "text";
|
|
4900
|
+
return { header, index, dataType: mostCommon };
|
|
4901
|
+
});
|
|
4902
|
+
const rows = grid.slice(1).map((row) => row.map((cell) => cell ?? ""));
|
|
4903
|
+
return {
|
|
4904
|
+
label: headers[0] || "Table",
|
|
4905
|
+
columns,
|
|
4906
|
+
rows
|
|
4907
|
+
};
|
|
4908
|
+
}
|
|
4909
|
+
function detectList(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
|
|
4910
|
+
const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
|
|
4911
|
+
if (withBounds.length < config.minListItems) return null;
|
|
4912
|
+
const sorted = [...withBounds].sort((a, b) => a.y - b.y);
|
|
4913
|
+
const yPositions = sorted.map((b) => b.y);
|
|
4914
|
+
const rowClusters = clusterPositions(yPositions, config.rowTolerance);
|
|
4915
|
+
if (rowClusters.length < config.minListItems) return null;
|
|
4916
|
+
const rowGroups = /* @__PURE__ */ new Map();
|
|
4917
|
+
for (const b of sorted) {
|
|
4918
|
+
const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
|
|
4919
|
+
if (row >= 0) {
|
|
4920
|
+
if (!rowGroups.has(row)) rowGroups.set(row, []);
|
|
4921
|
+
rowGroups.get(row).push(b);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
const items = [];
|
|
4925
|
+
const fieldLabels = [];
|
|
4926
|
+
let fieldLabelsInitialized = false;
|
|
4927
|
+
for (const [, rowElements] of [...rowGroups.entries()].sort(([a], [b]) => a - b)) {
|
|
4928
|
+
const sortedRow = [...rowElements].sort((a, b) => a.x - b.x);
|
|
4929
|
+
const item = {};
|
|
4930
|
+
for (let i = 0; i < sortedRow.length; i++) {
|
|
4931
|
+
const label = `field_${i}`;
|
|
4932
|
+
if (!fieldLabelsInitialized) fieldLabels.push(label);
|
|
4933
|
+
item[label] = sortedRow[i].text;
|
|
4934
|
+
}
|
|
4935
|
+
fieldLabelsInitialized = true;
|
|
4936
|
+
items.push(item);
|
|
4937
|
+
}
|
|
4938
|
+
if (items.length < config.minListItems) return null;
|
|
4939
|
+
const fields = fieldLabels.map((label) => {
|
|
4940
|
+
const values = items.map((item) => item[label]).filter(Boolean);
|
|
4941
|
+
const types = values.map((v) => classifyDataType(v).type);
|
|
4942
|
+
return { label, dataType: mode(types) ?? "text" };
|
|
4943
|
+
});
|
|
4944
|
+
return {
|
|
4945
|
+
label: "List",
|
|
4946
|
+
fields,
|
|
4947
|
+
items
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
function extractStructuredData(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
|
|
4951
|
+
const tables = [];
|
|
4952
|
+
const lists = [];
|
|
4953
|
+
const table = detectTable(elements, config);
|
|
4954
|
+
if (table) {
|
|
4955
|
+
tables.push(table);
|
|
4956
|
+
}
|
|
4957
|
+
const listCandidates = elements.filter((el) => {
|
|
4958
|
+
const role = el.role || el.type;
|
|
4959
|
+
return ["listitem", "row", "option", "link", "button"].includes(role);
|
|
4960
|
+
});
|
|
4961
|
+
if (listCandidates.length >= config.minListItems) {
|
|
4962
|
+
const list = detectList(listCandidates, config);
|
|
4963
|
+
if (list) {
|
|
4964
|
+
lists.push(list);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
return { tables, lists };
|
|
4968
|
+
}
|
|
4969
|
+
function mode(arr) {
|
|
4970
|
+
if (arr.length === 0) return void 0;
|
|
4971
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4972
|
+
let best = arr[0];
|
|
4973
|
+
let bestCount = 0;
|
|
4974
|
+
for (const v of arr) {
|
|
4975
|
+
const c = (counts.get(v) ?? 0) + 1;
|
|
4976
|
+
counts.set(v, c);
|
|
4977
|
+
if (c > bestCount) {
|
|
4978
|
+
bestCount = c;
|
|
4979
|
+
best = v;
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
return best;
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
// src/ai/format-analysis.ts
|
|
4986
|
+
var DEFAULT_FORMAT_ANALYSIS_CONFIG = {
|
|
4987
|
+
lenientFormatting: true
|
|
4988
|
+
};
|
|
4989
|
+
function detectFormatPattern(value, dataType) {
|
|
4990
|
+
const trimmed = value.trim();
|
|
4991
|
+
switch (dataType) {
|
|
4992
|
+
case "currency": {
|
|
4993
|
+
const hasLeadingSymbol = /^[£$€¥₹]/.test(trimmed);
|
|
4994
|
+
const hasTrailingSymbol = /[£$€¥₹]$/.test(trimmed);
|
|
4995
|
+
const usesCommaThousands = /\d{1,3}(,\d{3})+/.test(trimmed);
|
|
4996
|
+
const usesPeriodThousands = /\d{1,3}(\.\d{3})+,/.test(trimmed);
|
|
4997
|
+
let pattern = hasLeadingSymbol ? "$" : "";
|
|
4998
|
+
if (usesCommaThousands) pattern += "#,###";
|
|
4999
|
+
else if (usesPeriodThousands) pattern += "#.###";
|
|
5000
|
+
else pattern += "#";
|
|
5001
|
+
if (/\.\d{2}$/.test(trimmed)) pattern += ".##";
|
|
5002
|
+
else if (/,\d{2}$/.test(trimmed)) pattern += ",##";
|
|
5003
|
+
if (hasTrailingSymbol) pattern += "$";
|
|
5004
|
+
return pattern;
|
|
5005
|
+
}
|
|
5006
|
+
case "date": {
|
|
5007
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return "YYYY-MM-DD";
|
|
5008
|
+
if (/^\d{2}\/\d{2}\/\d{4}$/.test(trimmed)) return "MM/DD/YYYY";
|
|
5009
|
+
if (/^\d{2}\.\d{2}\.\d{4}$/.test(trimmed)) return "DD.MM.YYYY";
|
|
5010
|
+
if (/^\d{1,2}\/\d{1,2}\/\d{2}$/.test(trimmed)) return "M/D/YY";
|
|
5011
|
+
if (/^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) return "Month DD, YYYY";
|
|
5012
|
+
return "date";
|
|
5013
|
+
}
|
|
5014
|
+
case "percentage":
|
|
5015
|
+
return /\s%$/.test(trimmed) ? "#.## %" : "#.##%";
|
|
5016
|
+
case "number": {
|
|
5017
|
+
const hasCommas = /,/.test(trimmed);
|
|
5018
|
+
const decimalPlaces = trimmed.includes(".") ? trimmed.split(".")[1]?.length || 0 : 0;
|
|
5019
|
+
return (hasCommas ? "#,###" : "#") + (decimalPlaces > 0 ? "." + "#".repeat(decimalPlaces) : "");
|
|
5020
|
+
}
|
|
5021
|
+
case "phone": {
|
|
5022
|
+
if (/^\(\d{3}\)\s?\d{3}-\d{4}$/.test(trimmed)) return "(###) ###-####";
|
|
5023
|
+
if (/^\d{3}-\d{3}-\d{4}$/.test(trimmed)) return "###-###-####";
|
|
5024
|
+
if (/^\+\d/.test(trimmed)) return "+# ###...";
|
|
5025
|
+
return "phone";
|
|
5026
|
+
}
|
|
5027
|
+
default:
|
|
5028
|
+
return dataType;
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
function analyzeFormat(elementId, label, rawValue) {
|
|
5032
|
+
const { type: dataType } = classifyDataType(rawValue);
|
|
5033
|
+
const pattern = detectFormatPattern(rawValue, dataType);
|
|
5034
|
+
return {
|
|
5035
|
+
elementId,
|
|
5036
|
+
label,
|
|
5037
|
+
dataType,
|
|
5038
|
+
pattern,
|
|
5039
|
+
example: rawValue.trim()
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
function analyzePageFormats(elements) {
|
|
5043
|
+
const descriptors = [];
|
|
5044
|
+
for (const el of elements) {
|
|
5045
|
+
const rawValue = el.state?.value ?? el.state?.textContent ?? "";
|
|
5046
|
+
if (!rawValue) continue;
|
|
5047
|
+
const label = el.accessibleName || el.labelText || el.label || el.description || el.id;
|
|
5048
|
+
descriptors.push(analyzeFormat(el.id, label, rawValue));
|
|
5049
|
+
}
|
|
5050
|
+
return descriptors;
|
|
5051
|
+
}
|
|
5052
|
+
function compareFormats(sourceFormats, targetFormats, config = DEFAULT_FORMAT_ANALYSIS_CONFIG) {
|
|
5053
|
+
const mismatches = [];
|
|
5054
|
+
const targetByLabel = /* @__PURE__ */ new Map();
|
|
5055
|
+
for (const t of targetFormats) {
|
|
5056
|
+
targetByLabel.set(t.label.toLowerCase(), t);
|
|
5057
|
+
}
|
|
5058
|
+
for (const source of sourceFormats) {
|
|
5059
|
+
const target = targetByLabel.get(source.label.toLowerCase());
|
|
5060
|
+
if (!target) continue;
|
|
5061
|
+
if (source.dataType !== target.dataType) {
|
|
5062
|
+
mismatches.push({
|
|
5063
|
+
label: source.label,
|
|
5064
|
+
sourceFormat: source,
|
|
5065
|
+
targetFormat: target,
|
|
5066
|
+
severity: "error",
|
|
5067
|
+
description: `Data type mismatch: source is ${source.dataType}, target is ${target.dataType}`
|
|
5068
|
+
});
|
|
5069
|
+
continue;
|
|
5070
|
+
}
|
|
5071
|
+
if (source.pattern !== target.pattern) {
|
|
5072
|
+
const severity = config.lenientFormatting ? "warning" : "error";
|
|
5073
|
+
mismatches.push({
|
|
5074
|
+
label: source.label,
|
|
5075
|
+
sourceFormat: source,
|
|
5076
|
+
targetFormat: target,
|
|
5077
|
+
severity,
|
|
5078
|
+
description: `Format differs: source uses "${source.pattern}", target uses "${target.pattern}"`
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
return mismatches;
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
// src/ai/cross-app-diff.ts
|
|
5086
|
+
var DEFAULT_CROSS_APP_DIFF_CONFIG = {
|
|
5087
|
+
matchThreshold: 0.5,
|
|
5088
|
+
accessibleNameWeight: 1,
|
|
5089
|
+
textWeight: 0.95,
|
|
5090
|
+
rolePositionWeight: 0.7
|
|
5091
|
+
};
|
|
5092
|
+
function getElementText(el) {
|
|
5093
|
+
return el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "";
|
|
5094
|
+
}
|
|
5095
|
+
function getRole(el) {
|
|
5096
|
+
return (el.role || el.type || "").toLowerCase();
|
|
5097
|
+
}
|
|
5098
|
+
function getCenter(el) {
|
|
5099
|
+
const rect = el.state?.rect;
|
|
5100
|
+
if (!rect) return null;
|
|
5101
|
+
return {
|
|
5102
|
+
x: rect.x + rect.width / 2,
|
|
5103
|
+
y: rect.y + rect.height / 2
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
function computeMatchScore(source, target, config) {
|
|
5107
|
+
let bestScore = 0;
|
|
5108
|
+
let bestStrategy = "none";
|
|
5109
|
+
const srcName = (source.accessibleName || "").trim();
|
|
5110
|
+
const tgtName = (target.accessibleName || "").trim();
|
|
5111
|
+
if (srcName && tgtName && srcName.toLowerCase() === tgtName.toLowerCase()) {
|
|
5112
|
+
return { score: config.accessibleNameWeight, strategy: "accessible-name-exact" };
|
|
5113
|
+
}
|
|
5114
|
+
const srcText = getElementText(source);
|
|
5115
|
+
const tgtText = getElementText(target);
|
|
5116
|
+
if (srcText && tgtText && srcText.toLowerCase() === tgtText.toLowerCase()) {
|
|
5117
|
+
const score = config.textWeight;
|
|
5118
|
+
if (score > bestScore) {
|
|
5119
|
+
bestScore = score;
|
|
5120
|
+
bestStrategy = "text-exact";
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
if (srcText && tgtText) {
|
|
5124
|
+
const srcNorm = normalizeString(srcText);
|
|
5125
|
+
const tgtNorm = normalizeString(tgtText);
|
|
5126
|
+
const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
|
|
5127
|
+
const score = similarity * 0.85;
|
|
5128
|
+
if (score > bestScore) {
|
|
5129
|
+
bestScore = score;
|
|
5130
|
+
bestStrategy = "text-fuzzy";
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
const srcRole = getRole(source);
|
|
5134
|
+
const tgtRole = getRole(target);
|
|
5135
|
+
if (srcRole && srcRole === tgtRole) {
|
|
5136
|
+
const srcCenter = getCenter(source);
|
|
5137
|
+
const tgtCenter = getCenter(target);
|
|
5138
|
+
if (srcCenter && tgtCenter) {
|
|
5139
|
+
const dx = Math.abs(srcCenter.x - tgtCenter.x) / 1920;
|
|
5140
|
+
const dy = Math.abs(srcCenter.y - tgtCenter.y) / 1080;
|
|
5141
|
+
const posSimilarity = 1 - Math.min(1, Math.sqrt(dx * dx + dy * dy));
|
|
5142
|
+
const score = config.rolePositionWeight * posSimilarity;
|
|
5143
|
+
if (score > bestScore) {
|
|
5144
|
+
bestScore = score;
|
|
5145
|
+
bestStrategy = "role-position";
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
const srcVal = source.state?.value ?? source.state?.textContent ?? "";
|
|
5150
|
+
const tgtVal = target.state?.value ?? target.state?.textContent ?? "";
|
|
5151
|
+
if (srcVal && tgtVal) {
|
|
5152
|
+
const srcType = classifyDataType(srcVal).type;
|
|
5153
|
+
const tgtType = classifyDataType(tgtVal).type;
|
|
5154
|
+
const srcNorm = normalizeValue(srcVal, srcType);
|
|
5155
|
+
const tgtNorm = normalizeValue(tgtVal, tgtType);
|
|
5156
|
+
if (srcNorm === tgtNorm && srcNorm !== "") {
|
|
5157
|
+
const score = 0.6;
|
|
5158
|
+
if (score > bestScore) {
|
|
5159
|
+
bestScore = score;
|
|
5160
|
+
bestStrategy = "data-overlap";
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
return { score: bestScore, strategy: bestStrategy };
|
|
5165
|
+
}
|
|
5166
|
+
function matchElements(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
|
|
5167
|
+
const candidates = [];
|
|
5168
|
+
for (let si = 0; si < sourceElements.length; si++) {
|
|
5169
|
+
for (let ti = 0; ti < targetElements.length; ti++) {
|
|
5170
|
+
const { score, strategy } = computeMatchScore(sourceElements[si], targetElements[ti], config);
|
|
5171
|
+
if (score >= config.matchThreshold) {
|
|
5172
|
+
candidates.push({ sourceIdx: si, targetIdx: ti, score, strategy });
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
5176
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
5177
|
+
const usedSource = /* @__PURE__ */ new Set();
|
|
5178
|
+
const usedTarget = /* @__PURE__ */ new Set();
|
|
5179
|
+
const pairs = [];
|
|
5180
|
+
for (const c of candidates) {
|
|
5181
|
+
if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
|
|
5182
|
+
usedSource.add(c.sourceIdx);
|
|
5183
|
+
usedTarget.add(c.targetIdx);
|
|
5184
|
+
const src = sourceElements[c.sourceIdx];
|
|
5185
|
+
const tgt = targetElements[c.targetIdx];
|
|
5186
|
+
pairs.push({
|
|
5187
|
+
sourceId: src.id,
|
|
5188
|
+
targetId: tgt.id,
|
|
5189
|
+
sourceLabel: getElementText(src) || src.id,
|
|
5190
|
+
targetLabel: getElementText(tgt) || tgt.id,
|
|
5191
|
+
confidence: Math.round(c.score * 100) / 100,
|
|
5192
|
+
matchStrategy: c.strategy
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
return pairs;
|
|
5196
|
+
}
|
|
5197
|
+
function computeCrossAppDiff(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
|
|
5198
|
+
const matchedPairs = matchElements(sourceElements, targetElements, config);
|
|
5199
|
+
const matchedSourceIds = new Set(matchedPairs.map((p) => p.sourceId));
|
|
5200
|
+
const matchedTargetIds = new Set(matchedPairs.map((p) => p.targetId));
|
|
5201
|
+
const unmatchedSourceIds = sourceElements.filter((e) => !matchedSourceIds.has(e.id)).map((e) => e.id);
|
|
5202
|
+
const unmatchedTargetIds = targetElements.filter((e) => !matchedTargetIds.has(e.id)).map((e) => e.id);
|
|
5203
|
+
const sourceData = extractPageData(sourceElements);
|
|
5204
|
+
const targetData = extractPageData(targetElements);
|
|
5205
|
+
const dataComparisons = [];
|
|
5206
|
+
for (const pair of matchedPairs) {
|
|
5207
|
+
const srcEntry = Object.values(sourceData.values).find((v) => v.elementId === pair.sourceId);
|
|
5208
|
+
const tgtEntry = Object.values(targetData.values).find((v) => v.elementId === pair.targetId);
|
|
5209
|
+
if (srcEntry && tgtEntry) {
|
|
5210
|
+
dataComparisons.push({
|
|
5211
|
+
label: pair.sourceLabel,
|
|
5212
|
+
sourceValue: srcEntry.rawValue,
|
|
5213
|
+
targetValue: tgtEntry.rawValue,
|
|
5214
|
+
valuesMatch: srcEntry.normalizedValue === tgtEntry.normalizedValue,
|
|
5215
|
+
formatsMatch: srcEntry.dataType === tgtEntry.dataType
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
const sourceFormats = analyzePageFormats(sourceElements);
|
|
5220
|
+
const targetFormats = analyzePageFormats(targetElements);
|
|
5221
|
+
const formatMismatches = compareFormats(sourceFormats, targetFormats);
|
|
5222
|
+
return {
|
|
5223
|
+
matchedPairs,
|
|
5224
|
+
unmatchedSourceIds,
|
|
5225
|
+
unmatchedTargetIds,
|
|
5226
|
+
dataComparisons,
|
|
5227
|
+
formatMismatches
|
|
5228
|
+
};
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
// src/ai/action-parity.ts
|
|
5232
|
+
var DEFAULT_ACTION_PARITY_CONFIG = {
|
|
5233
|
+
ignoreActions: []
|
|
5234
|
+
};
|
|
5235
|
+
function getActions(el, ignoreActions) {
|
|
5236
|
+
const actions = el.actions || el.suggestedActions || [];
|
|
5237
|
+
const ignoreSet = new Set(ignoreActions.map((a) => a.toLowerCase()));
|
|
5238
|
+
return actions.map(
|
|
5239
|
+
(a) => typeof a === "string" ? a : a.action || a.name || ""
|
|
5240
|
+
).filter((a) => a && !ignoreSet.has(a.toLowerCase()));
|
|
5241
|
+
}
|
|
5242
|
+
function analyzeActionParity(matchedPairs, sourceElements, targetElements, config = DEFAULT_ACTION_PARITY_CONFIG) {
|
|
5243
|
+
const sourceById = new Map(sourceElements.map((e) => [e.id, e]));
|
|
5244
|
+
const targetById = new Map(targetElements.map((e) => [e.id, e]));
|
|
5245
|
+
const results = [];
|
|
5246
|
+
for (const pair of matchedPairs) {
|
|
5247
|
+
const src = sourceById.get(pair.sourceId);
|
|
5248
|
+
const tgt = targetById.get(pair.targetId);
|
|
5249
|
+
if (!src || !tgt) continue;
|
|
5250
|
+
const sourceActions = getActions(src, config.ignoreActions);
|
|
5251
|
+
const targetActions = getActions(tgt, config.ignoreActions);
|
|
5252
|
+
const sourceSet = new Set(sourceActions.map((a) => a.toLowerCase()));
|
|
5253
|
+
const targetSet = new Set(targetActions.map((a) => a.toLowerCase()));
|
|
5254
|
+
const missingInTarget = sourceActions.filter((a) => !targetSet.has(a.toLowerCase()));
|
|
5255
|
+
const missingInSource = targetActions.filter((a) => !sourceSet.has(a.toLowerCase()));
|
|
5256
|
+
results.push({
|
|
5257
|
+
pair,
|
|
5258
|
+
sourceActions,
|
|
5259
|
+
targetActions,
|
|
5260
|
+
missingInTarget,
|
|
5261
|
+
missingInSource
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
return results;
|
|
5265
|
+
}
|
|
5266
|
+
|
|
5267
|
+
// src/ai/navigation-map.ts
|
|
5268
|
+
var DEFAULT_NAVIGATION_MAP_CONFIG = {
|
|
5269
|
+
labelMatchThreshold: 0.8
|
|
5270
|
+
};
|
|
5271
|
+
function isNavigationElement(el) {
|
|
5272
|
+
const role = (el.role || "").toLowerCase();
|
|
5273
|
+
const type = (el.type || "").toLowerCase();
|
|
5274
|
+
const semanticType = (el.semanticType || "").toLowerCase();
|
|
5275
|
+
if (["link", "menuitem", "tab"].includes(role)) return true;
|
|
5276
|
+
if (["link", "menuitem"].includes(type)) return true;
|
|
5277
|
+
if (semanticType.includes("nav") || semanticType.includes("menu") || semanticType.includes("tab")) {
|
|
5278
|
+
return true;
|
|
5279
|
+
}
|
|
5280
|
+
const context = (el.parentContext || "").toLowerCase();
|
|
5281
|
+
if (context.includes("nav") || context.includes("menu") || context.includes("sidebar")) {
|
|
5282
|
+
if (role === "button" || type === "button" || role === "link" || type === "link") {
|
|
5283
|
+
return true;
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
function getNavLabel(el) {
|
|
5289
|
+
return el.accessibleName || el.labelText || el.label || el.description || el.id;
|
|
5290
|
+
}
|
|
5291
|
+
function getHref(el) {
|
|
5292
|
+
const state = el.state;
|
|
5293
|
+
return state?.href || void 0;
|
|
5294
|
+
}
|
|
5295
|
+
function hrefsMatch(a, b) {
|
|
5296
|
+
if (!a || !b) return false;
|
|
5297
|
+
const normalize = (h) => h.replace(/^https?:\/\//, "").replace(/localhost:\d+/, "").replace(/\/+$/, "").toLowerCase();
|
|
5298
|
+
return normalize(a) === normalize(b);
|
|
5299
|
+
}
|
|
5300
|
+
function buildNavigationMap(sourceElements, targetElements, config = DEFAULT_NAVIGATION_MAP_CONFIG) {
|
|
5301
|
+
const sourceNav = sourceElements.filter(isNavigationElement);
|
|
5302
|
+
const targetNav = targetElements.filter(isNavigationElement);
|
|
5303
|
+
const pairs = [];
|
|
5304
|
+
const matchedTargetIds = /* @__PURE__ */ new Set();
|
|
5305
|
+
for (const src of sourceNav) {
|
|
5306
|
+
const srcLabel = getNavLabel(src);
|
|
5307
|
+
const srcNorm = normalizeString(srcLabel);
|
|
5308
|
+
let bestTarget = null;
|
|
5309
|
+
let bestScore = 0;
|
|
5310
|
+
for (const tgt of targetNav) {
|
|
5311
|
+
if (matchedTargetIds.has(tgt.id)) continue;
|
|
5312
|
+
const tgtLabel = getNavLabel(tgt);
|
|
5313
|
+
const tgtNorm = normalizeString(tgtLabel);
|
|
5314
|
+
if (srcNorm === tgtNorm) {
|
|
5315
|
+
bestTarget = tgt;
|
|
5316
|
+
bestScore = 1;
|
|
5317
|
+
break;
|
|
5318
|
+
}
|
|
5319
|
+
const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
|
|
5320
|
+
if (similarity > bestScore && similarity >= config.labelMatchThreshold) {
|
|
5321
|
+
bestScore = similarity;
|
|
5322
|
+
bestTarget = tgt;
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
if (bestTarget) {
|
|
5326
|
+
matchedTargetIds.add(bestTarget.id);
|
|
5327
|
+
const srcHref = getHref(src);
|
|
5328
|
+
const tgtHref = getHref(bestTarget);
|
|
5329
|
+
pairs.push({
|
|
5330
|
+
sourceId: src.id,
|
|
5331
|
+
targetId: bestTarget.id,
|
|
5332
|
+
label: srcLabel,
|
|
5333
|
+
sourceHref: srcHref,
|
|
5334
|
+
targetHref: tgtHref,
|
|
5335
|
+
destinationMatch: hrefsMatch(srcHref, tgtHref)
|
|
5336
|
+
});
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
const sourceOnly = sourceNav.filter((s) => !pairs.some((p) => p.sourceId === s.id)).map((s) => s.id);
|
|
5340
|
+
const targetOnly = targetNav.filter((t) => !matchedTargetIds.has(t.id)).map((t) => t.id);
|
|
5341
|
+
return { pairs, sourceOnly, targetOnly };
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5344
|
+
// src/ai/component-comparison.ts
|
|
5345
|
+
var DEFAULT_COMPONENT_COMPARISON_CONFIG = {
|
|
5346
|
+
nameMatchThreshold: 0.75
|
|
5347
|
+
};
|
|
5348
|
+
function computeComponentMatchScore(source, target) {
|
|
5349
|
+
if (source.name.toLowerCase() === target.name.toLowerCase()) return 1;
|
|
5350
|
+
let score = 0;
|
|
5351
|
+
if (source.type === target.type) {
|
|
5352
|
+
score += 0.3;
|
|
5353
|
+
}
|
|
5354
|
+
const nameSimilarity = jaroWinklerSimilarity(
|
|
5355
|
+
normalizeString(source.name),
|
|
5356
|
+
normalizeString(target.name)
|
|
5357
|
+
);
|
|
5358
|
+
score += nameSimilarity * 0.7;
|
|
5359
|
+
return score;
|
|
5360
|
+
}
|
|
5361
|
+
function compareComponents(sourceComponents, targetComponents, config = DEFAULT_COMPONENT_COMPARISON_CONFIG) {
|
|
5362
|
+
const candidates = [];
|
|
5363
|
+
for (let si = 0; si < sourceComponents.length; si++) {
|
|
5364
|
+
for (let ti = 0; ti < targetComponents.length; ti++) {
|
|
5365
|
+
const score = computeComponentMatchScore(sourceComponents[si], targetComponents[ti]);
|
|
5366
|
+
if (score >= config.nameMatchThreshold) {
|
|
5367
|
+
candidates.push({ sourceIdx: si, targetIdx: ti, score });
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
5372
|
+
const usedSource = /* @__PURE__ */ new Set();
|
|
5373
|
+
const usedTarget = /* @__PURE__ */ new Set();
|
|
5374
|
+
const matches = [];
|
|
5375
|
+
for (const c of candidates) {
|
|
5376
|
+
if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
|
|
5377
|
+
usedSource.add(c.sourceIdx);
|
|
5378
|
+
usedTarget.add(c.targetIdx);
|
|
5379
|
+
const src = sourceComponents[c.sourceIdx];
|
|
5380
|
+
const tgt = targetComponents[c.targetIdx];
|
|
5381
|
+
const srcKeys = new Set(src.stateKeys);
|
|
5382
|
+
const tgtKeys = new Set(tgt.stateKeys);
|
|
5383
|
+
const missingKeys = src.stateKeys.filter((k) => !tgtKeys.has(k));
|
|
5384
|
+
const extraKeys = tgt.stateKeys.filter((k) => !srcKeys.has(k));
|
|
5385
|
+
const srcActions = new Set(src.actions.map((a) => a.toLowerCase()));
|
|
5386
|
+
const tgtActions = new Set(tgt.actions.map((a) => a.toLowerCase()));
|
|
5387
|
+
const missingActions = src.actions.filter((a) => !tgtActions.has(a.toLowerCase()));
|
|
5388
|
+
const extraActions = tgt.actions.filter((a) => !srcActions.has(a.toLowerCase()));
|
|
5389
|
+
matches.push({
|
|
5390
|
+
source: src,
|
|
5391
|
+
target: tgt,
|
|
5392
|
+
confidence: Math.round(c.score * 100) / 100,
|
|
5393
|
+
stateKeyDiff: { missing: missingKeys, extra: extraKeys },
|
|
5394
|
+
actionDiff: { missing: missingActions, extra: extraActions }
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
const sourceOnly = sourceComponents.filter((_, i) => !usedSource.has(i));
|
|
5398
|
+
const targetOnly = targetComponents.filter((_, i) => !usedTarget.has(i));
|
|
5399
|
+
return { matches, sourceOnly, targetOnly };
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
// src/ai/layout-comparison.ts
|
|
5403
|
+
var DEFAULT_LAYOUT_COMPARISON_CONFIG = {
|
|
5404
|
+
gridTolerance: 20
|
|
5405
|
+
};
|
|
5406
|
+
function getRect(el) {
|
|
5407
|
+
const rect = el.state?.rect;
|
|
5408
|
+
if (!rect || !rect.width) return null;
|
|
5409
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
5410
|
+
}
|
|
5411
|
+
function clusterValues(values, tolerance) {
|
|
5412
|
+
if (values.length === 0) return [];
|
|
5413
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5414
|
+
const clusters = [sorted[0]];
|
|
5415
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
5416
|
+
if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
|
|
5417
|
+
clusters.push(sorted[i]);
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
return clusters;
|
|
5421
|
+
}
|
|
5422
|
+
function detectGridStructure(elements, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
|
|
5423
|
+
const rects = elements.map(getRect).filter((r) => r !== null);
|
|
5424
|
+
const xPositions = rects.map((r) => r.x);
|
|
5425
|
+
const yPositions = rects.map((r) => r.y);
|
|
5426
|
+
const columns = clusterValues(xPositions, config.gridTolerance);
|
|
5427
|
+
const rows = clusterValues(yPositions, config.gridTolerance);
|
|
5428
|
+
return {
|
|
5429
|
+
columns,
|
|
5430
|
+
rows,
|
|
5431
|
+
columnCount: columns.length,
|
|
5432
|
+
rowCount: rows.length
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
function computeMaxDepth(elements) {
|
|
5436
|
+
let maxDepth = 0;
|
|
5437
|
+
for (const el of elements) {
|
|
5438
|
+
const context = el.parentContext || "";
|
|
5439
|
+
const depth = context ? context.split(">").length : 1;
|
|
5440
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
5441
|
+
}
|
|
5442
|
+
return maxDepth;
|
|
5443
|
+
}
|
|
5444
|
+
function computeProminence(element, pageWidth, pageHeight) {
|
|
5445
|
+
const rect = getRect(element);
|
|
5446
|
+
if (!rect || pageWidth === 0 || pageHeight === 0) return 0;
|
|
5447
|
+
const sizeScore = rect.width * rect.height / (pageWidth * pageHeight);
|
|
5448
|
+
const positionScore = 1 - rect.y / pageHeight;
|
|
5449
|
+
return Math.min(1, sizeScore * 0.6 + positionScore * 0.4);
|
|
5450
|
+
}
|
|
5451
|
+
function compareLayouts(sourceElements, targetElements, sourceRegions, targetRegions, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
|
|
5452
|
+
const sourceGrid = detectGridStructure(sourceElements, config);
|
|
5453
|
+
const targetGrid = detectGridStructure(targetElements, config);
|
|
5454
|
+
const gridDiff = {
|
|
5455
|
+
sourceGrid,
|
|
5456
|
+
targetGrid,
|
|
5457
|
+
columnDiff: sourceGrid.columnCount - targetGrid.columnCount,
|
|
5458
|
+
rowDiff: sourceGrid.rowCount - targetGrid.rowCount
|
|
5459
|
+
};
|
|
5460
|
+
const sourceDepth = computeMaxDepth(sourceElements);
|
|
5461
|
+
const targetDepth = computeMaxDepth(targetElements);
|
|
5462
|
+
const hierarchyDiff = {
|
|
5463
|
+
sourceDepth,
|
|
5464
|
+
targetDepth,
|
|
5465
|
+
depthDiff: sourceDepth - targetDepth
|
|
5466
|
+
};
|
|
5467
|
+
const sourceRegionCount = sourceRegions?.regions.length || 1;
|
|
5468
|
+
const targetRegionCount = targetRegions?.regions.length || 1;
|
|
5469
|
+
const sourceDensity = sourceElements.length / sourceRegionCount;
|
|
5470
|
+
const targetDensity = targetElements.length / targetRegionCount;
|
|
5471
|
+
const density = {
|
|
5472
|
+
sourceDensity: Math.round(sourceDensity * 100) / 100,
|
|
5473
|
+
targetDensity: Math.round(targetDensity * 100) / 100,
|
|
5474
|
+
ratio: targetDensity > 0 ? Math.round(sourceDensity / targetDensity * 100) / 100 : 0
|
|
5475
|
+
};
|
|
5476
|
+
const gridSimilarity = sourceGrid.columnCount === 0 && targetGrid.columnCount === 0 ? 1 : 1 - Math.abs(gridDiff.columnDiff) / Math.max(sourceGrid.columnCount, targetGrid.columnCount, 1);
|
|
5477
|
+
const hierarchySimilarity = sourceDepth === 0 && targetDepth === 0 ? 1 : 1 - Math.abs(hierarchyDiff.depthDiff) / Math.max(sourceDepth, targetDepth, 1);
|
|
5478
|
+
const densitySimilarity = density.ratio > 0 ? Math.min(density.ratio, 1 / density.ratio) : 0;
|
|
5479
|
+
const similarity = Math.round((gridSimilarity * 0.4 + hierarchySimilarity * 0.3 + densitySimilarity * 0.3) * 100) / 100;
|
|
5480
|
+
return {
|
|
5481
|
+
gridDiff,
|
|
5482
|
+
hierarchyDiff,
|
|
5483
|
+
density,
|
|
5484
|
+
similarity
|
|
5485
|
+
};
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
// src/ai/content-comparison.ts
|
|
5489
|
+
var DEFAULT_CONTENT_COMPARISON_CONFIG = {
|
|
5490
|
+
labelMatchThreshold: 0.8,
|
|
5491
|
+
headingMatchThreshold: 0.75,
|
|
5492
|
+
maxCellDifferences: 50
|
|
5493
|
+
};
|
|
5494
|
+
function getElementText2(el) {
|
|
5495
|
+
return (el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "").trim();
|
|
5496
|
+
}
|
|
5497
|
+
function getContentRole(el) {
|
|
5498
|
+
if (el.contentMetadata?.contentRole) {
|
|
5499
|
+
return el.contentMetadata.contentRole;
|
|
5500
|
+
}
|
|
5501
|
+
const t = (el.type || "").toLowerCase();
|
|
5502
|
+
if (t === "heading" || t.startsWith("h") && /^h[1-6]$/.test(t)) return "heading";
|
|
5503
|
+
if (t === "metric-value" || t === "metric") return "metric";
|
|
5504
|
+
if (t === "status-message" || t === "status") return "status";
|
|
5505
|
+
if (t === "label") return "label";
|
|
5506
|
+
if (t === "badge") return "badge";
|
|
5507
|
+
if (t === "table-cell") return "table-cell";
|
|
5508
|
+
if (t === "table-header") return "table-header";
|
|
5509
|
+
if (t === "caption") return "caption";
|
|
5510
|
+
return null;
|
|
5511
|
+
}
|
|
5512
|
+
function getHeadingLevel(el) {
|
|
5513
|
+
if (el.contentMetadata?.headingLevel) {
|
|
5514
|
+
return el.contentMetadata.headingLevel;
|
|
5515
|
+
}
|
|
5516
|
+
const tag = (el.tagName || el.type || "").toLowerCase();
|
|
5517
|
+
const match = /^h([1-6])$/.exec(tag);
|
|
5518
|
+
if (match) return parseInt(match[1], 10);
|
|
5519
|
+
return void 0;
|
|
5520
|
+
}
|
|
5521
|
+
function isContentElement2(el) {
|
|
5522
|
+
if (el.category === "content") return true;
|
|
5523
|
+
if (el.contentMetadata) return true;
|
|
5524
|
+
const role = getContentRole(el);
|
|
5525
|
+
return role !== null;
|
|
5526
|
+
}
|
|
5527
|
+
function normalizeText(text) {
|
|
5528
|
+
return normalizeString(text, { caseSensitive: false, ignoreWhitespace: true });
|
|
5529
|
+
}
|
|
5530
|
+
function parseMetricText(el) {
|
|
5531
|
+
const text = getElementText2(el);
|
|
5532
|
+
const colonMatch = text.match(/^(.+?):\s*(.+)$/);
|
|
5533
|
+
if (colonMatch) {
|
|
5534
|
+
return { label: colonMatch[1].trim(), value: colonMatch[2].trim() };
|
|
5535
|
+
}
|
|
5536
|
+
const dashMatch = text.match(/^(.+?)\s*[-]\s*(.+)$/);
|
|
5537
|
+
if (dashMatch) {
|
|
5538
|
+
return { label: dashMatch[1].trim(), value: dashMatch[2].trim() };
|
|
5539
|
+
}
|
|
5540
|
+
const elLabel = el.accessibleName || el.labelText || el.label || el.id;
|
|
5541
|
+
return { label: elLabel, value: text };
|
|
5542
|
+
}
|
|
5543
|
+
function filterHeadings(elements) {
|
|
5544
|
+
return elements.filter((el) => getContentRole(el) === "heading");
|
|
5545
|
+
}
|
|
5546
|
+
function filterMetrics(elements) {
|
|
5547
|
+
return elements.filter((el) => getContentRole(el) === "metric");
|
|
5548
|
+
}
|
|
5549
|
+
function filterStatuses(elements) {
|
|
5550
|
+
return elements.filter((el) => {
|
|
5551
|
+
const role = getContentRole(el);
|
|
5552
|
+
return role === "status" || role === "badge";
|
|
5553
|
+
});
|
|
5554
|
+
}
|
|
5555
|
+
function filterLabels(elements) {
|
|
5556
|
+
return elements.filter((el) => {
|
|
5557
|
+
const role = getContentRole(el);
|
|
5558
|
+
return role === "label" || role === "caption";
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
5561
|
+
function matchTexts(sourceTexts, targetTexts, threshold) {
|
|
5562
|
+
const candidates = [];
|
|
5563
|
+
for (let si = 0; si < sourceTexts.length; si++) {
|
|
5564
|
+
const sNorm = normalizeText(sourceTexts[si]);
|
|
5565
|
+
if (!sNorm) continue;
|
|
5566
|
+
for (let ti = 0; ti < targetTexts.length; ti++) {
|
|
5567
|
+
const tNorm = normalizeText(targetTexts[ti]);
|
|
5568
|
+
if (!tNorm) continue;
|
|
5569
|
+
const score = sNorm === tNorm ? 1 : jaroWinklerSimilarity(sNorm, tNorm);
|
|
5570
|
+
if (score >= threshold) {
|
|
5571
|
+
candidates.push({ sourceIdx: si, targetIdx: ti, score });
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
5576
|
+
const usedSource = /* @__PURE__ */ new Set();
|
|
5577
|
+
const usedTarget = /* @__PURE__ */ new Set();
|
|
5578
|
+
const matched = [];
|
|
5579
|
+
for (const c of candidates) {
|
|
5580
|
+
if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
|
|
5581
|
+
usedSource.add(c.sourceIdx);
|
|
5582
|
+
usedTarget.add(c.targetIdx);
|
|
5583
|
+
matched.push(c);
|
|
5584
|
+
}
|
|
5585
|
+
const unmatchedSource = sourceTexts.map((_, i) => i).filter((i) => !usedSource.has(i));
|
|
5586
|
+
const unmatchedTarget = targetTexts.map((_, i) => i).filter((i) => !usedTarget.has(i));
|
|
5587
|
+
return { matched, unmatchedSource, unmatchedTarget };
|
|
5588
|
+
}
|
|
5589
|
+
function compareHeadings(sourceElements, targetElements, config) {
|
|
5590
|
+
const srcHeadings = filterHeadings(sourceElements);
|
|
5591
|
+
const tgtHeadings = filterHeadings(targetElements);
|
|
5592
|
+
const srcTexts = srcHeadings.map(getElementText2);
|
|
5593
|
+
const tgtTexts = tgtHeadings.map(getElementText2);
|
|
5594
|
+
const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
|
|
5595
|
+
srcTexts,
|
|
5596
|
+
tgtTexts,
|
|
5597
|
+
config.headingMatchThreshold
|
|
5598
|
+
);
|
|
5599
|
+
const headingMatched = [];
|
|
5600
|
+
const headingChanged = [];
|
|
5601
|
+
for (const m of matched) {
|
|
5602
|
+
const srcText = srcTexts[m.sourceIdx];
|
|
5603
|
+
const tgtText = tgtTexts[m.targetIdx];
|
|
5604
|
+
const srcLevel = getHeadingLevel(srcHeadings[m.sourceIdx]);
|
|
5605
|
+
const tgtLevel = getHeadingLevel(tgtHeadings[m.targetIdx]);
|
|
5606
|
+
if (normalizeText(srcText) === normalizeText(tgtText)) {
|
|
5607
|
+
headingMatched.push({
|
|
5608
|
+
source: srcText,
|
|
5609
|
+
target: tgtText,
|
|
5610
|
+
level: srcLevel
|
|
5611
|
+
});
|
|
5612
|
+
} else {
|
|
5613
|
+
headingChanged.push({
|
|
5614
|
+
source: srcText,
|
|
5615
|
+
target: tgtText,
|
|
5616
|
+
level: srcLevel ?? tgtLevel
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
return {
|
|
5621
|
+
matched: headingMatched,
|
|
5622
|
+
sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
|
|
5623
|
+
targetOnly: unmatchedTarget.map((i) => tgtTexts[i]),
|
|
5624
|
+
changed: headingChanged
|
|
5625
|
+
};
|
|
5626
|
+
}
|
|
5627
|
+
function compareMetrics(sourceElements, targetElements, config) {
|
|
5628
|
+
const srcMetrics = filterMetrics(sourceElements);
|
|
5629
|
+
const tgtMetrics = filterMetrics(targetElements);
|
|
5630
|
+
const srcParsed = srcMetrics.map(parseMetricText);
|
|
5631
|
+
const tgtParsed = tgtMetrics.map(parseMetricText);
|
|
5632
|
+
const srcLabels = srcParsed.map((p) => p.label);
|
|
5633
|
+
const tgtLabels = tgtParsed.map((p) => p.label);
|
|
5634
|
+
const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
|
|
5635
|
+
srcLabels,
|
|
5636
|
+
tgtLabels,
|
|
5637
|
+
config.labelMatchThreshold
|
|
5638
|
+
);
|
|
5639
|
+
const metricMatched = [];
|
|
5640
|
+
const metricChanged = [];
|
|
5641
|
+
for (const m of matched) {
|
|
5642
|
+
const src = srcParsed[m.sourceIdx];
|
|
5643
|
+
const tgt = tgtParsed[m.targetIdx];
|
|
5644
|
+
if (normalizeText(src.value) === normalizeText(tgt.value)) {
|
|
5645
|
+
metricMatched.push({
|
|
5646
|
+
label: src.label,
|
|
5647
|
+
sourceValue: src.value,
|
|
5648
|
+
targetValue: tgt.value
|
|
5649
|
+
});
|
|
5650
|
+
} else {
|
|
5651
|
+
metricChanged.push({
|
|
5652
|
+
label: src.label,
|
|
5653
|
+
sourceValue: src.value,
|
|
5654
|
+
targetValue: tgt.value
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
return {
|
|
5659
|
+
matched: metricMatched,
|
|
5660
|
+
changed: metricChanged,
|
|
5661
|
+
sourceOnly: unmatchedSource.map((i) => srcParsed[i].label),
|
|
5662
|
+
targetOnly: unmatchedTarget.map((i) => tgtParsed[i].label)
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
function compareStatuses(sourceElements, targetElements, config) {
|
|
5666
|
+
const srcStatuses = filterStatuses(sourceElements);
|
|
5667
|
+
const tgtStatuses = filterStatuses(targetElements);
|
|
5668
|
+
const srcParsed = srcStatuses.map(parseMetricText);
|
|
5669
|
+
const tgtParsed = tgtStatuses.map(parseMetricText);
|
|
5670
|
+
const srcLabels = srcParsed.map((p) => p.label);
|
|
5671
|
+
const tgtLabels = tgtParsed.map((p) => p.label);
|
|
5672
|
+
const { matched } = matchTexts(srcLabels, tgtLabels, config.labelMatchThreshold);
|
|
5673
|
+
const statusMatched = [];
|
|
5674
|
+
const statusChanged = [];
|
|
5675
|
+
for (const m of matched) {
|
|
5676
|
+
const src = srcParsed[m.sourceIdx];
|
|
5677
|
+
const tgt = tgtParsed[m.targetIdx];
|
|
5678
|
+
if (normalizeText(src.value) === normalizeText(tgt.value)) {
|
|
5679
|
+
statusMatched.push({
|
|
5680
|
+
label: src.label,
|
|
5681
|
+
sourceStatus: src.value,
|
|
5682
|
+
targetStatus: tgt.value
|
|
5683
|
+
});
|
|
5684
|
+
} else {
|
|
5685
|
+
statusChanged.push({
|
|
5686
|
+
label: src.label,
|
|
5687
|
+
sourceStatus: src.value,
|
|
5688
|
+
targetStatus: tgt.value
|
|
5689
|
+
});
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
return {
|
|
5693
|
+
matched: statusMatched,
|
|
5694
|
+
changed: statusChanged
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5697
|
+
function compareLabels(sourceElements, targetElements, config) {
|
|
5698
|
+
const srcLabels = filterLabels(sourceElements);
|
|
5699
|
+
const tgtLabels = filterLabels(targetElements);
|
|
5700
|
+
const srcTexts = srcLabels.map(getElementText2);
|
|
5701
|
+
const tgtTexts = tgtLabels.map(getElementText2);
|
|
5702
|
+
const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
|
|
5703
|
+
srcTexts,
|
|
5704
|
+
tgtTexts,
|
|
5705
|
+
config.labelMatchThreshold
|
|
5706
|
+
);
|
|
5707
|
+
return {
|
|
5708
|
+
matched: matched.map((m) => srcTexts[m.sourceIdx]),
|
|
5709
|
+
sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
|
|
5710
|
+
targetOnly: unmatchedTarget.map((i) => tgtTexts[i])
|
|
5711
|
+
};
|
|
5712
|
+
}
|
|
5713
|
+
function compareTables(sourceElements, targetElements, config) {
|
|
5714
|
+
const srcData = extractStructuredData(sourceElements);
|
|
5715
|
+
const tgtData = extractStructuredData(targetElements);
|
|
5716
|
+
const srcTables = srcData.tables;
|
|
5717
|
+
const tgtTables = tgtData.tables;
|
|
5718
|
+
if (srcTables.length === 0 || tgtTables.length === 0) {
|
|
5719
|
+
return [];
|
|
5720
|
+
}
|
|
5721
|
+
const srcTableLabels = srcTables.map((t) => t.label || "");
|
|
5722
|
+
const tgtTableLabels = tgtTables.map((t) => t.label || "");
|
|
5723
|
+
const { matched } = matchTexts(srcTableLabels, tgtTableLabels, config.labelMatchThreshold);
|
|
5724
|
+
const tablePairs = [];
|
|
5725
|
+
if (matched.length > 0) {
|
|
5726
|
+
for (const m of matched) {
|
|
5727
|
+
tablePairs.push({ srcIdx: m.sourceIdx, tgtIdx: m.targetIdx });
|
|
5728
|
+
}
|
|
5729
|
+
} else if (srcTables.length === 1 && tgtTables.length === 1) {
|
|
5730
|
+
tablePairs.push({ srcIdx: 0, tgtIdx: 0 });
|
|
5731
|
+
}
|
|
5732
|
+
const comparisons = [];
|
|
5733
|
+
for (const pair of tablePairs) {
|
|
5734
|
+
const srcTable = srcTables[pair.srcIdx];
|
|
5735
|
+
const tgtTable = tgtTables[pair.tgtIdx];
|
|
5736
|
+
const srcHeaders = srcTable.columns.map((c) => c.header);
|
|
5737
|
+
const tgtHeaders = tgtTable.columns.map((c) => c.header);
|
|
5738
|
+
const srcHeaderSet = new Set(srcHeaders.map(normalizeText));
|
|
5739
|
+
const tgtHeaderSet = new Set(tgtHeaders.map(normalizeText));
|
|
5740
|
+
const sourceOnlyColumns = srcHeaders.filter((h) => !tgtHeaderSet.has(normalizeText(h)));
|
|
5741
|
+
const targetOnlyColumns = tgtHeaders.filter((h) => !srcHeaderSet.has(normalizeText(h)));
|
|
5742
|
+
const columnsMatch = sourceOnlyColumns.length === 0 && targetOnlyColumns.length === 0;
|
|
5743
|
+
const cellDifferences = [];
|
|
5744
|
+
const commonHeaders = srcHeaders.filter((h) => tgtHeaderSet.has(normalizeText(h)));
|
|
5745
|
+
const minRows = Math.min(srcTable.rows.length, tgtTable.rows.length);
|
|
5746
|
+
for (let row = 0; row < minRows; row++) {
|
|
5747
|
+
if (cellDifferences.length >= config.maxCellDifferences) break;
|
|
5748
|
+
for (const header of commonHeaders) {
|
|
5749
|
+
const srcColIdx = srcHeaders.indexOf(header);
|
|
5750
|
+
const tgtColIdx = tgtHeaders.findIndex((h) => normalizeText(h) === normalizeText(header));
|
|
5751
|
+
if (srcColIdx < 0 || tgtColIdx < 0) continue;
|
|
5752
|
+
const srcValue = srcTable.rows[row]?.[srcColIdx] ?? "";
|
|
5753
|
+
const tgtValue = tgtTable.rows[row]?.[tgtColIdx] ?? "";
|
|
5754
|
+
if (normalizeText(srcValue) !== normalizeText(tgtValue)) {
|
|
5755
|
+
cellDifferences.push({
|
|
5756
|
+
row,
|
|
5757
|
+
column: header,
|
|
5758
|
+
sourceValue: srcValue,
|
|
5759
|
+
targetValue: tgtValue
|
|
5760
|
+
});
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
comparisons.push({
|
|
5765
|
+
sourceLabel: srcTable.label,
|
|
5766
|
+
targetLabel: tgtTable.label,
|
|
5767
|
+
columnsMatch,
|
|
5768
|
+
sourceOnlyColumns,
|
|
5769
|
+
targetOnlyColumns,
|
|
5770
|
+
sourceRowCount: srcTable.rows.length,
|
|
5771
|
+
targetRowCount: tgtTable.rows.length,
|
|
5772
|
+
cellDifferences
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5775
|
+
return comparisons;
|
|
5776
|
+
}
|
|
5777
|
+
function compareHeadingHierarchy(sourceElements, targetElements) {
|
|
5778
|
+
const srcHeadings = filterHeadings(sourceElements);
|
|
5779
|
+
const tgtHeadings = filterHeadings(targetElements);
|
|
5780
|
+
const srcByLevel = /* @__PURE__ */ new Map();
|
|
5781
|
+
const tgtByLevel = /* @__PURE__ */ new Map();
|
|
5782
|
+
for (const el of srcHeadings) {
|
|
5783
|
+
const level = getHeadingLevel(el) ?? 0;
|
|
5784
|
+
srcByLevel.set(level, (srcByLevel.get(level) ?? 0) + 1);
|
|
5785
|
+
}
|
|
5786
|
+
for (const el of tgtHeadings) {
|
|
5787
|
+
const level = getHeadingLevel(el) ?? 0;
|
|
5788
|
+
tgtByLevel.set(level, (tgtByLevel.get(level) ?? 0) + 1);
|
|
5789
|
+
}
|
|
5790
|
+
const allLevels = /* @__PURE__ */ new Set([...srcByLevel.keys(), ...tgtByLevel.keys()]);
|
|
5791
|
+
const result = [];
|
|
5792
|
+
for (const level of [...allLevels].sort()) {
|
|
5793
|
+
result.push({
|
|
5794
|
+
level,
|
|
5795
|
+
sourceCount: srcByLevel.get(level) ?? 0,
|
|
5796
|
+
targetCount: tgtByLevel.get(level) ?? 0
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
return result;
|
|
5800
|
+
}
|
|
5801
|
+
function compareContent(sourceElements, targetElements, config = DEFAULT_CONTENT_COMPARISON_CONFIG) {
|
|
5802
|
+
const srcContent = sourceElements.filter(isContentElement2);
|
|
5803
|
+
const tgtContent = targetElements.filter(isContentElement2);
|
|
5804
|
+
const headings = compareHeadings(srcContent, tgtContent, config);
|
|
5805
|
+
const metrics = compareMetrics(srcContent, tgtContent, config);
|
|
5806
|
+
const statuses = compareStatuses(srcContent, tgtContent, config);
|
|
5807
|
+
const labels = compareLabels(srcContent, tgtContent, config);
|
|
5808
|
+
const tables = compareTables(sourceElements, targetElements, config);
|
|
5809
|
+
const headingHierarchy = compareHeadingHierarchy(srcContent, tgtContent);
|
|
5810
|
+
const contentParity = calculateContentParity(headings, metrics, statuses, labels, tables);
|
|
5811
|
+
return {
|
|
5812
|
+
headings,
|
|
5813
|
+
metrics,
|
|
5814
|
+
statuses,
|
|
5815
|
+
labels,
|
|
5816
|
+
tables,
|
|
5817
|
+
headingHierarchy,
|
|
5818
|
+
contentParity
|
|
5819
|
+
};
|
|
5820
|
+
}
|
|
5821
|
+
function calculateContentParity(headings, metrics, statuses, labels, tables) {
|
|
5822
|
+
const scores = [];
|
|
5823
|
+
const totalHeadings = headings.matched.length + headings.changed.length + headings.sourceOnly.length + headings.targetOnly.length;
|
|
5824
|
+
if (totalHeadings > 0) {
|
|
5825
|
+
scores.push(headings.matched.length / totalHeadings);
|
|
5826
|
+
}
|
|
5827
|
+
const totalMetrics = metrics.matched.length + metrics.changed.length + metrics.sourceOnly.length + metrics.targetOnly.length;
|
|
5828
|
+
if (totalMetrics > 0) {
|
|
5829
|
+
const metricScore = (metrics.matched.length + metrics.changed.length * 0.5) / totalMetrics;
|
|
5830
|
+
scores.push(metricScore);
|
|
5831
|
+
}
|
|
5832
|
+
const totalStatuses = statuses.matched.length + statuses.changed.length;
|
|
5833
|
+
if (totalStatuses > 0) {
|
|
5834
|
+
scores.push(statuses.matched.length / totalStatuses);
|
|
5835
|
+
}
|
|
5836
|
+
const totalLabels = labels.matched.length + labels.sourceOnly.length + labels.targetOnly.length;
|
|
5837
|
+
if (totalLabels > 0) {
|
|
5838
|
+
scores.push(labels.matched.length / totalLabels);
|
|
5839
|
+
}
|
|
5840
|
+
if (tables.length > 0) {
|
|
5841
|
+
let tableScore = 0;
|
|
5842
|
+
for (const table of tables) {
|
|
5843
|
+
let tScore = table.columnsMatch ? 0.5 : 0;
|
|
5844
|
+
if (table.sourceRowCount > 0) {
|
|
5845
|
+
const rowRatio = Math.min(
|
|
5846
|
+
table.targetRowCount / table.sourceRowCount,
|
|
5847
|
+
table.sourceRowCount / table.targetRowCount
|
|
5848
|
+
);
|
|
5849
|
+
tScore += rowRatio * 0.3;
|
|
5850
|
+
} else {
|
|
5851
|
+
tScore += 0.3;
|
|
5852
|
+
}
|
|
5853
|
+
const totalCells = Math.max(table.sourceRowCount, 1) * Math.max(
|
|
5854
|
+
table.sourceOnlyColumns.length + table.targetOnlyColumns.length + (table.columnsMatch ? 1 : 0),
|
|
5855
|
+
1
|
|
5856
|
+
);
|
|
5857
|
+
const diffRatio = totalCells > 0 ? 1 - Math.min(table.cellDifferences.length / totalCells, 1) : 1;
|
|
5858
|
+
tScore += diffRatio * 0.2;
|
|
5859
|
+
tableScore += tScore;
|
|
5860
|
+
}
|
|
5861
|
+
scores.push(tableScore / tables.length);
|
|
5862
|
+
}
|
|
5863
|
+
if (scores.length === 0) return 1;
|
|
5864
|
+
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 100) / 100;
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
// src/ai/comparison-report.ts
|
|
5868
|
+
var DEFAULT_COMPARISON_REPORT_CONFIG = {
|
|
5869
|
+
includeComponents: false
|
|
5870
|
+
};
|
|
5871
|
+
function generateComparisonReport(source, target, options) {
|
|
5872
|
+
const startTime = Date.now();
|
|
5873
|
+
const config = { ...DEFAULT_COMPARISON_REPORT_CONFIG, ...options?.config };
|
|
5874
|
+
const srcElements = source.elements;
|
|
5875
|
+
const tgtElements = target.elements;
|
|
5876
|
+
const diff = computeCrossAppDiff(srcElements, tgtElements);
|
|
5877
|
+
const navigation = buildNavigationMap(srcElements, tgtElements);
|
|
5878
|
+
const sourceRegions = segmentPageRegions(srcElements);
|
|
5879
|
+
const targetRegions = segmentPageRegions(tgtElements);
|
|
5880
|
+
const layout = compareLayouts(srcElements, tgtElements, sourceRegions, targetRegions);
|
|
5881
|
+
const actionParityResults = analyzeActionParity(diff.matchedPairs, srcElements, tgtElements);
|
|
5882
|
+
const componentComparison = config.includeComponents && options?.sourceComponents && options?.targetComponents ? compareComponents(options.sourceComponents, options.targetComponents) : null;
|
|
5883
|
+
const contentComparison = compareContent(srcElements, tgtElements);
|
|
5884
|
+
const sourceData = extractPageData(srcElements);
|
|
5885
|
+
extractPageData(tgtElements);
|
|
5886
|
+
const sourceFieldCount = Object.keys(sourceData.values).length;
|
|
5887
|
+
const matchedDataCount = diff.dataComparisons.length;
|
|
5888
|
+
const dataCompleteness = sourceFieldCount > 0 ? Math.round(matchedDataCount / sourceFieldCount * 100) / 100 : 1;
|
|
5889
|
+
const formatMatchCount = diff.dataComparisons.filter((c) => c.formatsMatch).length;
|
|
5890
|
+
const formatAlignment = matchedDataCount > 0 ? Math.round(formatMatchCount / matchedDataCount * 100) / 100 : 1;
|
|
5891
|
+
const presentationAlignment = layout.similarity;
|
|
5892
|
+
const totalNavItems = navigation.pairs.length + navigation.sourceOnly.length;
|
|
5893
|
+
const navigationParity = totalNavItems > 0 ? Math.round(navigation.pairs.length / totalNavItems * 100) / 100 : 1;
|
|
5894
|
+
const totalActionChecks = actionParityResults.length;
|
|
5895
|
+
const fullParityCount = actionParityResults.filter((r) => r.missingInTarget.length === 0).length;
|
|
5896
|
+
const actionParity = totalActionChecks > 0 ? Math.round(fullParityCount / totalActionChecks * 100) / 100 : 1;
|
|
5897
|
+
const contentParity = contentComparison.contentParity;
|
|
5898
|
+
const overallScore = Math.round(
|
|
5899
|
+
(dataCompleteness * 0.2 + formatAlignment * 0.1 + presentationAlignment * 0.15 + navigationParity * 0.15 + actionParity * 0.15 + contentParity * 0.25) * 100
|
|
5900
|
+
) / 100;
|
|
5901
|
+
const issues = [];
|
|
5902
|
+
for (const srcId of diff.unmatchedSourceIds) {
|
|
5903
|
+
const srcVal = Object.values(sourceData.values).find((v) => v.elementId === srcId);
|
|
5904
|
+
if (srcVal) {
|
|
5905
|
+
issues.push({
|
|
5906
|
+
severity: "warning",
|
|
5907
|
+
category: "missing-data",
|
|
5908
|
+
description: `Data field "${srcVal.label}" (${srcVal.dataType}) exists in source but has no match in target`,
|
|
5909
|
+
sourceElementId: srcId
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
for (const comp of diff.dataComparisons) {
|
|
5914
|
+
if (!comp.valuesMatch) {
|
|
5915
|
+
issues.push({
|
|
5916
|
+
severity: "error",
|
|
5917
|
+
category: "value-mismatch",
|
|
5918
|
+
description: `Value mismatch for "${comp.label}": source="${comp.sourceValue}", target="${comp.targetValue}"`
|
|
5919
|
+
});
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
for (const fm of diff.formatMismatches) {
|
|
5923
|
+
issues.push({
|
|
5924
|
+
severity: fm.severity,
|
|
5925
|
+
category: "format-mismatch",
|
|
5926
|
+
description: fm.description
|
|
5927
|
+
});
|
|
5928
|
+
}
|
|
5929
|
+
for (const ap of actionParityResults) {
|
|
5930
|
+
for (const action of ap.missingInTarget) {
|
|
5931
|
+
issues.push({
|
|
5932
|
+
severity: "warning",
|
|
5933
|
+
category: "missing-action",
|
|
5934
|
+
description: `Action "${action}" available on source element "${ap.pair.sourceLabel}" is missing in target`,
|
|
5935
|
+
sourceElementId: ap.pair.sourceId,
|
|
5936
|
+
targetElementId: ap.pair.targetId
|
|
5937
|
+
});
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
for (const srcId of navigation.sourceOnly) {
|
|
5941
|
+
issues.push({
|
|
5942
|
+
severity: "warning",
|
|
5943
|
+
category: "navigation-gap",
|
|
5944
|
+
description: `Navigation item "${srcId}" in source has no match in target`,
|
|
5945
|
+
sourceElementId: srcId
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
if (layout.similarity < 0.5) {
|
|
5949
|
+
issues.push({
|
|
5950
|
+
severity: "warning",
|
|
5951
|
+
category: "layout-difference",
|
|
5952
|
+
description: `Layout similarity is low (${layout.similarity}). Grid: ${layout.gridDiff.sourceGrid.columnCount} cols vs ${layout.gridDiff.targetGrid.columnCount} cols`
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
if (componentComparison) {
|
|
5956
|
+
for (const src of componentComparison.sourceOnly) {
|
|
5957
|
+
issues.push({
|
|
5958
|
+
severity: "info",
|
|
5959
|
+
category: "component-mismatch",
|
|
5960
|
+
description: `Component "${src.name}" (${src.type}) exists in source but not target`
|
|
5961
|
+
});
|
|
5962
|
+
}
|
|
5963
|
+
for (const match of componentComparison.matches) {
|
|
5964
|
+
if (match.stateKeyDiff.missing.length > 0) {
|
|
5965
|
+
issues.push({
|
|
5966
|
+
severity: "warning",
|
|
5967
|
+
category: "component-mismatch",
|
|
5968
|
+
description: `Component "${match.source.name}": state keys missing in target: ${match.stateKeyDiff.missing.join(", ")}`
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
for (const heading of contentComparison.headings.sourceOnly) {
|
|
5974
|
+
issues.push({
|
|
5975
|
+
severity: "warning",
|
|
5976
|
+
category: "content-difference",
|
|
5977
|
+
description: `Heading "${heading}" exists in source but not in target`
|
|
5978
|
+
});
|
|
5979
|
+
}
|
|
5980
|
+
for (const heading of contentComparison.headings.targetOnly) {
|
|
5981
|
+
issues.push({
|
|
5982
|
+
severity: "info",
|
|
5983
|
+
category: "content-difference",
|
|
5984
|
+
description: `Heading "${heading}" exists in target but not in source`
|
|
5985
|
+
});
|
|
5986
|
+
}
|
|
5987
|
+
for (const change of contentComparison.headings.changed) {
|
|
5988
|
+
issues.push({
|
|
5989
|
+
severity: "warning",
|
|
5990
|
+
category: "content-difference",
|
|
5991
|
+
description: `Heading changed: "${change.source}" -> "${change.target}"`
|
|
5992
|
+
});
|
|
5993
|
+
}
|
|
5994
|
+
for (const change of contentComparison.metrics.changed) {
|
|
5995
|
+
issues.push({
|
|
5996
|
+
severity: "warning",
|
|
5997
|
+
category: "content-difference",
|
|
5998
|
+
description: `Metric "${change.label}" value differs: "${change.sourceValue}" vs "${change.targetValue}"`
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
for (const label of contentComparison.metrics.sourceOnly) {
|
|
6002
|
+
issues.push({
|
|
6003
|
+
severity: "warning",
|
|
6004
|
+
category: "content-difference",
|
|
6005
|
+
description: `Metric "${label}" exists in source but not in target`
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
for (const change of contentComparison.statuses.changed) {
|
|
6009
|
+
issues.push({
|
|
6010
|
+
severity: "warning",
|
|
6011
|
+
category: "content-difference",
|
|
6012
|
+
description: `Status "${change.label}" differs: "${change.sourceStatus}" vs "${change.targetStatus}"`
|
|
6013
|
+
});
|
|
6014
|
+
}
|
|
6015
|
+
for (const table of contentComparison.tables) {
|
|
6016
|
+
if (!table.columnsMatch) {
|
|
6017
|
+
issues.push({
|
|
6018
|
+
severity: "warning",
|
|
6019
|
+
category: "content-difference",
|
|
6020
|
+
description: `Table "${table.sourceLabel}" column mismatch: source-only=[${table.sourceOnlyColumns.join(", ")}], target-only=[${table.targetOnlyColumns.join(", ")}]`
|
|
6021
|
+
});
|
|
6022
|
+
}
|
|
6023
|
+
if (table.sourceRowCount !== table.targetRowCount) {
|
|
6024
|
+
issues.push({
|
|
6025
|
+
severity: "info",
|
|
6026
|
+
category: "content-difference",
|
|
6027
|
+
description: `Table "${table.sourceLabel}" row count differs: ${table.sourceRowCount} vs ${table.targetRowCount}`
|
|
6028
|
+
});
|
|
6029
|
+
}
|
|
6030
|
+
if (table.cellDifferences.length > 0) {
|
|
6031
|
+
issues.push({
|
|
6032
|
+
severity: "warning",
|
|
6033
|
+
category: "content-difference",
|
|
6034
|
+
description: `Table "${table.sourceLabel}" has ${table.cellDifferences.length} cell value difference(s)`
|
|
6035
|
+
});
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
6039
|
+
issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
6040
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
6041
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
6042
|
+
const infoCount = issues.filter((i) => i.severity === "info").length;
|
|
6043
|
+
const summaryLines = [
|
|
6044
|
+
`Cross-app comparison: ${source.page.url} vs ${target.page.url}`,
|
|
6045
|
+
`Overall score: ${(overallScore * 100).toFixed(0)}%`,
|
|
6046
|
+
`Matched elements: ${diff.matchedPairs.length}`,
|
|
6047
|
+
`Unmatched: ${diff.unmatchedSourceIds.length} source, ${diff.unmatchedTargetIds.length} target`,
|
|
6048
|
+
`Navigation: ${navigation.pairs.length} matched, ${navigation.sourceOnly.length} source-only, ${navigation.targetOnly.length} target-only`
|
|
6049
|
+
];
|
|
6050
|
+
if (componentComparison) {
|
|
6051
|
+
summaryLines.push(
|
|
6052
|
+
`Components: ${componentComparison.matches.length} matched, ${componentComparison.sourceOnly.length} source-only, ${componentComparison.targetOnly.length} target-only`
|
|
6053
|
+
);
|
|
6054
|
+
}
|
|
6055
|
+
const hMatched = contentComparison.headings.matched.length;
|
|
6056
|
+
const hChanged = contentComparison.headings.changed.length;
|
|
6057
|
+
const hSrcOnly = contentComparison.headings.sourceOnly.length;
|
|
6058
|
+
const hTgtOnly = contentComparison.headings.targetOnly.length;
|
|
6059
|
+
const mMatched = contentComparison.metrics.matched.length;
|
|
6060
|
+
const mChanged = contentComparison.metrics.changed.length;
|
|
6061
|
+
const sMatched = contentComparison.statuses.matched.length;
|
|
6062
|
+
const sChanged = contentComparison.statuses.changed.length;
|
|
6063
|
+
const totalContent = hMatched + hChanged + hSrcOnly + hTgtOnly + mMatched + mChanged + sMatched + sChanged;
|
|
6064
|
+
if (totalContent > 0) {
|
|
6065
|
+
summaryLines.push(
|
|
6066
|
+
`Content: headings=${hMatched} matched/${hChanged} changed/${hSrcOnly + hTgtOnly} unmatched, metrics=${mMatched} matched/${mChanged} changed, statuses=${sMatched} matched/${sChanged} changed, parity=${(contentParity * 100).toFixed(0)}%`
|
|
6067
|
+
);
|
|
6068
|
+
}
|
|
6069
|
+
summaryLines.push(`Issues: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
|
|
6070
|
+
const summary = summaryLines.join("\n");
|
|
6071
|
+
const report = {
|
|
6072
|
+
sourceUrl: source.page.url,
|
|
6073
|
+
targetUrl: target.page.url,
|
|
6074
|
+
timestamp: Date.now(),
|
|
6075
|
+
durationMs: Date.now() - startTime,
|
|
6076
|
+
scores: {
|
|
6077
|
+
dataCompleteness,
|
|
6078
|
+
formatAlignment,
|
|
6079
|
+
presentationAlignment,
|
|
6080
|
+
navigationParity,
|
|
6081
|
+
actionParity,
|
|
6082
|
+
overallScore
|
|
6083
|
+
},
|
|
6084
|
+
diff,
|
|
6085
|
+
navigation,
|
|
6086
|
+
layout,
|
|
6087
|
+
contentComparison,
|
|
6088
|
+
issues,
|
|
6089
|
+
summary
|
|
6090
|
+
};
|
|
6091
|
+
if (componentComparison) {
|
|
6092
|
+
report.components = componentComparison;
|
|
6093
|
+
}
|
|
6094
|
+
return report;
|
|
6095
|
+
}
|
|
3836
6096
|
|
|
3837
|
-
export { AssertionExecutor, DEFAULT_ALIAS_CONFIG, DEFAULT_ASSERTION_CONFIG, DEFAULT_DIFF_CONFIG, DEFAULT_EXECUTOR_CONFIG, DEFAULT_FUZZY_CONFIG, DEFAULT_SEARCH_CONFIG, DEFAULT_SNAPSHOT_CONFIG, ErrorCodes, NLActionExecutor, SearchEngine, SemanticDiffManager, SemanticSnapshotManager, areSynonyms, computeDiff, createAssertionExecutor, createDiffManager, createErrorContext, createNLActionExecutor, createSearchEngine, createSimpleError, createSnapshotManager, describeAction, describeDiff, extractModifiers, findAllMatches, findBestMatch, formatErrorContext, fuzzyContains, fuzzyMatch, generateAliases, generateDescription, generateDiffSummary, generateElementDescription, generateNgrams, generatePageSummary, generatePurpose, generateSnapshotSummary, generateSuggestedActions, getBestRecoverySuggestion, getSynonyms, hasSignificantChanges, inferPageType, isRecoverableError, jaroSimilarity, jaroWinklerSimilarity, levenshteinDistance, levenshteinSimilarity, ngramSimilarity, normalizeString, parseNLInstruction, parseNLInstructions, splitCompoundInstruction, tokenSimilarity, tokenize, validateParsedAction, wordSimilarity };
|
|
6097
|
+
export { AssertionExecutor, DEFAULT_ACTION_PARITY_CONFIG, DEFAULT_ALIAS_CONFIG, DEFAULT_ASSERTION_CONFIG, DEFAULT_COMPARISON_REPORT_CONFIG, DEFAULT_COMPONENT_COMPARISON_CONFIG, DEFAULT_CONTENT_COMPARISON_CONFIG, DEFAULT_CROSS_APP_DIFF_CONFIG, DEFAULT_DATA_EXTRACTION_CONFIG, DEFAULT_DIFF_CONFIG, DEFAULT_EXECUTOR_CONFIG, DEFAULT_FORMAT_ANALYSIS_CONFIG, DEFAULT_FUZZY_CONFIG, DEFAULT_LAYOUT_COMPARISON_CONFIG, DEFAULT_NAVIGATION_MAP_CONFIG, DEFAULT_REGION_SEGMENTATION_CONFIG, DEFAULT_SEARCH_CONFIG, DEFAULT_SNAPSHOT_CONFIG, DEFAULT_TABLE_EXTRACTION_CONFIG, ErrorCodes, NLActionExecutor, SearchEngine, SemanticDiffManager, SemanticSnapshotManager, analyzeActionParity, analyzeFormat, analyzePageFormats, areSynonyms, buildNavigationMap, classifyDataType, classifyRegionType, classifyStatusDirection, compareComponents, compareContent, compareFormats, compareLayouts, computeCrossAppDiff, computeDiff, computeProminence, createAssertionExecutor, createDiffManager, createErrorContext, createNLActionExecutor, createSearchEngine, createSimpleError, createSnapshotManager, describeAction, describeDiff, detectFormatPattern, detectGridStructure, detectList, detectTable, extractModifiers, extractPageData, extractStructuredData, findAllMatches, findBestMatch, formatErrorContext, fuzzyContains, fuzzyMatch, generateAliases, generateComparisonReport, generateDescription, generateDiffSummary, generateElementDescription, generateNgrams, generatePageSummary, generatePurpose, generateSnapshotSummary, generateSuggestedActions, getBestRecoverySuggestion, getSynonyms, hasSignificantChanges, inferPageType, isNavigationElement, isRecoverableError, jaroSimilarity, jaroWinklerSimilarity, levenshteinDistance, levenshteinSimilarity, matchElements, ngramSimilarity, normalizeString, normalizeValue, parseNLInstruction, parseNLInstructions, parseNumericValue, segmentPageRegions, splitCompoundInstruction, tokenSimilarity, tokenize, validateParsedAction, wordSimilarity };
|
|
3838
6098
|
//# sourceMappingURL=index.mjs.map
|
|
3839
6099
|
//# sourceMappingURL=index.mjs.map
|