@navsi.ai/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +348 -0
- package/dist/chunk-427NHGTX.js +3 -0
- package/dist/chunk-427NHGTX.js.map +1 -0
- package/dist/chunk-6FUUG5WB.js +77 -0
- package/dist/chunk-6FUUG5WB.js.map +1 -0
- package/dist/chunk-EHZXIZIP.js +3752 -0
- package/dist/chunk-EHZXIZIP.js.map +1 -0
- package/dist/components/index.d.ts +66 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/hooks/index.d.ts +104 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +286 -0
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,3752 @@
|
|
|
1
|
+
import { ChatbotContext, useChatbot, useActionExecution, useWebSocket } from './chunk-6FUUG5WB.js';
|
|
2
|
+
import React2, { useMemo, useReducer, useRef, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { createLogger, normalizeWidgetConfig, createMessageId } from '@navsi.ai/shared';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var MessageQueue = class {
|
|
7
|
+
queue = [];
|
|
8
|
+
maxSize;
|
|
9
|
+
constructor(maxSize = 100) {
|
|
10
|
+
this.maxSize = maxSize;
|
|
11
|
+
}
|
|
12
|
+
enqueue(message) {
|
|
13
|
+
if (this.queue.length >= this.maxSize) {
|
|
14
|
+
this.queue.shift();
|
|
15
|
+
}
|
|
16
|
+
this.queue.push(message);
|
|
17
|
+
}
|
|
18
|
+
dequeueAll() {
|
|
19
|
+
const messages = [...this.queue];
|
|
20
|
+
this.queue = [];
|
|
21
|
+
return messages;
|
|
22
|
+
}
|
|
23
|
+
get length() {
|
|
24
|
+
return this.queue.length;
|
|
25
|
+
}
|
|
26
|
+
clear() {
|
|
27
|
+
this.queue = [];
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var WebSocketClient = class {
|
|
31
|
+
ws = null;
|
|
32
|
+
config;
|
|
33
|
+
state = "disconnected";
|
|
34
|
+
logger = createLogger("SDK.WebSocket");
|
|
35
|
+
reconnectAttempts = 0;
|
|
36
|
+
reconnectTimeout = null;
|
|
37
|
+
heartbeatInterval = null;
|
|
38
|
+
lastPongTime = 0;
|
|
39
|
+
messageQueue = new MessageQueue();
|
|
40
|
+
sessionId = null;
|
|
41
|
+
refreshToken = null;
|
|
42
|
+
tokenExpiresAt = 0;
|
|
43
|
+
tokenRefreshTimeout = null;
|
|
44
|
+
// Event listeners
|
|
45
|
+
listeners = /* @__PURE__ */ new Map();
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = {
|
|
48
|
+
serverUrl: config.serverUrl,
|
|
49
|
+
apiKey: config.apiKey,
|
|
50
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
51
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
52
|
+
reconnectDelay: config.reconnectDelay ?? 1e3,
|
|
53
|
+
heartbeatInterval: config.heartbeatInterval ?? 3e4,
|
|
54
|
+
debug: config.debug ?? false
|
|
55
|
+
};
|
|
56
|
+
this.logger = createLogger("SDK.WebSocket", { enabled: this.config.debug, level: "debug" });
|
|
57
|
+
}
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Public API
|
|
60
|
+
// ============================================================================
|
|
61
|
+
/**
|
|
62
|
+
* Connect to the WebSocket server
|
|
63
|
+
*/
|
|
64
|
+
connect() {
|
|
65
|
+
if (this.state === "connecting" || this.state === "connected") {
|
|
66
|
+
this.log("Already connected or connecting");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.setState("connecting");
|
|
70
|
+
this.createWebSocket();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Disconnect from the server
|
|
74
|
+
*/
|
|
75
|
+
disconnect() {
|
|
76
|
+
this.cleanup();
|
|
77
|
+
this.setState("disconnected");
|
|
78
|
+
this.log("Disconnected");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Send a message to the server
|
|
82
|
+
*/
|
|
83
|
+
send(message) {
|
|
84
|
+
if (this.state === "connected" && this.ws?.readyState === WebSocket.OPEN) {
|
|
85
|
+
this.ws.send(JSON.stringify(message));
|
|
86
|
+
this.log("Sent message:", message.type, summarizeClientMessage(message));
|
|
87
|
+
} else {
|
|
88
|
+
this.messageQueue.enqueue(message);
|
|
89
|
+
this.log("Queued message (offline):", message.type, summarizeClientMessage(message));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get current connection state
|
|
94
|
+
*/
|
|
95
|
+
getState() {
|
|
96
|
+
return this.state;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get session ID
|
|
100
|
+
*/
|
|
101
|
+
getSessionId() {
|
|
102
|
+
return this.sessionId;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if connected
|
|
106
|
+
*/
|
|
107
|
+
isConnected() {
|
|
108
|
+
return this.state === "connected";
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Event Handling
|
|
112
|
+
// ============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Subscribe to an event
|
|
115
|
+
*/
|
|
116
|
+
on(event, callback) {
|
|
117
|
+
if (!this.listeners.has(event)) {
|
|
118
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
119
|
+
}
|
|
120
|
+
this.listeners.get(event).add(callback);
|
|
121
|
+
return () => {
|
|
122
|
+
this.listeners.get(event)?.delete(callback);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Emit an event to all listeners
|
|
127
|
+
*/
|
|
128
|
+
emit(event, data) {
|
|
129
|
+
this.listeners.get(event)?.forEach((callback) => {
|
|
130
|
+
try {
|
|
131
|
+
callback(data);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.logger.error(`Error in ${event} listener`, error);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Connection Management
|
|
139
|
+
// ============================================================================
|
|
140
|
+
createWebSocket() {
|
|
141
|
+
try {
|
|
142
|
+
const urlObj = new URL(this.config.serverUrl);
|
|
143
|
+
urlObj.searchParams.set("apiKey", this.config.apiKey);
|
|
144
|
+
if (this.sessionId) {
|
|
145
|
+
urlObj.searchParams.set("sessionId", this.sessionId);
|
|
146
|
+
}
|
|
147
|
+
const url = urlObj.toString();
|
|
148
|
+
this.ws = new WebSocket(url);
|
|
149
|
+
this.ws.onopen = this.handleOpen.bind(this);
|
|
150
|
+
this.ws.onmessage = this.handleMessage.bind(this);
|
|
151
|
+
this.ws.onclose = this.handleClose.bind(this);
|
|
152
|
+
this.ws.onerror = this.handleError.bind(this);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger.error("Failed to create WebSocket", error);
|
|
155
|
+
this.handleConnectionFailure();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
handleOpen() {
|
|
159
|
+
this.log("WebSocket connected, authenticating...");
|
|
160
|
+
this.ws?.send(JSON.stringify({
|
|
161
|
+
type: "auth",
|
|
162
|
+
apiKey: this.config.apiKey,
|
|
163
|
+
sessionId: this.sessionId
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
handleMessage(event) {
|
|
167
|
+
try {
|
|
168
|
+
const message = JSON.parse(event.data);
|
|
169
|
+
this.log("Received message:", message.type);
|
|
170
|
+
switch (message.type) {
|
|
171
|
+
case "connected":
|
|
172
|
+
this.handleConnected();
|
|
173
|
+
break;
|
|
174
|
+
case "auth_success":
|
|
175
|
+
this.handleAuthSuccess(message);
|
|
176
|
+
break;
|
|
177
|
+
case "auth_error":
|
|
178
|
+
this.handleAuthError(message);
|
|
179
|
+
break;
|
|
180
|
+
case "pong":
|
|
181
|
+
this.handlePong(message);
|
|
182
|
+
break;
|
|
183
|
+
case "token_refreshed":
|
|
184
|
+
this.handleTokenRefreshed(message);
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
this.emit("message", message);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.warn("Failed to parse message", error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
handleClose(event) {
|
|
194
|
+
this.log("WebSocket closed:", event.code, event.reason);
|
|
195
|
+
this.stopHeartbeat();
|
|
196
|
+
if (this.state !== "disconnected") {
|
|
197
|
+
this.emit("disconnected", { reason: event.reason, code: event.code });
|
|
198
|
+
if (this.config.autoReconnect) {
|
|
199
|
+
this.scheduleReconnect();
|
|
200
|
+
} else {
|
|
201
|
+
this.setState("disconnected");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
handleError(event) {
|
|
206
|
+
this.logger.error("WebSocket error", event);
|
|
207
|
+
this.emit("error", { error: new Error("WebSocket error") });
|
|
208
|
+
}
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Authentication
|
|
211
|
+
// ============================================================================
|
|
212
|
+
handleConnected() {
|
|
213
|
+
this.log("Server connection acknowledged");
|
|
214
|
+
}
|
|
215
|
+
handleAuthSuccess(message) {
|
|
216
|
+
this.sessionId = message.sessionId;
|
|
217
|
+
this.refreshToken = message.refreshToken;
|
|
218
|
+
this.tokenExpiresAt = message.expiresAt;
|
|
219
|
+
this.reconnectAttempts = 0;
|
|
220
|
+
this.setState("connected");
|
|
221
|
+
this.startHeartbeat();
|
|
222
|
+
this.scheduleTokenRefresh();
|
|
223
|
+
this.flushMessageQueue();
|
|
224
|
+
this.emit("authenticated", {
|
|
225
|
+
sessionId: message.sessionId,
|
|
226
|
+
expiresAt: message.expiresAt
|
|
227
|
+
});
|
|
228
|
+
this.emit("connected", { sessionId: message.sessionId });
|
|
229
|
+
this.log("Authenticated successfully, session:", message.sessionId);
|
|
230
|
+
}
|
|
231
|
+
handleAuthError(message) {
|
|
232
|
+
this.logger.warn("Authentication failed", message.error);
|
|
233
|
+
this.emit("error", { error: new Error(message.error), code: message.code });
|
|
234
|
+
this.disconnect();
|
|
235
|
+
}
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Token Refresh
|
|
238
|
+
// ============================================================================
|
|
239
|
+
scheduleTokenRefresh() {
|
|
240
|
+
if (this.tokenRefreshTimeout) {
|
|
241
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
242
|
+
}
|
|
243
|
+
const refreshIn = Math.max(0, this.tokenExpiresAt - Date.now() - 5 * 60 * 1e3);
|
|
244
|
+
this.tokenRefreshTimeout = setTimeout(() => {
|
|
245
|
+
this.refreshAccessToken();
|
|
246
|
+
}, refreshIn);
|
|
247
|
+
this.log("Token refresh scheduled in", Math.round(refreshIn / 1e3), "seconds");
|
|
248
|
+
}
|
|
249
|
+
refreshAccessToken() {
|
|
250
|
+
if (!this.refreshToken || this.state !== "connected") {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.log("Refreshing token...");
|
|
254
|
+
this.ws?.send(JSON.stringify({
|
|
255
|
+
type: "token_refresh",
|
|
256
|
+
refreshToken: this.refreshToken
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
handleTokenRefreshed(message) {
|
|
260
|
+
this.refreshToken = message.refreshToken;
|
|
261
|
+
this.tokenExpiresAt = message.expiresAt;
|
|
262
|
+
this.scheduleTokenRefresh();
|
|
263
|
+
this.emit("token_refreshed", { expiresAt: message.expiresAt });
|
|
264
|
+
this.log("Token refreshed successfully");
|
|
265
|
+
}
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Heartbeat
|
|
268
|
+
// ============================================================================
|
|
269
|
+
startHeartbeat() {
|
|
270
|
+
this.stopHeartbeat();
|
|
271
|
+
this.lastPongTime = Date.now();
|
|
272
|
+
this.heartbeatInterval = setInterval(() => {
|
|
273
|
+
if (this.state !== "connected") {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (Date.now() - this.lastPongTime > this.config.heartbeatInterval * 2) {
|
|
277
|
+
this.logger.warn("Heartbeat timeout, reconnecting...");
|
|
278
|
+
this.ws?.close();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.ws?.send(JSON.stringify({
|
|
282
|
+
type: "ping",
|
|
283
|
+
timestamp: Date.now()
|
|
284
|
+
}));
|
|
285
|
+
this.log("Sent ping");
|
|
286
|
+
}, this.config.heartbeatInterval);
|
|
287
|
+
}
|
|
288
|
+
stopHeartbeat() {
|
|
289
|
+
if (this.heartbeatInterval) {
|
|
290
|
+
clearInterval(this.heartbeatInterval);
|
|
291
|
+
this.heartbeatInterval = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
handlePong(message) {
|
|
295
|
+
this.lastPongTime = Date.now();
|
|
296
|
+
const latency = Date.now() - message.timestamp;
|
|
297
|
+
this.log("Received pong, latency:", latency, "ms");
|
|
298
|
+
}
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// Reconnection
|
|
301
|
+
// ============================================================================
|
|
302
|
+
scheduleReconnect() {
|
|
303
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
304
|
+
this.handleConnectionFailure();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.setState("reconnecting");
|
|
308
|
+
this.reconnectAttempts++;
|
|
309
|
+
const delay = Math.min(
|
|
310
|
+
this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
311
|
+
3e4
|
|
312
|
+
);
|
|
313
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`);
|
|
314
|
+
this.emit("reconnecting", {
|
|
315
|
+
attempt: this.reconnectAttempts,
|
|
316
|
+
maxAttempts: this.config.maxReconnectAttempts
|
|
317
|
+
});
|
|
318
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
319
|
+
this.createWebSocket();
|
|
320
|
+
}, delay);
|
|
321
|
+
}
|
|
322
|
+
handleConnectionFailure() {
|
|
323
|
+
this.setState("failed");
|
|
324
|
+
this.emit("reconnect_failed", { reason: "Max reconnection attempts reached" });
|
|
325
|
+
this.logger.error("Connection failed - max reconnection attempts reached");
|
|
326
|
+
}
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// Message Queue
|
|
329
|
+
// ============================================================================
|
|
330
|
+
flushMessageQueue() {
|
|
331
|
+
const messages = this.messageQueue.dequeueAll();
|
|
332
|
+
if (messages.length > 0) {
|
|
333
|
+
this.log(`Flushing ${messages.length} queued messages`);
|
|
334
|
+
messages.forEach((message) => this.send(message));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// Utilities
|
|
339
|
+
// ============================================================================
|
|
340
|
+
setState(state) {
|
|
341
|
+
if (this.state !== state) {
|
|
342
|
+
this.log("State:", this.state, "->", state);
|
|
343
|
+
this.state = state;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
cleanup() {
|
|
347
|
+
if (this.reconnectTimeout) {
|
|
348
|
+
clearTimeout(this.reconnectTimeout);
|
|
349
|
+
this.reconnectTimeout = null;
|
|
350
|
+
}
|
|
351
|
+
if (this.tokenRefreshTimeout) {
|
|
352
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
353
|
+
this.tokenRefreshTimeout = null;
|
|
354
|
+
}
|
|
355
|
+
this.stopHeartbeat();
|
|
356
|
+
if (this.ws) {
|
|
357
|
+
const ws = this.ws;
|
|
358
|
+
ws.onopen = null;
|
|
359
|
+
ws.onmessage = null;
|
|
360
|
+
ws.onclose = null;
|
|
361
|
+
ws.onerror = null;
|
|
362
|
+
this.ws = null;
|
|
363
|
+
if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) {
|
|
364
|
+
try {
|
|
365
|
+
ws.close(1e3, "Client disconnect");
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
log(...args) {
|
|
372
|
+
this.logger.debug(...args);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
function summarizeClientMessage(message) {
|
|
376
|
+
switch (message.type) {
|
|
377
|
+
case "message":
|
|
378
|
+
return {
|
|
379
|
+
messageId: message.messageId,
|
|
380
|
+
mode: message.mode,
|
|
381
|
+
length: message.content.length,
|
|
382
|
+
route: message.context?.route
|
|
383
|
+
};
|
|
384
|
+
case "context":
|
|
385
|
+
return {
|
|
386
|
+
route: message.context?.route,
|
|
387
|
+
actions: message.context?.actions?.length ?? 0,
|
|
388
|
+
headings: message.context?.content?.headings?.length ?? 0
|
|
389
|
+
};
|
|
390
|
+
case "action_result":
|
|
391
|
+
return {
|
|
392
|
+
actionId: message.actionId,
|
|
393
|
+
success: message.success,
|
|
394
|
+
commandId: message.commandId
|
|
395
|
+
};
|
|
396
|
+
case "server_action_request":
|
|
397
|
+
return {
|
|
398
|
+
actionId: message.actionId,
|
|
399
|
+
requestId: message.requestId
|
|
400
|
+
};
|
|
401
|
+
case "auth":
|
|
402
|
+
return {
|
|
403
|
+
hasSessionId: Boolean(message.sessionId)
|
|
404
|
+
};
|
|
405
|
+
case "token_refresh":
|
|
406
|
+
return {};
|
|
407
|
+
case "ping":
|
|
408
|
+
return { timestamp: message.timestamp };
|
|
409
|
+
default:
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function createWebSocketClient(config) {
|
|
414
|
+
return new WebSocketClient(config);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/core/navigation/adapter.ts
|
|
418
|
+
var DEFAULT_ROUTE_TIMEOUT = 1e4;
|
|
419
|
+
function createMemoryAdapter(initialPath = "/") {
|
|
420
|
+
let currentPath = initialPath;
|
|
421
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
422
|
+
return {
|
|
423
|
+
navigate(path) {
|
|
424
|
+
currentPath = path;
|
|
425
|
+
listeners.forEach((callback) => callback(path));
|
|
426
|
+
},
|
|
427
|
+
getCurrentPath() {
|
|
428
|
+
return currentPath;
|
|
429
|
+
},
|
|
430
|
+
onRouteChange(callback) {
|
|
431
|
+
listeners.add(callback);
|
|
432
|
+
return () => listeners.delete(callback);
|
|
433
|
+
},
|
|
434
|
+
async waitForRoute(path, timeout = DEFAULT_ROUTE_TIMEOUT) {
|
|
435
|
+
return new Promise((resolve) => {
|
|
436
|
+
if (currentPath === path) {
|
|
437
|
+
resolve(true);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const unsubscribe = this.onRouteChange((newPath) => {
|
|
441
|
+
if (newPath === path) {
|
|
442
|
+
unsubscribe();
|
|
443
|
+
resolve(true);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
unsubscribe();
|
|
448
|
+
resolve(currentPath === path);
|
|
449
|
+
}, timeout);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
var NavigationController = class {
|
|
455
|
+
adapter;
|
|
456
|
+
config;
|
|
457
|
+
scanCallback = null;
|
|
458
|
+
logger = createLogger("SDK.Navigation");
|
|
459
|
+
constructor(adapter, config = {}) {
|
|
460
|
+
this.adapter = adapter;
|
|
461
|
+
this.config = {
|
|
462
|
+
domStabilityDelay: config.domStabilityDelay ?? 300,
|
|
463
|
+
domStabilityTimeout: config.domStabilityTimeout ?? 5e3,
|
|
464
|
+
debug: config.debug ?? false,
|
|
465
|
+
onDOMStable: config.onDOMStable
|
|
466
|
+
};
|
|
467
|
+
this.logger = createLogger("SDK.Navigation", { enabled: this.config.debug, level: "debug" });
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Set the page scanner callback
|
|
471
|
+
*/
|
|
472
|
+
setScanner(scanner) {
|
|
473
|
+
this.scanCallback = scanner;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get current route
|
|
477
|
+
*/
|
|
478
|
+
getCurrentRoute() {
|
|
479
|
+
return this.adapter.getCurrentPath();
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Navigate to a route and wait for it to load
|
|
483
|
+
*/
|
|
484
|
+
async navigateTo(route, timeout) {
|
|
485
|
+
const currentRoute = this.adapter.getCurrentPath();
|
|
486
|
+
if (currentRoute === route) {
|
|
487
|
+
this.log("Already on route:", route);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
this.log("Navigating to:", route);
|
|
491
|
+
this.adapter.navigate(route);
|
|
492
|
+
const success = await this.adapter.waitForRoute(route, timeout);
|
|
493
|
+
if (!success) {
|
|
494
|
+
this.log("Navigation timeout for route:", route);
|
|
495
|
+
}
|
|
496
|
+
return success;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Execute navigation and rescan page context
|
|
500
|
+
*/
|
|
501
|
+
async executeNavigation(route) {
|
|
502
|
+
const success = await this.navigateTo(route);
|
|
503
|
+
if (!success) {
|
|
504
|
+
this.log("Navigation failed to:", route);
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
await this.waitForDOMStable();
|
|
508
|
+
if (this.scanCallback) {
|
|
509
|
+
const context = await this.scanCallback();
|
|
510
|
+
this.log("Rescanned context on route:", route);
|
|
511
|
+
return context;
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Wait for DOM to become stable after navigation
|
|
517
|
+
* Uses MutationObserver to detect when DOM stops changing
|
|
518
|
+
*/
|
|
519
|
+
async waitForDOMStable() {
|
|
520
|
+
this.logger.debug("Waiting for DOM stability", {
|
|
521
|
+
delayMs: this.config.domStabilityDelay,
|
|
522
|
+
timeoutMs: this.config.domStabilityTimeout
|
|
523
|
+
});
|
|
524
|
+
const done = () => {
|
|
525
|
+
this.config.onDOMStable?.();
|
|
526
|
+
};
|
|
527
|
+
return new Promise((resolve) => {
|
|
528
|
+
let resolved = false;
|
|
529
|
+
const resolveOnce = () => {
|
|
530
|
+
if (resolved) return;
|
|
531
|
+
resolved = true;
|
|
532
|
+
done();
|
|
533
|
+
resolve();
|
|
534
|
+
};
|
|
535
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
|
536
|
+
resolveOnce();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
let mutationTimeout;
|
|
540
|
+
let maxTimeout;
|
|
541
|
+
const startTime = Date.now();
|
|
542
|
+
const cleanup = () => {
|
|
543
|
+
observer.disconnect();
|
|
544
|
+
clearTimeout(mutationTimeout);
|
|
545
|
+
clearTimeout(maxTimeout);
|
|
546
|
+
};
|
|
547
|
+
const observer = new MutationObserver(() => {
|
|
548
|
+
clearTimeout(mutationTimeout);
|
|
549
|
+
if (Date.now() - startTime >= this.config.domStabilityTimeout) {
|
|
550
|
+
cleanup();
|
|
551
|
+
this.log("DOM stability timeout reached");
|
|
552
|
+
resolveOnce();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
mutationTimeout = setTimeout(() => {
|
|
556
|
+
cleanup();
|
|
557
|
+
this.log("DOM is stable");
|
|
558
|
+
resolveOnce();
|
|
559
|
+
}, this.config.domStabilityDelay);
|
|
560
|
+
});
|
|
561
|
+
observer.observe(document.body, {
|
|
562
|
+
childList: true,
|
|
563
|
+
subtree: true,
|
|
564
|
+
attributes: false
|
|
565
|
+
// Ignore attribute changes for stability
|
|
566
|
+
});
|
|
567
|
+
mutationTimeout = setTimeout(() => {
|
|
568
|
+
cleanup();
|
|
569
|
+
this.log("DOM is stable (no initial mutations)");
|
|
570
|
+
resolveOnce();
|
|
571
|
+
}, this.config.domStabilityDelay);
|
|
572
|
+
maxTimeout = setTimeout(() => {
|
|
573
|
+
cleanup();
|
|
574
|
+
this.log("DOM stability max timeout reached");
|
|
575
|
+
resolveOnce();
|
|
576
|
+
}, this.config.domStabilityTimeout);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Subscribe to route changes
|
|
581
|
+
*/
|
|
582
|
+
onRouteChange(callback) {
|
|
583
|
+
return this.adapter.onRouteChange(callback);
|
|
584
|
+
}
|
|
585
|
+
log(...args) {
|
|
586
|
+
this.logger.debug(...args);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function createNavigationController(adapter, config) {
|
|
590
|
+
return new NavigationController(adapter, config);
|
|
591
|
+
}
|
|
592
|
+
var INTERACTIVE_SELECTORS = [
|
|
593
|
+
"button:not([disabled])",
|
|
594
|
+
"a[href]:not([disabled])",
|
|
595
|
+
'input:not([type="hidden"]):not([disabled])',
|
|
596
|
+
"select:not([disabled])",
|
|
597
|
+
"textarea:not([disabled])",
|
|
598
|
+
'[contenteditable="true"]',
|
|
599
|
+
'[contenteditable="plaintext-only"]',
|
|
600
|
+
'[role="textbox"]',
|
|
601
|
+
'[role="searchbox"]',
|
|
602
|
+
'[role="combobox"]',
|
|
603
|
+
'[role="checkbox"]',
|
|
604
|
+
'[role="switch"]',
|
|
605
|
+
'[role="radio"]',
|
|
606
|
+
'[role="button"]:not([disabled])',
|
|
607
|
+
'[role="link"]',
|
|
608
|
+
'[role="menuitem"]',
|
|
609
|
+
'[role="tab"]',
|
|
610
|
+
"[onclick]",
|
|
611
|
+
"[data-chatbot-action]"
|
|
612
|
+
];
|
|
613
|
+
var IGNORE_SELECTORS = [
|
|
614
|
+
"[data-chatbot-ignore]",
|
|
615
|
+
'[aria-hidden="true"]',
|
|
616
|
+
"[hidden]",
|
|
617
|
+
".chatbot-widget",
|
|
618
|
+
// Legacy class
|
|
619
|
+
".navsi-chatbot-container",
|
|
620
|
+
// Ignore our own widget
|
|
621
|
+
'[type="password"]',
|
|
622
|
+
// Security: don't interact with password fields
|
|
623
|
+
'[autocomplete*="cc-"]'
|
|
624
|
+
// Security: credit card fields
|
|
625
|
+
];
|
|
626
|
+
function generateSelector(element) {
|
|
627
|
+
const chatbotAction = element.getAttribute("data-chatbot-action");
|
|
628
|
+
if (chatbotAction) {
|
|
629
|
+
return `[data-chatbot-action="${chatbotAction}"]`;
|
|
630
|
+
}
|
|
631
|
+
if (element.id) {
|
|
632
|
+
return `#${CSS.escape(element.id)}`;
|
|
633
|
+
}
|
|
634
|
+
const testId = element.getAttribute("data-testid");
|
|
635
|
+
if (testId) {
|
|
636
|
+
return `[data-testid="${CSS.escape(testId)}"]`;
|
|
637
|
+
}
|
|
638
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
639
|
+
if (ariaLabel) {
|
|
640
|
+
const selector = `[aria-label="${CSS.escape(ariaLabel)}"]`;
|
|
641
|
+
const matches = document.querySelectorAll(selector);
|
|
642
|
+
if (matches.length === 1) {
|
|
643
|
+
return selector;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const name = element.getAttribute?.("name");
|
|
647
|
+
if (name && /^(input|select|textarea|button)$/i.test(element.tagName)) {
|
|
648
|
+
const selector = `${element.tagName.toLowerCase()}[name="${CSS.escape(name)}"]`;
|
|
649
|
+
const matches = document.querySelectorAll(selector);
|
|
650
|
+
if (matches.length === 1) {
|
|
651
|
+
return selector;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (element.className && typeof element.className === "string") {
|
|
655
|
+
const classes = element.className.split(/\s+/).filter(Boolean).slice(0, 3);
|
|
656
|
+
if (classes.length > 0) {
|
|
657
|
+
const selector = `${element.tagName.toLowerCase()}.${classes.map((c) => CSS.escape(c)).join(".")}`;
|
|
658
|
+
const matches = document.querySelectorAll(selector);
|
|
659
|
+
if (matches.length === 1) {
|
|
660
|
+
return selector;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return buildPathSelector(element);
|
|
665
|
+
}
|
|
666
|
+
function buildPathSelector(element) {
|
|
667
|
+
const path = [];
|
|
668
|
+
let current = element;
|
|
669
|
+
while (current && current !== document.body) {
|
|
670
|
+
let selector = current.tagName.toLowerCase();
|
|
671
|
+
if (current.id) {
|
|
672
|
+
selector = `#${CSS.escape(current.id)}`;
|
|
673
|
+
path.unshift(selector);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
const parent = current.parentElement;
|
|
677
|
+
if (parent) {
|
|
678
|
+
const tagName = current.tagName;
|
|
679
|
+
const children = parent.children;
|
|
680
|
+
const sameTagSiblings = [];
|
|
681
|
+
for (let i = 0; i < children.length; i++) {
|
|
682
|
+
if (children[i].tagName === tagName) {
|
|
683
|
+
sameTagSiblings.push(children[i]);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (sameTagSiblings.length > 1) {
|
|
687
|
+
const index = sameTagSiblings.indexOf(current) + 1;
|
|
688
|
+
selector += `:nth-of-type(${index})`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
path.unshift(selector);
|
|
692
|
+
current = parent;
|
|
693
|
+
}
|
|
694
|
+
return path.join(" > ");
|
|
695
|
+
}
|
|
696
|
+
function isVisible(element) {
|
|
697
|
+
const style = window.getComputedStyle(element);
|
|
698
|
+
if (style.display === "none") return false;
|
|
699
|
+
if (style.visibility === "hidden") return false;
|
|
700
|
+
if (style.opacity === "0") return false;
|
|
701
|
+
const rect = element.getBoundingClientRect();
|
|
702
|
+
if (rect.width === 0 && rect.height === 0) return false;
|
|
703
|
+
if (element.closest('[aria-hidden="true"]')) return false;
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
function shouldIgnore(element) {
|
|
707
|
+
for (const selector of IGNORE_SELECTORS) {
|
|
708
|
+
if (element.matches(selector)) return true;
|
|
709
|
+
if (element.closest(selector)) return true;
|
|
710
|
+
}
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
function getActionType(element) {
|
|
714
|
+
const tagName = element.tagName.toLowerCase();
|
|
715
|
+
const role = element.getAttribute("role")?.toLowerCase();
|
|
716
|
+
if (tagName === "input") {
|
|
717
|
+
const type = element.type;
|
|
718
|
+
if (type === "checkbox" || type === "radio") return "click";
|
|
719
|
+
return "type";
|
|
720
|
+
}
|
|
721
|
+
if (tagName === "textarea") return "type";
|
|
722
|
+
if (tagName === "select") return "select";
|
|
723
|
+
if (role === "textbox" || role === "searchbox") return "type";
|
|
724
|
+
if (role === "combobox") return "click";
|
|
725
|
+
if (role === "checkbox" || role === "radio" || role === "switch") return "click";
|
|
726
|
+
return "click";
|
|
727
|
+
}
|
|
728
|
+
function extractLabel(element) {
|
|
729
|
+
const chatbotAction = element.getAttribute("data-chatbot-action");
|
|
730
|
+
if (chatbotAction) {
|
|
731
|
+
return chatbotAction.replace(/-/g, " ");
|
|
732
|
+
}
|
|
733
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
734
|
+
if (ariaLabel) return ariaLabel;
|
|
735
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
736
|
+
if (labelledBy) {
|
|
737
|
+
const labelText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
|
|
738
|
+
if (labelText) return labelText;
|
|
739
|
+
}
|
|
740
|
+
const title = element.getAttribute("title");
|
|
741
|
+
if (title) return title;
|
|
742
|
+
const textContent = element.textContent?.trim();
|
|
743
|
+
if (textContent && textContent.length < 50) {
|
|
744
|
+
return textContent;
|
|
745
|
+
}
|
|
746
|
+
const placeholder = element.getAttribute("placeholder");
|
|
747
|
+
if (placeholder) return placeholder;
|
|
748
|
+
const name = element.getAttribute("name");
|
|
749
|
+
if (name) return name.replace(/[-_]/g, " ");
|
|
750
|
+
if (element.id) {
|
|
751
|
+
const label = document.querySelector(`label[for="${element.id}"]`);
|
|
752
|
+
if (label?.textContent) return label.textContent.trim();
|
|
753
|
+
}
|
|
754
|
+
return element.tagName.toLowerCase();
|
|
755
|
+
}
|
|
756
|
+
var CONTAINER_SELECTORS = [
|
|
757
|
+
"tr",
|
|
758
|
+
'[role="row"]',
|
|
759
|
+
"article",
|
|
760
|
+
'[role="listitem"]',
|
|
761
|
+
"section",
|
|
762
|
+
'[role="group"]',
|
|
763
|
+
"main",
|
|
764
|
+
'[role="region"]'
|
|
765
|
+
];
|
|
766
|
+
var MAX_CONTAINER_TEXT_LENGTH = 120;
|
|
767
|
+
function getContainerText(element) {
|
|
768
|
+
let current = element;
|
|
769
|
+
let depth = 0;
|
|
770
|
+
const maxDepth = 8;
|
|
771
|
+
while (current && current !== document.body && depth < maxDepth) {
|
|
772
|
+
for (const sel of CONTAINER_SELECTORS) {
|
|
773
|
+
try {
|
|
774
|
+
if (current.matches(sel)) {
|
|
775
|
+
const text = current.textContent?.trim().replace(/\s+/g, " ");
|
|
776
|
+
if (text && text.length > 0) {
|
|
777
|
+
return text.length > MAX_CONTAINER_TEXT_LENGTH ? text.slice(0, MAX_CONTAINER_TEXT_LENGTH) + "\u2026" : text;
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
current = current.parentElement;
|
|
785
|
+
depth++;
|
|
786
|
+
}
|
|
787
|
+
return void 0;
|
|
788
|
+
}
|
|
789
|
+
function getNearbyHeading(element) {
|
|
790
|
+
let current = element.parentElement;
|
|
791
|
+
let depth = 0;
|
|
792
|
+
const maxDepth = 5;
|
|
793
|
+
while (current && depth < maxDepth) {
|
|
794
|
+
const heading = current.querySelector("h1, h2, h3, h4, h5, h6");
|
|
795
|
+
if (heading?.textContent) {
|
|
796
|
+
return heading.textContent.trim();
|
|
797
|
+
}
|
|
798
|
+
let sibling = current.previousElementSibling;
|
|
799
|
+
while (sibling) {
|
|
800
|
+
if (/^H[1-6]$/.test(sibling.tagName)) {
|
|
801
|
+
return sibling.textContent?.trim();
|
|
802
|
+
}
|
|
803
|
+
sibling = sibling.previousElementSibling;
|
|
804
|
+
}
|
|
805
|
+
current = current.parentElement;
|
|
806
|
+
depth++;
|
|
807
|
+
}
|
|
808
|
+
return void 0;
|
|
809
|
+
}
|
|
810
|
+
var DOMScanner = class {
|
|
811
|
+
config;
|
|
812
|
+
mutationObserver = null;
|
|
813
|
+
scanTimeout = null;
|
|
814
|
+
onChangeCallback = null;
|
|
815
|
+
logger = createLogger("SDK.Scanner");
|
|
816
|
+
constructor(config = {}) {
|
|
817
|
+
this.config = {
|
|
818
|
+
root: config.root ?? (typeof document !== "undefined" ? document.body : null),
|
|
819
|
+
debug: config.debug ?? false,
|
|
820
|
+
maxActions: config.maxActions ?? 100,
|
|
821
|
+
debounceMs: config.debounceMs ?? 300
|
|
822
|
+
};
|
|
823
|
+
this.logger = createLogger("SDK.Scanner", { enabled: this.config.debug, level: "debug" });
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Scan the DOM for interactive elements
|
|
827
|
+
*/
|
|
828
|
+
scan() {
|
|
829
|
+
if (typeof document === "undefined") {
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
const root = this.config.root ?? document.body;
|
|
833
|
+
const actions = [];
|
|
834
|
+
const currentRoute = typeof window !== "undefined" ? window.location.pathname : "/";
|
|
835
|
+
const selector = INTERACTIVE_SELECTORS.join(", ");
|
|
836
|
+
const elements = this.collectInteractiveElements(root, selector);
|
|
837
|
+
this.log(`Found ${elements.length} potential interactive elements`);
|
|
838
|
+
for (const element of elements) {
|
|
839
|
+
if (actions.length >= this.config.maxActions) break;
|
|
840
|
+
if (shouldIgnore(element)) continue;
|
|
841
|
+
if (!isVisible(element)) continue;
|
|
842
|
+
const action = this.createElement(element, currentRoute);
|
|
843
|
+
if (action) {
|
|
844
|
+
actions.push(action);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
this.log(`Discovered ${actions.length} actions`);
|
|
848
|
+
return actions;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Create action definition from element
|
|
852
|
+
*/
|
|
853
|
+
createElement(element, route) {
|
|
854
|
+
try {
|
|
855
|
+
const selector = generateSelector(element);
|
|
856
|
+
const label = extractLabel(element);
|
|
857
|
+
const type = getActionType(element);
|
|
858
|
+
const heading = getNearbyHeading(element);
|
|
859
|
+
const id = `action_${btoa(selector).replace(/[^a-zA-Z0-9]/g, "").slice(0, 16)}`;
|
|
860
|
+
const action = {
|
|
861
|
+
id,
|
|
862
|
+
type,
|
|
863
|
+
selector,
|
|
864
|
+
label,
|
|
865
|
+
route,
|
|
866
|
+
isAutoDiscovered: true,
|
|
867
|
+
priority: element.hasAttribute("data-chatbot-action") ? 10 : 1
|
|
868
|
+
};
|
|
869
|
+
const containerText = getContainerText(element);
|
|
870
|
+
if (heading || containerText) {
|
|
871
|
+
action.metadata = {
|
|
872
|
+
...heading ? { context: heading } : {},
|
|
873
|
+
...containerText ? { containerText } : {}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return action;
|
|
877
|
+
} catch (error) {
|
|
878
|
+
this.log("Error creating action:", error);
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Collect interactive elements from light DOM and open shadow roots.
|
|
884
|
+
*/
|
|
885
|
+
collectInteractiveElements(root, selector) {
|
|
886
|
+
const results = /* @__PURE__ */ new Set();
|
|
887
|
+
const roots = [root];
|
|
888
|
+
while (roots.length > 0) {
|
|
889
|
+
const currentRoot = roots.shift();
|
|
890
|
+
if (!currentRoot) {
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
const scope = currentRoot instanceof ShadowRoot ? currentRoot : currentRoot;
|
|
894
|
+
const matches = scope.querySelectorAll(selector);
|
|
895
|
+
for (const match of matches) {
|
|
896
|
+
results.add(match);
|
|
897
|
+
}
|
|
898
|
+
const hostRoot = currentRoot instanceof ShadowRoot ? currentRoot : currentRoot;
|
|
899
|
+
const hostElements = hostRoot.querySelectorAll("*");
|
|
900
|
+
for (const host of hostElements) {
|
|
901
|
+
const shadowRoot = host.shadowRoot;
|
|
902
|
+
if (shadowRoot) {
|
|
903
|
+
roots.push(shadowRoot);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return Array.from(results);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Start observing DOM for changes. Disconnects any existing observer first.
|
|
911
|
+
*/
|
|
912
|
+
observe(callback) {
|
|
913
|
+
if (typeof MutationObserver === "undefined") return;
|
|
914
|
+
this.disconnect();
|
|
915
|
+
this.onChangeCallback = callback;
|
|
916
|
+
this.mutationObserver = new MutationObserver(() => {
|
|
917
|
+
this.debouncedScan();
|
|
918
|
+
});
|
|
919
|
+
this.mutationObserver.observe(this.config.root ?? document.body, {
|
|
920
|
+
childList: true,
|
|
921
|
+
subtree: true,
|
|
922
|
+
attributes: true,
|
|
923
|
+
attributeFilter: ["disabled", "hidden", "aria-hidden"]
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Stop observing DOM
|
|
928
|
+
*/
|
|
929
|
+
disconnect() {
|
|
930
|
+
if (this.mutationObserver) {
|
|
931
|
+
this.mutationObserver.disconnect();
|
|
932
|
+
this.mutationObserver = null;
|
|
933
|
+
}
|
|
934
|
+
if (this.scanTimeout) {
|
|
935
|
+
clearTimeout(this.scanTimeout);
|
|
936
|
+
this.scanTimeout = null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Debounced scan
|
|
941
|
+
*/
|
|
942
|
+
debouncedScan() {
|
|
943
|
+
if (this.scanTimeout) {
|
|
944
|
+
clearTimeout(this.scanTimeout);
|
|
945
|
+
}
|
|
946
|
+
this.scanTimeout = setTimeout(() => {
|
|
947
|
+
const actions = this.scan();
|
|
948
|
+
this.onChangeCallback?.(actions);
|
|
949
|
+
}, this.config.debounceMs);
|
|
950
|
+
}
|
|
951
|
+
log(...args) {
|
|
952
|
+
this.logger.debug(...args);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
var ActionRegistry = class {
|
|
956
|
+
actionsByRoute = /* @__PURE__ */ new Map();
|
|
957
|
+
serverActions = /* @__PURE__ */ new Map();
|
|
958
|
+
manualActions = /* @__PURE__ */ new Map();
|
|
959
|
+
debug;
|
|
960
|
+
logger = createLogger("SDK.Registry");
|
|
961
|
+
constructor(config = {}) {
|
|
962
|
+
this.debug = config.debug ?? false;
|
|
963
|
+
this.logger = createLogger("SDK.Registry", { enabled: this.debug, level: "debug" });
|
|
964
|
+
}
|
|
965
|
+
// ============================================================================
|
|
966
|
+
// DOM Actions
|
|
967
|
+
// ============================================================================
|
|
968
|
+
/**
|
|
969
|
+
* Register auto-discovered actions for a route
|
|
970
|
+
*/
|
|
971
|
+
registerDiscoveredActions(route, actions) {
|
|
972
|
+
if (!this.actionsByRoute.has(route)) {
|
|
973
|
+
this.actionsByRoute.set(route, /* @__PURE__ */ new Map());
|
|
974
|
+
}
|
|
975
|
+
const routeActions = this.actionsByRoute.get(route);
|
|
976
|
+
for (const [id, action] of routeActions) {
|
|
977
|
+
if (action.isAutoDiscovered) {
|
|
978
|
+
routeActions.delete(id);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
for (const action of actions) {
|
|
982
|
+
const manualAction = this.manualActions.get(action.id);
|
|
983
|
+
if (manualAction) {
|
|
984
|
+
routeActions.set(action.id, { ...action, ...manualAction });
|
|
985
|
+
} else {
|
|
986
|
+
routeActions.set(action.id, action);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
this.log(`Registered ${actions.length} discovered actions for route: ${route}`);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Register a manual action definition (takes priority over auto-discovered)
|
|
993
|
+
*/
|
|
994
|
+
registerManualAction(action) {
|
|
995
|
+
this.manualActions.set(action.id, action);
|
|
996
|
+
const routeActions = this.actionsByRoute.get(action.route);
|
|
997
|
+
if (routeActions?.has(action.id)) {
|
|
998
|
+
const existing = routeActions.get(action.id);
|
|
999
|
+
routeActions.set(action.id, { ...existing, ...action, isAutoDiscovered: false });
|
|
1000
|
+
}
|
|
1001
|
+
this.log(`Registered manual action: ${action.id}`);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Get all actions for a route
|
|
1005
|
+
*/
|
|
1006
|
+
getActionsForRoute(route) {
|
|
1007
|
+
const routeActions = this.actionsByRoute.get(route);
|
|
1008
|
+
if (!routeActions) return [];
|
|
1009
|
+
return Array.from(routeActions.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Find action by ID
|
|
1013
|
+
*/
|
|
1014
|
+
findActionById(id) {
|
|
1015
|
+
for (const routeActions of this.actionsByRoute.values()) {
|
|
1016
|
+
const action = routeActions.get(id);
|
|
1017
|
+
if (action) return action;
|
|
1018
|
+
}
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Find action by selector on a specific route
|
|
1023
|
+
*/
|
|
1024
|
+
findActionBySelector(route, selector) {
|
|
1025
|
+
const routeActions = this.actionsByRoute.get(route);
|
|
1026
|
+
if (!routeActions) return null;
|
|
1027
|
+
for (const action of routeActions.values()) {
|
|
1028
|
+
if (action.selector === selector) return action;
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Find action by label (fuzzy match). When context is provided, prefer actions
|
|
1034
|
+
* whose metadata.containerText or metadata.context contains the context string.
|
|
1035
|
+
*/
|
|
1036
|
+
findActionByLabel(route, label, context) {
|
|
1037
|
+
const routeActions = this.actionsByRoute.get(route);
|
|
1038
|
+
if (!routeActions) return null;
|
|
1039
|
+
const normalizedLabel = label.toLowerCase().trim();
|
|
1040
|
+
const normalizedContext = context?.toLowerCase().trim();
|
|
1041
|
+
const matchesContext = (action) => {
|
|
1042
|
+
if (!normalizedContext) return true;
|
|
1043
|
+
const containerText = action.metadata?.containerText?.toLowerCase();
|
|
1044
|
+
const metaContext = action.metadata?.context?.toLowerCase();
|
|
1045
|
+
return (containerText?.includes(normalizedContext) ?? false) || (metaContext?.includes(normalizedContext) ?? false);
|
|
1046
|
+
};
|
|
1047
|
+
if (normalizedContext) {
|
|
1048
|
+
for (const action of routeActions.values()) {
|
|
1049
|
+
if (action.label.toLowerCase() === normalizedLabel && matchesContext(action)) {
|
|
1050
|
+
return action;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const action of routeActions.values()) {
|
|
1054
|
+
if (action.label.toLowerCase().includes(normalizedLabel) && matchesContext(action)) {
|
|
1055
|
+
return action;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
for (const action of routeActions.values()) {
|
|
1060
|
+
if (action.label.toLowerCase() === normalizedLabel) {
|
|
1061
|
+
return action;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
for (const action of routeActions.values()) {
|
|
1065
|
+
if (action.label.toLowerCase().includes(normalizedLabel)) {
|
|
1066
|
+
return action;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Clear actions for a route
|
|
1073
|
+
*/
|
|
1074
|
+
clearRoute(route) {
|
|
1075
|
+
this.actionsByRoute.delete(route);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Clear all actions
|
|
1079
|
+
*/
|
|
1080
|
+
clearAll() {
|
|
1081
|
+
this.actionsByRoute.clear();
|
|
1082
|
+
}
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
// Server Actions
|
|
1085
|
+
// ============================================================================
|
|
1086
|
+
/**
|
|
1087
|
+
* Register a server action
|
|
1088
|
+
*/
|
|
1089
|
+
registerServerAction(action) {
|
|
1090
|
+
this.serverActions.set(action.id, action);
|
|
1091
|
+
this.log(`Registered server action: ${action.id}`);
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Get all server actions
|
|
1095
|
+
*/
|
|
1096
|
+
getServerActions() {
|
|
1097
|
+
return Array.from(this.serverActions.values());
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Find server action by ID
|
|
1101
|
+
*/
|
|
1102
|
+
findServerActionById(id) {
|
|
1103
|
+
return this.serverActions.get(id) ?? null;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Find server action by name (fuzzy match)
|
|
1107
|
+
*/
|
|
1108
|
+
findServerActionByName(name) {
|
|
1109
|
+
const normalizedName = name.toLowerCase().trim();
|
|
1110
|
+
for (const action of this.serverActions.values()) {
|
|
1111
|
+
if (action.name.toLowerCase() === normalizedName) {
|
|
1112
|
+
return action;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
for (const action of this.serverActions.values()) {
|
|
1116
|
+
if (action.name.toLowerCase().includes(normalizedName)) {
|
|
1117
|
+
return action;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Remove a server action
|
|
1124
|
+
*/
|
|
1125
|
+
removeServerAction(id) {
|
|
1126
|
+
this.serverActions.delete(id);
|
|
1127
|
+
}
|
|
1128
|
+
// ============================================================================
|
|
1129
|
+
// Utilities
|
|
1130
|
+
// ============================================================================
|
|
1131
|
+
/**
|
|
1132
|
+
* Get statistics
|
|
1133
|
+
*/
|
|
1134
|
+
getStats() {
|
|
1135
|
+
let totalActions = 0;
|
|
1136
|
+
for (const routeActions of this.actionsByRoute.values()) {
|
|
1137
|
+
totalActions += routeActions.size;
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
routes: this.actionsByRoute.size,
|
|
1141
|
+
actions: totalActions,
|
|
1142
|
+
serverActions: this.serverActions.size
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
log(...args) {
|
|
1146
|
+
this.logger.debug(...args);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
function normalizeWhitespace(s) {
|
|
1150
|
+
return s.replace(/\s+/g, " ").trim();
|
|
1151
|
+
}
|
|
1152
|
+
function extractHeadings() {
|
|
1153
|
+
if (typeof document === "undefined") return [];
|
|
1154
|
+
const chatbotKeywords = ["chatbot", "ai assistant", "ask mode", "navigate mode", "navsi", "intelligent agent"];
|
|
1155
|
+
const headings = [];
|
|
1156
|
+
const elements = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
1157
|
+
for (const element of elements) {
|
|
1158
|
+
if (element.closest(".chatbot-widget") || element.closest(".navsi-chatbot-container")) continue;
|
|
1159
|
+
const text = element.textContent?.trim();
|
|
1160
|
+
if (text && text.length < 200) {
|
|
1161
|
+
const lower = text.toLowerCase();
|
|
1162
|
+
const isChatbotRelated = chatbotKeywords.some((keyword) => lower.includes(keyword));
|
|
1163
|
+
if (!isChatbotRelated) {
|
|
1164
|
+
headings.push(text);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return headings.slice(0, 20);
|
|
1169
|
+
}
|
|
1170
|
+
function extractMainContent(maxLength) {
|
|
1171
|
+
if (typeof document === "undefined") return "";
|
|
1172
|
+
const mainContent = document.querySelector("main") ?? document.querySelector('[role="main"]') ?? document.querySelector("article") ?? document.querySelector(".content") ?? document.body;
|
|
1173
|
+
const walker = document.createTreeWalker(
|
|
1174
|
+
mainContent,
|
|
1175
|
+
NodeFilter.SHOW_TEXT,
|
|
1176
|
+
{
|
|
1177
|
+
acceptNode: (node2) => {
|
|
1178
|
+
const parent = node2.parentElement;
|
|
1179
|
+
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
1180
|
+
const tagName = parent.tagName.toLowerCase();
|
|
1181
|
+
if (["script", "style", "noscript"].includes(tagName)) {
|
|
1182
|
+
return NodeFilter.FILTER_REJECT;
|
|
1183
|
+
}
|
|
1184
|
+
if (parent.closest(".chatbot-widget") || parent.closest(".navsi-chatbot-container")) {
|
|
1185
|
+
return NodeFilter.FILTER_REJECT;
|
|
1186
|
+
}
|
|
1187
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
);
|
|
1191
|
+
let content = "";
|
|
1192
|
+
let node;
|
|
1193
|
+
while ((node = walker.nextNode()) && content.length < maxLength) {
|
|
1194
|
+
const text = node.textContent?.trim();
|
|
1195
|
+
if (text && text.length > 0) {
|
|
1196
|
+
content += text + " ";
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return normalizeWhitespace(content).slice(0, maxLength);
|
|
1200
|
+
}
|
|
1201
|
+
function extractForms() {
|
|
1202
|
+
if (typeof document === "undefined") return [];
|
|
1203
|
+
const forms = [];
|
|
1204
|
+
const formElements = document.querySelectorAll("form");
|
|
1205
|
+
for (const form of formElements) {
|
|
1206
|
+
if (form.closest(".chatbot-widget") || form.closest(".navsi-chatbot-container")) continue;
|
|
1207
|
+
const fields = [];
|
|
1208
|
+
const inputs = form.querySelectorAll("input, select, textarea");
|
|
1209
|
+
for (const input of inputs) {
|
|
1210
|
+
const element = input;
|
|
1211
|
+
if (element.type === "hidden") continue;
|
|
1212
|
+
let label;
|
|
1213
|
+
if (element.id) {
|
|
1214
|
+
const labelElement = document.querySelector(`label[for="${element.id}"]`);
|
|
1215
|
+
label = labelElement?.textContent?.trim();
|
|
1216
|
+
}
|
|
1217
|
+
if (!label && element.getAttribute("aria-label")) {
|
|
1218
|
+
label = element.getAttribute("aria-label") ?? void 0;
|
|
1219
|
+
}
|
|
1220
|
+
fields.push({
|
|
1221
|
+
name: element.name || element.id || "",
|
|
1222
|
+
type: element.type || element.tagName.toLowerCase(),
|
|
1223
|
+
label,
|
|
1224
|
+
placeholder: "placeholder" in element ? element.placeholder : void 0,
|
|
1225
|
+
required: element.required,
|
|
1226
|
+
value: element.value || void 0
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if (fields.length > 0) {
|
|
1230
|
+
forms.push({
|
|
1231
|
+
id: form.id || `form_${forms.length}`,
|
|
1232
|
+
name: form.name || void 0,
|
|
1233
|
+
action: form.action || void 0,
|
|
1234
|
+
method: form.method || void 0,
|
|
1235
|
+
fields
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return forms.slice(0, 10);
|
|
1240
|
+
}
|
|
1241
|
+
function getMetaDescription() {
|
|
1242
|
+
if (typeof document === "undefined") return void 0;
|
|
1243
|
+
const meta = document.querySelector('meta[name="description"]');
|
|
1244
|
+
return meta?.getAttribute("content") ?? void 0;
|
|
1245
|
+
}
|
|
1246
|
+
var PageContextBuilder = class {
|
|
1247
|
+
config;
|
|
1248
|
+
logger = createLogger("SDK.Context");
|
|
1249
|
+
constructor(config = {}) {
|
|
1250
|
+
this.config = {
|
|
1251
|
+
maxContentLength: config.maxContentLength ?? 2e3,
|
|
1252
|
+
maxActions: config.maxActions ?? 50,
|
|
1253
|
+
debug: config.debug ?? false
|
|
1254
|
+
};
|
|
1255
|
+
this.logger = createLogger("SDK.Context", { enabled: this.config.debug, level: "debug" });
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Build page context
|
|
1259
|
+
*/
|
|
1260
|
+
build(actions, serverActions, route) {
|
|
1261
|
+
const currentRoute = route ?? (typeof window !== "undefined" ? window.location.pathname : "/");
|
|
1262
|
+
const title = typeof document !== "undefined" ? document.title : "";
|
|
1263
|
+
const content = {
|
|
1264
|
+
headings: extractHeadings(),
|
|
1265
|
+
mainContent: extractMainContent(this.config.maxContentLength),
|
|
1266
|
+
forms: extractForms(),
|
|
1267
|
+
metaDescription: getMetaDescription()
|
|
1268
|
+
};
|
|
1269
|
+
const limitedActions = actions.slice(0, this.config.maxActions);
|
|
1270
|
+
const context = {
|
|
1271
|
+
route: currentRoute,
|
|
1272
|
+
title,
|
|
1273
|
+
actions: limitedActions,
|
|
1274
|
+
serverActions,
|
|
1275
|
+
content,
|
|
1276
|
+
timestamp: Date.now()
|
|
1277
|
+
};
|
|
1278
|
+
this.log("Built page context:", {
|
|
1279
|
+
route: context.route,
|
|
1280
|
+
title: context.title,
|
|
1281
|
+
actions: context.actions.length,
|
|
1282
|
+
serverActions: context.serverActions.length,
|
|
1283
|
+
headings: context.content.headings.length,
|
|
1284
|
+
forms: context.content.forms.length
|
|
1285
|
+
});
|
|
1286
|
+
return context;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Build minimal context (for rapid updates)
|
|
1290
|
+
*/
|
|
1291
|
+
buildMinimal(actions, serverActions, route) {
|
|
1292
|
+
const currentRoute = route ?? (typeof window !== "undefined" ? window.location.pathname : "/");
|
|
1293
|
+
return {
|
|
1294
|
+
route: currentRoute,
|
|
1295
|
+
title: typeof document !== "undefined" ? document.title : "",
|
|
1296
|
+
actions: actions.slice(0, this.config.maxActions),
|
|
1297
|
+
serverActions,
|
|
1298
|
+
content: {
|
|
1299
|
+
headings: [],
|
|
1300
|
+
mainContent: "",
|
|
1301
|
+
forms: []
|
|
1302
|
+
},
|
|
1303
|
+
timestamp: Date.now()
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
log(...args) {
|
|
1307
|
+
this.logger.debug(...args);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
function querySelectorDeep(selector, options) {
|
|
1311
|
+
const preferVisible = options?.preferVisible ?? false;
|
|
1312
|
+
const roots = [document];
|
|
1313
|
+
let firstMatch = null;
|
|
1314
|
+
while (roots.length > 0) {
|
|
1315
|
+
const root = roots.shift();
|
|
1316
|
+
const match = root.querySelector(selector);
|
|
1317
|
+
if (match) {
|
|
1318
|
+
if (!preferVisible) return match;
|
|
1319
|
+
if (isElementVisible(match)) return match;
|
|
1320
|
+
if (!firstMatch) firstMatch = match;
|
|
1321
|
+
}
|
|
1322
|
+
const hostRoot = root instanceof Document ? root.body ?? root.documentElement : root;
|
|
1323
|
+
if (!hostRoot) continue;
|
|
1324
|
+
const hosts = hostRoot.querySelectorAll("*");
|
|
1325
|
+
for (const host of hosts) {
|
|
1326
|
+
const shadowRoot = host.shadowRoot;
|
|
1327
|
+
if (shadowRoot) {
|
|
1328
|
+
roots.push(shadowRoot);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return preferVisible ? firstMatch : null;
|
|
1333
|
+
}
|
|
1334
|
+
async function waitForElement(selector, timeout, requireVisible = true) {
|
|
1335
|
+
const startTime = Date.now();
|
|
1336
|
+
const isReady = (element) => {
|
|
1337
|
+
if (!element) return false;
|
|
1338
|
+
return !requireVisible || isElementVisible(element);
|
|
1339
|
+
};
|
|
1340
|
+
return new Promise((resolve) => {
|
|
1341
|
+
const element = querySelectorDeep(selector, { preferVisible: requireVisible });
|
|
1342
|
+
if (isReady(element)) {
|
|
1343
|
+
resolve(element);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const observer = new MutationObserver(() => {
|
|
1347
|
+
const el = querySelectorDeep(selector, { preferVisible: requireVisible });
|
|
1348
|
+
if (isReady(el)) {
|
|
1349
|
+
observer.disconnect();
|
|
1350
|
+
resolve(el);
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
observer.observe(document.body, {
|
|
1354
|
+
childList: true,
|
|
1355
|
+
subtree: true,
|
|
1356
|
+
attributes: true
|
|
1357
|
+
});
|
|
1358
|
+
const checkInterval = setInterval(() => {
|
|
1359
|
+
const el = querySelectorDeep(selector, { preferVisible: requireVisible });
|
|
1360
|
+
if (isReady(el)) {
|
|
1361
|
+
clearInterval(checkInterval);
|
|
1362
|
+
observer.disconnect();
|
|
1363
|
+
resolve(el);
|
|
1364
|
+
}
|
|
1365
|
+
if (Date.now() - startTime >= timeout) {
|
|
1366
|
+
clearInterval(checkInterval);
|
|
1367
|
+
observer.disconnect();
|
|
1368
|
+
resolve(null);
|
|
1369
|
+
}
|
|
1370
|
+
}, 100);
|
|
1371
|
+
setTimeout(() => {
|
|
1372
|
+
clearInterval(checkInterval);
|
|
1373
|
+
observer.disconnect();
|
|
1374
|
+
const el = querySelectorDeep(selector, { preferVisible: requireVisible });
|
|
1375
|
+
resolve(isReady(el) ? el : null);
|
|
1376
|
+
}, timeout);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
function isElementVisible(element) {
|
|
1380
|
+
const style = window.getComputedStyle(element);
|
|
1381
|
+
if (style.display === "none") return false;
|
|
1382
|
+
if (style.visibility === "hidden") return false;
|
|
1383
|
+
if (style.opacity === "0") return false;
|
|
1384
|
+
const rect = element.getBoundingClientRect();
|
|
1385
|
+
if (rect.width === 0 && rect.height === 0) return false;
|
|
1386
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
1387
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
1388
|
+
const horizontallyVisible = rect.right > 0 && rect.left < viewportWidth;
|
|
1389
|
+
const verticallyVisible = rect.bottom > 0 && rect.top < viewportHeight;
|
|
1390
|
+
return horizontallyVisible && verticallyVisible;
|
|
1391
|
+
}
|
|
1392
|
+
function getElementCenter(element) {
|
|
1393
|
+
const rect = element.getBoundingClientRect();
|
|
1394
|
+
return {
|
|
1395
|
+
x: rect.left + rect.width / 2,
|
|
1396
|
+
y: rect.top + rect.height / 2
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function setNativeInputValue(element, value) {
|
|
1400
|
+
const prototype = element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
1401
|
+
const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
1402
|
+
if (setter) {
|
|
1403
|
+
setter.call(element, value);
|
|
1404
|
+
} else {
|
|
1405
|
+
element.value = value;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function scrollToElement(element) {
|
|
1409
|
+
element.scrollIntoView({
|
|
1410
|
+
behavior: "smooth",
|
|
1411
|
+
block: "center",
|
|
1412
|
+
inline: "center"
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
function highlightElement(element, duration = 500) {
|
|
1416
|
+
const htmlElement = element;
|
|
1417
|
+
const originalOutline = htmlElement.style.outline;
|
|
1418
|
+
const originalTransition = htmlElement.style.transition;
|
|
1419
|
+
htmlElement.style.transition = "outline 0.2s ease";
|
|
1420
|
+
htmlElement.style.outline = "3px solid #6366f1";
|
|
1421
|
+
setTimeout(() => {
|
|
1422
|
+
htmlElement.style.outline = originalOutline;
|
|
1423
|
+
htmlElement.style.transition = originalTransition;
|
|
1424
|
+
}, duration);
|
|
1425
|
+
}
|
|
1426
|
+
async function simulateTyping(element, text, delay) {
|
|
1427
|
+
element.focus();
|
|
1428
|
+
setNativeInputValue(element, "");
|
|
1429
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1430
|
+
for (const char of text) {
|
|
1431
|
+
try {
|
|
1432
|
+
if (typeof InputEvent !== "undefined") {
|
|
1433
|
+
element.dispatchEvent(
|
|
1434
|
+
new InputEvent("beforeinput", {
|
|
1435
|
+
bubbles: true,
|
|
1436
|
+
cancelable: true,
|
|
1437
|
+
data: char,
|
|
1438
|
+
inputType: "insertText"
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
const nextValue = (element.value ?? "") + char;
|
|
1445
|
+
setNativeInputValue(element, nextValue);
|
|
1446
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1447
|
+
element.dispatchEvent(new KeyboardEvent("keydown", { key: char, bubbles: true }));
|
|
1448
|
+
element.dispatchEvent(new KeyboardEvent("keypress", { key: char, bubbles: true }));
|
|
1449
|
+
element.dispatchEvent(new KeyboardEvent("keyup", { key: char, bubbles: true }));
|
|
1450
|
+
if (delay > 0) {
|
|
1451
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1455
|
+
}
|
|
1456
|
+
async function simulateContentEditableTyping(element, text, delay) {
|
|
1457
|
+
element.focus();
|
|
1458
|
+
element.textContent = "";
|
|
1459
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1460
|
+
for (const char of text) {
|
|
1461
|
+
try {
|
|
1462
|
+
if (typeof InputEvent !== "undefined") {
|
|
1463
|
+
element.dispatchEvent(
|
|
1464
|
+
new InputEvent("beforeinput", {
|
|
1465
|
+
bubbles: true,
|
|
1466
|
+
cancelable: true,
|
|
1467
|
+
data: char,
|
|
1468
|
+
inputType: "insertText"
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
element.textContent = (element.textContent ?? "") + char;
|
|
1475
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1476
|
+
element.dispatchEvent(new KeyboardEvent("keydown", { key: char, bubbles: true }));
|
|
1477
|
+
element.dispatchEvent(new KeyboardEvent("keypress", { key: char, bubbles: true }));
|
|
1478
|
+
element.dispatchEvent(new KeyboardEvent("keyup", { key: char, bubbles: true }));
|
|
1479
|
+
if (delay > 0) {
|
|
1480
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1484
|
+
}
|
|
1485
|
+
function collectElementMetadata(element) {
|
|
1486
|
+
const rect = element.getBoundingClientRect();
|
|
1487
|
+
const tagName = element.tagName.toLowerCase();
|
|
1488
|
+
const attrs = {};
|
|
1489
|
+
if (element instanceof HTMLElement) {
|
|
1490
|
+
for (const attr of Array.from(element.attributes)) {
|
|
1491
|
+
if (attr.name.startsWith("data-") || attr.name === "aria-label" || attr.name === "name" || attr.name === "id") {
|
|
1492
|
+
attrs[attr.name] = attr.value;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
tagName,
|
|
1498
|
+
text: (element.textContent ?? "").trim().slice(0, 80),
|
|
1499
|
+
rect: {
|
|
1500
|
+
x: rect.x,
|
|
1501
|
+
y: rect.y,
|
|
1502
|
+
width: rect.width,
|
|
1503
|
+
height: rect.height
|
|
1504
|
+
},
|
|
1505
|
+
attributes: attrs
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
var DOMExecutor = class _DOMExecutor {
|
|
1509
|
+
config;
|
|
1510
|
+
logger = createLogger("SDK.Executor");
|
|
1511
|
+
constructor(config = {}) {
|
|
1512
|
+
this.config = {
|
|
1513
|
+
elementTimeout: config.elementTimeout ?? 6e3,
|
|
1514
|
+
typeDelay: config.typeDelay ?? 30,
|
|
1515
|
+
scrollIntoView: config.scrollIntoView ?? true,
|
|
1516
|
+
highlightOnInteract: config.highlightOnInteract ?? true,
|
|
1517
|
+
debug: config.debug ?? false
|
|
1518
|
+
};
|
|
1519
|
+
this.logger = createLogger("SDK.Executor", { enabled: this.config.debug, level: "debug" });
|
|
1520
|
+
}
|
|
1521
|
+
/** Delay before retry on failure (ms) */
|
|
1522
|
+
static RETRY_DELAY_MS = 600;
|
|
1523
|
+
/** Maximum number of retry attempts */
|
|
1524
|
+
static MAX_RETRIES = 2;
|
|
1525
|
+
/**
|
|
1526
|
+
* Click an element (synthetic events + native click fallback; one retry on failure)
|
|
1527
|
+
* Prevents default behavior on links to avoid page reloads
|
|
1528
|
+
*/
|
|
1529
|
+
async click(selector) {
|
|
1530
|
+
const startTime = Date.now();
|
|
1531
|
+
const attempt = async () => {
|
|
1532
|
+
const element = await waitForElement(selector, this.config.elementTimeout, true);
|
|
1533
|
+
if (!element) {
|
|
1534
|
+
return this.createResult(
|
|
1535
|
+
selector,
|
|
1536
|
+
"click",
|
|
1537
|
+
startTime,
|
|
1538
|
+
false,
|
|
1539
|
+
"Element not found",
|
|
1540
|
+
"ELEMENT_NOT_FOUND"
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
if (this.config.scrollIntoView) {
|
|
1544
|
+
scrollToElement(element);
|
|
1545
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1546
|
+
}
|
|
1547
|
+
if (this.config.highlightOnInteract) {
|
|
1548
|
+
highlightElement(element);
|
|
1549
|
+
}
|
|
1550
|
+
if (!isElementVisible(element)) {
|
|
1551
|
+
return this.createResult(
|
|
1552
|
+
selector,
|
|
1553
|
+
"click",
|
|
1554
|
+
startTime,
|
|
1555
|
+
false,
|
|
1556
|
+
"Element is not visible",
|
|
1557
|
+
"NOT_VISIBLE",
|
|
1558
|
+
collectElementMetadata(element)
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
const htmlElement = element;
|
|
1562
|
+
const isDisabled = htmlElement.disabled === true || htmlElement.getAttribute("aria-disabled") === "true";
|
|
1563
|
+
if (isDisabled) {
|
|
1564
|
+
return this.createResult(
|
|
1565
|
+
selector,
|
|
1566
|
+
"click",
|
|
1567
|
+
startTime,
|
|
1568
|
+
false,
|
|
1569
|
+
"Element is disabled",
|
|
1570
|
+
"DISABLED",
|
|
1571
|
+
collectElementMetadata(element)
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
htmlElement.focus();
|
|
1575
|
+
const { x, y } = getElementCenter(element);
|
|
1576
|
+
try {
|
|
1577
|
+
if (typeof PointerEvent !== "undefined") {
|
|
1578
|
+
element.dispatchEvent(
|
|
1579
|
+
new PointerEvent("pointerover", { bubbles: true, cancelable: true, clientX: x, clientY: y })
|
|
1580
|
+
);
|
|
1581
|
+
element.dispatchEvent(
|
|
1582
|
+
new PointerEvent("pointerenter", { bubbles: true, cancelable: true, clientX: x, clientY: y })
|
|
1583
|
+
);
|
|
1584
|
+
element.dispatchEvent(
|
|
1585
|
+
new PointerEvent("pointermove", { bubbles: true, cancelable: true, clientX: x, clientY: y })
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
element.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
1591
|
+
element.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
1592
|
+
element.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
1593
|
+
try {
|
|
1594
|
+
if (typeof PointerEvent !== "undefined") {
|
|
1595
|
+
element.dispatchEvent(
|
|
1596
|
+
new PointerEvent("pointerdown", {
|
|
1597
|
+
bubbles: true,
|
|
1598
|
+
cancelable: true,
|
|
1599
|
+
button: 0,
|
|
1600
|
+
clientX: x,
|
|
1601
|
+
clientY: y
|
|
1602
|
+
})
|
|
1603
|
+
);
|
|
1604
|
+
element.dispatchEvent(
|
|
1605
|
+
new PointerEvent("pointerup", {
|
|
1606
|
+
bubbles: true,
|
|
1607
|
+
cancelable: true,
|
|
1608
|
+
button: 0,
|
|
1609
|
+
clientX: x,
|
|
1610
|
+
clientY: y
|
|
1611
|
+
})
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
element.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
1617
|
+
element.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
1618
|
+
element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, clientX: x, clientY: y }));
|
|
1619
|
+
htmlElement.click();
|
|
1620
|
+
this.log("Clicked:", selector);
|
|
1621
|
+
return this.createResult(
|
|
1622
|
+
selector,
|
|
1623
|
+
"click",
|
|
1624
|
+
startTime,
|
|
1625
|
+
true,
|
|
1626
|
+
void 0,
|
|
1627
|
+
void 0,
|
|
1628
|
+
collectElementMetadata(element)
|
|
1629
|
+
);
|
|
1630
|
+
};
|
|
1631
|
+
const executeWithRetries = async () => {
|
|
1632
|
+
let lastResult = null;
|
|
1633
|
+
for (let retry = 0; retry <= _DOMExecutor.MAX_RETRIES; retry++) {
|
|
1634
|
+
if (retry > 0) {
|
|
1635
|
+
this.log(`Click retry ${retry}/${_DOMExecutor.MAX_RETRIES} for:`, selector);
|
|
1636
|
+
await new Promise((r) => setTimeout(r, _DOMExecutor.RETRY_DELAY_MS * retry));
|
|
1637
|
+
}
|
|
1638
|
+
try {
|
|
1639
|
+
lastResult = await attempt();
|
|
1640
|
+
if (lastResult.success) {
|
|
1641
|
+
return lastResult;
|
|
1642
|
+
}
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
lastResult = this.createResult(
|
|
1645
|
+
selector,
|
|
1646
|
+
"click",
|
|
1647
|
+
startTime,
|
|
1648
|
+
false,
|
|
1649
|
+
String(error),
|
|
1650
|
+
"RUNTIME_ERROR"
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return lastResult;
|
|
1655
|
+
};
|
|
1656
|
+
return executeWithRetries();
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Type text into an element (one retry on failure)
|
|
1660
|
+
*/
|
|
1661
|
+
async type(selector, text, clearFirst = true) {
|
|
1662
|
+
const startTime = Date.now();
|
|
1663
|
+
const attempt = async () => {
|
|
1664
|
+
const element = await waitForElement(selector, this.config.elementTimeout, true);
|
|
1665
|
+
if (!element) {
|
|
1666
|
+
return this.createResult(
|
|
1667
|
+
selector,
|
|
1668
|
+
"type",
|
|
1669
|
+
startTime,
|
|
1670
|
+
false,
|
|
1671
|
+
"Element not found",
|
|
1672
|
+
"ELEMENT_NOT_FOUND"
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
if (!this.isTypeable(element)) {
|
|
1676
|
+
return this.createResult(
|
|
1677
|
+
selector,
|
|
1678
|
+
"type",
|
|
1679
|
+
startTime,
|
|
1680
|
+
false,
|
|
1681
|
+
"Element is not typeable",
|
|
1682
|
+
"TYPE_MISMATCH",
|
|
1683
|
+
collectElementMetadata(element)
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (this.config.scrollIntoView) {
|
|
1687
|
+
scrollToElement(element);
|
|
1688
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1689
|
+
}
|
|
1690
|
+
if (this.config.highlightOnInteract) {
|
|
1691
|
+
highlightElement(element);
|
|
1692
|
+
}
|
|
1693
|
+
const metadata = collectElementMetadata(element);
|
|
1694
|
+
if (!isElementVisible(element)) {
|
|
1695
|
+
return this.createResult(
|
|
1696
|
+
selector,
|
|
1697
|
+
"type",
|
|
1698
|
+
startTime,
|
|
1699
|
+
false,
|
|
1700
|
+
"Element is not visible",
|
|
1701
|
+
"NOT_VISIBLE",
|
|
1702
|
+
metadata
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
1706
|
+
const inputElement = element;
|
|
1707
|
+
if (inputElement.disabled || inputElement.getAttribute("aria-disabled") === "true") {
|
|
1708
|
+
return this.createResult(
|
|
1709
|
+
selector,
|
|
1710
|
+
"type",
|
|
1711
|
+
startTime,
|
|
1712
|
+
false,
|
|
1713
|
+
"Element is disabled",
|
|
1714
|
+
"DISABLED",
|
|
1715
|
+
metadata
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
if (clearFirst) {
|
|
1719
|
+
await simulateTyping(inputElement, text, this.config.typeDelay);
|
|
1720
|
+
} else {
|
|
1721
|
+
inputElement.focus();
|
|
1722
|
+
setNativeInputValue(inputElement, (inputElement.value ?? "") + text);
|
|
1723
|
+
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1724
|
+
inputElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1725
|
+
}
|
|
1726
|
+
} else if (element instanceof HTMLElement && element.isContentEditable) {
|
|
1727
|
+
if (element.getAttribute("aria-disabled") === "true") {
|
|
1728
|
+
return this.createResult(
|
|
1729
|
+
selector,
|
|
1730
|
+
"type",
|
|
1731
|
+
startTime,
|
|
1732
|
+
false,
|
|
1733
|
+
"Element is disabled",
|
|
1734
|
+
"DISABLED",
|
|
1735
|
+
metadata
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
await simulateContentEditableTyping(element, text, this.config.typeDelay);
|
|
1739
|
+
} else {
|
|
1740
|
+
return this.createResult(
|
|
1741
|
+
selector,
|
|
1742
|
+
"type",
|
|
1743
|
+
startTime,
|
|
1744
|
+
false,
|
|
1745
|
+
"Element is not a supported type target",
|
|
1746
|
+
"TYPE_MISMATCH",
|
|
1747
|
+
metadata
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
this.log("Typed into:", selector, "text:", text.slice(0, 20) + (text.length > 20 ? "..." : ""));
|
|
1751
|
+
return this.createResult(
|
|
1752
|
+
selector,
|
|
1753
|
+
"type",
|
|
1754
|
+
startTime,
|
|
1755
|
+
true,
|
|
1756
|
+
void 0,
|
|
1757
|
+
void 0,
|
|
1758
|
+
{
|
|
1759
|
+
...metadata,
|
|
1760
|
+
textLength: text.length
|
|
1761
|
+
}
|
|
1762
|
+
);
|
|
1763
|
+
};
|
|
1764
|
+
const executeWithRetries = async () => {
|
|
1765
|
+
let lastResult = null;
|
|
1766
|
+
for (let retry = 0; retry <= _DOMExecutor.MAX_RETRIES; retry++) {
|
|
1767
|
+
if (retry > 0) {
|
|
1768
|
+
this.log(`Type retry ${retry}/${_DOMExecutor.MAX_RETRIES} for:`, selector);
|
|
1769
|
+
await new Promise((r) => setTimeout(r, _DOMExecutor.RETRY_DELAY_MS * retry));
|
|
1770
|
+
}
|
|
1771
|
+
try {
|
|
1772
|
+
lastResult = await attempt();
|
|
1773
|
+
if (lastResult.success) {
|
|
1774
|
+
return lastResult;
|
|
1775
|
+
}
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
lastResult = this.createResult(
|
|
1778
|
+
selector,
|
|
1779
|
+
"type",
|
|
1780
|
+
startTime,
|
|
1781
|
+
false,
|
|
1782
|
+
String(error),
|
|
1783
|
+
"RUNTIME_ERROR"
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return lastResult;
|
|
1788
|
+
};
|
|
1789
|
+
return executeWithRetries();
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Select an option from a dropdown (one retry on failure)
|
|
1793
|
+
*/
|
|
1794
|
+
async select(selector, value) {
|
|
1795
|
+
const startTime = Date.now();
|
|
1796
|
+
const attempt = async () => {
|
|
1797
|
+
const element = await waitForElement(selector, this.config.elementTimeout, true);
|
|
1798
|
+
if (!element) {
|
|
1799
|
+
return this.createResult(
|
|
1800
|
+
selector,
|
|
1801
|
+
"select",
|
|
1802
|
+
startTime,
|
|
1803
|
+
false,
|
|
1804
|
+
"Element not found",
|
|
1805
|
+
"ELEMENT_NOT_FOUND"
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
if (element.tagName.toLowerCase() !== "select") {
|
|
1809
|
+
return this.createResult(
|
|
1810
|
+
selector,
|
|
1811
|
+
"select",
|
|
1812
|
+
startTime,
|
|
1813
|
+
false,
|
|
1814
|
+
"Element is not a select",
|
|
1815
|
+
"TYPE_MISMATCH",
|
|
1816
|
+
collectElementMetadata(element)
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
if (this.config.scrollIntoView) {
|
|
1820
|
+
scrollToElement(element);
|
|
1821
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1822
|
+
}
|
|
1823
|
+
if (this.config.highlightOnInteract) {
|
|
1824
|
+
highlightElement(element);
|
|
1825
|
+
}
|
|
1826
|
+
if (!isElementVisible(element)) {
|
|
1827
|
+
return this.createResult(
|
|
1828
|
+
selector,
|
|
1829
|
+
"select",
|
|
1830
|
+
startTime,
|
|
1831
|
+
false,
|
|
1832
|
+
"Element is not visible",
|
|
1833
|
+
"NOT_VISIBLE",
|
|
1834
|
+
collectElementMetadata(element)
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
const selectElement = element;
|
|
1838
|
+
if (selectElement.disabled || selectElement.getAttribute("aria-disabled") === "true") {
|
|
1839
|
+
return this.createResult(
|
|
1840
|
+
selector,
|
|
1841
|
+
"select",
|
|
1842
|
+
startTime,
|
|
1843
|
+
false,
|
|
1844
|
+
"Element is disabled",
|
|
1845
|
+
"DISABLED",
|
|
1846
|
+
collectElementMetadata(element)
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
let found = false;
|
|
1850
|
+
for (const option of selectElement.options) {
|
|
1851
|
+
if (option.value === value || option.textContent?.toLowerCase().includes(value.toLowerCase())) {
|
|
1852
|
+
selectElement.value = option.value;
|
|
1853
|
+
found = true;
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (!found) {
|
|
1858
|
+
return this.createResult(
|
|
1859
|
+
selector,
|
|
1860
|
+
"select",
|
|
1861
|
+
startTime,
|
|
1862
|
+
false,
|
|
1863
|
+
`Option not found: ${value}`,
|
|
1864
|
+
"OPTION_NOT_FOUND",
|
|
1865
|
+
{
|
|
1866
|
+
...collectElementMetadata(element),
|
|
1867
|
+
attemptedValue: value
|
|
1868
|
+
}
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
selectElement.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1872
|
+
selectElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1873
|
+
this.log("Selected:", selector, "value:", value);
|
|
1874
|
+
return this.createResult(
|
|
1875
|
+
selector,
|
|
1876
|
+
"select",
|
|
1877
|
+
startTime,
|
|
1878
|
+
true,
|
|
1879
|
+
void 0,
|
|
1880
|
+
void 0,
|
|
1881
|
+
{
|
|
1882
|
+
...collectElementMetadata(element),
|
|
1883
|
+
selectedValue: value
|
|
1884
|
+
}
|
|
1885
|
+
);
|
|
1886
|
+
};
|
|
1887
|
+
const executeWithRetries = async () => {
|
|
1888
|
+
let lastResult = null;
|
|
1889
|
+
for (let retry = 0; retry <= _DOMExecutor.MAX_RETRIES; retry++) {
|
|
1890
|
+
if (retry > 0) {
|
|
1891
|
+
this.log(`Select retry ${retry}/${_DOMExecutor.MAX_RETRIES} for:`, selector);
|
|
1892
|
+
await new Promise((r) => setTimeout(r, _DOMExecutor.RETRY_DELAY_MS * retry));
|
|
1893
|
+
}
|
|
1894
|
+
try {
|
|
1895
|
+
lastResult = await attempt();
|
|
1896
|
+
if (lastResult.success) {
|
|
1897
|
+
return lastResult;
|
|
1898
|
+
}
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
lastResult = this.createResult(
|
|
1901
|
+
selector,
|
|
1902
|
+
"select",
|
|
1903
|
+
startTime,
|
|
1904
|
+
false,
|
|
1905
|
+
String(error),
|
|
1906
|
+
"RUNTIME_ERROR"
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return lastResult;
|
|
1911
|
+
};
|
|
1912
|
+
return executeWithRetries();
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Scroll to an element or position
|
|
1916
|
+
*/
|
|
1917
|
+
async scroll(target) {
|
|
1918
|
+
const startTime = Date.now();
|
|
1919
|
+
const selectorStr = typeof target === "string" ? target : `${target.x},${target.y}`;
|
|
1920
|
+
try {
|
|
1921
|
+
if (typeof target === "string") {
|
|
1922
|
+
const element = await waitForElement(target, this.config.elementTimeout, false);
|
|
1923
|
+
if (!element) {
|
|
1924
|
+
return this.createResult(
|
|
1925
|
+
selectorStr,
|
|
1926
|
+
"scroll",
|
|
1927
|
+
startTime,
|
|
1928
|
+
false,
|
|
1929
|
+
"Element not found",
|
|
1930
|
+
"ELEMENT_NOT_FOUND"
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (this.config.highlightOnInteract) {
|
|
1934
|
+
highlightElement(element);
|
|
1935
|
+
}
|
|
1936
|
+
scrollToElement(element);
|
|
1937
|
+
} else {
|
|
1938
|
+
window.scrollTo({
|
|
1939
|
+
left: target.x,
|
|
1940
|
+
top: target.y,
|
|
1941
|
+
behavior: "smooth"
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
this.log("Scrolled to:", target);
|
|
1945
|
+
return this.createResult(
|
|
1946
|
+
selectorStr,
|
|
1947
|
+
"scroll",
|
|
1948
|
+
startTime,
|
|
1949
|
+
true,
|
|
1950
|
+
void 0,
|
|
1951
|
+
void 0,
|
|
1952
|
+
typeof target === "string" ? (() => {
|
|
1953
|
+
const element = querySelectorDeep(target);
|
|
1954
|
+
return element ? collectElementMetadata(element) : { selector: target };
|
|
1955
|
+
})() : { position: target }
|
|
1956
|
+
);
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
return this.createResult(
|
|
1959
|
+
selectorStr,
|
|
1960
|
+
"scroll",
|
|
1961
|
+
startTime,
|
|
1962
|
+
false,
|
|
1963
|
+
String(error),
|
|
1964
|
+
"RUNTIME_ERROR"
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Submit a form
|
|
1970
|
+
*/
|
|
1971
|
+
async submit(selector) {
|
|
1972
|
+
const startTime = Date.now();
|
|
1973
|
+
try {
|
|
1974
|
+
const element = await waitForElement(selector, this.config.elementTimeout, true);
|
|
1975
|
+
if (!element) {
|
|
1976
|
+
return this.createResult(
|
|
1977
|
+
selector,
|
|
1978
|
+
"submit",
|
|
1979
|
+
startTime,
|
|
1980
|
+
false,
|
|
1981
|
+
"Element not found",
|
|
1982
|
+
"ELEMENT_NOT_FOUND"
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
const form = element.tagName.toLowerCase() === "form" ? element : element.closest("form");
|
|
1986
|
+
if (!form) {
|
|
1987
|
+
const submitButton = element.querySelector('button[type="submit"], input[type="submit"]');
|
|
1988
|
+
if (submitButton) {
|
|
1989
|
+
return this.click(this.generateSelector(submitButton));
|
|
1990
|
+
}
|
|
1991
|
+
return this.createResult(
|
|
1992
|
+
selector,
|
|
1993
|
+
"submit",
|
|
1994
|
+
startTime,
|
|
1995
|
+
false,
|
|
1996
|
+
"No form found",
|
|
1997
|
+
"NO_FORM",
|
|
1998
|
+
collectElementMetadata(element)
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
if (this.config.highlightOnInteract) {
|
|
2002
|
+
highlightElement(form);
|
|
2003
|
+
}
|
|
2004
|
+
const submitter = form.querySelector('button[type="submit"], input[type="submit"]');
|
|
2005
|
+
if (typeof form.requestSubmit === "function") {
|
|
2006
|
+
form.requestSubmit(submitter ?? void 0);
|
|
2007
|
+
} else {
|
|
2008
|
+
const submitEvent = new Event("submit", { bubbles: true, cancelable: true });
|
|
2009
|
+
const shouldSubmit = form.dispatchEvent(submitEvent);
|
|
2010
|
+
if (shouldSubmit) {
|
|
2011
|
+
form.submit();
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
this.log("Submitted form:", selector);
|
|
2015
|
+
return this.createResult(
|
|
2016
|
+
selector,
|
|
2017
|
+
"submit",
|
|
2018
|
+
startTime,
|
|
2019
|
+
true,
|
|
2020
|
+
void 0,
|
|
2021
|
+
void 0,
|
|
2022
|
+
collectElementMetadata(form)
|
|
2023
|
+
);
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
return this.createResult(
|
|
2026
|
+
selector,
|
|
2027
|
+
"submit",
|
|
2028
|
+
startTime,
|
|
2029
|
+
false,
|
|
2030
|
+
String(error),
|
|
2031
|
+
"RUNTIME_ERROR"
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Check if element is typeable
|
|
2037
|
+
*/
|
|
2038
|
+
isTypeable(element) {
|
|
2039
|
+
const tagName = element.tagName.toLowerCase();
|
|
2040
|
+
if (tagName === "input") {
|
|
2041
|
+
const type = element.type;
|
|
2042
|
+
return ["text", "email", "password", "search", "tel", "url", "number"].includes(type);
|
|
2043
|
+
}
|
|
2044
|
+
if (tagName === "textarea") return true;
|
|
2045
|
+
if (element.hasAttribute("contenteditable")) return true;
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Generate a selector for an element
|
|
2050
|
+
*/
|
|
2051
|
+
generateSelector(element) {
|
|
2052
|
+
if (element.id) return `#${element.id}`;
|
|
2053
|
+
const dataTestId = element.getAttribute("data-testid");
|
|
2054
|
+
if (dataTestId) return `[data-testid="${dataTestId}"]`;
|
|
2055
|
+
return element.tagName.toLowerCase();
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Create execution result
|
|
2059
|
+
*/
|
|
2060
|
+
createResult(selector, action, startTime, success, error, errorCode, metadata) {
|
|
2061
|
+
if (!success) {
|
|
2062
|
+
this.logger.warn("Action failed", {
|
|
2063
|
+
action,
|
|
2064
|
+
selector,
|
|
2065
|
+
error,
|
|
2066
|
+
errorCode,
|
|
2067
|
+
metadata
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
success,
|
|
2072
|
+
selector,
|
|
2073
|
+
action,
|
|
2074
|
+
duration: Date.now() - startTime,
|
|
2075
|
+
errorCode,
|
|
2076
|
+
error,
|
|
2077
|
+
metadata
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
log(...args) {
|
|
2081
|
+
this.logger.debug(...args);
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
var CommandProcessor = class {
|
|
2085
|
+
config;
|
|
2086
|
+
executor;
|
|
2087
|
+
navController;
|
|
2088
|
+
registry;
|
|
2089
|
+
logger = createLogger("SDK.Processor");
|
|
2090
|
+
isExecuting = false;
|
|
2091
|
+
shouldAbort = false;
|
|
2092
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2093
|
+
// Server action handler
|
|
2094
|
+
serverActionHandler = null;
|
|
2095
|
+
constructor(executor, navController, registry, config = {}) {
|
|
2096
|
+
this.executor = executor;
|
|
2097
|
+
this.navController = navController;
|
|
2098
|
+
this.registry = registry;
|
|
2099
|
+
this.config = {
|
|
2100
|
+
commandDelay: config.commandDelay ?? 650,
|
|
2101
|
+
commandTimeout: config.commandTimeout ?? 3e4,
|
|
2102
|
+
debug: config.debug ?? false
|
|
2103
|
+
};
|
|
2104
|
+
this.logger = createLogger("SDK.Processor", { enabled: this.config.debug, level: "debug" });
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Set server action handler
|
|
2108
|
+
*/
|
|
2109
|
+
setServerActionHandler(handler) {
|
|
2110
|
+
this.serverActionHandler = handler;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Execute a list of commands
|
|
2114
|
+
*/
|
|
2115
|
+
async execute(commands) {
|
|
2116
|
+
if (this.isExecuting) {
|
|
2117
|
+
throw new Error("Already executing commands");
|
|
2118
|
+
}
|
|
2119
|
+
this.isExecuting = true;
|
|
2120
|
+
this.shouldAbort = false;
|
|
2121
|
+
const results = [];
|
|
2122
|
+
let previousCommandType = null;
|
|
2123
|
+
try {
|
|
2124
|
+
for (let i = 0; i < commands.length; i++) {
|
|
2125
|
+
if (this.shouldAbort) {
|
|
2126
|
+
this.log("Execution aborted");
|
|
2127
|
+
break;
|
|
2128
|
+
}
|
|
2129
|
+
const command = commands[i];
|
|
2130
|
+
const needsStability = ["click", "type", "select", "scroll"].includes(command.type);
|
|
2131
|
+
const previousWasNavigation = previousCommandType && ["navigate", "wait_for_route"].includes(previousCommandType);
|
|
2132
|
+
if (needsStability && previousWasNavigation) {
|
|
2133
|
+
this.log("Auto-waiting for DOM stability before:", command.type);
|
|
2134
|
+
await this.navController.waitForDOMStable();
|
|
2135
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2136
|
+
}
|
|
2137
|
+
this.emit("stateChange", {
|
|
2138
|
+
isExecuting: true,
|
|
2139
|
+
currentCommand: command,
|
|
2140
|
+
currentIndex: i,
|
|
2141
|
+
totalCommands: commands.length,
|
|
2142
|
+
description: this.describeCommand(command)
|
|
2143
|
+
});
|
|
2144
|
+
this.emit("commandStart", command, i);
|
|
2145
|
+
const result = await this.executeCommand(command);
|
|
2146
|
+
results.push(result);
|
|
2147
|
+
this.emit("commandComplete", result);
|
|
2148
|
+
previousCommandType = command.type;
|
|
2149
|
+
if (!result.success) {
|
|
2150
|
+
this.logger.warn("Command failed, stopping execution", {
|
|
2151
|
+
type: command.type,
|
|
2152
|
+
error: result.error,
|
|
2153
|
+
result: result.result
|
|
2154
|
+
});
|
|
2155
|
+
break;
|
|
2156
|
+
}
|
|
2157
|
+
if (i < commands.length - 1) {
|
|
2158
|
+
await new Promise((resolve) => setTimeout(resolve, this.config.commandDelay));
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
this.emit("complete", results);
|
|
2162
|
+
return results;
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2165
|
+
this.logger.error("Execution error", err);
|
|
2166
|
+
this.emit("error", err);
|
|
2167
|
+
throw err;
|
|
2168
|
+
} finally {
|
|
2169
|
+
this.isExecuting = false;
|
|
2170
|
+
this.emit("stateChange", {
|
|
2171
|
+
isExecuting: false,
|
|
2172
|
+
currentCommand: void 0,
|
|
2173
|
+
currentIndex: 0,
|
|
2174
|
+
totalCommands: commands.length,
|
|
2175
|
+
description: void 0
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Abort current execution
|
|
2181
|
+
*/
|
|
2182
|
+
abort() {
|
|
2183
|
+
this.shouldAbort = true;
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Get execution status
|
|
2187
|
+
*/
|
|
2188
|
+
getIsExecuting() {
|
|
2189
|
+
return this.isExecuting;
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Execute a single command
|
|
2193
|
+
*/
|
|
2194
|
+
async executeCommand(command) {
|
|
2195
|
+
const startTime = Date.now();
|
|
2196
|
+
try {
|
|
2197
|
+
switch (command.type) {
|
|
2198
|
+
case "navigate":
|
|
2199
|
+
return await this.executeNavigate(command, startTime);
|
|
2200
|
+
case "wait_for_route":
|
|
2201
|
+
return await this.executeWaitForRoute(command, startTime);
|
|
2202
|
+
case "wait_for_dom_stable":
|
|
2203
|
+
return await this.executeWaitForDomStable(command, startTime);
|
|
2204
|
+
case "scan_context":
|
|
2205
|
+
return await this.executeScanContext(command, startTime);
|
|
2206
|
+
case "click":
|
|
2207
|
+
return await this.executeClick(command, startTime);
|
|
2208
|
+
case "type":
|
|
2209
|
+
return await this.executeType(command, startTime);
|
|
2210
|
+
case "select":
|
|
2211
|
+
return await this.executeSelect(command, startTime);
|
|
2212
|
+
case "scroll":
|
|
2213
|
+
return await this.executeScroll(command, startTime);
|
|
2214
|
+
case "server_action":
|
|
2215
|
+
return await this.executeServerAction(command, startTime);
|
|
2216
|
+
case "report_result":
|
|
2217
|
+
return await this.executeReportResult(command, startTime);
|
|
2218
|
+
default:
|
|
2219
|
+
return this.createResult(command, startTime, false, `Unknown command type`);
|
|
2220
|
+
}
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
this.logger.error("Command execution error", {
|
|
2223
|
+
type: command.type,
|
|
2224
|
+
error: String(error)
|
|
2225
|
+
});
|
|
2226
|
+
return this.createResult(command, startTime, false, String(error));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// ============================================================================
|
|
2230
|
+
// Command Executors
|
|
2231
|
+
// ============================================================================
|
|
2232
|
+
async executeNavigate(command, startTime) {
|
|
2233
|
+
this.log("Navigating to:", command.target);
|
|
2234
|
+
const context = await this.navController.executeNavigation(command.target);
|
|
2235
|
+
if (!context) {
|
|
2236
|
+
return this.createResult(command, startTime, false, "Navigation failed");
|
|
2237
|
+
}
|
|
2238
|
+
return this.createResult(command, startTime, true);
|
|
2239
|
+
}
|
|
2240
|
+
async executeWaitForRoute(command, startTime) {
|
|
2241
|
+
this.log("Waiting for route:", command.target);
|
|
2242
|
+
const success = await this.navController.navigateTo(command.target, command.timeout);
|
|
2243
|
+
return this.createResult(command, startTime, success, success ? void 0 : "Route timeout");
|
|
2244
|
+
}
|
|
2245
|
+
async executeWaitForDomStable(command, startTime) {
|
|
2246
|
+
this.log("Waiting for DOM to stabilize");
|
|
2247
|
+
await this.navController.waitForDOMStable();
|
|
2248
|
+
return this.createResult(command, startTime, true);
|
|
2249
|
+
}
|
|
2250
|
+
async executeScanContext(command, startTime) {
|
|
2251
|
+
this.log("Scanning page context");
|
|
2252
|
+
return this.createResult(command, startTime, true);
|
|
2253
|
+
}
|
|
2254
|
+
async executeClick(command, startTime) {
|
|
2255
|
+
let selector = command.selector;
|
|
2256
|
+
const route = this.navController.getCurrentRoute();
|
|
2257
|
+
if (selector) {
|
|
2258
|
+
this.log("Clicking with selector:", selector);
|
|
2259
|
+
const result = await this.executor.click(selector);
|
|
2260
|
+
if (result.success) {
|
|
2261
|
+
return this.createResult(command, startTime, true);
|
|
2262
|
+
}
|
|
2263
|
+
this.log("Selector failed, trying text fallback:", command.text || selector);
|
|
2264
|
+
}
|
|
2265
|
+
if (command.text) {
|
|
2266
|
+
const action = this.registry.findActionByLabel(route, command.text, command.context);
|
|
2267
|
+
if (action && action.selector !== selector) {
|
|
2268
|
+
this.log("Found action by label, clicking:", action.selector);
|
|
2269
|
+
const result = await this.executor.click(action.selector);
|
|
2270
|
+
if (result.success) {
|
|
2271
|
+
return this.createResult(command, startTime, true);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (command.text) {
|
|
2276
|
+
const actionAttr = command.text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
2277
|
+
const chatbotSelector = `[data-chatbot-action*="${actionAttr}"]`;
|
|
2278
|
+
this.log("Trying data-chatbot-action selector:", chatbotSelector);
|
|
2279
|
+
const result = await this.executor.click(chatbotSelector);
|
|
2280
|
+
if (result.success) {
|
|
2281
|
+
return this.createResult(command, startTime, true);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (command.text) {
|
|
2285
|
+
const ariaSelector = `[aria-label="${command.text}"]`;
|
|
2286
|
+
this.log("Trying aria-label selector:", ariaSelector);
|
|
2287
|
+
const result = await this.executor.click(ariaSelector);
|
|
2288
|
+
if (result.success) {
|
|
2289
|
+
return this.createResult(command, startTime, true);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
if (command.text) {
|
|
2293
|
+
const textToFind = command.text.toLowerCase().trim();
|
|
2294
|
+
const clickables = document.querySelectorAll('button, a, [role="button"], [data-chatbot-action]');
|
|
2295
|
+
for (const el of clickables) {
|
|
2296
|
+
const elText = el.textContent?.toLowerCase().trim() || "";
|
|
2297
|
+
const ariaLabel = el.getAttribute("aria-label")?.toLowerCase().trim() || "";
|
|
2298
|
+
const chatbotAction = el.getAttribute("data-chatbot-action")?.toLowerCase().replace(/-/g, " ") || "";
|
|
2299
|
+
if (elText === textToFind || ariaLabel === textToFind || elText.includes(textToFind) || chatbotAction === textToFind || chatbotAction.includes(textToFind.replace(/\s+/g, "-"))) {
|
|
2300
|
+
let foundSelector = "";
|
|
2301
|
+
if (el.id) {
|
|
2302
|
+
foundSelector = `#${el.id}`;
|
|
2303
|
+
} else if (el.getAttribute("data-chatbot-action")) {
|
|
2304
|
+
foundSelector = `[data-chatbot-action="${el.getAttribute("data-chatbot-action")}"]`;
|
|
2305
|
+
} else if (el.getAttribute("aria-label")) {
|
|
2306
|
+
foundSelector = `[aria-label="${el.getAttribute("aria-label")}"]`;
|
|
2307
|
+
}
|
|
2308
|
+
if (foundSelector) {
|
|
2309
|
+
this.log("Found element by text content, clicking:", foundSelector);
|
|
2310
|
+
const result = await this.executor.click(foundSelector);
|
|
2311
|
+
if (result.success) {
|
|
2312
|
+
return this.createResult(command, startTime, true);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
const errorMsg = selector ? `Element not found or click failed: ${selector}` : `No selector found for: ${command.text || "unknown"}`;
|
|
2319
|
+
return this.createResult(command, startTime, false, errorMsg);
|
|
2320
|
+
}
|
|
2321
|
+
async executeType(command, startTime) {
|
|
2322
|
+
let selector = command.selector;
|
|
2323
|
+
if (!selector && command.text) {
|
|
2324
|
+
const action = this.registry.findActionByLabel(
|
|
2325
|
+
this.navController.getCurrentRoute(),
|
|
2326
|
+
command.text
|
|
2327
|
+
);
|
|
2328
|
+
if (action) selector = action.selector;
|
|
2329
|
+
}
|
|
2330
|
+
if (!selector) {
|
|
2331
|
+
return this.createResult(command, startTime, false, "No selector provided");
|
|
2332
|
+
}
|
|
2333
|
+
this.log("Typing into:", selector, "value:", command.value);
|
|
2334
|
+
let result = await this.executor.type(selector, command.value, command.clear ?? true);
|
|
2335
|
+
if (!result.success && command.text && result.error?.includes("not found")) {
|
|
2336
|
+
const action = this.registry.findActionByLabel(
|
|
2337
|
+
this.navController.getCurrentRoute(),
|
|
2338
|
+
command.text
|
|
2339
|
+
);
|
|
2340
|
+
if (action) {
|
|
2341
|
+
result = await this.executor.type(action.selector, command.value, command.clear ?? true);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return this.createResult(command, startTime, result.success, result.error);
|
|
2345
|
+
}
|
|
2346
|
+
async executeSelect(command, startTime) {
|
|
2347
|
+
let selector = command.selector;
|
|
2348
|
+
if (!selector && command.text) {
|
|
2349
|
+
const action = this.registry.findActionByLabel(
|
|
2350
|
+
this.navController.getCurrentRoute(),
|
|
2351
|
+
command.text
|
|
2352
|
+
);
|
|
2353
|
+
if (action) selector = action.selector;
|
|
2354
|
+
}
|
|
2355
|
+
if (!selector) {
|
|
2356
|
+
return this.createResult(command, startTime, false, "No selector provided");
|
|
2357
|
+
}
|
|
2358
|
+
this.log("Selecting:", selector, "value:", command.value);
|
|
2359
|
+
let result = await this.executor.select(selector, command.value);
|
|
2360
|
+
if (!result.success && command.text && result.error?.includes("not found")) {
|
|
2361
|
+
const action = this.registry.findActionByLabel(
|
|
2362
|
+
this.navController.getCurrentRoute(),
|
|
2363
|
+
command.text
|
|
2364
|
+
);
|
|
2365
|
+
if (action) {
|
|
2366
|
+
result = await this.executor.select(action.selector, command.value);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return this.createResult(command, startTime, result.success, result.error);
|
|
2370
|
+
}
|
|
2371
|
+
async executeScroll(command, startTime) {
|
|
2372
|
+
const target = command.selector ?? command.position ?? { x: 0, y: 0 };
|
|
2373
|
+
this.log("Scrolling to:", target);
|
|
2374
|
+
const result = await this.executor.scroll(target);
|
|
2375
|
+
return this.createResult(command, startTime, result.success, result.error);
|
|
2376
|
+
}
|
|
2377
|
+
async executeServerAction(command, startTime) {
|
|
2378
|
+
if (!this.serverActionHandler) {
|
|
2379
|
+
return this.createResult(command, startTime, false, "No server action handler set");
|
|
2380
|
+
}
|
|
2381
|
+
const action = this.registry.findServerActionById(command.actionId);
|
|
2382
|
+
if (!action) {
|
|
2383
|
+
return this.createResult(command, startTime, false, `Server action not found: ${command.actionId}`);
|
|
2384
|
+
}
|
|
2385
|
+
this.log("Executing server action:", command.actionId);
|
|
2386
|
+
try {
|
|
2387
|
+
const result = await this.serverActionHandler(command.actionId, command.params);
|
|
2388
|
+
return this.createResult(command, startTime, true, void 0, result);
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
return this.createResult(command, startTime, false, String(error));
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async executeReportResult(command, startTime) {
|
|
2394
|
+
this.log("Report result command executed");
|
|
2395
|
+
return this.createResult(command, startTime, true);
|
|
2396
|
+
}
|
|
2397
|
+
// ============================================================================
|
|
2398
|
+
// Utilities
|
|
2399
|
+
// ============================================================================
|
|
2400
|
+
createResult(command, startTime, success, error, result) {
|
|
2401
|
+
return {
|
|
2402
|
+
command,
|
|
2403
|
+
success,
|
|
2404
|
+
result,
|
|
2405
|
+
error,
|
|
2406
|
+
duration: Date.now() - startTime
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Build a simple, human-readable description for the current command.
|
|
2411
|
+
* This is used by the widget to surface clear progress messages.
|
|
2412
|
+
*/
|
|
2413
|
+
describeCommand(command) {
|
|
2414
|
+
switch (command.type) {
|
|
2415
|
+
case "click":
|
|
2416
|
+
return command.text ? `Click "${command.text}"` : `Click element${command.selector ? ` (${command.selector})` : ""}`;
|
|
2417
|
+
case "type":
|
|
2418
|
+
return command.text ? `Type into "${command.text}"` : `Type into element${command.selector ? ` (${command.selector})` : ""}`;
|
|
2419
|
+
case "select":
|
|
2420
|
+
return command.text ? `Select "${command.value}" in "${command.text}"` : `Select "${command.value}"${command.selector ? ` (${command.selector})` : ""}`;
|
|
2421
|
+
case "scroll":
|
|
2422
|
+
if (command.selector) return `Scroll to element (${command.selector})`;
|
|
2423
|
+
if (command.position) return `Scroll to (${command.position.x}, ${command.position.y})`;
|
|
2424
|
+
return "Scroll";
|
|
2425
|
+
case "navigate":
|
|
2426
|
+
return `Navigate to "${command.target}"`;
|
|
2427
|
+
case "wait_for_route":
|
|
2428
|
+
return `Wait for route "${command.target}"`;
|
|
2429
|
+
case "wait_for_dom_stable":
|
|
2430
|
+
return "Wait for page to finish loading";
|
|
2431
|
+
case "scan_context":
|
|
2432
|
+
return "Scan page for available actions";
|
|
2433
|
+
case "server_action":
|
|
2434
|
+
return `Execute server action "${command.actionId}"`;
|
|
2435
|
+
case "report_result":
|
|
2436
|
+
return "Report result to server";
|
|
2437
|
+
default:
|
|
2438
|
+
return "Execute action";
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Subscribe to events
|
|
2443
|
+
*/
|
|
2444
|
+
on(event, callback) {
|
|
2445
|
+
if (!this.listeners.has(event)) {
|
|
2446
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
2447
|
+
}
|
|
2448
|
+
this.listeners.get(event).add(callback);
|
|
2449
|
+
return () => {
|
|
2450
|
+
this.listeners.get(event)?.delete(callback);
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
emit(event, ...args) {
|
|
2454
|
+
this.listeners.get(event)?.forEach((callback) => {
|
|
2455
|
+
try {
|
|
2456
|
+
callback(...args);
|
|
2457
|
+
} catch (error) {
|
|
2458
|
+
this.logger.error(`Error in ${event} listener`, error);
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
log(...args) {
|
|
2463
|
+
this.logger.debug(...args);
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
var ServerActionBridge = class {
|
|
2467
|
+
config;
|
|
2468
|
+
actions = /* @__PURE__ */ new Map();
|
|
2469
|
+
context = null;
|
|
2470
|
+
logger = createLogger("SDK.ServerAction");
|
|
2471
|
+
constructor(config = {}) {
|
|
2472
|
+
this.config = {
|
|
2473
|
+
defaultTimeout: config.defaultTimeout ?? 3e4,
|
|
2474
|
+
debug: config.debug ?? false,
|
|
2475
|
+
webhookHeaders: config.webhookHeaders ?? {},
|
|
2476
|
+
baseUrl: config.baseUrl ?? ""
|
|
2477
|
+
};
|
|
2478
|
+
this.logger = createLogger("SDK.ServerAction", { enabled: this.config.debug, level: "debug" });
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Set the execution context
|
|
2482
|
+
*/
|
|
2483
|
+
setContext(context) {
|
|
2484
|
+
this.context = context;
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Register a server action
|
|
2488
|
+
*/
|
|
2489
|
+
register(action) {
|
|
2490
|
+
this.actions.set(action.id, action);
|
|
2491
|
+
this.log(`Registered server action: ${action.id}`);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Register multiple server actions
|
|
2495
|
+
*/
|
|
2496
|
+
registerMany(actions) {
|
|
2497
|
+
for (const action of actions) {
|
|
2498
|
+
this.register(action);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Unregister a server action
|
|
2503
|
+
*/
|
|
2504
|
+
unregister(actionId) {
|
|
2505
|
+
this.actions.delete(actionId);
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Get all registered actions
|
|
2509
|
+
*/
|
|
2510
|
+
getActions() {
|
|
2511
|
+
return Array.from(this.actions.values());
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Get action by ID
|
|
2515
|
+
*/
|
|
2516
|
+
getAction(actionId) {
|
|
2517
|
+
return this.actions.get(actionId);
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Execute a server action
|
|
2521
|
+
*/
|
|
2522
|
+
async execute(actionId, params) {
|
|
2523
|
+
const action = this.actions.get(actionId);
|
|
2524
|
+
if (!action) {
|
|
2525
|
+
return {
|
|
2526
|
+
success: false,
|
|
2527
|
+
error: `Server action not found: ${actionId}`
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
if (!this.context) {
|
|
2531
|
+
return {
|
|
2532
|
+
success: false,
|
|
2533
|
+
error: "No execution context set"
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
const missingParams = action.parameters.filter((p) => p.required && !(p.name in params)).map((p) => p.name);
|
|
2537
|
+
if (missingParams.length > 0) {
|
|
2538
|
+
return {
|
|
2539
|
+
success: false,
|
|
2540
|
+
error: `Missing required parameters: ${missingParams.join(", ")}`
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
const finalParams = { ...params };
|
|
2544
|
+
for (const param of action.parameters) {
|
|
2545
|
+
if (!(param.name in finalParams) && param.defaultValue !== void 0) {
|
|
2546
|
+
finalParams[param.name] = param.defaultValue;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
this.log(`Executing server action: ${actionId}`, finalParams);
|
|
2550
|
+
try {
|
|
2551
|
+
if (action.handler) {
|
|
2552
|
+
return await this.executeHandler(action, finalParams);
|
|
2553
|
+
} else if (action.webhookUrl) {
|
|
2554
|
+
return await this.executeWebhook(action, finalParams);
|
|
2555
|
+
} else {
|
|
2556
|
+
return {
|
|
2557
|
+
success: false,
|
|
2558
|
+
error: "Server action has no handler or webhookUrl"
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2563
|
+
this.log(`Server action error: ${errorMessage}`);
|
|
2564
|
+
return {
|
|
2565
|
+
success: false,
|
|
2566
|
+
error: errorMessage
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Execute handler mode action
|
|
2572
|
+
*/
|
|
2573
|
+
async executeHandler(action, params) {
|
|
2574
|
+
if (!action.handler) {
|
|
2575
|
+
return { success: false, error: "No handler defined" };
|
|
2576
|
+
}
|
|
2577
|
+
const timeout = action.timeout ?? this.config.defaultTimeout;
|
|
2578
|
+
const result = await Promise.race([
|
|
2579
|
+
action.handler(params, this.context),
|
|
2580
|
+
new Promise(
|
|
2581
|
+
(_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)
|
|
2582
|
+
)
|
|
2583
|
+
]);
|
|
2584
|
+
return result;
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Execute webhook mode action
|
|
2588
|
+
*/
|
|
2589
|
+
async executeWebhook(action, params) {
|
|
2590
|
+
if (!action.webhookUrl) {
|
|
2591
|
+
return { success: false, error: "No webhookUrl defined" };
|
|
2592
|
+
}
|
|
2593
|
+
const timeout = action.timeout ?? this.config.defaultTimeout;
|
|
2594
|
+
const url = action.webhookUrl.startsWith("http") ? action.webhookUrl : `${this.config.baseUrl}${action.webhookUrl}`;
|
|
2595
|
+
const headers = {
|
|
2596
|
+
"Content-Type": "application/json",
|
|
2597
|
+
...this.config.webhookHeaders
|
|
2598
|
+
};
|
|
2599
|
+
if (action.webhookSecret) {
|
|
2600
|
+
const signature = await this.generateSignature(
|
|
2601
|
+
JSON.stringify(params),
|
|
2602
|
+
action.webhookSecret
|
|
2603
|
+
);
|
|
2604
|
+
headers["X-Signature"] = signature;
|
|
2605
|
+
}
|
|
2606
|
+
if (this.context) {
|
|
2607
|
+
headers["X-Session-Id"] = this.context.sessionId;
|
|
2608
|
+
if (this.context.authToken) {
|
|
2609
|
+
headers["Authorization"] = `Bearer ${this.context.authToken}`;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
const controller = new AbortController();
|
|
2613
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2614
|
+
try {
|
|
2615
|
+
const response = await fetch(url, {
|
|
2616
|
+
method: "POST",
|
|
2617
|
+
headers,
|
|
2618
|
+
body: JSON.stringify({
|
|
2619
|
+
actionId: action.id,
|
|
2620
|
+
params,
|
|
2621
|
+
context: {
|
|
2622
|
+
sessionId: this.context?.sessionId,
|
|
2623
|
+
userId: this.context?.userId,
|
|
2624
|
+
currentRoute: this.context?.currentRoute
|
|
2625
|
+
}
|
|
2626
|
+
}),
|
|
2627
|
+
signal: controller.signal
|
|
2628
|
+
});
|
|
2629
|
+
clearTimeout(timeoutId);
|
|
2630
|
+
if (!response.ok) {
|
|
2631
|
+
this.logger.warn("Webhook failed", { actionId: action.id, status: response.status });
|
|
2632
|
+
return {
|
|
2633
|
+
success: false,
|
|
2634
|
+
error: `Webhook failed with status ${response.status}`
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
const data = await response.json();
|
|
2638
|
+
this.logger.debug("Webhook success", { actionId: action.id });
|
|
2639
|
+
return {
|
|
2640
|
+
success: true,
|
|
2641
|
+
data
|
|
2642
|
+
};
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
clearTimeout(timeoutId);
|
|
2645
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2646
|
+
return { success: false, error: "Request timeout" };
|
|
2647
|
+
}
|
|
2648
|
+
throw error;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Generate HMAC signature for webhook
|
|
2653
|
+
*/
|
|
2654
|
+
async generateSignature(payload, secret) {
|
|
2655
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
2656
|
+
const encoder = new TextEncoder();
|
|
2657
|
+
const key = await crypto.subtle.importKey(
|
|
2658
|
+
"raw",
|
|
2659
|
+
encoder.encode(secret),
|
|
2660
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2661
|
+
false,
|
|
2662
|
+
["sign"]
|
|
2663
|
+
);
|
|
2664
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
2665
|
+
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
2666
|
+
}
|
|
2667
|
+
return "";
|
|
2668
|
+
}
|
|
2669
|
+
log(...args) {
|
|
2670
|
+
this.logger.debug(...args);
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
var storageLogger = createLogger("SDK.Storage", { enabled: true, level: "warn" });
|
|
2674
|
+
var persistDebounceTimer = null;
|
|
2675
|
+
function persistMessages(messages, storagePrefix = "chatbot") {
|
|
2676
|
+
if (typeof window !== "undefined") {
|
|
2677
|
+
if (persistDebounceTimer) clearTimeout(persistDebounceTimer);
|
|
2678
|
+
persistDebounceTimer = setTimeout(() => {
|
|
2679
|
+
try {
|
|
2680
|
+
const messagesToSave = messages.slice(-100);
|
|
2681
|
+
localStorage.setItem(`${storagePrefix}-messages`, JSON.stringify(messagesToSave));
|
|
2682
|
+
} catch (e) {
|
|
2683
|
+
storageLogger.warn("Failed to save messages to localStorage", e);
|
|
2684
|
+
try {
|
|
2685
|
+
const messagesToSave = messages.slice(-50);
|
|
2686
|
+
localStorage.setItem(`${storagePrefix}-messages`, JSON.stringify(messagesToSave));
|
|
2687
|
+
} catch (e2) {
|
|
2688
|
+
storageLogger.warn("Failed to save even reduced messages", e2);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}, 500);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
function chatbotReducer(state, action) {
|
|
2695
|
+
let newState;
|
|
2696
|
+
switch (action.type) {
|
|
2697
|
+
case "ADD_MESSAGE": {
|
|
2698
|
+
newState = { ...state, messages: [...state.messages, action.message] };
|
|
2699
|
+
persistMessages(newState.messages);
|
|
2700
|
+
return newState;
|
|
2701
|
+
}
|
|
2702
|
+
case "UPDATE_MESSAGE": {
|
|
2703
|
+
newState = {
|
|
2704
|
+
...state,
|
|
2705
|
+
messages: state.messages.map(
|
|
2706
|
+
(m) => m.id === action.id ? { ...m, ...action.updates } : m
|
|
2707
|
+
)
|
|
2708
|
+
};
|
|
2709
|
+
persistMessages(newState.messages);
|
|
2710
|
+
return newState;
|
|
2711
|
+
}
|
|
2712
|
+
case "SET_MESSAGES": {
|
|
2713
|
+
newState = { ...state, messages: action.messages };
|
|
2714
|
+
persistMessages(newState.messages);
|
|
2715
|
+
return newState;
|
|
2716
|
+
}
|
|
2717
|
+
case "CLEAR_MESSAGES": {
|
|
2718
|
+
newState = { ...state, messages: [] };
|
|
2719
|
+
persistMessages([]);
|
|
2720
|
+
return newState;
|
|
2721
|
+
}
|
|
2722
|
+
case "SET_MODE":
|
|
2723
|
+
return { ...state, mode: action.mode };
|
|
2724
|
+
case "SET_CONNECTION_STATE":
|
|
2725
|
+
return { ...state, connectionState: action.state };
|
|
2726
|
+
case "SET_EXECUTING":
|
|
2727
|
+
return { ...state, isExecuting: action.isExecuting };
|
|
2728
|
+
case "SET_EXECUTION_STATE":
|
|
2729
|
+
return { ...state, executionState: action.state };
|
|
2730
|
+
case "SET_ERROR":
|
|
2731
|
+
return { ...state, error: action.error };
|
|
2732
|
+
case "CLEAR_ERROR":
|
|
2733
|
+
return { ...state, error: null };
|
|
2734
|
+
default:
|
|
2735
|
+
return state;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function getInitialState(storagePrefix = "chatbot") {
|
|
2739
|
+
let messages = [];
|
|
2740
|
+
let mode = "ask";
|
|
2741
|
+
if (typeof window !== "undefined") {
|
|
2742
|
+
try {
|
|
2743
|
+
const saved = localStorage.getItem(`${storagePrefix}-messages`);
|
|
2744
|
+
if (saved) {
|
|
2745
|
+
messages = JSON.parse(saved);
|
|
2746
|
+
}
|
|
2747
|
+
} catch (e) {
|
|
2748
|
+
storageLogger.warn("Failed to load messages from localStorage", e);
|
|
2749
|
+
}
|
|
2750
|
+
const savedMode = localStorage.getItem(`${storagePrefix}-mode`);
|
|
2751
|
+
if (savedMode === "ask" || savedMode === "navigate") {
|
|
2752
|
+
mode = savedMode;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return {
|
|
2756
|
+
messages,
|
|
2757
|
+
mode,
|
|
2758
|
+
connectionState: "disconnected",
|
|
2759
|
+
isExecuting: false,
|
|
2760
|
+
executionState: null,
|
|
2761
|
+
error: null
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
function ChatbotProvider({
|
|
2765
|
+
apiKey,
|
|
2766
|
+
serverUrl,
|
|
2767
|
+
navigationAdapter,
|
|
2768
|
+
serverActions = [],
|
|
2769
|
+
config: configFromApi,
|
|
2770
|
+
options,
|
|
2771
|
+
routes,
|
|
2772
|
+
debug = false,
|
|
2773
|
+
autoConnect = true,
|
|
2774
|
+
children
|
|
2775
|
+
}) {
|
|
2776
|
+
const storagePrefix = useMemo(() => `chatbot-${apiKey.slice(0, 8)}`, [apiKey]);
|
|
2777
|
+
const logger = useMemo(() => createLogger("SDK", { enabled: debug, level: "debug" }), [debug]);
|
|
2778
|
+
const normalizedFromApi = useMemo(() => normalizeWidgetConfig(configFromApi ?? void 0), [configFromApi]);
|
|
2779
|
+
const widgetConfig = useMemo(() => {
|
|
2780
|
+
const base = { ...normalizedFromApi };
|
|
2781
|
+
if (options?.theme) {
|
|
2782
|
+
base.theme = { ...base.theme, ...options.theme };
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
...base,
|
|
2786
|
+
...options,
|
|
2787
|
+
theme: base.theme
|
|
2788
|
+
};
|
|
2789
|
+
}, [normalizedFromApi, options]);
|
|
2790
|
+
const [state, dispatch] = useReducer(chatbotReducer, storagePrefix, getInitialState);
|
|
2791
|
+
const messagesRef = useRef(state.messages);
|
|
2792
|
+
const [isWidgetOpen, setWidgetOpen] = useState(() => {
|
|
2793
|
+
if (typeof window !== "undefined") {
|
|
2794
|
+
const saved = localStorage.getItem(`${storagePrefix}-widget-open`);
|
|
2795
|
+
if (saved !== null) {
|
|
2796
|
+
return saved === "true";
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return options?.defaultOpen ?? false;
|
|
2800
|
+
});
|
|
2801
|
+
const [selectedVoiceLanguage, setSelectedVoiceLanguage] = useState(null);
|
|
2802
|
+
const effectiveVoiceLanguage = selectedVoiceLanguage ?? widgetConfig?.voiceLanguage ?? void 0;
|
|
2803
|
+
const defaultModeApplied = useRef(false);
|
|
2804
|
+
useEffect(() => {
|
|
2805
|
+
if (defaultModeApplied.current) return;
|
|
2806
|
+
const mode = widgetConfig.defaultMode;
|
|
2807
|
+
if (mode === "ask" || mode === "navigate") {
|
|
2808
|
+
dispatch({ type: "SET_MODE", mode });
|
|
2809
|
+
defaultModeApplied.current = true;
|
|
2810
|
+
}
|
|
2811
|
+
}, [widgetConfig.defaultMode]);
|
|
2812
|
+
useEffect(() => {
|
|
2813
|
+
if (typeof window !== "undefined") {
|
|
2814
|
+
localStorage.setItem(`${storagePrefix}-widget-open`, String(isWidgetOpen));
|
|
2815
|
+
}
|
|
2816
|
+
}, [isWidgetOpen, storagePrefix]);
|
|
2817
|
+
useEffect(() => {
|
|
2818
|
+
messagesRef.current = state.messages;
|
|
2819
|
+
}, [state.messages]);
|
|
2820
|
+
useEffect(() => {
|
|
2821
|
+
if (typeof window !== "undefined") {
|
|
2822
|
+
localStorage.setItem(`${storagePrefix}-mode`, state.mode);
|
|
2823
|
+
}
|
|
2824
|
+
}, [state.mode, storagePrefix]);
|
|
2825
|
+
const wsClientRef = useRef(null);
|
|
2826
|
+
const navControllerRef = useRef(null);
|
|
2827
|
+
const serverActionsRef = useRef(/* @__PURE__ */ new Map());
|
|
2828
|
+
const pageContextRef = useRef(null);
|
|
2829
|
+
const scannerRef = useRef(null);
|
|
2830
|
+
const registryRef = useRef(null);
|
|
2831
|
+
const contextBuilderRef = useRef(null);
|
|
2832
|
+
const executorRef = useRef(null);
|
|
2833
|
+
const commandProcessorRef = useRef(null);
|
|
2834
|
+
const serverActionBridgeRef = useRef(null);
|
|
2835
|
+
const routesRef = useRef(void 0);
|
|
2836
|
+
useEffect(() => {
|
|
2837
|
+
routesRef.current = routes;
|
|
2838
|
+
}, [routes]);
|
|
2839
|
+
const adapter = useMemo(() => {
|
|
2840
|
+
return navigationAdapter ?? createMemoryAdapter();
|
|
2841
|
+
}, [navigationAdapter]);
|
|
2842
|
+
useEffect(() => {
|
|
2843
|
+
const client = new WebSocketClient({
|
|
2844
|
+
serverUrl,
|
|
2845
|
+
apiKey,
|
|
2846
|
+
debug
|
|
2847
|
+
});
|
|
2848
|
+
wsClientRef.current = client;
|
|
2849
|
+
client.on("connected", ({ sessionId }) => {
|
|
2850
|
+
log("Connected, session:", sessionId);
|
|
2851
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "connected" });
|
|
2852
|
+
});
|
|
2853
|
+
client.on("authenticated", ({ sessionId }) => {
|
|
2854
|
+
const bridge = serverActionBridgeRef.current;
|
|
2855
|
+
const currentRoute = adapter.getCurrentPath();
|
|
2856
|
+
if (bridge) {
|
|
2857
|
+
bridge.setContext({
|
|
2858
|
+
sessionId,
|
|
2859
|
+
currentRoute
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
client.on("disconnected", () => {
|
|
2864
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "disconnected" });
|
|
2865
|
+
});
|
|
2866
|
+
client.on("reconnecting", ({ attempt, maxAttempts }) => {
|
|
2867
|
+
log(`Reconnecting ${attempt}/${maxAttempts}`);
|
|
2868
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "reconnecting" });
|
|
2869
|
+
});
|
|
2870
|
+
client.on("reconnect_failed", () => {
|
|
2871
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "failed" });
|
|
2872
|
+
dispatch({ type: "SET_ERROR", error: new Error("Connection failed") });
|
|
2873
|
+
});
|
|
2874
|
+
client.on("error", ({ error }) => {
|
|
2875
|
+
dispatch({ type: "SET_ERROR", error });
|
|
2876
|
+
});
|
|
2877
|
+
client.on("message", (message) => {
|
|
2878
|
+
handleServerMessage(message);
|
|
2879
|
+
});
|
|
2880
|
+
if (autoConnect) {
|
|
2881
|
+
client.connect();
|
|
2882
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "connecting" });
|
|
2883
|
+
}
|
|
2884
|
+
return () => {
|
|
2885
|
+
client.disconnect();
|
|
2886
|
+
wsClientRef.current = null;
|
|
2887
|
+
};
|
|
2888
|
+
}, [serverUrl, apiKey, debug, autoConnect]);
|
|
2889
|
+
useEffect(() => {
|
|
2890
|
+
const registry = new ActionRegistry({ debug });
|
|
2891
|
+
const scanner = new DOMScanner({ debug });
|
|
2892
|
+
const builder = new PageContextBuilder({ debug });
|
|
2893
|
+
const executor = new DOMExecutor({ debug });
|
|
2894
|
+
const serverBridge = new ServerActionBridge({ debug });
|
|
2895
|
+
registryRef.current = registry;
|
|
2896
|
+
scannerRef.current = scanner;
|
|
2897
|
+
contextBuilderRef.current = builder;
|
|
2898
|
+
executorRef.current = executor;
|
|
2899
|
+
serverActionBridgeRef.current = serverBridge;
|
|
2900
|
+
const sendContext = (context) => {
|
|
2901
|
+
pageContextRef.current = context;
|
|
2902
|
+
wsClientRef.current?.send({ type: "context", context });
|
|
2903
|
+
};
|
|
2904
|
+
const updateBridgeContext = (route) => {
|
|
2905
|
+
serverBridge.setContext({
|
|
2906
|
+
sessionId: wsClientRef.current?.getSessionId() ?? "",
|
|
2907
|
+
currentRoute: route
|
|
2908
|
+
});
|
|
2909
|
+
};
|
|
2910
|
+
const runFullScan = () => {
|
|
2911
|
+
const nav2 = navControllerRef.current;
|
|
2912
|
+
const route = nav2 ? nav2.getCurrentRoute() : typeof window !== "undefined" ? window.location.pathname : "/";
|
|
2913
|
+
const actions = scanner.scan();
|
|
2914
|
+
registry.registerDiscoveredActions(route, actions);
|
|
2915
|
+
const context = {
|
|
2916
|
+
...builder.build(actions, registry.getServerActions(), route),
|
|
2917
|
+
...routes?.length ? { routes } : {}
|
|
2918
|
+
};
|
|
2919
|
+
updateBridgeContext(context.route);
|
|
2920
|
+
sendContext(context);
|
|
2921
|
+
return context;
|
|
2922
|
+
};
|
|
2923
|
+
const nav = new NavigationController(adapter, {
|
|
2924
|
+
debug,
|
|
2925
|
+
onDOMStable: () => runFullScan()
|
|
2926
|
+
});
|
|
2927
|
+
navControllerRef.current = nav;
|
|
2928
|
+
nav.setScanner(async () => runFullScan());
|
|
2929
|
+
const processor = new CommandProcessor(executor, nav, registry, { debug });
|
|
2930
|
+
commandProcessorRef.current = processor;
|
|
2931
|
+
processor.setServerActionHandler(async (actionId, params) => {
|
|
2932
|
+
return serverBridge.execute(actionId, params);
|
|
2933
|
+
});
|
|
2934
|
+
const processorUnsubscribes = [];
|
|
2935
|
+
if (debug) {
|
|
2936
|
+
processorUnsubscribes.push(
|
|
2937
|
+
processor.on("commandStart", (command, index) => {
|
|
2938
|
+
log("Command start:", { index, type: command.type, command });
|
|
2939
|
+
})
|
|
2940
|
+
);
|
|
2941
|
+
processorUnsubscribes.push(
|
|
2942
|
+
processor.on("commandComplete", (result) => {
|
|
2943
|
+
log("Command complete:", {
|
|
2944
|
+
type: result.command.type,
|
|
2945
|
+
success: result.success,
|
|
2946
|
+
duration: result.duration,
|
|
2947
|
+
error: result.error
|
|
2948
|
+
});
|
|
2949
|
+
})
|
|
2950
|
+
);
|
|
2951
|
+
processorUnsubscribes.push(
|
|
2952
|
+
processor.on("complete", (results) => {
|
|
2953
|
+
log("Command batch complete:", {
|
|
2954
|
+
total: results.length,
|
|
2955
|
+
failed: results.filter((r) => !r.success).length
|
|
2956
|
+
});
|
|
2957
|
+
})
|
|
2958
|
+
);
|
|
2959
|
+
processorUnsubscribes.push(
|
|
2960
|
+
processor.on("error", (err) => {
|
|
2961
|
+
log("Command processor error:", err);
|
|
2962
|
+
})
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
serverActions.forEach((action) => {
|
|
2966
|
+
serverActionsRef.current.set(action.id, action);
|
|
2967
|
+
registry.registerServerAction(action);
|
|
2968
|
+
serverBridge.register(action);
|
|
2969
|
+
});
|
|
2970
|
+
scanner.observe((actions) => {
|
|
2971
|
+
registry.registerDiscoveredActions(nav.getCurrentRoute(), actions);
|
|
2972
|
+
const context = {
|
|
2973
|
+
...builder.build(actions, registry.getServerActions(), nav.getCurrentRoute()),
|
|
2974
|
+
...routes?.length ? { routes } : {}
|
|
2975
|
+
};
|
|
2976
|
+
updateBridgeContext(context.route);
|
|
2977
|
+
sendContext(context);
|
|
2978
|
+
});
|
|
2979
|
+
const unsubscribeRoute = nav.onRouteChange(() => {
|
|
2980
|
+
updateBridgeContext(nav.getCurrentRoute());
|
|
2981
|
+
runFullScan();
|
|
2982
|
+
});
|
|
2983
|
+
if (typeof window !== "undefined") {
|
|
2984
|
+
if (document.readyState === "complete") {
|
|
2985
|
+
updateBridgeContext(nav.getCurrentRoute());
|
|
2986
|
+
runFullScan();
|
|
2987
|
+
} else {
|
|
2988
|
+
window.addEventListener("load", () => {
|
|
2989
|
+
updateBridgeContext(nav.getCurrentRoute());
|
|
2990
|
+
runFullScan();
|
|
2991
|
+
}, { once: true });
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return () => {
|
|
2995
|
+
scanner.disconnect();
|
|
2996
|
+
processor.abort();
|
|
2997
|
+
processorUnsubscribes.forEach((fn) => fn());
|
|
2998
|
+
unsubscribeRoute();
|
|
2999
|
+
};
|
|
3000
|
+
}, [adapter, debug, routes]);
|
|
3001
|
+
useEffect(() => {
|
|
3002
|
+
const registry = registryRef.current;
|
|
3003
|
+
const bridge = serverActionBridgeRef.current;
|
|
3004
|
+
if (!registry || !bridge) return;
|
|
3005
|
+
serverActions.forEach((action) => {
|
|
3006
|
+
serverActionsRef.current.set(action.id, action);
|
|
3007
|
+
registry.registerServerAction(action);
|
|
3008
|
+
bridge.register(action);
|
|
3009
|
+
});
|
|
3010
|
+
}, [serverActions]);
|
|
3011
|
+
const executeCommandBatch = useCallback(async (commands, batchId) => {
|
|
3012
|
+
const processor = commandProcessorRef.current;
|
|
3013
|
+
if (!processor) {
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
dispatch({ type: "SET_EXECUTING", isExecuting: true });
|
|
3017
|
+
const unsubscribes = [];
|
|
3018
|
+
unsubscribes.push(
|
|
3019
|
+
processor.on("stateChange", (execState) => {
|
|
3020
|
+
dispatch({ type: "SET_EXECUTION_STATE", state: execState });
|
|
3021
|
+
dispatch({ type: "SET_EXECUTING", isExecuting: execState.isExecuting });
|
|
3022
|
+
})
|
|
3023
|
+
);
|
|
3024
|
+
unsubscribes.push(
|
|
3025
|
+
processor.on("error", (err) => {
|
|
3026
|
+
dispatch({ type: "SET_ERROR", error: err });
|
|
3027
|
+
})
|
|
3028
|
+
);
|
|
3029
|
+
try {
|
|
3030
|
+
const results = await processor.execute(commands);
|
|
3031
|
+
const ws = wsClientRef.current;
|
|
3032
|
+
results.forEach((result) => {
|
|
3033
|
+
if (!ws) return;
|
|
3034
|
+
if (result.command.type === "server_action") {
|
|
3035
|
+
ws.send({
|
|
3036
|
+
type: "action_result",
|
|
3037
|
+
actionId: result.command.actionId,
|
|
3038
|
+
success: result.success,
|
|
3039
|
+
result: result.result,
|
|
3040
|
+
error: result.error,
|
|
3041
|
+
commandId: batchId
|
|
3042
|
+
});
|
|
3043
|
+
} else {
|
|
3044
|
+
ws.send({
|
|
3045
|
+
type: "action_result",
|
|
3046
|
+
actionId: result.command.type,
|
|
3047
|
+
success: result.success,
|
|
3048
|
+
error: result.error,
|
|
3049
|
+
commandId: batchId
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
});
|
|
3053
|
+
if (scannerRef.current && registryRef.current && contextBuilderRef.current && navControllerRef.current) {
|
|
3054
|
+
const actions = scannerRef.current.scan();
|
|
3055
|
+
const route = navControllerRef.current.getCurrentRoute();
|
|
3056
|
+
registryRef.current.registerDiscoveredActions(route, actions);
|
|
3057
|
+
const built = contextBuilderRef.current.build(actions, registryRef.current.getServerActions(), route);
|
|
3058
|
+
const context = {
|
|
3059
|
+
...built,
|
|
3060
|
+
...routesRef.current?.length ? { routes: routesRef.current } : {}
|
|
3061
|
+
};
|
|
3062
|
+
pageContextRef.current = context;
|
|
3063
|
+
if (wsClientRef.current) {
|
|
3064
|
+
wsClientRef.current.send({ type: "context", context });
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3069
|
+
dispatch({ type: "SET_ERROR", error: err });
|
|
3070
|
+
const ws = wsClientRef.current;
|
|
3071
|
+
const errorMessage = err.message || "Execution failed";
|
|
3072
|
+
commands.forEach((cmd) => {
|
|
3073
|
+
if (!ws) return;
|
|
3074
|
+
const actionId = cmd.type === "server_action" ? cmd.actionId : cmd.type;
|
|
3075
|
+
ws.send({
|
|
3076
|
+
type: "action_result",
|
|
3077
|
+
actionId,
|
|
3078
|
+
success: false,
|
|
3079
|
+
error: errorMessage,
|
|
3080
|
+
commandId: batchId
|
|
3081
|
+
});
|
|
3082
|
+
});
|
|
3083
|
+
} finally {
|
|
3084
|
+
unsubscribes.forEach((fn) => fn());
|
|
3085
|
+
dispatch({ type: "SET_EXECUTING", isExecuting: false });
|
|
3086
|
+
dispatch({ type: "SET_EXECUTION_STATE", state: null });
|
|
3087
|
+
}
|
|
3088
|
+
}, []);
|
|
3089
|
+
const handleServerMessage = useCallback((message) => {
|
|
3090
|
+
log("Server message received:", message.type, message);
|
|
3091
|
+
switch (message.type) {
|
|
3092
|
+
case "response":
|
|
3093
|
+
{
|
|
3094
|
+
const existing = messagesRef.current.find((m) => m.id === message.messageId);
|
|
3095
|
+
if (existing && existing.role === "assistant") {
|
|
3096
|
+
dispatch({
|
|
3097
|
+
type: "UPDATE_MESSAGE",
|
|
3098
|
+
id: existing.id,
|
|
3099
|
+
updates: {
|
|
3100
|
+
content: message.message,
|
|
3101
|
+
timestamp: Date.now(),
|
|
3102
|
+
status: "sent"
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
} else {
|
|
3106
|
+
dispatch({
|
|
3107
|
+
type: "ADD_MESSAGE",
|
|
3108
|
+
message: {
|
|
3109
|
+
id: existing ? createMessageId() : message.messageId,
|
|
3110
|
+
role: "assistant",
|
|
3111
|
+
content: message.message,
|
|
3112
|
+
timestamp: Date.now(),
|
|
3113
|
+
status: "sent"
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
break;
|
|
3119
|
+
case "command": {
|
|
3120
|
+
log("Received commands:", message.commands);
|
|
3121
|
+
const commands = Array.isArray(message.commands) ? message.commands : [];
|
|
3122
|
+
const batchId = message.batchId ?? createMessageId();
|
|
3123
|
+
if (message.message) {
|
|
3124
|
+
dispatch({
|
|
3125
|
+
type: "ADD_MESSAGE",
|
|
3126
|
+
message: {
|
|
3127
|
+
id: createMessageId(),
|
|
3128
|
+
role: "assistant",
|
|
3129
|
+
content: message.message,
|
|
3130
|
+
timestamp: Date.now(),
|
|
3131
|
+
commands: commands.length > 0 ? commands : void 0
|
|
3132
|
+
}
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
if (commands.length === 0) {
|
|
3136
|
+
break;
|
|
3137
|
+
}
|
|
3138
|
+
const processor = commandProcessorRef.current;
|
|
3139
|
+
if (!processor) {
|
|
3140
|
+
dispatch({ type: "SET_ERROR", error: new Error("Action runner not ready") });
|
|
3141
|
+
commands.forEach((cmd) => {
|
|
3142
|
+
const actionId = cmd.type === "server_action" ? cmd.actionId : cmd.type;
|
|
3143
|
+
if (wsClientRef.current) {
|
|
3144
|
+
wsClientRef.current.send({
|
|
3145
|
+
type: "action_result",
|
|
3146
|
+
actionId,
|
|
3147
|
+
success: false,
|
|
3148
|
+
error: "Action runner not ready",
|
|
3149
|
+
commandId: batchId
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
});
|
|
3153
|
+
break;
|
|
3154
|
+
}
|
|
3155
|
+
executeCommandBatch(commands, batchId);
|
|
3156
|
+
break;
|
|
3157
|
+
}
|
|
3158
|
+
case "typing":
|
|
3159
|
+
break;
|
|
3160
|
+
case "server_action_result":
|
|
3161
|
+
log("Server action result:", message.actionId, message.result);
|
|
3162
|
+
break;
|
|
3163
|
+
case "error":
|
|
3164
|
+
log("Server error:", message.code, message.error, message.details);
|
|
3165
|
+
dispatch({
|
|
3166
|
+
type: "SET_ERROR",
|
|
3167
|
+
error: new Error(message.error)
|
|
3168
|
+
});
|
|
3169
|
+
break;
|
|
3170
|
+
case "request_context":
|
|
3171
|
+
if (pageContextRef.current && wsClientRef.current) {
|
|
3172
|
+
wsClientRef.current.send({
|
|
3173
|
+
type: "context",
|
|
3174
|
+
context: pageContextRef.current
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
break;
|
|
3178
|
+
}
|
|
3179
|
+
}, [executeCommandBatch]);
|
|
3180
|
+
const sendMessage = useCallback((content) => {
|
|
3181
|
+
const messageId = createMessageId();
|
|
3182
|
+
log("Sending message:", {
|
|
3183
|
+
messageId,
|
|
3184
|
+
mode: state.mode,
|
|
3185
|
+
length: content.length,
|
|
3186
|
+
connectionState: state.connectionState,
|
|
3187
|
+
route: pageContextRef.current?.route ?? adapter.getCurrentPath()
|
|
3188
|
+
});
|
|
3189
|
+
dispatch({
|
|
3190
|
+
type: "ADD_MESSAGE",
|
|
3191
|
+
message: {
|
|
3192
|
+
id: messageId,
|
|
3193
|
+
role: "user",
|
|
3194
|
+
content,
|
|
3195
|
+
timestamp: Date.now(),
|
|
3196
|
+
status: "sending"
|
|
3197
|
+
}
|
|
3198
|
+
});
|
|
3199
|
+
const context = pageContextRef.current ?? {
|
|
3200
|
+
route: adapter.getCurrentPath(),
|
|
3201
|
+
title: typeof document !== "undefined" ? document.title : "",
|
|
3202
|
+
actions: [],
|
|
3203
|
+
serverActions: Array.from(serverActionsRef.current.values()),
|
|
3204
|
+
content: { headings: [], mainContent: "", forms: [] },
|
|
3205
|
+
timestamp: Date.now(),
|
|
3206
|
+
...routesRef.current?.length ? { routes: routesRef.current } : {}
|
|
3207
|
+
};
|
|
3208
|
+
const locale = selectedVoiceLanguage ?? widgetConfig?.voiceLanguage ?? void 0;
|
|
3209
|
+
wsClientRef.current?.send({
|
|
3210
|
+
type: "message",
|
|
3211
|
+
content,
|
|
3212
|
+
context,
|
|
3213
|
+
mode: state.mode,
|
|
3214
|
+
messageId,
|
|
3215
|
+
...locale != null ? { locale } : {}
|
|
3216
|
+
});
|
|
3217
|
+
dispatch({
|
|
3218
|
+
type: "UPDATE_MESSAGE",
|
|
3219
|
+
id: messageId,
|
|
3220
|
+
updates: { status: "sent" }
|
|
3221
|
+
});
|
|
3222
|
+
}, [state.mode, adapter, selectedVoiceLanguage, widgetConfig?.voiceLanguage]);
|
|
3223
|
+
const setVoiceLanguage = useCallback((lang) => {
|
|
3224
|
+
setSelectedVoiceLanguage(lang ?? null);
|
|
3225
|
+
}, []);
|
|
3226
|
+
const setMode = useCallback((mode) => {
|
|
3227
|
+
dispatch({ type: "SET_MODE", mode });
|
|
3228
|
+
}, []);
|
|
3229
|
+
const executeAction = useCallback(async (actionId, params) => {
|
|
3230
|
+
const bridge = serverActionBridgeRef.current;
|
|
3231
|
+
if (!bridge) {
|
|
3232
|
+
throw new Error("Server action bridge not ready");
|
|
3233
|
+
}
|
|
3234
|
+
if (!bridge.getAction(actionId)) {
|
|
3235
|
+
const action = serverActionsRef.current.get(actionId);
|
|
3236
|
+
if (action) {
|
|
3237
|
+
bridge.register(action);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
const result = await bridge.execute(actionId, params ?? {});
|
|
3241
|
+
if (wsClientRef.current) {
|
|
3242
|
+
wsClientRef.current.send({
|
|
3243
|
+
type: "action_result",
|
|
3244
|
+
actionId,
|
|
3245
|
+
success: result.success,
|
|
3246
|
+
result: result.data,
|
|
3247
|
+
error: result.error
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
return result;
|
|
3251
|
+
}, []);
|
|
3252
|
+
const registerServerAction = useCallback((action) => {
|
|
3253
|
+
serverActionsRef.current.set(action.id, action);
|
|
3254
|
+
registryRef.current?.registerServerAction(action);
|
|
3255
|
+
serverActionBridgeRef.current?.register(action);
|
|
3256
|
+
logger.debug("Registered server action:", action.id);
|
|
3257
|
+
}, [logger]);
|
|
3258
|
+
const clearMessages = useCallback(() => {
|
|
3259
|
+
dispatch({ type: "CLEAR_MESSAGES" });
|
|
3260
|
+
}, []);
|
|
3261
|
+
const clearError = useCallback(() => {
|
|
3262
|
+
dispatch({ type: "CLEAR_ERROR" });
|
|
3263
|
+
}, []);
|
|
3264
|
+
const stopExecution = useCallback(() => {
|
|
3265
|
+
commandProcessorRef.current?.abort();
|
|
3266
|
+
}, []);
|
|
3267
|
+
const connect = useCallback(() => {
|
|
3268
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "connecting" });
|
|
3269
|
+
wsClientRef.current?.connect();
|
|
3270
|
+
}, []);
|
|
3271
|
+
const disconnect = useCallback(() => {
|
|
3272
|
+
wsClientRef.current?.disconnect();
|
|
3273
|
+
dispatch({ type: "SET_CONNECTION_STATE", state: "disconnected" });
|
|
3274
|
+
}, []);
|
|
3275
|
+
function log(...args) {
|
|
3276
|
+
logger.debug(...args);
|
|
3277
|
+
}
|
|
3278
|
+
const contextValue = useMemo(() => ({
|
|
3279
|
+
widgetConfig,
|
|
3280
|
+
// State
|
|
3281
|
+
messages: state.messages,
|
|
3282
|
+
mode: state.mode,
|
|
3283
|
+
connectionState: state.connectionState,
|
|
3284
|
+
isExecuting: state.isExecuting,
|
|
3285
|
+
executionState: state.executionState,
|
|
3286
|
+
error: state.error,
|
|
3287
|
+
// Widget visibility (persists across navigation)
|
|
3288
|
+
isWidgetOpen,
|
|
3289
|
+
setWidgetOpen,
|
|
3290
|
+
voiceLanguage: effectiveVoiceLanguage,
|
|
3291
|
+
setVoiceLanguage,
|
|
3292
|
+
// Actions
|
|
3293
|
+
sendMessage,
|
|
3294
|
+
setMode,
|
|
3295
|
+
executeAction,
|
|
3296
|
+
registerServerAction,
|
|
3297
|
+
clearMessages,
|
|
3298
|
+
clearError,
|
|
3299
|
+
stopExecution,
|
|
3300
|
+
// Connection
|
|
3301
|
+
connect,
|
|
3302
|
+
disconnect,
|
|
3303
|
+
isConnected: state.connectionState === "connected"
|
|
3304
|
+
}), [
|
|
3305
|
+
widgetConfig,
|
|
3306
|
+
state,
|
|
3307
|
+
isWidgetOpen,
|
|
3308
|
+
effectiveVoiceLanguage,
|
|
3309
|
+
setVoiceLanguage,
|
|
3310
|
+
sendMessage,
|
|
3311
|
+
setMode,
|
|
3312
|
+
executeAction,
|
|
3313
|
+
registerServerAction,
|
|
3314
|
+
clearMessages,
|
|
3315
|
+
clearError,
|
|
3316
|
+
stopExecution,
|
|
3317
|
+
connect,
|
|
3318
|
+
disconnect
|
|
3319
|
+
]);
|
|
3320
|
+
return /* @__PURE__ */ jsx(ChatbotContext.Provider, { value: contextValue, children });
|
|
3321
|
+
}
|
|
3322
|
+
function renderInlineWithBold(text) {
|
|
3323
|
+
const parts = [];
|
|
3324
|
+
let key = 0;
|
|
3325
|
+
let remaining = text;
|
|
3326
|
+
while (remaining.length > 0) {
|
|
3327
|
+
const i = remaining.indexOf("**");
|
|
3328
|
+
if (i === -1) {
|
|
3329
|
+
parts.push(/* @__PURE__ */ jsx(React2.Fragment, { children: remaining }, key++));
|
|
3330
|
+
break;
|
|
3331
|
+
}
|
|
3332
|
+
const before = remaining.slice(0, i);
|
|
3333
|
+
const after = remaining.slice(i + 2);
|
|
3334
|
+
const j = after.indexOf("**");
|
|
3335
|
+
if (j === -1) {
|
|
3336
|
+
parts.push(/* @__PURE__ */ jsx(React2.Fragment, { children: remaining }, key++));
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3339
|
+
if (before) parts.push(/* @__PURE__ */ jsx(React2.Fragment, { children: before }, key++));
|
|
3340
|
+
parts.push(/* @__PURE__ */ jsx("strong", { children: after.slice(0, j) }, key++));
|
|
3341
|
+
remaining = after.slice(j + 2);
|
|
3342
|
+
}
|
|
3343
|
+
return parts.length === 1 ? parts[0] : /* @__PURE__ */ jsx(Fragment, { children: parts });
|
|
3344
|
+
}
|
|
3345
|
+
function MessageContent({ content, isUser }) {
|
|
3346
|
+
if (isUser) return /* @__PURE__ */ jsx(Fragment, { children: content });
|
|
3347
|
+
const raw = typeof content === "string" ? content : "";
|
|
3348
|
+
if (!raw.trim()) return /* @__PURE__ */ jsx(Fragment, { children: "\xA0" });
|
|
3349
|
+
const lines = raw.split(/\n/);
|
|
3350
|
+
const nodes = [];
|
|
3351
|
+
let listItems = [];
|
|
3352
|
+
const flushList = () => {
|
|
3353
|
+
if (listItems.length > 0) {
|
|
3354
|
+
nodes.push(
|
|
3355
|
+
/* @__PURE__ */ jsx("ul", { className: "navsi-chatbot-message-list", children: listItems.map((item, i) => /* @__PURE__ */ jsx("li", { className: "navsi-chatbot-message-list-item", children: renderInlineWithBold(item) }, i)) }, nodes.length)
|
|
3356
|
+
);
|
|
3357
|
+
listItems = [];
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3361
|
+
const line = lines[i];
|
|
3362
|
+
const trimmed = line.trim();
|
|
3363
|
+
const bulletMatch = trimmed.match(/^[*-]\s+(.*)$/);
|
|
3364
|
+
if (bulletMatch) {
|
|
3365
|
+
flushList();
|
|
3366
|
+
listItems.push(bulletMatch[1].trim());
|
|
3367
|
+
} else {
|
|
3368
|
+
flushList();
|
|
3369
|
+
if (trimmed) {
|
|
3370
|
+
nodes.push(
|
|
3371
|
+
/* @__PURE__ */ jsx("p", { className: "navsi-chatbot-message-paragraph", children: renderInlineWithBold(trimmed) }, nodes.length)
|
|
3372
|
+
);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
flushList();
|
|
3377
|
+
if (nodes.length === 0) return /* @__PURE__ */ jsx(Fragment, { children: raw });
|
|
3378
|
+
return /* @__PURE__ */ jsx("div", { className: "navsi-chatbot-message-content", children: nodes });
|
|
3379
|
+
}
|
|
3380
|
+
function getSpeechRecognitionConstructor() {
|
|
3381
|
+
if (typeof window === "undefined") return null;
|
|
3382
|
+
const win = window;
|
|
3383
|
+
const recognition = win.SpeechRecognition ?? win.webkitSpeechRecognition;
|
|
3384
|
+
return recognition ?? null;
|
|
3385
|
+
}
|
|
3386
|
+
function isBraveBrowser() {
|
|
3387
|
+
return typeof navigator !== "undefined" && "brave" in navigator;
|
|
3388
|
+
}
|
|
3389
|
+
function describeSpeechError(error) {
|
|
3390
|
+
if (error === "network") {
|
|
3391
|
+
const online = typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
3392
|
+
if (!online) {
|
|
3393
|
+
return "Voice input error: network. You appear to be offline.";
|
|
3394
|
+
}
|
|
3395
|
+
if (isBraveBrowser()) {
|
|
3396
|
+
return "Voice input error: network. Brave may block speech services; disable Shields or try Chrome/Edge.";
|
|
3397
|
+
}
|
|
3398
|
+
const isLocalhost = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
|
3399
|
+
const isSecure = typeof window !== "undefined" && window.location.protocol === "https:";
|
|
3400
|
+
if (!isSecure && !isLocalhost) {
|
|
3401
|
+
return "Voice input error: network. Microphone access requires HTTPS.";
|
|
3402
|
+
}
|
|
3403
|
+
return "Voice input error: network. Check microphone permission and reload the page.";
|
|
3404
|
+
}
|
|
3405
|
+
if (error === "not-allowed" || error === "service-not-allowed") {
|
|
3406
|
+
return "Voice input error: microphone permission blocked.";
|
|
3407
|
+
}
|
|
3408
|
+
if (error === "no-speech") {
|
|
3409
|
+
return "Voice input error: no speech detected.";
|
|
3410
|
+
}
|
|
3411
|
+
if (error === "audio-capture") {
|
|
3412
|
+
return "Voice input error: no microphone found.";
|
|
3413
|
+
}
|
|
3414
|
+
return `Voice input error: ${error}`;
|
|
3415
|
+
}
|
|
3416
|
+
function ChatbotWidget({ className, windowClassName, buttonClassName }) {
|
|
3417
|
+
const [input, setInput] = useState("");
|
|
3418
|
+
const [isListening, setIsListening] = useState(false);
|
|
3419
|
+
const [voiceTranscript, setVoiceTranscript] = useState("");
|
|
3420
|
+
const [voiceError, setVoiceError] = useState(null);
|
|
3421
|
+
const messagesEndRef = useRef(null);
|
|
3422
|
+
const messagesContainerRef = useRef(null);
|
|
3423
|
+
const recognitionRef = useRef(null);
|
|
3424
|
+
const finalTranscriptRef = useRef("");
|
|
3425
|
+
const lastSpokenIdRef = useRef(null);
|
|
3426
|
+
const hasInitializedSpeechRef = useRef(false);
|
|
3427
|
+
const shouldSpeakNextAssistantRef = useRef(false);
|
|
3428
|
+
const { messages, sendMessage, isConnected, mode, setMode, isExecuting, error, clearError, clearMessages, isWidgetOpen, setWidgetOpen, stopExecution, widgetConfig, voiceLanguage, setVoiceLanguage } = useChatbot();
|
|
3429
|
+
const effectiveVoiceLang = voiceLanguage ?? widgetConfig?.voiceLanguage ?? (typeof navigator !== "undefined" ? navigator.language : void 0) ?? "en-US";
|
|
3430
|
+
const { progress } = useActionExecution();
|
|
3431
|
+
const { isReconnecting } = useWebSocket();
|
|
3432
|
+
const speechRecognitionConstructor = useMemo(getSpeechRecognitionConstructor, []);
|
|
3433
|
+
const isVoiceSupported = !!speechRecognitionConstructor;
|
|
3434
|
+
useEffect(() => {
|
|
3435
|
+
if (messages.length > 0 && messagesContainerRef.current && isWidgetOpen) {
|
|
3436
|
+
requestAnimationFrame(() => {
|
|
3437
|
+
if (messagesContainerRef.current) {
|
|
3438
|
+
messagesContainerRef.current.scrollTo({
|
|
3439
|
+
top: messagesContainerRef.current.scrollHeight,
|
|
3440
|
+
behavior: "smooth"
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3445
|
+
}, [messages, isWidgetOpen]);
|
|
3446
|
+
useEffect(() => {
|
|
3447
|
+
if (!hasInitializedSpeechRef.current) {
|
|
3448
|
+
const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant");
|
|
3449
|
+
lastSpokenIdRef.current = lastAssistant?.id ?? null;
|
|
3450
|
+
hasInitializedSpeechRef.current = true;
|
|
3451
|
+
}
|
|
3452
|
+
}, []);
|
|
3453
|
+
useEffect(() => {
|
|
3454
|
+
const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant");
|
|
3455
|
+
if (!lastAssistant) return;
|
|
3456
|
+
if (!hasInitializedSpeechRef.current) return;
|
|
3457
|
+
if (lastAssistant.id === lastSpokenIdRef.current) return;
|
|
3458
|
+
if (isListening) return;
|
|
3459
|
+
if (!shouldSpeakNextAssistantRef.current) return;
|
|
3460
|
+
if (typeof window === "undefined" || !("speechSynthesis" in window)) return;
|
|
3461
|
+
const content = lastAssistant.content?.trim();
|
|
3462
|
+
if (!content) return;
|
|
3463
|
+
const ttsContent = content.replace(/\*\*(.*?)\*\*/g, "$1").replace(/[*_~`]/g, "");
|
|
3464
|
+
const ttsLang = effectiveVoiceLang;
|
|
3465
|
+
window.speechSynthesis.cancel();
|
|
3466
|
+
const utterance = new SpeechSynthesisUtterance(ttsContent);
|
|
3467
|
+
utterance.lang = ttsLang;
|
|
3468
|
+
window.speechSynthesis.speak(utterance);
|
|
3469
|
+
lastSpokenIdRef.current = lastAssistant.id;
|
|
3470
|
+
shouldSpeakNextAssistantRef.current = false;
|
|
3471
|
+
}, [messages, isListening, effectiveVoiceLang]);
|
|
3472
|
+
const handleSend = () => {
|
|
3473
|
+
if (input.trim()) {
|
|
3474
|
+
shouldSpeakNextAssistantRef.current = false;
|
|
3475
|
+
sendMessage(input.trim());
|
|
3476
|
+
setInput("");
|
|
3477
|
+
}
|
|
3478
|
+
};
|
|
3479
|
+
const handleKeyDown = (e) => {
|
|
3480
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
3481
|
+
e.preventDefault();
|
|
3482
|
+
handleSend();
|
|
3483
|
+
}
|
|
3484
|
+
};
|
|
3485
|
+
const startVoiceInput = useCallback(() => {
|
|
3486
|
+
if (isListening) return;
|
|
3487
|
+
if (!speechRecognitionConstructor) {
|
|
3488
|
+
setVoiceError("Voice input is not supported in this browser.");
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
setVoiceError(null);
|
|
3492
|
+
finalTranscriptRef.current = "";
|
|
3493
|
+
const recognition = new speechRecognitionConstructor();
|
|
3494
|
+
recognitionRef.current = recognition;
|
|
3495
|
+
recognition.continuous = false;
|
|
3496
|
+
recognition.interimResults = true;
|
|
3497
|
+
recognition.maxAlternatives = 1;
|
|
3498
|
+
recognition.lang = effectiveVoiceLang;
|
|
3499
|
+
recognition.onstart = () => {
|
|
3500
|
+
setIsListening(true);
|
|
3501
|
+
setVoiceTranscript("");
|
|
3502
|
+
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
|
3503
|
+
window.speechSynthesis.cancel();
|
|
3504
|
+
}
|
|
3505
|
+
};
|
|
3506
|
+
recognition.onresult = (event) => {
|
|
3507
|
+
let interimTranscript = "";
|
|
3508
|
+
let finalTranscript = "";
|
|
3509
|
+
for (let i = event.resultIndex; i < event.results.length; i += 1) {
|
|
3510
|
+
const result = event.results[i];
|
|
3511
|
+
const text = result[0]?.transcript ?? "";
|
|
3512
|
+
if (result.isFinal) {
|
|
3513
|
+
finalTranscript += text;
|
|
3514
|
+
} else {
|
|
3515
|
+
interimTranscript += text;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
if (finalTranscript) {
|
|
3519
|
+
finalTranscriptRef.current = `${finalTranscriptRef.current} ${finalTranscript}`.trim();
|
|
3520
|
+
setVoiceTranscript(finalTranscriptRef.current);
|
|
3521
|
+
} else if (interimTranscript) {
|
|
3522
|
+
setVoiceTranscript(interimTranscript.trim());
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
recognition.onerror = (event) => {
|
|
3526
|
+
setVoiceError(event.error ? describeSpeechError(event.error) : "Voice input error");
|
|
3527
|
+
};
|
|
3528
|
+
recognition.onend = () => {
|
|
3529
|
+
setIsListening(false);
|
|
3530
|
+
const finalText = finalTranscriptRef.current.trim();
|
|
3531
|
+
finalTranscriptRef.current = "";
|
|
3532
|
+
setVoiceTranscript("");
|
|
3533
|
+
if (finalText) {
|
|
3534
|
+
shouldSpeakNextAssistantRef.current = true;
|
|
3535
|
+
sendMessage(finalText);
|
|
3536
|
+
setInput("");
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
recognition.start();
|
|
3540
|
+
}, [isListening, speechRecognitionConstructor, sendMessage, effectiveVoiceLang]);
|
|
3541
|
+
const stopVoiceInput = useCallback(() => {
|
|
3542
|
+
recognitionRef.current?.stop();
|
|
3543
|
+
}, []);
|
|
3544
|
+
useEffect(() => {
|
|
3545
|
+
return () => {
|
|
3546
|
+
recognitionRef.current?.abort();
|
|
3547
|
+
if (typeof window !== "undefined" && "speechSynthesis" in window) {
|
|
3548
|
+
window.speechSynthesis.cancel();
|
|
3549
|
+
}
|
|
3550
|
+
};
|
|
3551
|
+
}, []);
|
|
3552
|
+
const containerStyle = {
|
|
3553
|
+
position: "fixed",
|
|
3554
|
+
zIndex: 9999
|
|
3555
|
+
};
|
|
3556
|
+
const title = widgetConfig?.headerTitle ?? "AI Assistant";
|
|
3557
|
+
const welcomeMessage = widgetConfig?.welcomeMessage ?? "How can I help you today?";
|
|
3558
|
+
const welcomeHint = widgetConfig?.subtitle ?? "Type a message to get started";
|
|
3559
|
+
const askPlaceholder = widgetConfig?.askPlaceholder ?? "Ask a question...";
|
|
3560
|
+
const navigatePlaceholder = widgetConfig?.navigatePlaceholder ?? "What should I do? Try: Go to cart, Add product 1 to cart";
|
|
3561
|
+
const supportedLanguages = widgetConfig?.supportedLanguages ?? [
|
|
3562
|
+
{ code: "en-US", label: "EN" },
|
|
3563
|
+
{ code: "hi-IN", label: "\u0939\u093F\u0902\u0926\u0940" }
|
|
3564
|
+
];
|
|
3565
|
+
return /* @__PURE__ */ jsxs("div", { style: containerStyle, className: `navsi-chatbot-container ${className || ""}`, children: [
|
|
3566
|
+
isWidgetOpen && /* @__PURE__ */ jsxs("div", { className: `navsi-chatbot-window ${windowClassName || ""}`, children: [
|
|
3567
|
+
/* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-header", children: [
|
|
3568
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-header-left", children: /* @__PURE__ */ jsxs("div", { children: [
|
|
3569
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-title", children: title }),
|
|
3570
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-status", children: isConnected ? "Connected" : isReconnecting ? "Reconnecting" : "Disconnected" }),
|
|
3571
|
+
isListening && /* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-voice-indicator", "aria-live": "polite", children: [
|
|
3572
|
+
/* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3573
|
+
/* @__PURE__ */ jsx("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
|
|
3574
|
+
/* @__PURE__ */ jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
3575
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
|
|
3576
|
+
/* @__PURE__ */ jsx("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
|
|
3577
|
+
] }),
|
|
3578
|
+
"Listening\u2026"
|
|
3579
|
+
] })
|
|
3580
|
+
] }) }),
|
|
3581
|
+
/* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-mode-toggle", children: [
|
|
3582
|
+
/* @__PURE__ */ jsx(
|
|
3583
|
+
"button",
|
|
3584
|
+
{
|
|
3585
|
+
className: `navsi-chatbot-mode-button ${mode === "ask" ? "navsi-chatbot-mode-active" : ""}`,
|
|
3586
|
+
onClick: () => setMode("ask"),
|
|
3587
|
+
children: "Ask"
|
|
3588
|
+
}
|
|
3589
|
+
),
|
|
3590
|
+
/* @__PURE__ */ jsx(
|
|
3591
|
+
"button",
|
|
3592
|
+
{
|
|
3593
|
+
className: `navsi-chatbot-mode-button ${mode === "navigate" ? "navsi-chatbot-mode-active" : ""}`,
|
|
3594
|
+
onClick: () => setMode("navigate"),
|
|
3595
|
+
children: "Action"
|
|
3596
|
+
}
|
|
3597
|
+
)
|
|
3598
|
+
] }),
|
|
3599
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-lang-toggle", role: "group", "aria-label": "Voice and response language", children: supportedLanguages.map((lang) => /* @__PURE__ */ jsx(
|
|
3600
|
+
"button",
|
|
3601
|
+
{
|
|
3602
|
+
type: "button",
|
|
3603
|
+
className: `navsi-chatbot-lang-button ${effectiveVoiceLang.startsWith(lang.code.split("-")[0]) ? "navsi-chatbot-lang-active" : ""}`,
|
|
3604
|
+
onClick: () => setVoiceLanguage(lang.code),
|
|
3605
|
+
title: lang.label,
|
|
3606
|
+
"aria-pressed": effectiveVoiceLang.startsWith(lang.code.split("-")[0]),
|
|
3607
|
+
children: lang.label
|
|
3608
|
+
},
|
|
3609
|
+
lang.code
|
|
3610
|
+
)) }),
|
|
3611
|
+
messages.length > 0 && /* @__PURE__ */ jsx(
|
|
3612
|
+
"button",
|
|
3613
|
+
{
|
|
3614
|
+
type: "button",
|
|
3615
|
+
onClick: () => clearMessages(),
|
|
3616
|
+
className: "navsi-chatbot-clear",
|
|
3617
|
+
title: "Clear chat",
|
|
3618
|
+
"aria-label": "Clear chat",
|
|
3619
|
+
children: "Clear"
|
|
3620
|
+
}
|
|
3621
|
+
)
|
|
3622
|
+
] }),
|
|
3623
|
+
isExecuting && /* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-banner", children: [
|
|
3624
|
+
/* @__PURE__ */ jsx("span", { children: "\u26A1 Executing actions\u2026" }),
|
|
3625
|
+
progress && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3626
|
+
/* @__PURE__ */ jsxs("span", { className: "navsi-chatbot-pill", children: [
|
|
3627
|
+
progress.current + 1,
|
|
3628
|
+
"/",
|
|
3629
|
+
progress.total
|
|
3630
|
+
] }),
|
|
3631
|
+
progress.description && /* @__PURE__ */ jsx("span", { className: "navsi-chatbot-step", children: progress.description })
|
|
3632
|
+
] })
|
|
3633
|
+
] }),
|
|
3634
|
+
progress && /* @__PURE__ */ jsx("div", { className: "navsi-chatbot-progress-container", children: /* @__PURE__ */ jsx("div", { className: "navsi-chatbot-progress-bar", children: /* @__PURE__ */ jsx(
|
|
3635
|
+
"div",
|
|
3636
|
+
{
|
|
3637
|
+
className: "navsi-chatbot-progress-fill",
|
|
3638
|
+
style: { width: `${progress.percentage ?? 0}%` }
|
|
3639
|
+
}
|
|
3640
|
+
) }) }),
|
|
3641
|
+
error && /* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-error", children: [
|
|
3642
|
+
/* @__PURE__ */ jsx("span", { children: error.message }),
|
|
3643
|
+
/* @__PURE__ */ jsx(
|
|
3644
|
+
"button",
|
|
3645
|
+
{
|
|
3646
|
+
className: "navsi-chatbot-error-close",
|
|
3647
|
+
onClick: clearError,
|
|
3648
|
+
children: "\u2715"
|
|
3649
|
+
}
|
|
3650
|
+
)
|
|
3651
|
+
] }),
|
|
3652
|
+
voiceError && /* @__PURE__ */ jsx("div", { className: "navsi-chatbot-voice-error", role: "alert", children: voiceError }),
|
|
3653
|
+
/* @__PURE__ */ jsxs(
|
|
3654
|
+
"div",
|
|
3655
|
+
{
|
|
3656
|
+
ref: messagesContainerRef,
|
|
3657
|
+
className: "navsi-chatbot-messages",
|
|
3658
|
+
role: "log",
|
|
3659
|
+
"aria-live": "polite",
|
|
3660
|
+
"aria-label": "Chat messages",
|
|
3661
|
+
children: [
|
|
3662
|
+
messages.length === 0 && /* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-welcome", children: [
|
|
3663
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-welcome-icon", children: "\u{1F44B}" }),
|
|
3664
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-welcome-text", children: welcomeMessage }),
|
|
3665
|
+
/* @__PURE__ */ jsx("div", { className: "navsi-chatbot-welcome-hint", children: welcomeHint })
|
|
3666
|
+
] }),
|
|
3667
|
+
messages.map((msg) => /* @__PURE__ */ jsx(
|
|
3668
|
+
"div",
|
|
3669
|
+
{
|
|
3670
|
+
className: `navsi-chatbot-message ${msg.role === "user" ? "navsi-chatbot-message-user" : "navsi-chatbot-message-assistant"}`,
|
|
3671
|
+
children: /* @__PURE__ */ jsx(MessageContent, { content: msg.content, isUser: msg.role === "user" })
|
|
3672
|
+
},
|
|
3673
|
+
msg.id
|
|
3674
|
+
)),
|
|
3675
|
+
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
|
|
3676
|
+
]
|
|
3677
|
+
}
|
|
3678
|
+
),
|
|
3679
|
+
/* @__PURE__ */ jsxs("div", { className: "navsi-chatbot-input-area", children: [
|
|
3680
|
+
/* @__PURE__ */ jsx(
|
|
3681
|
+
"input",
|
|
3682
|
+
{
|
|
3683
|
+
type: "text",
|
|
3684
|
+
value: input,
|
|
3685
|
+
onChange: (e) => setInput(e.target.value),
|
|
3686
|
+
onKeyDown: handleKeyDown,
|
|
3687
|
+
placeholder: mode === "ask" ? askPlaceholder : navigatePlaceholder,
|
|
3688
|
+
className: "navsi-chatbot-input",
|
|
3689
|
+
maxLength: 8e3,
|
|
3690
|
+
"aria-label": mode === "ask" ? askPlaceholder : navigatePlaceholder
|
|
3691
|
+
}
|
|
3692
|
+
),
|
|
3693
|
+
/* @__PURE__ */ jsx(
|
|
3694
|
+
"button",
|
|
3695
|
+
{
|
|
3696
|
+
className: `navsi-chatbot-voice-button ${isListening ? "navsi-chatbot-voice-button-active" : ""}`,
|
|
3697
|
+
onClick: () => {
|
|
3698
|
+
if (isListening) {
|
|
3699
|
+
stopVoiceInput();
|
|
3700
|
+
} else {
|
|
3701
|
+
startVoiceInput();
|
|
3702
|
+
}
|
|
3703
|
+
},
|
|
3704
|
+
disabled: !isConnected || !isVoiceSupported,
|
|
3705
|
+
"aria-label": !isVoiceSupported ? "Voice input not supported in this browser" : isListening ? "Stop voice input" : "Start voice input",
|
|
3706
|
+
"aria-pressed": isListening,
|
|
3707
|
+
title: !isVoiceSupported ? "Voice input is not available in this browser" : isListening ? "Click to stop and send" : "Click to start listening",
|
|
3708
|
+
type: "button",
|
|
3709
|
+
children: isListening ? /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "8" }) }) : /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3710
|
+
/* @__PURE__ */ jsx("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
|
|
3711
|
+
/* @__PURE__ */ jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
3712
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
|
|
3713
|
+
/* @__PURE__ */ jsx("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
|
|
3714
|
+
] })
|
|
3715
|
+
}
|
|
3716
|
+
),
|
|
3717
|
+
isExecuting ? /* @__PURE__ */ jsx(
|
|
3718
|
+
"button",
|
|
3719
|
+
{
|
|
3720
|
+
className: "navsi-chatbot-stop-button",
|
|
3721
|
+
onClick: stopExecution,
|
|
3722
|
+
"aria-label": "Stop",
|
|
3723
|
+
title: "Stop current action",
|
|
3724
|
+
children: /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "1" }) })
|
|
3725
|
+
}
|
|
3726
|
+
) : /* @__PURE__ */ jsx(
|
|
3727
|
+
"button",
|
|
3728
|
+
{
|
|
3729
|
+
className: "navsi-chatbot-send-button",
|
|
3730
|
+
onClick: handleSend,
|
|
3731
|
+
disabled: !isConnected,
|
|
3732
|
+
children: /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
|
|
3733
|
+
}
|
|
3734
|
+
)
|
|
3735
|
+
] }),
|
|
3736
|
+
voiceTranscript && /* @__PURE__ */ jsx("div", { className: "navsi-chatbot-voice-transcript", "aria-live": "polite", children: voiceTranscript })
|
|
3737
|
+
] }),
|
|
3738
|
+
/* @__PURE__ */ jsx(
|
|
3739
|
+
"button",
|
|
3740
|
+
{
|
|
3741
|
+
className: `navsi-chatbot-toggle ${buttonClassName || ""}`,
|
|
3742
|
+
onClick: () => setWidgetOpen(!isWidgetOpen),
|
|
3743
|
+
"aria-label": isWidgetOpen ? "Close chat" : "Open chat",
|
|
3744
|
+
children: isWidgetOpen ? /* @__PURE__ */ jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }) : /* @__PURE__ */ jsx("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z" }) })
|
|
3745
|
+
}
|
|
3746
|
+
)
|
|
3747
|
+
] });
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
export { ChatbotProvider, ChatbotWidget, DEFAULT_ROUTE_TIMEOUT, NavigationController, WebSocketClient, createMemoryAdapter, createNavigationController, createWebSocketClient };
|
|
3751
|
+
//# sourceMappingURL=chunk-EHZXIZIP.js.map
|
|
3752
|
+
//# sourceMappingURL=chunk-EHZXIZIP.js.map
|