@nextpa/chat-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/constants/defaults.d.ts +6 -0
- package/dist/constants/defaults.js +69 -0
- package/dist/core/chat-manager.d.ts +17 -0
- package/dist/core/chat-manager.js +39 -0
- package/dist/core/config-manager.d.ts +14 -0
- package/dist/core/config-manager.js +75 -0
- package/dist/core/connection-manager.d.ts +68 -0
- package/dist/core/connection-manager.js +273 -0
- package/dist/core/connection-retry-handler.d.ts +11 -0
- package/dist/core/connection-retry-handler.js +33 -0
- package/dist/core/consent-manager.d.ts +15 -0
- package/dist/core/consent-manager.js +40 -0
- package/dist/core/conversation-persistence.d.ts +23 -0
- package/dist/core/conversation-persistence.js +109 -0
- package/dist/core/event-emitter.d.ts +7 -0
- package/dist/core/event-emitter.js +27 -0
- package/dist/core/greeting-gate.d.ts +16 -0
- package/dist/core/greeting-gate.js +53 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +7 -0
- package/dist/core/link-target.d.ts +15 -0
- package/dist/core/link-target.js +64 -0
- package/dist/core/local-storage-persistence.d.ts +11 -0
- package/dist/core/local-storage-persistence.js +111 -0
- package/dist/core/navigation-manager.d.ts +24 -0
- package/dist/core/navigation-manager.js +132 -0
- package/dist/core/popover-manager.d.ts +14 -0
- package/dist/core/popover-manager.js +36 -0
- package/dist/core/session-beacon.d.ts +11 -0
- package/dist/core/session-beacon.js +60 -0
- package/dist/core/text-connection-manager.d.ts +58 -0
- package/dist/core/text-connection-manager.js +200 -0
- package/dist/core/text-mode-manager.d.ts +50 -0
- package/dist/core/text-mode-manager.js +154 -0
- package/dist/core/text-types.d.ts +20 -0
- package/dist/core/text-types.js +1 -0
- package/dist/core/text-widget-controller.d.ts +47 -0
- package/dist/core/text-widget-controller.js +278 -0
- package/dist/core/theme-manager.d.ts +10 -0
- package/dist/core/theme-manager.js +25 -0
- package/dist/core/types.d.ts +50 -0
- package/dist/core/types.js +1 -0
- package/dist/core/voice-state-manager.d.ts +85 -0
- package/dist/core/voice-state-manager.js +317 -0
- package/dist/core/widget-controller.d.ts +58 -0
- package/dist/core/widget-controller.js +533 -0
- package/dist/core/widget-core.d.ts +51 -0
- package/dist/core/widget-core.js +160 -0
- package/dist/google.d.ts +1 -0
- package/dist/google.js +12 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +40 -0
- package/dist/openai.d.ts +1 -0
- package/dist/openai.js +7 -0
- package/dist/services/api.d.ts +114 -0
- package/dist/services/api.js +336 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/page-interaction/constants.d.ts +1 -0
- package/dist/services/page-interaction/constants.js +1 -0
- package/dist/services/page-interaction/content-extractor.d.ts +2 -0
- package/dist/services/page-interaction/content-extractor.js +104 -0
- package/dist/services/page-interaction/deep-query.d.ts +8 -0
- package/dist/services/page-interaction/deep-query.js +44 -0
- package/dist/services/page-interaction/dom-executor.d.ts +7 -0
- package/dist/services/page-interaction/dom-executor.js +172 -0
- package/dist/services/page-interaction/dom-extractor.d.ts +8 -0
- package/dist/services/page-interaction/dom-extractor.js +252 -0
- package/dist/services/page-interaction/extension-bridge.d.ts +10 -0
- package/dist/services/page-interaction/extension-bridge.js +86 -0
- package/dist/services/page-interaction/iframe-navigation-manager.d.ts +28 -0
- package/dist/services/page-interaction/iframe-navigation-manager.js +154 -0
- package/dist/services/page-interaction/index.d.ts +5 -0
- package/dist/services/page-interaction/index.js +4 -0
- package/dist/services/page-interaction/page-interaction-handler.d.ts +21 -0
- package/dist/services/page-interaction/page-interaction-handler.js +129 -0
- package/dist/services/page-interaction/selector-generator.d.ts +1 -0
- package/dist/services/page-interaction/selector-generator.js +48 -0
- package/dist/services/page-interaction/types.d.ts +75 -0
- package/dist/services/page-interaction/types.js +1 -0
- package/dist/services/realtime/google/google-audio.d.ts +44 -0
- package/dist/services/realtime/google/google-audio.js +410 -0
- package/dist/services/realtime/google/google-provider.d.ts +47 -0
- package/dist/services/realtime/google/google-provider.js +465 -0
- package/dist/services/realtime/google/google-tool-mapper.d.ts +3 -0
- package/dist/services/realtime/google/google-tool-mapper.js +70 -0
- package/dist/services/realtime/google/index.d.ts +1 -0
- package/dist/services/realtime/google/index.js +1 -0
- package/dist/services/realtime/openai/index.d.ts +1 -0
- package/dist/services/realtime/openai/index.js +1 -0
- package/dist/services/realtime/openai/openai-audio-monitor.d.ts +1 -0
- package/dist/services/realtime/openai/openai-audio-monitor.js +55 -0
- package/dist/services/realtime/openai/openai-provider.d.ts +50 -0
- package/dist/services/realtime/openai/openai-provider.js +579 -0
- package/dist/services/realtime/openai/openai-tool-mapper.d.ts +15 -0
- package/dist/services/realtime/openai/openai-tool-mapper.js +14 -0
- package/dist/services/realtime/provider-factory.d.ts +4 -0
- package/dist/services/realtime/provider-factory.js +12 -0
- package/dist/services/realtime/types.d.ts +78 -0
- package/dist/services/realtime/types.js +1 -0
- package/dist/services/realtime/vertexai/index.d.ts +1 -0
- package/dist/services/realtime/vertexai/index.js +1 -0
- package/dist/services/realtime/vertexai/vertexai-provider.d.ts +53 -0
- package/dist/services/realtime/vertexai/vertexai-provider.js +561 -0
- package/dist/services/realtime/voice-session-tracker.d.ts +10 -0
- package/dist/services/realtime/voice-session-tracker.js +62 -0
- package/dist/services/signalr.d.ts +37 -0
- package/dist/services/signalr.js +145 -0
- package/dist/services/thinking-audio-pulse.d.ts +2 -0
- package/dist/services/thinking-audio-pulse.js +72 -0
- package/dist/services/thinking-audio.d.ts +2 -0
- package/dist/services/thinking-audio.js +105 -0
- package/dist/services/voice-health-watchdog.d.ts +42 -0
- package/dist/services/voice-health-watchdog.js +90 -0
- package/dist/services/webrtc-event-handler.d.ts +4 -0
- package/dist/services/webrtc-event-handler.js +42 -0
- package/dist/services/webrtc-greeting.d.ts +3 -0
- package/dist/services/webrtc-greeting.js +62 -0
- package/dist/services/webrtc-microphone.d.ts +15 -0
- package/dist/services/webrtc-microphone.js +64 -0
- package/dist/services/webrtc.d.ts +44 -0
- package/dist/services/webrtc.js +196 -0
- package/dist/types/index.d.ts +151 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/audio-context.d.ts +11 -0
- package/dist/utils/audio-context.js +33 -0
- package/dist/utils/connect-chime.d.ts +11 -0
- package/dist/utils/connect-chime.js +230 -0
- package/dist/utils/consent.d.ts +4 -0
- package/dist/utils/consent.js +30 -0
- package/dist/utils/language-detection.d.ts +1 -0
- package/dist/utils/language-detection.js +17 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/mic-level-analyser.d.ts +13 -0
- package/dist/utils/mic-level-analyser.js +50 -0
- package/dist/utils/proactive-events.d.ts +5 -0
- package/dist/utils/proactive-events.js +1 -0
- package/dist/utils/styles.d.ts +4 -0
- package/dist/utils/styles.js +35 -0
- package/dist/utils/theme-detection.d.ts +3 -0
- package/dist/utils/theme-detection.js +121 -0
- package/dist/utils/tool-status.d.ts +10 -0
- package/dist/utils/tool-status.js +61 -0
- package/dist/utils/ui-sounds.d.ts +3 -0
- package/dist/utils/ui-sounds.js +57 -0
- package/package.json +56 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { querySelectorDeep } from './deep-query';
|
|
2
|
+
const HIGHLIGHT_DEFAULT_COLOR = '#3b82f6';
|
|
3
|
+
const HIGHLIGHT_DEFAULT_DURATION_MS = 3000;
|
|
4
|
+
const HIGHLIGHT_FADEOUT_MS = 300;
|
|
5
|
+
const HIGHLIGHT_PADDING_PX = 4;
|
|
6
|
+
const HIGHLIGHT_BORDER_WIDTH = '3px';
|
|
7
|
+
const HIGHLIGHT_BORDER_RADIUS = '4px';
|
|
8
|
+
const HIGHLIGHT_OVERLAY_Z_INDEX = '2147483646';
|
|
9
|
+
const WAIT_FOR_DEFAULT_TIMEOUT_MS = 3000;
|
|
10
|
+
export function fillField(selector, value, doc = document) {
|
|
11
|
+
const element = findElement(selector, doc);
|
|
12
|
+
if (!element)
|
|
13
|
+
return elementNotFound(selector);
|
|
14
|
+
const tag = element.tagName;
|
|
15
|
+
if (tag === 'INPUT') {
|
|
16
|
+
return fillInputElement(element, value);
|
|
17
|
+
}
|
|
18
|
+
if (tag === 'SELECT') {
|
|
19
|
+
return fillSelectElement(element, value);
|
|
20
|
+
}
|
|
21
|
+
if (tag === 'TEXTAREA') {
|
|
22
|
+
setValueWithNativeSetter(element, value);
|
|
23
|
+
dispatchInputEvents(element);
|
|
24
|
+
return { success: true };
|
|
25
|
+
}
|
|
26
|
+
return { success: false, errorMessage: `Element '${selector}' is not a form input.` };
|
|
27
|
+
}
|
|
28
|
+
export function fillFields(fields, doc = document) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
for (const [selector, value] of Object.entries(fields)) {
|
|
31
|
+
const result = fillField(selector, value, doc);
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
errors.push(result.errorMessage || `Failed to fill '${selector}'.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (errors.length > 0) {
|
|
37
|
+
return { success: false, errorMessage: errors.join(' ') };
|
|
38
|
+
}
|
|
39
|
+
return { success: true };
|
|
40
|
+
}
|
|
41
|
+
export function clickElement(selector, doc = document) {
|
|
42
|
+
const element = findElement(selector, doc);
|
|
43
|
+
if (!element)
|
|
44
|
+
return elementNotFound(selector);
|
|
45
|
+
if (isElementDisabled(element)) {
|
|
46
|
+
return { success: false, errorMessage: `Element '${selector}' is disabled and cannot be clicked.` };
|
|
47
|
+
}
|
|
48
|
+
if ('click' in element && typeof element.click === 'function') {
|
|
49
|
+
if (typeof element.scrollIntoView === 'function') {
|
|
50
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
51
|
+
}
|
|
52
|
+
element.click();
|
|
53
|
+
return { success: true };
|
|
54
|
+
}
|
|
55
|
+
return { success: false, errorMessage: `Element '${selector}' cannot be clicked.` };
|
|
56
|
+
}
|
|
57
|
+
function isElementDisabled(element) {
|
|
58
|
+
if (element.disabled)
|
|
59
|
+
return true;
|
|
60
|
+
if (element.getAttribute('aria-disabled') === 'true')
|
|
61
|
+
return true;
|
|
62
|
+
return element.hasAttribute('disabled');
|
|
63
|
+
}
|
|
64
|
+
export function scrollToElement(selector, doc = document) {
|
|
65
|
+
const element = findElement(selector, doc);
|
|
66
|
+
if (!element)
|
|
67
|
+
return elementNotFound(selector);
|
|
68
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
69
|
+
return { success: true };
|
|
70
|
+
}
|
|
71
|
+
export function highlightElement(selector, color = HIGHLIGHT_DEFAULT_COLOR, durationMs = HIGHLIGHT_DEFAULT_DURATION_MS, doc = document) {
|
|
72
|
+
const element = findElement(selector, doc);
|
|
73
|
+
if (!element)
|
|
74
|
+
return elementNotFound(selector);
|
|
75
|
+
const rect = element.getBoundingClientRect();
|
|
76
|
+
const overlay = doc.createElement('div');
|
|
77
|
+
const pad = HIGHLIGHT_PADDING_PX;
|
|
78
|
+
overlay.style.cssText = [
|
|
79
|
+
`position: fixed`,
|
|
80
|
+
`top: ${rect.top - pad}px`,
|
|
81
|
+
`left: ${rect.left - pad}px`,
|
|
82
|
+
`width: ${rect.width + pad * 2}px`,
|
|
83
|
+
`height: ${rect.height + pad * 2}px`,
|
|
84
|
+
`border: ${HIGHLIGHT_BORDER_WIDTH} solid ${color}`,
|
|
85
|
+
`border-radius: ${HIGHLIGHT_BORDER_RADIUS}`,
|
|
86
|
+
`pointer-events: none`,
|
|
87
|
+
`z-index: ${HIGHLIGHT_OVERLAY_Z_INDEX}`,
|
|
88
|
+
`box-shadow: 0 0 12px ${color}40`,
|
|
89
|
+
`transition: opacity ${HIGHLIGHT_FADEOUT_MS}ms ease-out`,
|
|
90
|
+
].join('; ');
|
|
91
|
+
overlay.dataset.nextpaHighlight = 'true';
|
|
92
|
+
doc.body.appendChild(overlay);
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
overlay.style.opacity = '0';
|
|
95
|
+
setTimeout(() => overlay.remove(), HIGHLIGHT_FADEOUT_MS);
|
|
96
|
+
}, durationMs);
|
|
97
|
+
return { success: true };
|
|
98
|
+
}
|
|
99
|
+
export function waitForElement(selector, timeoutMs = WAIT_FOR_DEFAULT_TIMEOUT_MS, doc = document) {
|
|
100
|
+
return new Promise(resolve => {
|
|
101
|
+
if (findElement(selector, doc)) {
|
|
102
|
+
resolve({ success: true });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let settled = false;
|
|
106
|
+
const finish = (result) => {
|
|
107
|
+
if (settled)
|
|
108
|
+
return;
|
|
109
|
+
settled = true;
|
|
110
|
+
observer.disconnect();
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
resolve(result);
|
|
113
|
+
};
|
|
114
|
+
const observer = new MutationObserver(() => {
|
|
115
|
+
if (findElement(selector, doc)) {
|
|
116
|
+
finish({ success: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const root = doc.body ?? doc.documentElement;
|
|
120
|
+
observer.observe(root, { childList: true, subtree: true, attributes: true });
|
|
121
|
+
const timer = setTimeout(() => finish({ success: false, errorMessage: `Element '${selector}' did not appear within ${timeoutMs}ms.` }), timeoutMs);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function findElement(selector, doc) {
|
|
125
|
+
try {
|
|
126
|
+
return querySelectorDeep(doc, selector);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function elementNotFound(selector) {
|
|
133
|
+
return { success: false, errorMessage: `Element '${selector}' not found on the page.` };
|
|
134
|
+
}
|
|
135
|
+
function fillInputElement(input, value) {
|
|
136
|
+
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
137
|
+
const shouldCheck = value === 'true' || value === 'checked' || value === '1';
|
|
138
|
+
if (input.checked !== shouldCheck) {
|
|
139
|
+
input.checked = shouldCheck;
|
|
140
|
+
dispatchInputEvents(input);
|
|
141
|
+
}
|
|
142
|
+
return { success: true };
|
|
143
|
+
}
|
|
144
|
+
setValueWithNativeSetter(input, value);
|
|
145
|
+
dispatchInputEvents(input);
|
|
146
|
+
return { success: true };
|
|
147
|
+
}
|
|
148
|
+
function fillSelectElement(select, value) {
|
|
149
|
+
const option = Array.from(select.options).find(opt => opt.value === value || opt.text.trim().toLowerCase() === value.toLowerCase());
|
|
150
|
+
if (!option) {
|
|
151
|
+
return { success: false, errorMessage: `Option '${value}' not found in select element.` };
|
|
152
|
+
}
|
|
153
|
+
select.value = option.value;
|
|
154
|
+
dispatchInputEvents(select);
|
|
155
|
+
return { success: true };
|
|
156
|
+
}
|
|
157
|
+
function setValueWithNativeSetter(element, value) {
|
|
158
|
+
const prototype = element.tagName === 'INPUT'
|
|
159
|
+
? HTMLInputElement.prototype
|
|
160
|
+
: HTMLTextAreaElement.prototype;
|
|
161
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
|
|
162
|
+
if (nativeSetter) {
|
|
163
|
+
nativeSetter.call(element, value);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
element.value = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function dispatchInputEvents(element) {
|
|
170
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
171
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
172
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ButtonElement, FormElement, InteractiveElement, LinkElement, PageSnapshot, ProductCardElement, TableElement } from './types';
|
|
2
|
+
export declare function extractPageSnapshot(doc?: Document, win?: Window): PageSnapshot;
|
|
3
|
+
export declare function extractForms(doc?: Document, win?: Window): FormElement[];
|
|
4
|
+
export declare function extractButtons(doc?: Document, win?: Window): ButtonElement[];
|
|
5
|
+
export declare function extractLinks(doc?: Document, win?: Window): LinkElement[];
|
|
6
|
+
export declare function extractTables(doc?: Document, win?: Window): TableElement[];
|
|
7
|
+
export declare function extractProductCards(doc?: Document, win?: Window): ProductCardElement[];
|
|
8
|
+
export declare function extractInteractiveElements(doc?: Document, win?: Window): InteractiveElement[];
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { queryAllDeep } from './deep-query';
|
|
2
|
+
import { generateSelector } from './selector-generator';
|
|
3
|
+
const MAX_FORMS = 50;
|
|
4
|
+
const MAX_BUTTONS = 100;
|
|
5
|
+
const MAX_LINKS = 100;
|
|
6
|
+
const MAX_TABLES = 20;
|
|
7
|
+
const MAX_PRODUCT_CARDS = 50;
|
|
8
|
+
const MAX_INTERACTIVE_ELEMENTS = 100;
|
|
9
|
+
const MAX_SAMPLE_ROWS = 3;
|
|
10
|
+
const MAX_TEXT_LENGTH = 200;
|
|
11
|
+
const MIN_DESCRIPTION_LENGTH = 10;
|
|
12
|
+
const ROW_CONTEXT_SELECTOR = 'tr, [role="row"], li, fieldset';
|
|
13
|
+
const PRICE_PATTERN = /[$€£¥₹]\s*\d|\d{1,20}[.,]\d{2}\s*(?:€|USD|EUR|GBP)/;
|
|
14
|
+
export function extractPageSnapshot(doc = document, win = globalThis) {
|
|
15
|
+
return {
|
|
16
|
+
url: win.location.href,
|
|
17
|
+
title: doc.title || null,
|
|
18
|
+
forms: extractForms(doc, win),
|
|
19
|
+
buttons: extractButtons(doc, win),
|
|
20
|
+
links: extractLinks(doc, win),
|
|
21
|
+
tables: extractTables(doc, win),
|
|
22
|
+
productCards: extractProductCards(doc, win),
|
|
23
|
+
otherInteractiveElements: extractInteractiveElements(doc, win),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function toFormElement(input, doc) {
|
|
27
|
+
const form = input.closest('form');
|
|
28
|
+
const formSelector = form ? generateSelector(form, doc) : null;
|
|
29
|
+
const tag = input.tagName;
|
|
30
|
+
const label = findLabel(input, doc);
|
|
31
|
+
return {
|
|
32
|
+
selector: generateSelector(input, doc),
|
|
33
|
+
formSelector,
|
|
34
|
+
label,
|
|
35
|
+
context: label ? null : findRowContext(input),
|
|
36
|
+
inputType: getInputType(input),
|
|
37
|
+
currentValue: getInputValue(input),
|
|
38
|
+
isRequired: input.required || input.getAttribute('aria-required') === 'true',
|
|
39
|
+
isDisabled: input.disabled,
|
|
40
|
+
options: tag === 'SELECT' ? getSelectOptions(input) : null,
|
|
41
|
+
placeholder: tag === 'INPUT' || tag === 'TEXTAREA'
|
|
42
|
+
? input.placeholder || null
|
|
43
|
+
: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function extractForms(doc = document, win = globalThis) {
|
|
47
|
+
const result = [];
|
|
48
|
+
const inputs = queryAllDeep(doc, 'input, select, textarea');
|
|
49
|
+
for (const input of inputs) {
|
|
50
|
+
if (result.length >= MAX_FORMS)
|
|
51
|
+
break;
|
|
52
|
+
if (!isVisible(input, win))
|
|
53
|
+
continue;
|
|
54
|
+
if (input.tagName === 'INPUT' && input.type === 'hidden')
|
|
55
|
+
continue;
|
|
56
|
+
result.push(toFormElement(input, doc));
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
export function extractButtons(doc = document, win = globalThis) {
|
|
61
|
+
const result = [];
|
|
62
|
+
const buttons = queryAllDeep(doc, 'button, [role="button"], input[type="submit"], input[type="button"]');
|
|
63
|
+
for (const button of buttons) {
|
|
64
|
+
if (result.length >= MAX_BUTTONS)
|
|
65
|
+
break;
|
|
66
|
+
if (!isVisible(button, win))
|
|
67
|
+
continue;
|
|
68
|
+
result.push({
|
|
69
|
+
selector: generateSelector(button, doc),
|
|
70
|
+
text: getElementText(button),
|
|
71
|
+
isDisabled: button.disabled ?? false,
|
|
72
|
+
type: button.getAttribute('type') || null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
export function extractLinks(doc = document, win = globalThis) {
|
|
78
|
+
const result = [];
|
|
79
|
+
const links = queryAllDeep(doc, 'a[href]');
|
|
80
|
+
for (const link of links) {
|
|
81
|
+
if (result.length >= MAX_LINKS)
|
|
82
|
+
break;
|
|
83
|
+
if (!isVisible(link, win))
|
|
84
|
+
continue;
|
|
85
|
+
const text = getElementText(link);
|
|
86
|
+
if (!text)
|
|
87
|
+
continue;
|
|
88
|
+
result.push({
|
|
89
|
+
selector: generateSelector(link, doc),
|
|
90
|
+
text,
|
|
91
|
+
href: link.href || null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
export function extractTables(doc = document, win = globalThis) {
|
|
97
|
+
const result = [];
|
|
98
|
+
const tables = queryAllDeep(doc, 'table');
|
|
99
|
+
for (const table of tables) {
|
|
100
|
+
if (result.length >= MAX_TABLES)
|
|
101
|
+
break;
|
|
102
|
+
if (!isVisible(table, win))
|
|
103
|
+
continue;
|
|
104
|
+
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
|
|
105
|
+
const rows = table.querySelectorAll('tbody tr');
|
|
106
|
+
const sampleRows = [];
|
|
107
|
+
for (let i = 0; i < Math.min(rows.length, MAX_SAMPLE_ROWS); i++) {
|
|
108
|
+
const cells = Array.from(rows[i].querySelectorAll('td')).map(td => td.textContent?.trim() || '');
|
|
109
|
+
sampleRows.push(cells);
|
|
110
|
+
}
|
|
111
|
+
result.push({
|
|
112
|
+
selector: generateSelector(table, doc),
|
|
113
|
+
headers,
|
|
114
|
+
rowCount: rows.length,
|
|
115
|
+
sampleRows: sampleRows.length > 0 ? sampleRows : null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
export function extractProductCards(doc = document, win = globalThis) {
|
|
121
|
+
const result = [];
|
|
122
|
+
const candidates = queryAllDeep(doc, '[class*="product"], [class*="card"], [class*="item"], [data-product], [itemtype*="Product"]');
|
|
123
|
+
for (const card of candidates) {
|
|
124
|
+
if (result.length >= MAX_PRODUCT_CARDS)
|
|
125
|
+
break;
|
|
126
|
+
if (!isVisible(card, win))
|
|
127
|
+
continue;
|
|
128
|
+
const priceEl = findElementWithPrice(card);
|
|
129
|
+
if (!priceEl)
|
|
130
|
+
continue;
|
|
131
|
+
const title = findCardTitle(card);
|
|
132
|
+
const img = card.querySelector('img');
|
|
133
|
+
result.push({
|
|
134
|
+
selector: generateSelector(card, doc),
|
|
135
|
+
title,
|
|
136
|
+
price: priceEl.textContent?.trim() || null,
|
|
137
|
+
imageUrl: img?.src || null,
|
|
138
|
+
description: findCardDescription(card, title),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
export function extractInteractiveElements(doc = document, win = globalThis) {
|
|
144
|
+
const result = [];
|
|
145
|
+
const elements = queryAllDeep(doc, '[role="tab"], [role="tabpanel"], [role="menuitem"], [role="option"], [role="switch"], ' +
|
|
146
|
+
'[role="slider"], [role="spinbutton"], [role="combobox"], [role="listbox"], ' +
|
|
147
|
+
'[contenteditable="true"], details, summary');
|
|
148
|
+
for (const el of elements) {
|
|
149
|
+
if (result.length >= MAX_INTERACTIVE_ELEMENTS)
|
|
150
|
+
break;
|
|
151
|
+
if (!isVisible(el, win))
|
|
152
|
+
continue;
|
|
153
|
+
result.push({
|
|
154
|
+
selector: generateSelector(el, doc),
|
|
155
|
+
tagName: el.tagName.toLowerCase(),
|
|
156
|
+
text: getElementText(el),
|
|
157
|
+
role: el.getAttribute('role') || null,
|
|
158
|
+
ariaLabel: el.getAttribute('aria-label') || null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
function isVisible(element, win = globalThis) {
|
|
164
|
+
if (element.offsetParent === null && element.tagName !== 'BODY' && element.tagName !== 'HTML') {
|
|
165
|
+
const style = win.getComputedStyle(element);
|
|
166
|
+
if (style.position !== 'fixed' && style.position !== 'sticky') {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const rect = element.getBoundingClientRect();
|
|
171
|
+
return rect.width > 0 && rect.height > 0;
|
|
172
|
+
}
|
|
173
|
+
function findLabel(input, doc = document) {
|
|
174
|
+
const id = input.id;
|
|
175
|
+
if (id) {
|
|
176
|
+
const label = doc.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
177
|
+
if (label)
|
|
178
|
+
return label.textContent?.trim() || null;
|
|
179
|
+
}
|
|
180
|
+
const parentLabel = input.closest('label');
|
|
181
|
+
if (parentLabel) {
|
|
182
|
+
const text = parentLabel.textContent?.trim() || '';
|
|
183
|
+
const inputText = input.value || '';
|
|
184
|
+
return text.replace(inputText, '').trim() || null;
|
|
185
|
+
}
|
|
186
|
+
const ariaLabel = input.getAttribute('aria-label');
|
|
187
|
+
if (ariaLabel)
|
|
188
|
+
return ariaLabel;
|
|
189
|
+
const ariaLabelledBy = input.getAttribute('aria-labelledby');
|
|
190
|
+
if (ariaLabelledBy) {
|
|
191
|
+
const labelEl = doc.getElementById(ariaLabelledBy);
|
|
192
|
+
if (labelEl)
|
|
193
|
+
return labelEl.textContent?.trim() || null;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function findRowContext(input) {
|
|
198
|
+
const row = input.closest(ROW_CONTEXT_SELECTOR);
|
|
199
|
+
if (!row)
|
|
200
|
+
return null;
|
|
201
|
+
const text = row.textContent?.replace(/\s+/g, ' ').trim() ?? '';
|
|
202
|
+
if (!text)
|
|
203
|
+
return null;
|
|
204
|
+
return text.length > MAX_TEXT_LENGTH ? text.substring(0, MAX_TEXT_LENGTH) + '...' : text;
|
|
205
|
+
}
|
|
206
|
+
function getInputType(input) {
|
|
207
|
+
if (input.tagName === 'SELECT')
|
|
208
|
+
return 'select';
|
|
209
|
+
if (input.tagName === 'TEXTAREA')
|
|
210
|
+
return 'textarea';
|
|
211
|
+
return input.type || 'text';
|
|
212
|
+
}
|
|
213
|
+
function getInputValue(input) {
|
|
214
|
+
if (input.tagName !== 'INPUT') {
|
|
215
|
+
return input.value || null;
|
|
216
|
+
}
|
|
217
|
+
const inputEl = input;
|
|
218
|
+
if (inputEl.type === 'checkbox' || inputEl.type === 'radio') {
|
|
219
|
+
return inputEl.checked ? 'checked' : 'unchecked';
|
|
220
|
+
}
|
|
221
|
+
return inputEl.value || null;
|
|
222
|
+
}
|
|
223
|
+
function getSelectOptions(select) {
|
|
224
|
+
return Array.from(select.options).map(opt => opt.text?.trim() || opt.value);
|
|
225
|
+
}
|
|
226
|
+
function getElementText(element) {
|
|
227
|
+
const text = element.textContent?.trim();
|
|
228
|
+
if (text && text.length > MAX_TEXT_LENGTH)
|
|
229
|
+
return text.substring(0, MAX_TEXT_LENGTH) + '...';
|
|
230
|
+
return text || null;
|
|
231
|
+
}
|
|
232
|
+
function findElementWithPrice(container) {
|
|
233
|
+
const allElements = container.querySelectorAll('*');
|
|
234
|
+
for (const el of allElements) {
|
|
235
|
+
if (el.children.length === 0 && el.textContent && PRICE_PATTERN.test(el.textContent)) {
|
|
236
|
+
return el;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function findCardTitle(card) {
|
|
242
|
+
const heading = card.querySelector('h1, h2, h3, h4, h5, h6, [class*="title"], [class*="name"]');
|
|
243
|
+
return heading?.textContent?.trim() || null;
|
|
244
|
+
}
|
|
245
|
+
function findCardDescription(card, title) {
|
|
246
|
+
const desc = card.querySelector('p, [class*="desc"], [class*="summary"]');
|
|
247
|
+
const text = desc?.textContent?.trim() || null;
|
|
248
|
+
if (text && text !== title && text.length > MIN_DESCRIPTION_LENGTH) {
|
|
249
|
+
return text.length > MAX_TEXT_LENGTH ? text.substring(0, MAX_TEXT_LENGTH) + '...' : text;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExternalExecutor } from './page-interaction-handler';
|
|
2
|
+
export type PageContext = {
|
|
3
|
+
url: string;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
type PageContextCallback = (context: PageContext) => void;
|
|
7
|
+
type SetExternalExecutorFn = (executor: ExternalExecutor | null) => void;
|
|
8
|
+
export declare function refreshExtensionContext(): Promise<PageContext | null>;
|
|
9
|
+
export declare function initExtensionBridge(setExternalExecutors: ReadonlyArray<SetExternalExecutorFn>, onPageContext?: PageContextCallback): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const RESPONSE_TIMEOUT_MS = 15_000;
|
|
2
|
+
const CONTEXT_REFRESH_TIMEOUT_MS = 2000;
|
|
3
|
+
let bridgeParentOrigin = null;
|
|
4
|
+
let contextRefreshResolve = null;
|
|
5
|
+
function getReferrerOrigin() {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(document.referrer).origin;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return '*';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isIframe() {
|
|
14
|
+
return globalThis.parent !== globalThis;
|
|
15
|
+
}
|
|
16
|
+
export function refreshExtensionContext() {
|
|
17
|
+
const targetOrigin = bridgeParentOrigin;
|
|
18
|
+
if (!isIframe() || !targetOrigin)
|
|
19
|
+
return Promise.resolve(null);
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
contextRefreshResolve = null;
|
|
23
|
+
resolve(null);
|
|
24
|
+
}, CONTEXT_REFRESH_TIMEOUT_MS);
|
|
25
|
+
contextRefreshResolve = (context) => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
contextRefreshResolve = null;
|
|
28
|
+
resolve(context);
|
|
29
|
+
};
|
|
30
|
+
globalThis.parent.postMessage({ type: 'nextpa-context-refresh' }, targetOrigin);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function initExtensionBridge(setExternalExecutors, onPageContext) {
|
|
34
|
+
if (!isIframe())
|
|
35
|
+
return;
|
|
36
|
+
const pendingRequests = new Map();
|
|
37
|
+
const executor = (request) => {
|
|
38
|
+
if (!bridgeParentOrigin) {
|
|
39
|
+
return Promise.resolve({ success: false, errorMessage: 'Extension bridge not initialized.' });
|
|
40
|
+
}
|
|
41
|
+
const targetOrigin = bridgeParentOrigin;
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
pendingRequests.delete(request.requestId);
|
|
45
|
+
resolve({ success: false, errorMessage: 'Extension bridge timeout.' });
|
|
46
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
47
|
+
pendingRequests.set(request.requestId, { resolve, timer });
|
|
48
|
+
globalThis.parent.postMessage({
|
|
49
|
+
type: 'nextpa-page-interaction-request',
|
|
50
|
+
requestId: request.requestId,
|
|
51
|
+
action: request.action,
|
|
52
|
+
selector: request.selector,
|
|
53
|
+
value: request.value,
|
|
54
|
+
fields: request.fields,
|
|
55
|
+
}, targetOrigin);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
globalThis.addEventListener('message', (event) => {
|
|
59
|
+
if (bridgeParentOrigin && event.origin !== bridgeParentOrigin)
|
|
60
|
+
return;
|
|
61
|
+
if (event.data?.type === 'nextpa-extension-init') {
|
|
62
|
+
bridgeParentOrigin = event.origin;
|
|
63
|
+
for (const setExecutor of setExternalExecutors) {
|
|
64
|
+
setExecutor(executor);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (event.data?.type === 'nextpa-page-context') {
|
|
68
|
+
const context = { url: event.data.url, title: event.data.title };
|
|
69
|
+
if (contextRefreshResolve) {
|
|
70
|
+
contextRefreshResolve(context);
|
|
71
|
+
}
|
|
72
|
+
onPageContext?.(context);
|
|
73
|
+
}
|
|
74
|
+
if (event.data?.type === 'nextpa-page-interaction-response') {
|
|
75
|
+
const { requestId, result } = event.data;
|
|
76
|
+
const pending = pendingRequests.get(requestId);
|
|
77
|
+
if (pending) {
|
|
78
|
+
clearTimeout(pending.timer);
|
|
79
|
+
pendingRequests.delete(requestId);
|
|
80
|
+
pending.resolve(result);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const referrerOrigin = getReferrerOrigin();
|
|
85
|
+
globalThis.parent.postMessage({ type: 'nextpa-extension-ready' }, referrerOrigin);
|
|
86
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type NavigateResult = {
|
|
2
|
+
status: 'iframe-loaded';
|
|
3
|
+
} | {
|
|
4
|
+
status: 'blocked';
|
|
5
|
+
reason: string;
|
|
6
|
+
};
|
|
7
|
+
type CloseCallback = () => void;
|
|
8
|
+
export declare class IframeNavigationManager {
|
|
9
|
+
private overlay;
|
|
10
|
+
private iframe;
|
|
11
|
+
private readonly closeCallbacks;
|
|
12
|
+
private widgetElevated;
|
|
13
|
+
private previousOverflow;
|
|
14
|
+
navigateTo(url: string): Promise<NavigateResult>;
|
|
15
|
+
close(): void;
|
|
16
|
+
isActive(): boolean;
|
|
17
|
+
getTargetDocument(): Document;
|
|
18
|
+
getTargetWindow(): Window;
|
|
19
|
+
onClose(callback: CloseCallback): void;
|
|
20
|
+
private createIframeOverlay;
|
|
21
|
+
private navigateExistingIframe;
|
|
22
|
+
private loadUrlInIframe;
|
|
23
|
+
private elevateWidget;
|
|
24
|
+
private hideParentScrollbar;
|
|
25
|
+
private restoreParentScrollbar;
|
|
26
|
+
private cleanup;
|
|
27
|
+
}
|
|
28
|
+
export {};
|