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