@navieo/react 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/dist/index.js ADDED
@@ -0,0 +1,1356 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ ChatWidget: () => ChatWidget,
25
+ NavieoProvider: () => NavieoProvider,
26
+ logger: () => logger,
27
+ useNavieo: () => useNavieo,
28
+ useTourRenderer: () => useTourRenderer
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/NavieoProvider.tsx
33
+ var import_react4 = require("react");
34
+
35
+ // src/ChatWidget.tsx
36
+ var import_react2 = require("react");
37
+ var import_react_dom = require("react-dom");
38
+
39
+ // src/useNavieo.ts
40
+ var import_react = require("react");
41
+ function useNavieo() {
42
+ const context = (0, import_react.useContext)(NavieoContext);
43
+ if (!context) {
44
+ throw new Error("useNavieo must be used within a <NavieoProvider>");
45
+ }
46
+ return context;
47
+ }
48
+
49
+ // src/ChatWidget.tsx
50
+ var import_jsx_runtime = require("react/jsx-runtime");
51
+ function ChatWidget() {
52
+ const {
53
+ isChatOpen,
54
+ toggleChat,
55
+ startTour,
56
+ tourState,
57
+ chatHistory,
58
+ clearChatHistory,
59
+ suggestions,
60
+ endTour
61
+ } = useNavieo();
62
+ const [inputValue, setInputValue] = (0, import_react2.useState)("");
63
+ const messagesEndRef = (0, import_react2.useRef)(null);
64
+ const [mounted, setMounted] = (0, import_react2.useState)(false);
65
+ (0, import_react2.useEffect)(() => setMounted(true), []);
66
+ (0, import_react2.useEffect)(() => {
67
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
68
+ }, [chatHistory]);
69
+ const handleSubmit = (e) => {
70
+ e.preventDefault();
71
+ if (!inputValue.trim() || tourState === "loading") return;
72
+ startTour(inputValue.trim());
73
+ setInputValue("");
74
+ };
75
+ if (!mounted) return null;
76
+ const isTourActive = tourState === "active";
77
+ const widget = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `fixed bottom-6 right-6 flex flex-col items-end gap-3 font-sans ${isTourActive ? "z-[1100000000]" : "z-[40]"}`, children: [
78
+ !isTourActive && isChatOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "w-[380px] h-[520px] bg-white rounded-2xl shadow-2xl border border-gray-200 flex flex-col overflow-hidden", children: [
79
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "px-5 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white flex justify-between items-center", children: [
80
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
81
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-semibold text-sm", children: "Navieo Assistant" }),
82
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-blue-200 text-xs", children: "Ask me how to do anything" })
83
+ ] }),
84
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1", children: [
85
+ chatHistory.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
86
+ "button",
87
+ {
88
+ onClick: clearChatHistory,
89
+ "aria-label": "Clear chat history",
90
+ title: "Clear chat history",
91
+ className: "hover:bg-white/10 rounded-lg p-1 transition-colors",
92
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
93
+ "svg",
94
+ {
95
+ width: "16",
96
+ height: "16",
97
+ viewBox: "0 0 24 24",
98
+ fill: "none",
99
+ stroke: "currentColor",
100
+ strokeWidth: "2",
101
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14" })
102
+ }
103
+ )
104
+ }
105
+ ),
106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
107
+ "button",
108
+ {
109
+ onClick: toggleChat,
110
+ "aria-label": "Close chat",
111
+ className: "hover:bg-white/10 rounded-lg p-1 transition-colors",
112
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
113
+ "svg",
114
+ {
115
+ width: "18",
116
+ height: "18",
117
+ viewBox: "0 0 24 24",
118
+ fill: "none",
119
+ stroke: "currentColor",
120
+ strokeWidth: "2",
121
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
122
+ }
123
+ )
124
+ }
125
+ )
126
+ ] })
127
+ ] }),
128
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-1 overflow-y-auto p-4 space-y-3", children: [
129
+ chatHistory.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-center mt-6 px-4", children: [
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
131
+ "svg",
132
+ {
133
+ className: "w-6 h-6 text-blue-600",
134
+ fill: "none",
135
+ stroke: "currentColor",
136
+ strokeWidth: "2",
137
+ viewBox: "0 0 24 24",
138
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" })
139
+ }
140
+ ) }),
141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-gray-900 font-medium text-sm mb-1", children: "How can I help?" }),
142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-gray-400 text-xs leading-relaxed mb-4", children: "Ask me how to do anything in this app and I'll guide you step by step." }),
143
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-1.5", children: suggestions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
144
+ "button",
145
+ {
146
+ disabled: tourState === "loading",
147
+ onClick: () => {
148
+ if (tourState === "loading") return;
149
+ setInputValue(s.label);
150
+ startTour(s.label);
151
+ },
152
+ className: "flex items-center gap-2 w-full text-left text-xs bg-gray-50 hover:bg-gray-100 text-gray-600 rounded-lg px-3 py-2 transition-colors disabled:opacity-50",
153
+ children: [
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "flex-1", children: s.label }),
155
+ s.tag && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-blue-100 text-blue-600", children: s.tag })
156
+ ]
157
+ },
158
+ s.label
159
+ )) })
160
+ ] }),
161
+ chatHistory.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
162
+ "div",
163
+ {
164
+ className: `flex ${msg.role === "user" ? "justify-end" : "justify-start"}`,
165
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
166
+ "div",
167
+ {
168
+ className: `max-w-[85%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-line ${msg.role === "user" ? "bg-blue-600 text-white rounded-br-md" : "bg-gray-100 text-gray-800 rounded-bl-md"}`,
169
+ children: msg.content
170
+ }
171
+ )
172
+ },
173
+ i
174
+ )),
175
+ tourState === "loading" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "bg-gray-100 rounded-2xl rounded-bl-md px-4 py-2.5 text-sm text-gray-500", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex gap-1", children: [
176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce [animation-delay:0ms]" }),
177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce [animation-delay:150ms]" }),
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce [animation-delay:300ms]" })
179
+ ] }) }) }),
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: messagesEndRef })
181
+ ] }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
183
+ "form",
184
+ {
185
+ onSubmit: handleSubmit,
186
+ className: "p-3 border-t border-gray-200 bg-white",
187
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-2", children: [
188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
189
+ "input",
190
+ {
191
+ type: "text",
192
+ value: inputValue,
193
+ onChange: (e) => setInputValue(e.target.value),
194
+ placeholder: "How do I...",
195
+ className: "flex-1 border border-gray-300 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
196
+ disabled: tourState === "loading"
197
+ }
198
+ ),
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
200
+ "button",
201
+ {
202
+ type: "submit",
203
+ disabled: tourState === "loading" || !inputValue.trim(),
204
+ className: "bg-blue-600 text-white px-4 py-2.5 rounded-xl text-sm hover:bg-blue-700 disabled:opacity-50 transition-colors",
205
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
206
+ "svg",
207
+ {
208
+ className: "w-4 h-4",
209
+ fill: "none",
210
+ stroke: "currentColor",
211
+ strokeWidth: "2",
212
+ viewBox: "0 0 24 24",
213
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 12h14M12 5l7 7-7 7" })
214
+ }
215
+ )
216
+ }
217
+ )
218
+ ] })
219
+ }
220
+ )
221
+ ] }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
223
+ "button",
224
+ {
225
+ onClick: isTourActive ? endTour : toggleChat,
226
+ "aria-label": isTourActive ? "Stop tour" : "Open Navieo assistant",
227
+ className: `w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-105 ${isTourActive ? "bg-red-500 hover:bg-red-600" : isChatOpen ? "bg-gray-700 hover:bg-gray-800" : "bg-blue-600 hover:bg-blue-700"} text-white`,
228
+ children: isTourActive ? (
229
+ /* Stop icon */
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
231
+ "svg",
232
+ {
233
+ width: "20",
234
+ height: "20",
235
+ viewBox: "0 0 24 24",
236
+ fill: "currentColor",
237
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" })
238
+ }
239
+ )
240
+ ) : isChatOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
241
+ "svg",
242
+ {
243
+ width: "22",
244
+ height: "22",
245
+ viewBox: "0 0 24 24",
246
+ fill: "none",
247
+ stroke: "currentColor",
248
+ strokeWidth: "2",
249
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
250
+ }
251
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
252
+ "svg",
253
+ {
254
+ width: "22",
255
+ height: "22",
256
+ viewBox: "0 0 24 24",
257
+ fill: "none",
258
+ stroke: "currentColor",
259
+ strokeWidth: "2",
260
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" })
261
+ }
262
+ )
263
+ }
264
+ )
265
+ ] }) });
266
+ return (0, import_react_dom.createPortal)(widget, document.body);
267
+ }
268
+
269
+ // src/TourRenderer.tsx
270
+ var import_driver = require("driver.js");
271
+ var import_driver2 = require("driver.js/dist/driver.css");
272
+ var import_react3 = require("react");
273
+
274
+ // src/routeUtils.ts
275
+ function stripRouteGroups(route) {
276
+ return route.replace(/\/\([^)]+\)/g, "") || "/";
277
+ }
278
+ function normalizeRoute(route) {
279
+ let r = route.trim();
280
+ r = stripRouteGroups(r);
281
+ const qIdx = r.indexOf("?");
282
+ if (qIdx !== -1) r = r.slice(0, qIdx);
283
+ const hIdx = r.indexOf("#");
284
+ if (hIdx !== -1) r = r.slice(0, hIdx);
285
+ if (r.length > 1 && r.endsWith("/")) r = r.slice(0, -1);
286
+ return r;
287
+ }
288
+ function isDynamicRoute(route) {
289
+ return /\[.+?\]/.test(route);
290
+ }
291
+ function routeMatches(actual, pattern) {
292
+ const a = normalizeRoute(actual);
293
+ const p = normalizeRoute(pattern);
294
+ if (a === p) return true;
295
+ const actualParts = a.split("/");
296
+ const patternParts = p.split("/");
297
+ const lastPart = patternParts[patternParts.length - 1];
298
+ if (lastPart && lastPart.startsWith("[[...") && lastPart.endsWith("]]")) {
299
+ const prefixParts = patternParts.slice(0, -1);
300
+ if (actualParts.length < prefixParts.length) return false;
301
+ return prefixParts.every(
302
+ (part, i) => part.startsWith("[") && part.endsWith("]") || part === actualParts[i]
303
+ );
304
+ }
305
+ if (lastPart && lastPart.startsWith("[...") && lastPart.endsWith("]")) {
306
+ const prefixParts = patternParts.slice(0, -1);
307
+ if (actualParts.length <= prefixParts.length) return false;
308
+ return prefixParts.every(
309
+ (part, i) => part.startsWith("[") && part.endsWith("]") || part === actualParts[i]
310
+ );
311
+ }
312
+ if (actualParts.length !== patternParts.length) return false;
313
+ return patternParts.every(
314
+ (part, i) => part.startsWith("[") && part.endsWith("]") || part === actualParts[i]
315
+ );
316
+ }
317
+
318
+ // src/constants.ts
319
+ var TOUR_ERRORS = {
320
+ /** Element polling timed out — page elements never appeared in the DOM. */
321
+ elementsNotFound: (stepLabels) => `Could not find the page elements needed for the next steps (${stepLabels}). The page may still be loading or the layout may have changed.`,
322
+ /** Transition between route groups timed out (60s safety). */
323
+ transitionTimeout: "The tour couldn't continue because the next page didn't load in time.",
324
+ /** Driver.js threw an unexpected exception. */
325
+ driverCrash: "The tour encountered an unexpected error and had to stop.",
326
+ /** continueAfterNavigation couldn't find a matching route group. */
327
+ noMatchingGroup: "The tour couldn't find steps for the page you navigated to. The tour has been stopped."
328
+ };
329
+ var CHAT_MESSAGES = {
330
+ /** Wraps a tour error for display in chat. */
331
+ tourError: (detail) => `The tour ran into a problem: ${detail} Please try again or ask a different question.`,
332
+ /** Navigation timeout — route never changed within NAV_TIMEOUT_MS. */
333
+ navigationTimeout: "The tour was stopped because the expected page navigation didn't complete in time. Please try again.",
334
+ /** API returned no steps and no answer text. */
335
+ noStepsFound: "I couldn't find specific steps for that. Try asking something else.",
336
+ /** API returned steps but none had the required fields. */
337
+ invalidSteps: "The response didn't contain valid steps. Try rephrasing your question.",
338
+ /** Generic API/network error. */
339
+ genericError: "Something went wrong. Please try again.",
340
+ /** Tour started successfully. */
341
+ tourStarted: (stepCount) => `I found ${stepCount} step${stepCount !== 1 ? "s" : ""} to help you. Starting the tour!`
342
+ };
343
+
344
+ // src/logger.ts
345
+ var LEVEL_ORDER = {
346
+ debug: 0,
347
+ info: 1,
348
+ warn: 2,
349
+ error: 3,
350
+ silent: 4
351
+ };
352
+ var PREFIX = "[Navieo]";
353
+ function getDefaultLevel() {
354
+ if (typeof window !== "undefined") {
355
+ const override = window.__NAVIEO_LOG_LEVEL__;
356
+ if (typeof override === "string" && override in LEVEL_ORDER) {
357
+ return override;
358
+ }
359
+ }
360
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
361
+ return "warn";
362
+ }
363
+ return "debug";
364
+ }
365
+ var NavieoLogger = class {
366
+ constructor() {
367
+ this.level = getDefaultLevel();
368
+ }
369
+ /** Change the active log level at runtime. */
370
+ setLevel(level) {
371
+ this.level = level;
372
+ }
373
+ getLevel() {
374
+ return this.level;
375
+ }
376
+ shouldLog(level) {
377
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[this.level];
378
+ }
379
+ /** Verbose diagnostic info — only shown at "debug" level. */
380
+ debug(message, ...args) {
381
+ if (this.shouldLog("debug")) {
382
+ console.log(`${PREFIX} ${message}`, ...args);
383
+ }
384
+ }
385
+ /** Standard operational info — shown at "info" level and above. */
386
+ info(message, ...args) {
387
+ if (this.shouldLog("info")) {
388
+ console.log(`${PREFIX} ${message}`, ...args);
389
+ }
390
+ }
391
+ /** Warnings — shown at "warn" level and above. */
392
+ warn(message, ...args) {
393
+ if (this.shouldLog("warn")) {
394
+ console.warn(`${PREFIX} ${message}`, ...args);
395
+ }
396
+ }
397
+ /** Errors — always shown unless "silent". */
398
+ error(message, ...args) {
399
+ if (this.shouldLog("error")) {
400
+ console.error(`${PREFIX} ${message}`, ...args);
401
+ }
402
+ }
403
+ /**
404
+ * Log a structured diagnostic table for tour steps.
405
+ * Only shown at "debug" level.
406
+ */
407
+ debugSteps(label, steps) {
408
+ if (this.shouldLog("debug")) {
409
+ console.log(`${PREFIX} ${label}`);
410
+ console.table(steps);
411
+ }
412
+ }
413
+ };
414
+ var logger = new NavieoLogger();
415
+
416
+ // src/TourRenderer.tsx
417
+ function escapeHtml(str) {
418
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
419
+ }
420
+ function groupByRoute(steps) {
421
+ const groups = [];
422
+ let currentGroup = null;
423
+ steps.forEach((step, i) => {
424
+ if (!currentGroup || currentGroup.route !== step.route) {
425
+ currentGroup = { route: step.route, steps: [], globalStartIndex: i };
426
+ groups.push(currentGroup);
427
+ }
428
+ currentGroup.steps.push(step);
429
+ });
430
+ return groups;
431
+ }
432
+ function useTourRenderer(options = {}) {
433
+ const driverRef = (0, import_react3.useRef)(null);
434
+ const optionsRef = (0, import_react3.useRef)(options);
435
+ optionsRef.current = options;
436
+ const allStepsRef = (0, import_react3.useRef)([]);
437
+ const groupsRef = (0, import_react3.useRef)([]);
438
+ const currentGroupIndexRef = (0, import_react3.useRef)(0);
439
+ const transitioningRef = (0, import_react3.useRef)(false);
440
+ const transitionTimeoutRef = (0, import_react3.useRef)(null);
441
+ const forceDestroyRef = (0, import_react3.useRef)(false);
442
+ const lastLocalIndexRef = (0, import_react3.useRef)(0);
443
+ function resolveSelector(step) {
444
+ const candidates = [];
445
+ if (step.selector?.cssSelector) candidates.push(step.selector.cssSelector);
446
+ if (step.elementId) candidates.push(`#${CSS.escape(step.elementId)}`);
447
+ if (step.selector?.testId) candidates.push(`[data-testid="${CSS.escape(step.selector.testId)}"]`);
448
+ if (step.selector?.ariaLabel) candidates.push(`[aria-label="${CSS.escape(step.selector.ariaLabel)}"]`);
449
+ const stepLabel = step.tooltipTitle || step.elementId || "unknown";
450
+ if (candidates.length === 0) {
451
+ logger.warn(`resolveSelector("${stepLabel}"): no selectors defined on step`);
452
+ return void 0;
453
+ }
454
+ logger.debug(`resolveSelector("${stepLabel}"): trying ${candidates.length} candidates: ${candidates.join(", ")}`);
455
+ for (const sel of candidates) {
456
+ try {
457
+ const el = document.querySelector(sel);
458
+ if (el) {
459
+ logger.debug(`resolveSelector("${stepLabel}"): FOUND via "${sel}" \u2192 <${el.tagName.toLowerCase()}>`);
460
+ return sel;
461
+ }
462
+ logger.debug(`resolveSelector("${stepLabel}"): miss "${sel}"`);
463
+ } catch {
464
+ logger.warn(`resolveSelector("${stepLabel}"): invalid selector syntax "${sel}"`);
465
+ }
466
+ }
467
+ logger.warn(`resolveSelector("${stepLabel}"): NO element found. Tried: ${candidates.join(", ")}`);
468
+ return void 0;
469
+ }
470
+ function buildDriverSteps(group, totalSteps, isLastGroup) {
471
+ return group.steps.map((s, i) => {
472
+ const globalIndex = group.globalStartIndex + i;
473
+ const isLastStepInGroup = i === group.steps.length - 1;
474
+ return {
475
+ element: s.selector?.cssSelector || `#${s.elementId}`,
476
+ popover: {
477
+ title: escapeHtml(s.tooltipTitle || `Step ${globalIndex + 1} of ${totalSteps}`),
478
+ description: escapeHtml(s.tooltipText),
479
+ progressText: `${globalIndex + 1} of ${totalSteps}`,
480
+ ...isLastStepInGroup && !isLastGroup ? { doneBtnText: "Next \u2192" } : {}
481
+ }
482
+ };
483
+ });
484
+ }
485
+ function destroyCurrentDriver() {
486
+ const d = driverRef.current;
487
+ if (!d) return;
488
+ logger.debug("destroyCurrentDriver: destroying active driver instance");
489
+ forceDestroyRef.current = true;
490
+ try {
491
+ d.destroy();
492
+ } catch {
493
+ }
494
+ forceDestroyRef.current = false;
495
+ driverRef.current = null;
496
+ }
497
+ const pollIntervalRef = (0, import_react3.useRef)(null);
498
+ function clearTransitionTimeout() {
499
+ if (transitionTimeoutRef.current) {
500
+ clearTimeout(transitionTimeoutRef.current);
501
+ transitionTimeoutRef.current = null;
502
+ }
503
+ }
504
+ function clearPollInterval() {
505
+ if (pollIntervalRef.current) {
506
+ clearInterval(pollIntervalRef.current);
507
+ pollIntervalRef.current = null;
508
+ }
509
+ }
510
+ function anyElementExists(steps) {
511
+ for (const step of steps) {
512
+ const candidates = [];
513
+ if (step.selector?.cssSelector) candidates.push(step.selector.cssSelector);
514
+ if (step.elementId) candidates.push(`#${CSS.escape(step.elementId)}`);
515
+ if (step.selector?.testId) candidates.push(`[data-testid="${CSS.escape(step.selector.testId)}"]`);
516
+ if (step.selector?.ariaLabel) candidates.push(`[aria-label="${CSS.escape(step.selector.ariaLabel)}"]`);
517
+ for (const sel of candidates) {
518
+ try {
519
+ if (document.querySelector(sel)) return true;
520
+ } catch {
521
+ }
522
+ }
523
+ }
524
+ return false;
525
+ }
526
+ function launchDriver(group, driverSteps) {
527
+ logger.info(`launchDriver: group="${group.route}", ${driverSteps.length} steps, globalStart=${group.globalStartIndex}`);
528
+ const resolvedSummary = [];
529
+ for (let idx = 0; idx < driverSteps.length; idx++) {
530
+ const resolved = resolveSelector(group.steps[idx]);
531
+ if (resolved) {
532
+ driverSteps[idx].element = resolved;
533
+ } else {
534
+ driverSteps[idx].element = void 0;
535
+ }
536
+ resolvedSummary.push({
537
+ step: group.steps[idx].tooltipTitle || `Step ${idx + 1}`,
538
+ selector: resolved || null
539
+ });
540
+ }
541
+ logger.debugSteps("launchDriver: resolved selectors", resolvedSummary);
542
+ try {
543
+ logger.debug("launchDriver: calling driver() constructor...");
544
+ driverRef.current = (0, import_driver.driver)({
545
+ showProgress: true,
546
+ animate: true,
547
+ overlayColor: "rgba(0, 0, 0, 0.6)",
548
+ stagePadding: 8,
549
+ stageRadius: 8,
550
+ steps: driverSteps,
551
+ onHighlightStarted: (_element, _step, opts) => {
552
+ const localIndex = opts.state.activeIndex ?? 0;
553
+ lastLocalIndexRef.current = localIndex;
554
+ const globalIndex = group.globalStartIndex + localIndex;
555
+ logger.debug(`onHighlightStarted: local=${localIndex}, global=${globalIndex}`);
556
+ optionsRef.current.onStepChange?.(globalIndex);
557
+ },
558
+ onDestroyStarted: () => {
559
+ const d = driverRef.current;
560
+ if (!d) {
561
+ logger.warn("onDestroyStarted: driverRef is null \u2014 skipping");
562
+ return;
563
+ }
564
+ if (forceDestroyRef.current) {
565
+ logger.debug("onDestroyStarted: forceDestroy=true, confirming");
566
+ driverRef.current = null;
567
+ d.destroy();
568
+ return;
569
+ }
570
+ const activeIndex = lastLocalIndexRef.current;
571
+ const isLastStep = activeIndex >= driverSteps.length - 1;
572
+ const isLastGroupNow = currentGroupIndexRef.current >= groupsRef.current.length - 1;
573
+ const hasMoreGroups = !isLastGroupNow;
574
+ logger.info(
575
+ `onDestroyStarted: step=${activeIndex}/${driverSteps.length - 1}, group=${currentGroupIndexRef.current}/${groupsRef.current.length - 1}, isLastStep=${isLastStep}, hasMoreGroups=${hasMoreGroups}`
576
+ );
577
+ if (isLastStep && hasMoreGroups) {
578
+ transitioningRef.current = true;
579
+ driverRef.current = null;
580
+ d.destroy();
581
+ const nextGroup = groupsRef.current[currentGroupIndexRef.current + 1];
582
+ logger.info(`onDestroyStarted: transitioning to next group, route="${nextGroup.route}"`);
583
+ clearTransitionTimeout();
584
+ transitionTimeoutRef.current = setTimeout(() => {
585
+ if (transitioningRef.current) {
586
+ logger.warn("Transition safety timeout (60s) \u2014 navigation never completed");
587
+ transitioningRef.current = false;
588
+ optionsRef.current.onClose?.();
589
+ }
590
+ transitionTimeoutRef.current = null;
591
+ }, 6e4);
592
+ optionsRef.current.onNavigationNeeded?.(nextGroup.route);
593
+ } else if (isLastStep && isLastGroupNow) {
594
+ driverRef.current = null;
595
+ d.destroy();
596
+ logger.info("onDestroyStarted: tour COMPLETE (last step of last group)");
597
+ optionsRef.current.onComplete?.();
598
+ } else {
599
+ driverRef.current = null;
600
+ d.destroy();
601
+ logger.info("onDestroyStarted: tour CLOSED by user (early exit)");
602
+ optionsRef.current.onClose?.();
603
+ }
604
+ },
605
+ onDestroyed: () => {
606
+ if (transitioningRef.current) {
607
+ logger.debug("onDestroyed: transitioning between groups, skipping");
608
+ return;
609
+ }
610
+ logger.debug("onDestroyed: driver fully destroyed");
611
+ }
612
+ });
613
+ logger.debug("launchDriver: driver() created, calling .drive()...");
614
+ driverRef.current.drive();
615
+ logger.info("launchDriver: .drive() called \u2014 tour should now be visible");
616
+ } catch (err) {
617
+ logger.error("launchDriver: Driver.js crashed!", err);
618
+ driverRef.current = null;
619
+ optionsRef.current.onError?.(TOUR_ERRORS.driverCrash);
620
+ }
621
+ }
622
+ const driveGroup = (0, import_react3.useCallback)((groupIndex) => {
623
+ logger.info(`driveGroup(${groupIndex}): entering`);
624
+ transitioningRef.current = false;
625
+ lastLocalIndexRef.current = 0;
626
+ clearTransitionTimeout();
627
+ clearPollInterval();
628
+ destroyCurrentDriver();
629
+ const groups = groupsRef.current;
630
+ const totalSteps = allStepsRef.current.length;
631
+ logger.debug(`driveGroup(${groupIndex}): totalGroups=${groups.length}, totalSteps=${totalSteps}`);
632
+ if (groupIndex >= groups.length) {
633
+ logger.warn(`driveGroup(${groupIndex}): index out of range (${groups.length} groups), calling onComplete`);
634
+ optionsRef.current.onComplete?.();
635
+ return;
636
+ }
637
+ currentGroupIndexRef.current = groupIndex;
638
+ const group = groups[groupIndex];
639
+ const isLastGroup = groupIndex >= groups.length - 1;
640
+ const driverSteps = buildDriverSteps(group, totalSteps, isLastGroup);
641
+ const stepDetails = group.steps.map((s, i) => ({
642
+ index: group.globalStartIndex + i,
643
+ route: s.route,
644
+ title: s.tooltipTitle || "(no title)",
645
+ cssSelector: s.selector?.cssSelector || "(none)",
646
+ elementId: s.elementId || "(none)",
647
+ testId: s.selector?.testId || "(none)"
648
+ }));
649
+ logger.info(`driveGroup(${groupIndex}): route="${group.route}", ${driverSteps.length} steps, isLastGroup=${isLastGroup}`);
650
+ logger.debugSteps(`driveGroup(${groupIndex}): step details`, stepDetails);
651
+ const POLL_INTERVAL_MS = 200;
652
+ const POLL_TIMEOUT_MS = 5e3;
653
+ const pollStartTime = Date.now();
654
+ if (anyElementExists(group.steps)) {
655
+ logger.info(`driveGroup(${groupIndex}): elements found immediately \u2014 launching`);
656
+ launchDriver(group, driverSteps);
657
+ return;
658
+ }
659
+ logger.info(`driveGroup(${groupIndex}): elements NOT found yet, starting poll (interval=${POLL_INTERVAL_MS}ms, timeout=${POLL_TIMEOUT_MS}ms)`);
660
+ pollIntervalRef.current = setInterval(() => {
661
+ const elapsed = Date.now() - pollStartTime;
662
+ if (anyElementExists(group.steps)) {
663
+ logger.info(`driveGroup(${groupIndex}): elements found after ${elapsed}ms \u2014 launching`);
664
+ clearPollInterval();
665
+ launchDriver(group, driverSteps);
666
+ return;
667
+ }
668
+ if (elapsed >= POLL_TIMEOUT_MS) {
669
+ clearPollInterval();
670
+ logger.warn(`driveGroup(${groupIndex}): poll timed out after ${POLL_TIMEOUT_MS}ms \u2014 launching anyway (soft fallback)`);
671
+ launchDriver(group, driverSteps);
672
+ }
673
+ }, POLL_INTERVAL_MS);
674
+ }, []);
675
+ const startTour = (0, import_react3.useCallback)(
676
+ (steps, currentRoute) => {
677
+ logger.info(`startTour: ${steps.length} steps, currentRoute="${currentRoute}"`);
678
+ const stepSummary = steps.map((s, i) => ({
679
+ step: i + 1,
680
+ route: s.route,
681
+ title: s.tooltipTitle || "(no title)",
682
+ cssSelector: s.selector?.cssSelector || "(none)",
683
+ elementId: s.elementId || "(none)"
684
+ }));
685
+ logger.debugSteps("startTour: received steps", stepSummary);
686
+ allStepsRef.current = steps;
687
+ groupsRef.current = groupByRoute(steps);
688
+ currentGroupIndexRef.current = 0;
689
+ transitioningRef.current = false;
690
+ const groups = groupsRef.current;
691
+ logger.info(`startTour: grouped into ${groups.length} route groups: ${groups.map((g) => `"${g.route}"(${g.steps.length})`).join(", ")}`);
692
+ if (groups.length === 0) {
693
+ logger.warn("startTour: no groups created \u2014 returning");
694
+ return;
695
+ }
696
+ if (!routeMatches(currentRoute, groups[0].route)) {
697
+ logger.info(
698
+ `startTour: first group route "${groups[0].route}" != currentRoute "${currentRoute}" \u2014 showing nav popover before redirect`
699
+ );
700
+ destroyCurrentDriver();
701
+ driverRef.current = (0, import_driver.driver)({
702
+ showProgress: false,
703
+ animate: true,
704
+ overlayColor: "rgba(0, 0, 0, 0.6)",
705
+ steps: [
706
+ {
707
+ popover: {
708
+ title: `Navigating to ${groups[0].route}`,
709
+ description: "This guide needs to take you to a different page. Click Next to continue.",
710
+ doneBtnText: "Next \u2192"
711
+ }
712
+ }
713
+ ],
714
+ onDestroyStarted: () => {
715
+ const d = driverRef.current;
716
+ if (!d) return;
717
+ driverRef.current = null;
718
+ d.destroy();
719
+ optionsRef.current.onNavigationNeeded?.(groups[0].route);
720
+ }
721
+ });
722
+ driverRef.current.drive();
723
+ } else {
724
+ logger.info(`startTour: first group route matches currentRoute \u2014 calling driveGroup(0)`);
725
+ driveGroup(0);
726
+ }
727
+ },
728
+ [driveGroup]
729
+ );
730
+ const continueAfterNavigation = (0, import_react3.useCallback)(
731
+ (arrivedRouteOrPattern) => {
732
+ const groups = groupsRef.current;
733
+ logger.info(
734
+ `continueAfterNavigation: arrivedAt="${arrivedRouteOrPattern}", currentGroup=${currentGroupIndexRef.current}, totalGroups=${groups.length}`
735
+ );
736
+ const nextIndex = groups.findIndex(
737
+ (g, i) => i > currentGroupIndexRef.current && (g.route === arrivedRouteOrPattern || routeMatches(arrivedRouteOrPattern, g.route))
738
+ );
739
+ if (nextIndex !== -1) {
740
+ logger.info(`continueAfterNavigation: found next group ${nextIndex}, route="${groups[nextIndex].route}"`);
741
+ driveGroup(nextIndex);
742
+ } else if (groups.length > 0 && (groups[0].route === arrivedRouteOrPattern || routeMatches(arrivedRouteOrPattern, groups[0].route)) && currentGroupIndexRef.current === 0) {
743
+ logger.info("continueAfterNavigation: first group matches \u2014 driving group 0");
744
+ driveGroup(0);
745
+ } else {
746
+ const currentGroup = groups[currentGroupIndexRef.current];
747
+ if (currentGroup && (currentGroup.route === arrivedRouteOrPattern || routeMatches(arrivedRouteOrPattern, currentGroup.route))) {
748
+ logger.debug(`continueAfterNavigation: current group already matches "${arrivedRouteOrPattern}" \u2014 duplicate, ignoring`);
749
+ return;
750
+ }
751
+ logger.warn(`continueAfterNavigation: no matching group for "${arrivedRouteOrPattern}" \u2014 ignoring`);
752
+ }
753
+ },
754
+ [driveGroup]
755
+ );
756
+ const restoreTour = (0, import_react3.useCallback)(
757
+ (steps, currentRoute) => {
758
+ logger.info(`restoreTour: ${steps.length} steps, currentRoute="${currentRoute}"`);
759
+ allStepsRef.current = steps;
760
+ groupsRef.current = groupByRoute(steps);
761
+ currentGroupIndexRef.current = 0;
762
+ transitioningRef.current = false;
763
+ const groups = groupsRef.current;
764
+ logger.info(`restoreTour: grouped into ${groups.length} route groups: ${groups.map((g) => `"${g.route}"(${g.steps.length})`).join(", ")}`);
765
+ if (groups.length === 0) {
766
+ logger.warn("restoreTour: no groups \u2014 aborting");
767
+ return;
768
+ }
769
+ const matchingIndex = groups.findIndex(
770
+ (g) => routeMatches(currentRoute, g.route)
771
+ );
772
+ if (matchingIndex !== -1) {
773
+ logger.info(`restoreTour: found matching group ${matchingIndex}, route="${groups[matchingIndex].route}" \u2014 driving`);
774
+ driveGroup(matchingIndex);
775
+ } else {
776
+ logger.warn(`restoreTour: no group matches currentRoute "${currentRoute}" \u2014 falling back to startTour logic`);
777
+ if (!routeMatches(currentRoute, groups[0].route)) {
778
+ optionsRef.current.onNavigationNeeded?.(groups[0].route);
779
+ } else {
780
+ driveGroup(0);
781
+ }
782
+ }
783
+ },
784
+ [driveGroup]
785
+ );
786
+ const stopTour = (0, import_react3.useCallback)(() => {
787
+ logger.info("stopTour: stopping tour and clearing all state");
788
+ transitioningRef.current = false;
789
+ clearTransitionTimeout();
790
+ clearPollInterval();
791
+ destroyCurrentDriver();
792
+ allStepsRef.current = [];
793
+ groupsRef.current = [];
794
+ currentGroupIndexRef.current = 0;
795
+ }, []);
796
+ const nextStep = (0, import_react3.useCallback)(() => {
797
+ driverRef.current?.moveNext();
798
+ }, []);
799
+ const prevStep = (0, import_react3.useCallback)(() => {
800
+ driverRef.current?.movePrevious();
801
+ }, []);
802
+ const goToStep = (0, import_react3.useCallback)((index) => {
803
+ driverRef.current?.moveTo(index);
804
+ }, []);
805
+ (0, import_react3.useEffect)(() => {
806
+ return () => {
807
+ clearTransitionTimeout();
808
+ clearPollInterval();
809
+ destroyCurrentDriver();
810
+ };
811
+ }, []);
812
+ return (0, import_react3.useMemo)(
813
+ () => ({ startTour, restoreTour, stopTour, nextStep, prevStep, goToStep, continueAfterNavigation }),
814
+ [startTour, restoreTour, stopTour, nextStep, prevStep, goToStep, continueAfterNavigation]
815
+ );
816
+ }
817
+
818
+ // src/NavieoProvider.tsx
819
+ var import_jsx_runtime2 = require("react/jsx-runtime");
820
+ var NavieoErrorBoundary = class extends import_react4.Component {
821
+ constructor(props) {
822
+ super(props);
823
+ this.state = { hasError: false };
824
+ }
825
+ static getDerivedStateFromError() {
826
+ return { hasError: true };
827
+ }
828
+ componentDidCatch(error, info) {
829
+ logger.error("Navieo caught a render error:", error, info.componentStack);
830
+ }
831
+ render() {
832
+ if (this.state.hasError) {
833
+ return this.props.children;
834
+ }
835
+ return this.props.children;
836
+ }
837
+ };
838
+ var NavieoContext = (0, import_react4.createContext)(null);
839
+ var CHAT_STORAGE_KEY = "navieo_chat_history";
840
+ var TOUR_STORAGE_KEY = "navieo_active_tour";
841
+ var MAX_STORED_MESSAGES = 50;
842
+ var CONTEXT_WINDOW = 6;
843
+ var NAV_TIMEOUT_MS = 3e4;
844
+ var MAX_TOUR_STEPS = 50;
845
+ var FETCH_TIMEOUT_MS = 3e4;
846
+ var MIN_REQUEST_INTERVAL_MS = 2e3;
847
+ function sanitizeInput(str) {
848
+ return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
849
+ }
850
+ function isValidEndpoint(endpoint) {
851
+ if (endpoint.startsWith("/")) return true;
852
+ try {
853
+ const url = new URL(endpoint);
854
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
855
+ if (typeof window !== "undefined" && url.origin === window.location.origin) return true;
856
+ return true;
857
+ } catch {
858
+ return false;
859
+ }
860
+ }
861
+ function saveTourState(state) {
862
+ if (typeof window === "undefined") return;
863
+ try {
864
+ if (state) {
865
+ sessionStorage.setItem(TOUR_STORAGE_KEY, JSON.stringify(state));
866
+ } else {
867
+ sessionStorage.removeItem(TOUR_STORAGE_KEY);
868
+ }
869
+ } catch {
870
+ }
871
+ }
872
+ function loadTourState() {
873
+ if (typeof window === "undefined") return null;
874
+ try {
875
+ const stored = sessionStorage.getItem(TOUR_STORAGE_KEY);
876
+ if (!stored) return null;
877
+ const parsed = JSON.parse(stored);
878
+ if (!parsed || !Array.isArray(parsed.steps) || parsed.steps.length === 0 || !parsed.steps.every(
879
+ (s) => s && typeof s === "object" && typeof s.route === "string" && typeof s.tooltipText === "string"
880
+ )) {
881
+ logger.warn("loadTourState: invalid persisted data \u2014 clearing");
882
+ sessionStorage.removeItem(TOUR_STORAGE_KEY);
883
+ return null;
884
+ }
885
+ return parsed;
886
+ } catch {
887
+ try {
888
+ sessionStorage.removeItem(TOUR_STORAGE_KEY);
889
+ } catch {
890
+ }
891
+ }
892
+ return null;
893
+ }
894
+ function loadChatHistory() {
895
+ if (typeof window === "undefined") return [];
896
+ try {
897
+ const stored = localStorage.getItem(CHAT_STORAGE_KEY);
898
+ if (stored) {
899
+ const parsed = JSON.parse(stored);
900
+ if (Array.isArray(parsed)) {
901
+ const valid = parsed.filter(
902
+ (m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant") && typeof m.content === "string"
903
+ );
904
+ return valid.slice(-MAX_STORED_MESSAGES);
905
+ }
906
+ }
907
+ } catch {
908
+ try {
909
+ localStorage.removeItem(CHAT_STORAGE_KEY);
910
+ } catch {
911
+ }
912
+ }
913
+ return [];
914
+ }
915
+ function saveChatHistory(messages) {
916
+ if (typeof window === "undefined") return;
917
+ try {
918
+ localStorage.setItem(
919
+ CHAT_STORAGE_KEY,
920
+ JSON.stringify(messages.slice(-MAX_STORED_MESSAGES))
921
+ );
922
+ } catch {
923
+ }
924
+ }
925
+ function NavieoProvider({
926
+ children,
927
+ currentRoute,
928
+ onNavigate,
929
+ apiEndpoint = "/api/guide",
930
+ appId,
931
+ suggestions,
932
+ renderChat = true
933
+ }) {
934
+ const apiKey = typeof process !== "undefined" ? process.env?.NEXT_PUBLIC_NAVIEO_PUBLISHABLE_KEY : void 0;
935
+ const [isChatOpen, setIsChatOpen] = (0, import_react4.useState)(false);
936
+ const [tourState, setTourState] = (0, import_react4.useState)("idle");
937
+ const [steps, setSteps] = (0, import_react4.useState)([]);
938
+ const [currentStepIndex, setCurrentStepIndex] = (0, import_react4.useState)(0);
939
+ const [chatHistory, setChatHistory] = (0, import_react4.useState)(loadChatHistory);
940
+ (0, import_react4.useEffect)(() => {
941
+ logger.info("NavieoProvider MOUNTED");
942
+ return () => {
943
+ logger.warn("NavieoProvider UNMOUNTED \u2014 cleaning up");
944
+ tourRef.current.stopTour();
945
+ if (navTimeoutRef.current) clearTimeout(navTimeoutRef.current);
946
+ if (startTourDelayRef.current) clearTimeout(startTourDelayRef.current);
947
+ if (errorRecoveryRef.current) clearTimeout(errorRecoveryRef.current);
948
+ };
949
+ }, []);
950
+ (0, import_react4.useEffect)(() => {
951
+ saveChatHistory(chatHistory);
952
+ }, [chatHistory]);
953
+ (0, import_react4.useEffect)(() => {
954
+ if (tourState !== "active") return;
955
+ const handler = (e) => {
956
+ e.preventDefault();
957
+ };
958
+ window.addEventListener("beforeunload", handler);
959
+ return () => window.removeEventListener("beforeunload", handler);
960
+ }, [tourState]);
961
+ const stepsRef = (0, import_react4.useRef)([]);
962
+ stepsRef.current = steps;
963
+ const chatHistoryRef = (0, import_react4.useRef)(chatHistory);
964
+ chatHistoryRef.current = chatHistory;
965
+ const pendingRouteRef = (0, import_react4.useRef)(null);
966
+ const resetTour = (0, import_react4.useCallback)(() => {
967
+ logger.info("resetTour: resetting tour state to idle");
968
+ setTourState("idle");
969
+ setSteps([]);
970
+ setCurrentStepIndex(0);
971
+ saveTourState(null);
972
+ }, []);
973
+ const navTimeoutRef = (0, import_react4.useRef)(null);
974
+ const startTourDelayRef = (0, import_react4.useRef)(null);
975
+ const errorRecoveryRef = (0, import_react4.useRef)(null);
976
+ const clearNavTimeout = (0, import_react4.useCallback)(() => {
977
+ if (navTimeoutRef.current) {
978
+ clearTimeout(navTimeoutRef.current);
979
+ navTimeoutRef.current = null;
980
+ }
981
+ }, []);
982
+ const tour = useTourRenderer({
983
+ onStepChange: (index) => {
984
+ logger.debug(`Provider.onStepChange: index=${index}`);
985
+ setCurrentStepIndex(index);
986
+ },
987
+ onComplete: () => {
988
+ logger.info("Provider.onComplete: tour finished, resetting");
989
+ clearNavTimeout();
990
+ pendingRouteRef.current = null;
991
+ resetTour();
992
+ },
993
+ onClose: () => {
994
+ logger.info("Provider.onClose: tour closed, resetting");
995
+ clearNavTimeout();
996
+ pendingRouteRef.current = null;
997
+ resetTour();
998
+ },
999
+ onError: (message) => {
1000
+ logger.error(`Provider.onError: ${message}`);
1001
+ pendingRouteRef.current = null;
1002
+ clearNavTimeout();
1003
+ resetTour();
1004
+ setChatHistory((prev) => [
1005
+ ...prev,
1006
+ {
1007
+ role: "assistant",
1008
+ content: CHAT_MESSAGES.tourError(message)
1009
+ }
1010
+ ]);
1011
+ setIsChatOpen(true);
1012
+ },
1013
+ onNavigationNeeded: (route) => {
1014
+ logger.info(`Provider.onNavigationNeeded: route="${route}", currentRoute="${currentRoute}"`);
1015
+ if (routeMatches(currentRoute, route)) {
1016
+ logger.info(`Provider.onNavigationNeeded: ALREADY on target route \u2014 continuing in 500ms`);
1017
+ saveTourState(null);
1018
+ const timer = setTimeout(() => {
1019
+ logger.debug(`Provider.onNavigationNeeded: delayed continueAfterNavigation("${route}")`);
1020
+ tourRef.current.continueAfterNavigation(route);
1021
+ }, 500);
1022
+ navTimeoutRef.current = timer;
1023
+ return;
1024
+ }
1025
+ pendingRouteRef.current = route;
1026
+ saveTourState({ steps: stepsRef.current, pendingRoute: route });
1027
+ clearNavTimeout();
1028
+ navTimeoutRef.current = setTimeout(() => {
1029
+ if (pendingRouteRef.current) {
1030
+ logger.warn(`Provider: navigation timeout \u2014 route "${route}" not reached after ${NAV_TIMEOUT_MS / 1e3}s, resetting`);
1031
+ pendingRouteRef.current = null;
1032
+ saveTourState(null);
1033
+ tourRef.current.stopTour();
1034
+ resetTour();
1035
+ setChatHistory((prev) => [
1036
+ ...prev,
1037
+ { role: "assistant", content: CHAT_MESSAGES.navigationTimeout }
1038
+ ]);
1039
+ setIsChatOpen(true);
1040
+ }
1041
+ }, NAV_TIMEOUT_MS);
1042
+ if (!route.startsWith("/") || route.startsWith("//")) {
1043
+ logger.error(`Provider.onNavigationNeeded: rejected invalid route "${route}"`);
1044
+ return;
1045
+ }
1046
+ if (isDynamicRoute(route)) {
1047
+ logger.info(`Provider.onNavigationNeeded: dynamic route "${route}" \u2014 waiting for user navigation`);
1048
+ } else {
1049
+ logger.info(`Provider.onNavigationNeeded: static route \u2014 calling onNavigate("${route}")`);
1050
+ onNavigate(route);
1051
+ }
1052
+ }
1053
+ });
1054
+ const tourRef = (0, import_react4.useRef)(tour);
1055
+ tourRef.current = tour;
1056
+ (0, import_react4.useEffect)(() => {
1057
+ const persisted = loadTourState();
1058
+ if (persisted && persisted.steps.length > 0) {
1059
+ logger.info(`Provider: restoring persisted tour: ${persisted.steps.length} steps, pendingRoute="${persisted.pendingRoute}"`);
1060
+ setSteps(persisted.steps);
1061
+ setTourState("active");
1062
+ const timer = setTimeout(() => {
1063
+ saveTourState(null);
1064
+ logger.info("Provider: calling restoreTour from persisted state");
1065
+ tourRef.current.restoreTour(persisted.steps, currentRoute);
1066
+ }, 500);
1067
+ return () => clearTimeout(timer);
1068
+ }
1069
+ }, []);
1070
+ (0, import_react4.useEffect)(() => {
1071
+ logger.debug(
1072
+ `Route watcher fired: currentRoute="${currentRoute}", tourState="${tourState}", stepIndex=${currentStepIndex}, stepsCount=${stepsRef.current.length}, pendingRoute=${pendingRouteRef.current || "null"}`
1073
+ );
1074
+ if (pendingRouteRef.current) {
1075
+ const matches = routeMatches(currentRoute, pendingRouteRef.current);
1076
+ logger.info(`Provider route watcher: currentRoute="${currentRoute}", pending="${pendingRouteRef.current}", matches=${matches}`);
1077
+ if (matches) {
1078
+ const pendingPattern = pendingRouteRef.current;
1079
+ pendingRouteRef.current = null;
1080
+ clearNavTimeout();
1081
+ saveTourState(null);
1082
+ const timer = setTimeout(() => {
1083
+ logger.info(`Provider route watcher: DOM ready, continuing tour for "${pendingPattern}"`);
1084
+ tourRef.current.continueAfterNavigation(pendingPattern);
1085
+ }, 500);
1086
+ return () => clearTimeout(timer);
1087
+ }
1088
+ return;
1089
+ }
1090
+ if (tourState === "active" && stepsRef.current.length > 0) {
1091
+ const currentStepRoute = stepsRef.current[currentStepIndex]?.route;
1092
+ if (currentStepRoute && !routeMatches(currentRoute, currentStepRoute)) {
1093
+ const futureStep = stepsRef.current.find(
1094
+ (s, i) => i > currentStepIndex && routeMatches(currentRoute, s.route)
1095
+ );
1096
+ if (futureStep) {
1097
+ logger.info(`Provider route watcher (Case 2): auto-detected navigation to future step route "${futureStep.route}"`);
1098
+ saveTourState(null);
1099
+ const timer = setTimeout(() => {
1100
+ tourRef.current.continueAfterNavigation(futureStep.route);
1101
+ }, 500);
1102
+ return () => clearTimeout(timer);
1103
+ }
1104
+ }
1105
+ }
1106
+ }, [currentRoute, clearNavTimeout, tourState, currentStepIndex]);
1107
+ const openChat = (0, import_react4.useCallback)(() => setIsChatOpen(true), []);
1108
+ const closeChat = (0, import_react4.useCallback)(() => setIsChatOpen(false), []);
1109
+ const toggleChat = (0, import_react4.useCallback)(() => setIsChatOpen((v) => !v), []);
1110
+ const clearChatHistory = (0, import_react4.useCallback)(() => {
1111
+ setChatHistory([]);
1112
+ saveChatHistory([]);
1113
+ }, []);
1114
+ const abortControllerRef = (0, import_react4.useRef)(null);
1115
+ const lastRequestTimeRef = (0, import_react4.useRef)(0);
1116
+ const startTour = (0, import_react4.useCallback)(
1117
+ async (query) => {
1118
+ const trimmedQuery = sanitizeInput(query.slice(0, 500));
1119
+ logger.info(`Provider.startTour: query="${trimmedQuery}", currentRoute="${currentRoute}"`);
1120
+ const now = Date.now();
1121
+ if (now - lastRequestTimeRef.current < MIN_REQUEST_INTERVAL_MS) {
1122
+ logger.warn("Provider.startTour: request throttled (too fast)");
1123
+ return;
1124
+ }
1125
+ lastRequestTimeRef.current = now;
1126
+ if (!isValidEndpoint(apiEndpoint)) {
1127
+ logger.error(`Provider.startTour: invalid API endpoint "${apiEndpoint}"`);
1128
+ setChatHistory((prev) => [
1129
+ ...prev,
1130
+ { role: "user", content: trimmedQuery },
1131
+ { role: "assistant", content: CHAT_MESSAGES.genericError }
1132
+ ]);
1133
+ setTourState("error");
1134
+ return;
1135
+ }
1136
+ if (abortControllerRef.current) {
1137
+ logger.info("Provider.startTour: aborting previous in-flight request");
1138
+ abortControllerRef.current.abort();
1139
+ }
1140
+ if (startTourDelayRef.current) {
1141
+ clearTimeout(startTourDelayRef.current);
1142
+ startTourDelayRef.current = null;
1143
+ }
1144
+ if (errorRecoveryRef.current) {
1145
+ clearTimeout(errorRecoveryRef.current);
1146
+ errorRecoveryRef.current = null;
1147
+ }
1148
+ const controller = new AbortController();
1149
+ abortControllerRef.current = controller;
1150
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1151
+ setTourState("loading");
1152
+ setChatHistory((prev) => [...prev, { role: "user", content: trimmedQuery }]);
1153
+ try {
1154
+ const headers = { "Content-Type": "application/json" };
1155
+ if (apiKey) headers["x-api-key"] = apiKey;
1156
+ const recentContext = chatHistoryRef.current.slice(-CONTEXT_WINDOW).map((m) => ({ role: m.role, content: m.content }));
1157
+ logger.debug(`Provider.startTour: fetching ${apiEndpoint} with ${recentContext.length} context messages`);
1158
+ const response = await fetch(apiEndpoint, {
1159
+ method: "POST",
1160
+ headers,
1161
+ body: JSON.stringify({
1162
+ query: trimmedQuery,
1163
+ currentRoute,
1164
+ appId,
1165
+ chatContext: recentContext
1166
+ }),
1167
+ signal: controller.signal
1168
+ });
1169
+ logger.debug(`Provider.startTour: API response status=${response.status}`);
1170
+ if (!response.ok) {
1171
+ const errorText = await response.text().catch(() => "");
1172
+ logger.error(`Provider.startTour: API error ${response.status}: ${errorText.slice(0, 200)}`);
1173
+ throw new Error("The guide service returned an error. Please try again.");
1174
+ }
1175
+ let data;
1176
+ try {
1177
+ data = await response.json();
1178
+ } catch {
1179
+ throw new Error("Invalid JSON response from API");
1180
+ }
1181
+ const parsed = data;
1182
+ const steps2 = Array.isArray(parsed?.steps) ? parsed.steps : [];
1183
+ const answer = typeof parsed?.answer === "string" ? parsed.answer : void 0;
1184
+ const error = typeof parsed?.error === "string" ? parsed.error : void 0;
1185
+ logger.info(`Provider.startTour: API returned ${steps2.length} steps, answer=${!!answer}, error=${error || "none"}`);
1186
+ if (error) {
1187
+ logger.warn(`Provider.startTour: API returned error: ${error}`);
1188
+ setTourState("error");
1189
+ setChatHistory((prev) => [
1190
+ ...prev,
1191
+ { role: "assistant", content: CHAT_MESSAGES.genericError }
1192
+ ]);
1193
+ return;
1194
+ }
1195
+ if (steps2.length === 0) {
1196
+ logger.info("Provider.startTour: 0 steps returned \u2014 showing answer text");
1197
+ setTourState("idle");
1198
+ setChatHistory((prev) => [
1199
+ ...prev,
1200
+ {
1201
+ role: "assistant",
1202
+ content: answer || CHAT_MESSAGES.noStepsFound
1203
+ }
1204
+ ]);
1205
+ return;
1206
+ }
1207
+ logger.debugSteps("Provider.startTour: raw steps from API", steps2.map((s, i) => ({
1208
+ step: i + 1,
1209
+ route: s.route,
1210
+ title: s.tooltipTitle || "(no title)",
1211
+ text: (s.tooltipText || "").slice(0, 60) + "...",
1212
+ cssSelector: s.selector?.cssSelector || "(none)",
1213
+ elementId: s.elementId || "(none)"
1214
+ })));
1215
+ const validSteps = steps2.filter(
1216
+ (s) => s && typeof s.route === "string" && typeof s.tooltipText === "string"
1217
+ ).slice(0, MAX_TOUR_STEPS);
1218
+ if (steps2.length > MAX_TOUR_STEPS) {
1219
+ logger.warn(`Provider.startTour: truncated ${steps2.length} steps to ${MAX_TOUR_STEPS}`);
1220
+ }
1221
+ for (const s of validSteps) {
1222
+ s.route = s.route.trim();
1223
+ if (s.elementId) s.elementId = s.elementId.trim();
1224
+ if (s.selector) {
1225
+ if (s.selector.cssSelector) s.selector.cssSelector = s.selector.cssSelector.trim();
1226
+ if (s.selector.testId) s.selector.testId = s.selector.testId.trim();
1227
+ if (s.selector.ariaLabel) s.selector.ariaLabel = s.selector.ariaLabel.trim();
1228
+ }
1229
+ }
1230
+ logger.info(`Provider.startTour: ${validSteps.length}/${steps2.length} steps passed validation`);
1231
+ if (validSteps.length === 0) {
1232
+ logger.warn("Provider.startTour: no valid steps after filtering");
1233
+ setTourState("idle");
1234
+ setChatHistory((prev) => [
1235
+ ...prev,
1236
+ { role: "assistant", content: answer || CHAT_MESSAGES.invalidSteps }
1237
+ ]);
1238
+ return;
1239
+ }
1240
+ setSteps(validSteps);
1241
+ setCurrentStepIndex(0);
1242
+ setTourState("active");
1243
+ setChatHistory((prev) => [
1244
+ ...prev,
1245
+ {
1246
+ role: "assistant",
1247
+ content: answer || CHAT_MESSAGES.tourStarted(validSteps.length)
1248
+ }
1249
+ ]);
1250
+ logger.info("Provider.startTour: closing chat, scheduling tourRenderer.startTour in 300ms");
1251
+ setIsChatOpen(false);
1252
+ if (startTourDelayRef.current) clearTimeout(startTourDelayRef.current);
1253
+ startTourDelayRef.current = setTimeout(() => {
1254
+ startTourDelayRef.current = null;
1255
+ logger.info(`Provider.startTour: 300ms elapsed, calling tourRef.current.startTour(${validSteps.length} steps, "${currentRoute}")`);
1256
+ tourRef.current.startTour(validSteps, currentRoute);
1257
+ }, 300);
1258
+ } catch (err) {
1259
+ if (err instanceof DOMException && err.name === "AbortError") {
1260
+ logger.debug("Provider.startTour: request aborted (superseded by newer request)");
1261
+ return;
1262
+ }
1263
+ logger.error("Provider.startTour: caught exception", err);
1264
+ setTourState("error");
1265
+ setChatHistory((prev) => [
1266
+ ...prev,
1267
+ {
1268
+ role: "assistant",
1269
+ content: CHAT_MESSAGES.genericError
1270
+ }
1271
+ ]);
1272
+ if (errorRecoveryRef.current) clearTimeout(errorRecoveryRef.current);
1273
+ errorRecoveryRef.current = setTimeout(() => {
1274
+ errorRecoveryRef.current = null;
1275
+ setTourState("idle");
1276
+ }, 2e3);
1277
+ } finally {
1278
+ clearTimeout(timeoutId);
1279
+ if (abortControllerRef.current === controller) {
1280
+ abortControllerRef.current = null;
1281
+ }
1282
+ }
1283
+ },
1284
+ [apiEndpoint, currentRoute, appId, apiKey]
1285
+ );
1286
+ const nextStep = (0, import_react4.useCallback)(() => tourRef.current.nextStep(), []);
1287
+ const prevStep = (0, import_react4.useCallback)(() => tourRef.current.prevStep(), []);
1288
+ const endTour = (0, import_react4.useCallback)(() => {
1289
+ logger.info("Provider.endTour: user explicitly stopped tour");
1290
+ pendingRouteRef.current = null;
1291
+ clearNavTimeout();
1292
+ saveTourState(null);
1293
+ tourRef.current.stopTour();
1294
+ resetTour();
1295
+ }, [resetTour, clearNavTimeout]);
1296
+ const goToStep = (0, import_react4.useCallback)(
1297
+ (index) => {
1298
+ if (index >= 0 && index < steps.length) tourRef.current.goToStep(index);
1299
+ },
1300
+ [steps.length]
1301
+ );
1302
+ const currentStep = steps[currentStepIndex] ?? null;
1303
+ const value = (0, import_react4.useMemo)(
1304
+ () => ({
1305
+ isChatOpen,
1306
+ openChat,
1307
+ closeChat,
1308
+ toggleChat,
1309
+ tourState,
1310
+ steps,
1311
+ currentStepIndex,
1312
+ currentStep,
1313
+ startTour,
1314
+ nextStep,
1315
+ prevStep,
1316
+ endTour,
1317
+ goToStep,
1318
+ chatHistory,
1319
+ clearChatHistory,
1320
+ currentRoute,
1321
+ suggestions
1322
+ }),
1323
+ [
1324
+ isChatOpen,
1325
+ openChat,
1326
+ closeChat,
1327
+ toggleChat,
1328
+ tourState,
1329
+ steps,
1330
+ currentStepIndex,
1331
+ currentStep,
1332
+ startTour,
1333
+ nextStep,
1334
+ prevStep,
1335
+ endTour,
1336
+ goToStep,
1337
+ chatHistory,
1338
+ clearChatHistory,
1339
+ currentRoute,
1340
+ suggestions
1341
+ ]
1342
+ );
1343
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(NavieoContext.Provider, { value, children: [
1344
+ children,
1345
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NavieoErrorBoundary, { children: renderChat && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChatWidget, {}) })
1346
+ ] });
1347
+ }
1348
+ // Annotate the CommonJS export names for ESM import in node:
1349
+ 0 && (module.exports = {
1350
+ ChatWidget,
1351
+ NavieoProvider,
1352
+ logger,
1353
+ useNavieo,
1354
+ useTourRenderer
1355
+ });
1356
+ //# sourceMappingURL=index.js.map