@eventop/sdk 1.2.2 → 1.2.10
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 +86 -22
- package/dist/core.cjs +1347 -983
- package/dist/core.js +1347 -983
- package/dist/index.cjs +33 -36
- package/dist/index.d.ts +118 -35
- package/dist/index.js +33 -36
- package/dist/react/core.cjs +1347 -983
- package/dist/react/core.js +1347 -983
- package/dist/react/index.cjs +33 -36
- package/dist/react/index.d.ts +91 -13
- package/dist/react/index.js +33 -36
- package/package.json +1 -1
package/dist/react/core.cjs
CHANGED
|
@@ -1,231 +1,509 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
// Single source of truth for all mutable SDK state.
|
|
4
|
+
// Every module imports from here so there is no circular dependency on core.js.
|
|
5
|
+
//
|
|
6
|
+
// NOTE: modules should mutate these properties directly, e.g.
|
|
7
|
+
// import * as state from './state.js';
|
|
8
|
+
// state.tour = new Shepherd.Tour(...);
|
|
9
|
+
|
|
10
|
+
/** @type {Function|null} The resolved AI provider function */
|
|
11
|
+
let provider = null;
|
|
12
|
+
|
|
13
|
+
/** @type {object|null} The merged SDK config object */
|
|
14
|
+
let config = null;
|
|
15
|
+
|
|
16
|
+
/** @type {Function|null} Framework router function (e.g. React Router's navigate) */
|
|
17
|
+
let router = null;
|
|
18
|
+
|
|
19
|
+
/** @type {object|null} Active Shepherd.Tour instance */
|
|
20
|
+
let tour = null;
|
|
21
|
+
|
|
22
|
+
/** @type {boolean} Whether the chat panel is currently visible */
|
|
23
|
+
let isOpen = false;
|
|
24
|
+
|
|
25
|
+
/** @type {Array<{role:string,content:string}>} AI conversation history */
|
|
26
|
+
let messages = [];
|
|
27
|
+
|
|
28
|
+
/** @type {Array<object>|null} Steps saved when a tour is paused */
|
|
29
|
+
let pausedSteps = null;
|
|
30
|
+
|
|
31
|
+
/** @type {number} Step index to resume from after a pause */
|
|
32
|
+
let pausedIndex = 0;
|
|
33
|
+
|
|
34
|
+
/** @type {Array<Function>} Cleanup callbacks — called when tour ends or is paused */
|
|
35
|
+
let cleanups = [];
|
|
36
|
+
|
|
37
|
+
// ─── Setters ─────────────────────────────────────────────────────────────────
|
|
38
|
+
// Using setter functions keeps mutations explicit and grep-friendly.
|
|
39
|
+
|
|
40
|
+
function setProvider(v) {
|
|
41
|
+
provider = v;
|
|
42
|
+
}
|
|
43
|
+
function setConfig(v) {
|
|
44
|
+
config = v;
|
|
45
|
+
}
|
|
46
|
+
function setRouter(v) {
|
|
47
|
+
router = v;
|
|
48
|
+
}
|
|
49
|
+
function setTour(v) {
|
|
50
|
+
tour = v;
|
|
51
|
+
}
|
|
52
|
+
function setIsOpen(v) {
|
|
53
|
+
isOpen = v;
|
|
54
|
+
}
|
|
55
|
+
function setMessages(v) {
|
|
56
|
+
messages = v;
|
|
57
|
+
}
|
|
58
|
+
function setPausedSteps(v) {
|
|
59
|
+
pausedSteps = v;
|
|
60
|
+
}
|
|
61
|
+
function setPausedIndex(v) {
|
|
62
|
+
pausedIndex = v;
|
|
63
|
+
}
|
|
64
|
+
function pushCleanup(fn) {
|
|
65
|
+
cleanups.push(fn);
|
|
66
|
+
}
|
|
67
|
+
function runAndClearCleanups() {
|
|
68
|
+
cleanups.forEach(fn => fn());
|
|
69
|
+
cleanups = [];
|
|
16
70
|
}
|
|
17
71
|
|
|
18
|
-
|
|
72
|
+
// Factory helpers for creating AI provider functions.
|
|
73
|
+
// Extend this file to add new built-in providers (OpenAI, Gemini, etc.).
|
|
19
74
|
|
|
20
|
-
|
|
75
|
+
const providers = {
|
|
76
|
+
custom(fn) {
|
|
77
|
+
if (typeof fn !== 'function') {
|
|
78
|
+
throw new Error('[Eventop] providers.custom() requires a function');
|
|
79
|
+
}
|
|
80
|
+
return fn;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Resolves design tokens from user config and builds CSS variable strings.
|
|
85
|
+
|
|
86
|
+
const DARK_TOKENS = {
|
|
87
|
+
accent: '#e94560',
|
|
88
|
+
accentSecondary: '#a855f7',
|
|
89
|
+
bg: '#0f0f1a',
|
|
90
|
+
surface: '#1a1a2e',
|
|
91
|
+
border: '#2a2a4a',
|
|
92
|
+
text: '#e0e0f0',
|
|
93
|
+
textDim: '#6060a0',
|
|
94
|
+
radius: '16px',
|
|
95
|
+
fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif"
|
|
96
|
+
};
|
|
97
|
+
const LIGHT_TOKENS = {
|
|
98
|
+
accent: '#e94560',
|
|
99
|
+
accentSecondary: '#7c3aed',
|
|
100
|
+
bg: '#ffffff',
|
|
101
|
+
surface: '#f8f8fc',
|
|
102
|
+
border: '#e4e4f0',
|
|
103
|
+
text: '#1a1a2e',
|
|
104
|
+
textDim: '#888899',
|
|
105
|
+
radius: '16px',
|
|
106
|
+
fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif"
|
|
107
|
+
};
|
|
108
|
+
const PRESETS = {
|
|
109
|
+
default: {},
|
|
110
|
+
minimal: {
|
|
111
|
+
accent: '#000000',
|
|
112
|
+
accentSecondary: '#333333',
|
|
113
|
+
radius: '8px'
|
|
114
|
+
},
|
|
115
|
+
soft: {
|
|
116
|
+
accent: '#6366f1',
|
|
117
|
+
accentSecondary: '#8b5cf6',
|
|
118
|
+
radius: '20px'
|
|
119
|
+
},
|
|
120
|
+
glass: {
|
|
121
|
+
radius: '14px'
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function resolveTheme(themeConfig = {}) {
|
|
125
|
+
var _window$matchMedia, _window;
|
|
126
|
+
const {
|
|
127
|
+
mode = 'auto',
|
|
128
|
+
preset = 'default',
|
|
129
|
+
tokens = {}
|
|
130
|
+
} = themeConfig;
|
|
131
|
+
const isDark = mode === 'auto' ? ((_window$matchMedia = (_window = window).matchMedia) === null || _window$matchMedia === void 0 ? void 0 : _window$matchMedia.call(_window, '(prefers-color-scheme: dark)').matches) ?? true : mode === 'dark';
|
|
132
|
+
const base = isDark ? {
|
|
133
|
+
...DARK_TOKENS
|
|
134
|
+
} : {
|
|
135
|
+
...LIGHT_TOKENS
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
...(PRESETS[preset] || {}),
|
|
140
|
+
...tokens,
|
|
141
|
+
_isDark: isDark
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function buildCSSVars(t) {
|
|
145
|
+
return `
|
|
146
|
+
--sai-accent: ${t.accent};
|
|
147
|
+
--sai-accent2: ${t.accentSecondary};
|
|
148
|
+
--sai-bg: ${t.bg};
|
|
149
|
+
--sai-surface: ${t.surface};
|
|
150
|
+
--sai-border: ${t.border};
|
|
151
|
+
--sai-text: ${t.text};
|
|
152
|
+
--sai-text-dim: ${t.textDim};
|
|
153
|
+
--sai-radius: ${t.radius};
|
|
154
|
+
--sai-font: ${t.fontFamily};
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
function applyTheme(t) {
|
|
158
|
+
const panel = document.getElementById('sai-panel');
|
|
159
|
+
const trigger = document.getElementById('sai-trigger');
|
|
160
|
+
if (panel) panel.style.cssText += buildCSSVars(t);
|
|
161
|
+
if (trigger) trigger.style.cssText += buildCSSVars(t);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Converts a corner/offset config into CSS position values for the
|
|
165
|
+
// trigger button and the chat panel.
|
|
166
|
+
|
|
167
|
+
function resolvePosition(posConfig = {}) {
|
|
168
|
+
const {
|
|
169
|
+
corner = 'bottom-right',
|
|
170
|
+
offsetX = 28,
|
|
171
|
+
offsetY = 28
|
|
172
|
+
} = posConfig;
|
|
173
|
+
const isLeft = corner.includes('left');
|
|
174
|
+
const isTop = corner.includes('top');
|
|
175
|
+
return {
|
|
176
|
+
trigger: {
|
|
177
|
+
[isLeft ? 'left' : 'right']: `${offsetX}px`,
|
|
178
|
+
[isTop ? 'top' : 'bottom']: `${offsetY}px`
|
|
179
|
+
},
|
|
180
|
+
panel: {
|
|
181
|
+
[isLeft ? 'left' : 'right']: `${offsetX}px`,
|
|
182
|
+
[isTop ? 'top' : 'bottom']: `${offsetY + 56 + 12}px`,
|
|
183
|
+
transformOrigin: isTop ? 'top center' : 'bottom center'
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function positionToCSS(obj) {
|
|
188
|
+
return Object.entries(obj).map(([k, v]) => `${k}:${v}`).join(';');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Injects the single <style> block that themes both the chat panel and the
|
|
192
|
+
// Shepherd step overlays. Split from the UI builder so styles can be refreshed
|
|
193
|
+
// independently on theme changes.
|
|
194
|
+
|
|
195
|
+
const SHEPHERD_CSS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/css/shepherd.css';
|
|
196
|
+
function loadShepherdCSS() {
|
|
197
|
+
if (document.querySelector(`link[href="${SHEPHERD_CSS}"]`)) return;
|
|
198
|
+
const l = document.createElement('link');
|
|
199
|
+
l.rel = 'stylesheet';
|
|
200
|
+
l.href = SHEPHERD_CSS;
|
|
201
|
+
document.head.appendChild(l);
|
|
202
|
+
}
|
|
21
203
|
|
|
22
204
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* ║ Provider: always proxy through your own server. ║
|
|
28
|
-
* ║ Never expose API keys in client-side code. ║
|
|
29
|
-
* ╚══════════════════════════════════════════════════════════════╝
|
|
205
|
+
* Injects (or replaces) the #sai-styles <style> element.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} theme — resolved theme token object
|
|
208
|
+
* @param {object} pos — resolved position object from resolvePosition()
|
|
30
209
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
210
|
+
function injectStyles(theme, pos) {
|
|
211
|
+
if (document.getElementById('sai-styles')) return;
|
|
212
|
+
const triggerCSS = positionToCSS(pos.trigger);
|
|
213
|
+
const panelCSS = positionToCSS(pos.panel);
|
|
214
|
+
const isDark = theme._isDark;
|
|
215
|
+
const stepBg = isDark ? 'var(--sai-bg)' : '#ffffff';
|
|
216
|
+
const stepSurf = isDark ? 'var(--sai-surface)' : '#f8f8fc';
|
|
217
|
+
const stepText = isDark ? 'var(--sai-text)' : '#1a1a2e';
|
|
218
|
+
const stepBorder = isDark ? 'var(--sai-border)' : '#e4e4f0';
|
|
219
|
+
const style = document.createElement('style');
|
|
220
|
+
style.id = 'sai-styles';
|
|
221
|
+
style.textContent = `
|
|
222
|
+
#sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
|
|
223
|
+
|
|
224
|
+
/* ── Trigger ── */
|
|
225
|
+
#sai-trigger {
|
|
226
|
+
position: fixed; ${triggerCSS};
|
|
227
|
+
width: 54px; height: 54px; border-radius: 50%;
|
|
228
|
+
background: var(--sai-surface); border: 2px solid var(--sai-accent);
|
|
229
|
+
color: var(--sai-text); font-size: 20px; cursor: pointer; z-index: 99998;
|
|
230
|
+
display: flex; align-items: center; justify-content: center;
|
|
231
|
+
box-shadow: 0 4px 20px color-mix(in srgb, var(--sai-accent) 40%, transparent);
|
|
232
|
+
transition: transform .2s ease, box-shadow .2s ease;
|
|
233
|
+
font-family: var(--sai-font); padding: 0;
|
|
36
234
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} else if (typeof global !== 'undefined') {
|
|
41
|
-
global.Eventop = Eventop;
|
|
235
|
+
#sai-trigger:hover {
|
|
236
|
+
transform: scale(1.08);
|
|
237
|
+
box-shadow: 0 6px 28px color-mix(in srgb, var(--sai-accent) 55%, transparent);
|
|
42
238
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// ─── Internal state ──────────────────────────────────────────────────────────
|
|
49
|
-
let _provider = null;
|
|
50
|
-
let _config = null;
|
|
51
|
-
let _tour = null;
|
|
52
|
-
let _isOpen = false;
|
|
53
|
-
let _messages = [];
|
|
54
|
-
let _mediaQuery = null;
|
|
55
|
-
|
|
56
|
-
// Pause/resume state
|
|
57
|
-
let _pausedSteps = null;
|
|
58
|
-
let _pausedIndex = 0;
|
|
59
|
-
|
|
60
|
-
// Active cleanup callbacks — cleared when tour ends or is paused
|
|
61
|
-
let _cleanups = [];
|
|
62
|
-
|
|
63
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
64
|
-
// THEME ENGINE
|
|
65
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
66
|
-
|
|
67
|
-
const DARK_TOKENS = {
|
|
68
|
-
accent: '#e94560',
|
|
69
|
-
accentSecondary: '#a855f7',
|
|
70
|
-
bg: '#0f0f1a',
|
|
71
|
-
surface: '#1a1a2e',
|
|
72
|
-
border: '#2a2a4a',
|
|
73
|
-
text: '#e0e0f0',
|
|
74
|
-
textDim: '#6060a0',
|
|
75
|
-
radius: '16px',
|
|
76
|
-
fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif"
|
|
77
|
-
};
|
|
78
|
-
const LIGHT_TOKENS = {
|
|
79
|
-
accent: '#e94560',
|
|
80
|
-
accentSecondary: '#7c3aed',
|
|
81
|
-
bg: '#ffffff',
|
|
82
|
-
surface: '#f8f8fc',
|
|
83
|
-
border: '#e4e4f0',
|
|
84
|
-
text: '#1a1a2e',
|
|
85
|
-
textDim: '#888899',
|
|
86
|
-
radius: '16px',
|
|
87
|
-
fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif"
|
|
88
|
-
};
|
|
89
|
-
const PRESETS = {
|
|
90
|
-
default: {},
|
|
91
|
-
minimal: {
|
|
92
|
-
accent: '#000000',
|
|
93
|
-
accentSecondary: '#333333',
|
|
94
|
-
radius: '8px'
|
|
95
|
-
},
|
|
96
|
-
soft: {
|
|
97
|
-
accent: '#6366f1',
|
|
98
|
-
accentSecondary: '#8b5cf6',
|
|
99
|
-
radius: '20px'
|
|
100
|
-
},
|
|
101
|
-
glass: {
|
|
102
|
-
radius: '14px'
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
function resolveTheme(themeConfig = {}) {
|
|
106
|
-
var _window$matchMedia, _window;
|
|
107
|
-
const {
|
|
108
|
-
mode = 'auto',
|
|
109
|
-
preset = 'default',
|
|
110
|
-
tokens = {}
|
|
111
|
-
} = themeConfig;
|
|
112
|
-
const isDark = mode === 'auto' ? ((_window$matchMedia = (_window = window).matchMedia) === null || _window$matchMedia === void 0 ? void 0 : _window$matchMedia.call(_window, '(prefers-color-scheme: dark)').matches) ?? true : mode === 'dark';
|
|
113
|
-
const base = isDark ? {
|
|
114
|
-
...DARK_TOKENS
|
|
115
|
-
} : {
|
|
116
|
-
...LIGHT_TOKENS
|
|
117
|
-
};
|
|
118
|
-
return {
|
|
119
|
-
...base,
|
|
120
|
-
...(PRESETS[preset] || {}),
|
|
121
|
-
...tokens,
|
|
122
|
-
_isDark: isDark
|
|
123
|
-
};
|
|
239
|
+
#sai-trigger .sai-pulse {
|
|
240
|
+
position: absolute; width: 100%; height: 100%; border-radius: 50%;
|
|
241
|
+
background: color-mix(in srgb, var(--sai-accent) 30%, transparent);
|
|
242
|
+
animation: sai-pulse 2.2s ease-out infinite; pointer-events: none;
|
|
124
243
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
--sai-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
--sai-border: ${t.border};
|
|
132
|
-
--sai-text: ${t.text};
|
|
133
|
-
--sai-text-dim: ${t.textDim};
|
|
134
|
-
--sai-radius: ${t.radius};
|
|
135
|
-
--sai-font: ${t.fontFamily};
|
|
136
|
-
`;
|
|
244
|
+
#sai-trigger.sai-paused .sai-pulse { animation: none; }
|
|
245
|
+
#sai-trigger.sai-paused::after {
|
|
246
|
+
content: '⏸'; position: absolute; bottom: -2px; right: -2px;
|
|
247
|
+
font-size: 12px; background: var(--sai-accent); border-radius: 50%;
|
|
248
|
+
width: 18px; height: 18px; display: flex; align-items: center;
|
|
249
|
+
justify-content: center; color: #fff; line-height: 1;
|
|
137
250
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
251
|
+
@keyframes sai-pulse {
|
|
252
|
+
0% { transform: scale(1); opacity: .8; }
|
|
253
|
+
100% { transform: scale(1.75); opacity: 0; }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ── Panel ── */
|
|
257
|
+
#sai-panel {
|
|
258
|
+
position: fixed; ${panelCSS};
|
|
259
|
+
width: 340px; background: var(--sai-bg);
|
|
260
|
+
border: 1px solid var(--sai-border); border-radius: var(--sai-radius);
|
|
261
|
+
display: flex; flex-direction: column; z-index: 99999; overflow: hidden;
|
|
262
|
+
box-shadow: 0 16px 48px rgba(0,0,0,.18); font-family: var(--sai-font);
|
|
263
|
+
transform: translateY(12px) scale(.97); opacity: 0; pointer-events: none;
|
|
264
|
+
transition: transform .25s cubic-bezier(.34,1.56,.64,1), opacity .2s ease;
|
|
265
|
+
max-height: 520px;
|
|
147
266
|
}
|
|
267
|
+
#sai-panel.sai-open { transform: translateY(0) scale(1); opacity: 1; pointer-events: all; }
|
|
148
268
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
function resolvePosition(posConfig = {}) {
|
|
154
|
-
const {
|
|
155
|
-
corner = 'bottom-right',
|
|
156
|
-
offsetX = 28,
|
|
157
|
-
offsetY = 28
|
|
158
|
-
} = posConfig;
|
|
159
|
-
const isLeft = corner.includes('left');
|
|
160
|
-
const isTop = corner.includes('top');
|
|
161
|
-
return {
|
|
162
|
-
trigger: {
|
|
163
|
-
[isLeft ? 'left' : 'right']: `${offsetX}px`,
|
|
164
|
-
[isTop ? 'top' : 'bottom']: `${offsetY}px`
|
|
165
|
-
},
|
|
166
|
-
panel: {
|
|
167
|
-
[isLeft ? 'left' : 'right']: `${offsetX}px`,
|
|
168
|
-
[isTop ? 'top' : 'bottom']: `${offsetY + 56 + 12}px`,
|
|
169
|
-
transformOrigin: isTop ? 'top center' : 'bottom center'
|
|
170
|
-
}
|
|
171
|
-
};
|
|
269
|
+
/* ── Header ── */
|
|
270
|
+
#sai-header {
|
|
271
|
+
display: flex; align-items: center; gap: 10px; padding: 13px 15px;
|
|
272
|
+
background: var(--sai-surface); border-bottom: 1px solid var(--sai-border);
|
|
172
273
|
}
|
|
173
|
-
|
|
174
|
-
|
|
274
|
+
#sai-avatar {
|
|
275
|
+
width: 30px; height: 30px; border-radius: 50%;
|
|
276
|
+
background: linear-gradient(135deg, var(--sai-accent), var(--sai-accent2));
|
|
277
|
+
display: flex; align-items: center; justify-content: center;
|
|
278
|
+
font-size: 14px; flex-shrink: 0; color: #fff;
|
|
175
279
|
}
|
|
280
|
+
#sai-header-info { flex: 1; min-width: 0; }
|
|
281
|
+
#sai-header-info h4 {
|
|
282
|
+
margin: 0; font-size: 13px; font-weight: 600; color: var(--sai-text);
|
|
283
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
284
|
+
}
|
|
285
|
+
#sai-header-info p {
|
|
286
|
+
margin: 0; font-size: 11px; color: var(--sai-text-dim);
|
|
287
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
288
|
+
}
|
|
289
|
+
#sai-close {
|
|
290
|
+
background: none; border: none; color: var(--sai-text-dim); cursor: pointer;
|
|
291
|
+
font-size: 18px; line-height: 1; padding: 0; flex-shrink: 0; transition: color .15s;
|
|
292
|
+
}
|
|
293
|
+
#sai-close:hover { color: var(--sai-text); }
|
|
176
294
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
295
|
+
/* ── Messages ── */
|
|
296
|
+
#sai-messages {
|
|
297
|
+
flex: 1; overflow-y: auto; padding: 12px;
|
|
298
|
+
display: flex; flex-direction: column; gap: 9px;
|
|
299
|
+
min-height: 160px; max-height: 280px;
|
|
300
|
+
scrollbar-width: thin; scrollbar-color: var(--sai-border) transparent;
|
|
301
|
+
}
|
|
302
|
+
#sai-messages::-webkit-scrollbar { width: 4px; }
|
|
303
|
+
#sai-messages::-webkit-scrollbar-thumb { background: var(--sai-border); border-radius: 2px; }
|
|
304
|
+
.sai-msg {
|
|
305
|
+
max-width: 86%; padding: 8px 11px; border-radius: 11px;
|
|
306
|
+
font-size: 13px; line-height: 1.55; animation: sai-in .18s ease both;
|
|
307
|
+
}
|
|
308
|
+
@keyframes sai-in {
|
|
309
|
+
from { opacity: 0; transform: translateY(5px); }
|
|
310
|
+
to { opacity: 1; transform: translateY(0); }
|
|
311
|
+
}
|
|
312
|
+
.sai-msg.sai-ai {
|
|
313
|
+
background: var(--sai-surface); color: var(--sai-text);
|
|
314
|
+
border-bottom-left-radius: 3px; align-self: flex-start;
|
|
315
|
+
border: 1px solid var(--sai-border);
|
|
316
|
+
}
|
|
317
|
+
.sai-msg.sai-user {
|
|
318
|
+
background: var(--sai-accent); color: #fff;
|
|
319
|
+
border-bottom-right-radius: 3px; align-self: flex-end;
|
|
320
|
+
}
|
|
321
|
+
.sai-msg.sai-error {
|
|
322
|
+
background: color-mix(in srgb, #ef4444 10%, var(--sai-bg));
|
|
323
|
+
color: #ef4444;
|
|
324
|
+
border: 1px solid color-mix(in srgb, #ef4444 25%, var(--sai-bg));
|
|
325
|
+
align-self: flex-start;
|
|
326
|
+
}
|
|
327
|
+
.sai-msg.sai-nav {
|
|
328
|
+
background: color-mix(in srgb, var(--sai-accent2) 8%, var(--sai-surface));
|
|
329
|
+
color: var(--sai-text);
|
|
330
|
+
border: 1px solid color-mix(in srgb, var(--sai-accent2) 20%, var(--sai-border));
|
|
331
|
+
border-bottom-left-radius: 3px; align-self: flex-start;
|
|
332
|
+
font-size: 12px;
|
|
333
|
+
}
|
|
204
334
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
335
|
+
/* ── Typing indicator ── */
|
|
336
|
+
.sai-typing {
|
|
337
|
+
display: flex; gap: 4px; padding: 9px 12px; align-self: flex-start;
|
|
338
|
+
background: var(--sai-surface); border: 1px solid var(--sai-border);
|
|
339
|
+
border-radius: 11px; border-bottom-left-radius: 3px;
|
|
340
|
+
}
|
|
341
|
+
.sai-typing span {
|
|
342
|
+
width: 5px; height: 5px; border-radius: 50%;
|
|
343
|
+
background: var(--sai-text-dim); animation: sai-bounce 1.2s infinite;
|
|
344
|
+
}
|
|
345
|
+
.sai-typing span:nth-child(2) { animation-delay: .15s; }
|
|
346
|
+
.sai-typing span:nth-child(3) { animation-delay: .3s; }
|
|
347
|
+
@keyframes sai-bounce {
|
|
348
|
+
0%,80%,100% { transform: translateY(0); }
|
|
349
|
+
40% { transform: translateY(-5px); }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ── Suggestions ── */
|
|
353
|
+
#sai-suggestions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px 10px; }
|
|
354
|
+
.sai-chip {
|
|
355
|
+
font-size: 11px; padding: 5px 10px; border-radius: 20px;
|
|
356
|
+
background: transparent; border: 1px solid var(--sai-border);
|
|
357
|
+
color: var(--sai-text-dim); cursor: pointer; font-family: inherit; transition: all .15s;
|
|
358
|
+
}
|
|
359
|
+
.sai-chip:hover {
|
|
360
|
+
border-color: var(--sai-accent); color: var(--sai-accent);
|
|
361
|
+
background: color-mix(in srgb, var(--sai-accent) 6%, transparent);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ── Input row ── */
|
|
365
|
+
#sai-inputrow {
|
|
366
|
+
display: flex; align-items: center; gap: 7px; padding: 9px 11px;
|
|
367
|
+
border-top: 1px solid var(--sai-border); background: var(--sai-bg);
|
|
368
|
+
}
|
|
369
|
+
#sai-input {
|
|
370
|
+
flex: 1; background: var(--sai-surface); border: 1px solid var(--sai-border);
|
|
371
|
+
border-radius: 8px; padding: 7px 11px; color: var(--sai-text);
|
|
372
|
+
font-size: 13px; outline: none; transition: border-color .15s; font-family: inherit;
|
|
373
|
+
}
|
|
374
|
+
#sai-input::placeholder { color: var(--sai-text-dim); }
|
|
375
|
+
#sai-input:focus { border-color: var(--sai-accent); }
|
|
376
|
+
#sai-send {
|
|
377
|
+
width: 32px; height: 32px; flex-shrink: 0; border-radius: 8px;
|
|
378
|
+
background: var(--sai-accent); border: none; color: #fff; font-size: 14px;
|
|
379
|
+
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
380
|
+
transition: filter .15s, transform .1s; line-height: 1;
|
|
381
|
+
}
|
|
382
|
+
#sai-send:hover { filter: brightness(1.1); }
|
|
383
|
+
#sai-send:active { transform: scale(.93); }
|
|
384
|
+
#sai-send:disabled { background: var(--sai-border); cursor: not-allowed; }
|
|
385
|
+
|
|
386
|
+
/* ── Shepherd step overrides ── */
|
|
387
|
+
.sai-shepherd-step {
|
|
388
|
+
background: ${stepBg} !important;
|
|
389
|
+
border: 1px solid ${stepBorder} !important;
|
|
390
|
+
border-radius: 12px !important;
|
|
391
|
+
box-shadow: 0 8px 36px rgba(0,0,0,.18) !important;
|
|
392
|
+
max-width: 290px !important;
|
|
393
|
+
font-family: var(--sai-font, system-ui) !important;
|
|
394
|
+
}
|
|
395
|
+
.sai-shepherd-step.shepherd-has-title .shepherd-content .shepherd-header {
|
|
396
|
+
background: ${stepSurf} !important; padding: 11px 15px 8px !important;
|
|
397
|
+
border-bottom: 1px solid ${stepBorder} !important;
|
|
398
|
+
}
|
|
399
|
+
.sai-shepherd-step .shepherd-title {
|
|
400
|
+
color: var(--sai-accent, #e94560) !important;
|
|
401
|
+
font-size: 13px !important; font-weight: 700 !important;
|
|
402
|
+
}
|
|
403
|
+
.sai-shepherd-step .shepherd-text {
|
|
404
|
+
color: ${stepText} !important; font-size: 13px !important;
|
|
405
|
+
line-height: 1.6 !important; padding: 10px 15px !important;
|
|
406
|
+
}
|
|
407
|
+
.sai-shepherd-step .shepherd-footer {
|
|
408
|
+
border-top: 1px solid ${stepBorder} !important; padding: 8px 11px !important;
|
|
409
|
+
background: ${stepBg} !important; display: flex; gap: 5px; flex-wrap: wrap;
|
|
410
|
+
}
|
|
411
|
+
.sai-shepherd-step .shepherd-button {
|
|
412
|
+
background: var(--sai-accent, #e94560) !important; color: #fff !important;
|
|
413
|
+
border: none !important; border-radius: 6px !important; padding: 6px 13px !important;
|
|
414
|
+
font-size: 12px !important; font-weight: 600 !important; cursor: pointer !important;
|
|
415
|
+
transition: filter .15s !important; font-family: var(--sai-font, system-ui) !important;
|
|
416
|
+
}
|
|
417
|
+
.sai-shepherd-step .shepherd-button:hover { filter: brightness(1.1) !important; }
|
|
418
|
+
.sai-shepherd-step .shepherd-button-secondary {
|
|
419
|
+
background: transparent !important; color: var(--sai-text-dim, #888) !important;
|
|
420
|
+
border: 1px solid ${stepBorder} !important;
|
|
421
|
+
}
|
|
422
|
+
.sai-shepherd-step .shepherd-button-secondary:hover {
|
|
423
|
+
background: ${stepSurf} !important; color: ${stepText} !important;
|
|
424
|
+
}
|
|
425
|
+
.sai-shepherd-step .shepherd-cancel-icon { color: var(--sai-text-dim, #888) !important; }
|
|
426
|
+
.sai-shepherd-step .shepherd-cancel-icon:hover { color: ${stepText} !important; }
|
|
427
|
+
.sai-shepherd-step[data-popper-placement] > .shepherd-arrow::before {
|
|
428
|
+
background: ${stepBorder} !important;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* ── Progress bar ── */
|
|
432
|
+
.sai-progress { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; }
|
|
433
|
+
.sai-progress-bar {
|
|
434
|
+
width: 60px; height: 3px; border-radius: 2px; background: ${stepBorder}; overflow: hidden;
|
|
435
|
+
}
|
|
436
|
+
.sai-progress-fill {
|
|
437
|
+
height: 100%; border-radius: 2px;
|
|
438
|
+
background: var(--sai-accent, #e94560); transition: width .3s ease;
|
|
439
|
+
}
|
|
440
|
+
.sai-progress-label {
|
|
441
|
+
font-size: 10px; color: var(--sai-text-dim, #888);
|
|
442
|
+
white-space: nowrap; font-family: var(--sai-font, system-ui);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ── Step error ── */
|
|
446
|
+
.sai-step-error {
|
|
447
|
+
margin-top: 8px; padding: 6px 10px;
|
|
448
|
+
background: color-mix(in srgb, #ef4444 12%, ${stepBg});
|
|
449
|
+
border: 1px solid color-mix(in srgb, #ef4444 25%, ${stepBorder});
|
|
450
|
+
border-radius: 6px; color: #ef4444; font-size: 12px; line-height: 1.5;
|
|
451
|
+
animation: sai-in .18s ease;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* ── Glass preset ── */
|
|
455
|
+
.sai-glass-preset .sai-shepherd-step {
|
|
456
|
+
background: rgba(255, 255, 255, 0.10) !important;
|
|
457
|
+
backdrop-filter: blur(18px) saturate(180%) !important;
|
|
458
|
+
-webkit-backdrop-filter: blur(18px) saturate(180%) !important;
|
|
459
|
+
border: 1px solid rgba(255, 255, 255, 0.22) !important;
|
|
460
|
+
box-shadow: 0 8px 36px rgba(0,0,0,.12) !important;
|
|
461
|
+
}
|
|
462
|
+
.sai-glass-preset .sai-shepherd-step .shepherd-text,
|
|
463
|
+
.sai-glass-preset .sai-shepherd-step .shepherd-title {
|
|
464
|
+
color: #ffffff !important;
|
|
465
|
+
text-shadow: 0 1px 3px rgba(0,0,0,.35);
|
|
466
|
+
}
|
|
467
|
+
.sai-glass-preset .sai-shepherd-step .shepherd-footer,
|
|
468
|
+
.sai-glass-preset .sai-shepherd-step .shepherd-header {
|
|
469
|
+
background: transparent !important;
|
|
470
|
+
border-color: rgba(255,255,255,0.15) !important;
|
|
471
|
+
}
|
|
472
|
+
`;
|
|
473
|
+
document.head.appendChild(style);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Builds the system prompt that is sent to the AI on every request.
|
|
477
|
+
// Keeping this separate makes it easy to iterate on prompt engineering
|
|
478
|
+
// without touching tour or UI logic.
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* @param {object} config — the live _config object from state.js
|
|
482
|
+
* @returns {string}
|
|
483
|
+
*/
|
|
484
|
+
function buildSystemPrompt(config) {
|
|
485
|
+
const screens = [...new Set((config.features || []).filter(f => {
|
|
486
|
+
var _f$screen;
|
|
487
|
+
return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
|
|
488
|
+
}).map(f => f.screen.id))];
|
|
489
|
+
const featureSummary = (config.features || []).map(f => {
|
|
490
|
+
var _f$screen2, _f$flow;
|
|
491
|
+
const entry = {
|
|
492
|
+
id: f.id,
|
|
493
|
+
name: f.name,
|
|
494
|
+
description: f.description,
|
|
495
|
+
screen: ((_f$screen2 = f.screen) === null || _f$screen2 === void 0 ? void 0 : _f$screen2.id) || 'default',
|
|
496
|
+
...(f.route ? {
|
|
497
|
+
route: f.route
|
|
498
|
+
} : {})
|
|
499
|
+
};
|
|
500
|
+
if ((_f$flow = f.flow) !== null && _f$flow !== void 0 && _f$flow.length) {
|
|
501
|
+
entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
|
|
502
|
+
}
|
|
503
|
+
return entry;
|
|
504
|
+
});
|
|
505
|
+
return `
|
|
506
|
+
You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
|
|
229
507
|
Your ONLY job: guide users step-by-step through tasks using the feature map below.
|
|
230
508
|
|
|
231
509
|
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
|
|
@@ -259,816 +537,902 @@ RULES:
|
|
|
259
537
|
the SDK expands them automatically.
|
|
260
538
|
8. Never skip features in a required sequence. Include every step end-to-end.
|
|
261
539
|
`.trim();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Handles the request/response cycle with the AI provider.
|
|
543
|
+
// Parses the structured JSON the AI must return and maintains conversation history.
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Sends the user's message to the provider, updates conversation history,
|
|
548
|
+
* and returns the parsed AI result.
|
|
549
|
+
*
|
|
550
|
+
* @param {string} userMessage
|
|
551
|
+
* @returns {Promise<{ message: string, steps: Array }>}
|
|
552
|
+
*/
|
|
553
|
+
async function callAI(userMessage) {
|
|
554
|
+
const systemPrompt = buildSystemPrompt(config);
|
|
555
|
+
const messagesWithNew = [...messages, {
|
|
556
|
+
role: 'user',
|
|
557
|
+
content: userMessage
|
|
558
|
+
}];
|
|
559
|
+
const result = await provider({
|
|
560
|
+
systemPrompt,
|
|
561
|
+
messages: messagesWithNew
|
|
562
|
+
});
|
|
563
|
+
setMessages([...messages, {
|
|
564
|
+
role: 'user',
|
|
565
|
+
content: userMessage
|
|
566
|
+
}, {
|
|
567
|
+
role: 'assistant',
|
|
568
|
+
content: result.message
|
|
569
|
+
}]);
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Handles all navigation concerns:
|
|
574
|
+
// • Route-based navigation (new, preferred)
|
|
575
|
+
// • Legacy screen-based navigation (backward-compat)
|
|
576
|
+
// • Pre-tour route announcement
|
|
577
|
+
// • Waiting for DOM elements and URL changes
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Returns the current pathname. Centralised so it's easy to mock in tests.
|
|
584
|
+
*/
|
|
585
|
+
function getCurrentRoute() {
|
|
586
|
+
return typeof window !== 'undefined' ? window.location.pathname : '/';
|
|
587
|
+
}
|
|
283
588
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Polls until `window.location.pathname === targetRoute` or timeout.
|
|
591
|
+
* Resolves (never rejects) so a slow router doesn't blow up the tour.
|
|
592
|
+
*
|
|
593
|
+
* @param {string} targetRoute
|
|
594
|
+
* @param {number} [timeout=8000]
|
|
595
|
+
* @returns {Promise<void>}
|
|
596
|
+
*/
|
|
597
|
+
function waitForRouteChange(targetRoute, timeout = 8000) {
|
|
598
|
+
return new Promise(resolve => {
|
|
599
|
+
if (window.location.pathname === targetRoute) return resolve();
|
|
600
|
+
const interval = setInterval(() => {
|
|
601
|
+
if (window.location.pathname === targetRoute) {
|
|
602
|
+
clearInterval(interval);
|
|
603
|
+
clearTimeout(timer);
|
|
604
|
+
resolve();
|
|
605
|
+
}
|
|
606
|
+
}, 50);
|
|
607
|
+
const timer = setTimeout(() => {
|
|
608
|
+
clearInterval(interval);
|
|
609
|
+
console.warn(`[Eventop] Route change to "${targetRoute}" timed out — continuing`);
|
|
610
|
+
resolve();
|
|
611
|
+
}, timeout);
|
|
612
|
+
pushCleanup(() => {
|
|
613
|
+
clearInterval(interval);
|
|
614
|
+
clearTimeout(timer);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
287
618
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
619
|
+
/**
|
|
620
|
+
* Waits for a CSS selector to appear in the DOM (up to `timeout` ms).
|
|
621
|
+
* Resolves without throwing so a missing element doesn't crash the tour.
|
|
622
|
+
*
|
|
623
|
+
* @param {string} selector
|
|
624
|
+
* @param {number} [timeout=8000]
|
|
625
|
+
* @returns {Promise<void>}
|
|
626
|
+
*/
|
|
627
|
+
function waitForElement(selector, timeout = 8000) {
|
|
628
|
+
return new Promise(resolve => {
|
|
629
|
+
if (document.querySelector(selector)) return resolve();
|
|
630
|
+
const timer = setTimeout(() => {
|
|
631
|
+
observer.disconnect();
|
|
632
|
+
console.warn(`[Eventop] waitFor("${selector}") timed out — continuing`);
|
|
633
|
+
resolve();
|
|
634
|
+
}, timeout);
|
|
635
|
+
const observer = new MutationObserver(() => {
|
|
636
|
+
if (document.querySelector(selector)) {
|
|
637
|
+
clearTimeout(timer);
|
|
638
|
+
observer.disconnect();
|
|
639
|
+
resolve();
|
|
294
640
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
641
|
+
});
|
|
642
|
+
observer.observe(document.body, {
|
|
643
|
+
childList: true,
|
|
644
|
+
subtree: true
|
|
645
|
+
});
|
|
646
|
+
pushCleanup(() => {
|
|
647
|
+
clearTimeout(timer);
|
|
648
|
+
observer.disconnect();
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ─── Route navigation ─────────────────────────────────────────────────────────
|
|
298
654
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
655
|
+
/**
|
|
656
|
+
* Navigate to `route` using (in priority order):
|
|
657
|
+
* 1. state.router — the function passed by the developer
|
|
658
|
+
* 2. window.history.pushState + popstate — best-effort fallback for SPAs
|
|
659
|
+
*
|
|
660
|
+
* Shows a friendly chat message, then waits for the URL to confirm the change.
|
|
661
|
+
*
|
|
662
|
+
* @param {string} route
|
|
663
|
+
* @param {string} [featureName]
|
|
664
|
+
* @returns {Promise<void>}
|
|
665
|
+
*/
|
|
666
|
+
async function navigateToRoute(route, featureName) {
|
|
667
|
+
if (!route || getCurrentRoute() === route) return;
|
|
668
|
+
addMsg('ai', `↗ Taking you to ${featureName ? `the ${featureName} area` : route}…`);
|
|
669
|
+
try {
|
|
670
|
+
if (router) {
|
|
671
|
+
await Promise.resolve(router(route));
|
|
672
|
+
} else {
|
|
673
|
+
window.history.pushState({}, '', route);
|
|
674
|
+
window.dispatchEvent(new PopStateEvent('popstate', {
|
|
675
|
+
state: {}
|
|
317
676
|
}));
|
|
318
677
|
}
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.warn('[Eventop] Navigation error:', err);
|
|
680
|
+
}
|
|
681
|
+
await waitForRouteChange(route, 8000);
|
|
682
|
+
await new Promise(r => setTimeout(r, 80));
|
|
683
|
+
}
|
|
319
684
|
|
|
320
|
-
|
|
321
|
-
// SHEPHERD RUNNER
|
|
322
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
685
|
+
// ─── Route preview / announcement ────────────────────────────────────────────
|
|
323
686
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
loadCSS(SHEPHERD_CSS);
|
|
344
|
-
await loadScript(SHEPHERD_JS);
|
|
345
|
-
}
|
|
346
|
-
function waitForElement(selector, timeout = 8000) {
|
|
347
|
-
return new Promise(resolve => {
|
|
348
|
-
if (document.querySelector(selector)) return resolve();
|
|
349
|
-
const timer = setTimeout(() => {
|
|
350
|
-
observer.disconnect();
|
|
351
|
-
console.warn(`[Eventop] waitFor("${selector}") timed out — continuing`);
|
|
352
|
-
resolve();
|
|
353
|
-
}, timeout);
|
|
354
|
-
const observer = new MutationObserver(() => {
|
|
355
|
-
if (document.querySelector(selector)) {
|
|
356
|
-
clearTimeout(timer);
|
|
357
|
-
observer.disconnect();
|
|
358
|
-
resolve();
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
observer.observe(document.body, {
|
|
362
|
-
childList: true,
|
|
363
|
-
subtree: true
|
|
364
|
-
});
|
|
365
|
-
_cleanups.push(() => {
|
|
366
|
-
clearTimeout(timer);
|
|
367
|
-
observer.disconnect();
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
function mergeWithFeature(step) {
|
|
372
|
-
var _config2;
|
|
373
|
-
if (!((_config2 = _config) !== null && _config2 !== void 0 && _config2.features)) return step;
|
|
374
|
-
const feature = _config.features.find(f => f.id === step.id);
|
|
375
|
-
if (!feature) return step;
|
|
376
|
-
return {
|
|
377
|
-
waitFor: feature.waitFor || null,
|
|
378
|
-
advanceOn: feature.advanceOn || null,
|
|
379
|
-
validate: feature.validate || null,
|
|
380
|
-
screen: feature.screen || null,
|
|
381
|
-
flow: feature.flow || null,
|
|
382
|
-
...step,
|
|
383
|
-
selector: feature.selector || step.selector // feature map always wins
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
function wireAdvanceOn(shepherdStep, advanceOn, tour) {
|
|
387
|
-
if (!(advanceOn !== null && advanceOn !== void 0 && advanceOn.selector) || !(advanceOn !== null && advanceOn !== void 0 && advanceOn.event)) return;
|
|
388
|
-
function handler(e) {
|
|
389
|
-
if (e.target.matches(advanceOn.selector) || e.target.closest(advanceOn.selector)) {
|
|
390
|
-
setTimeout(() => {
|
|
391
|
-
if (tour && !tour.isActive()) return;
|
|
392
|
-
tour.next();
|
|
393
|
-
}, advanceOn.delay || 300);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
document.addEventListener(advanceOn.event, handler, true);
|
|
397
|
-
_cleanups.push(() => document.removeEventListener(advanceOn.event, handler, true));
|
|
398
|
-
shepherdStep.on('show', () => {
|
|
399
|
-
var _shepherdStep$getElem;
|
|
400
|
-
const nextBtn = (_shepherdStep$getElem = shepherdStep.getElement()) === null || _shepherdStep$getElem === void 0 ? void 0 : _shepherdStep$getElem.querySelector('.shepherd-footer .shepherd-button:not(.shepherd-button-secondary)');
|
|
401
|
-
if (nextBtn && !shepherdStep._isLast) {
|
|
402
|
-
nextBtn.style.opacity = '0.4';
|
|
403
|
-
nextBtn.title = 'Complete the action above to continue';
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
function addProgressIndicator(shepherdStep, index, total) {
|
|
408
|
-
shepherdStep.on('show', () => {
|
|
409
|
-
var _shepherdStep$getElem2;
|
|
410
|
-
const header = (_shepherdStep$getElem2 = shepherdStep.getElement()) === null || _shepherdStep$getElem2 === void 0 ? void 0 : _shepherdStep$getElem2.querySelector('.shepherd-header');
|
|
411
|
-
if (!header || header.querySelector('.sai-progress')) return;
|
|
412
|
-
const pct = Math.round((index + 1) / total * 100);
|
|
413
|
-
const wrapper = document.createElement('div');
|
|
414
|
-
wrapper.className = 'sai-progress';
|
|
415
|
-
wrapper.innerHTML = `
|
|
416
|
-
<div class="sai-progress-bar">
|
|
417
|
-
<div class="sai-progress-fill" style="width:${pct}%"></div>
|
|
418
|
-
</div>
|
|
419
|
-
<span class="sai-progress-label">${index + 1} / ${total}</span>
|
|
420
|
-
`;
|
|
421
|
-
header.appendChild(wrapper);
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
function showResumeButton(fromIndex) {
|
|
425
|
-
var _document$getElementB, _document$getElementB2;
|
|
426
|
-
const msgs = document.getElementById('sai-messages');
|
|
427
|
-
if (!msgs) return;
|
|
428
|
-
(_document$getElementB = document.getElementById('sai-resume-prompt')) === null || _document$getElementB === void 0 || _document$getElementB.remove();
|
|
429
|
-
const div = document.createElement('div');
|
|
430
|
-
div.id = 'sai-resume-prompt';
|
|
431
|
-
div.className = 'sai-msg sai-ai';
|
|
432
|
-
div.innerHTML = `
|
|
433
|
-
Tour paused. Ready when you are.
|
|
434
|
-
<br/>
|
|
435
|
-
<button class="sai-chip" id="sai-resume-btn" style="display:inline-block;margin-top:8px;">
|
|
436
|
-
▶ Resume from step ${fromIndex + 1}
|
|
437
|
-
</button>
|
|
438
|
-
`;
|
|
439
|
-
msgs.appendChild(div);
|
|
440
|
-
msgs.scrollTop = msgs.scrollHeight;
|
|
441
|
-
(_document$getElementB2 = document.getElementById('sai-resume-btn')) === null || _document$getElementB2 === void 0 || _document$getElementB2.addEventListener('click', () => {
|
|
442
|
-
div.remove();
|
|
443
|
-
if (!_pausedSteps) return;
|
|
444
|
-
const steps = _pausedSteps;
|
|
445
|
-
const idx = _pausedIndex;
|
|
446
|
-
_pausedSteps = null;
|
|
447
|
-
_pausedIndex = 0;
|
|
448
|
-
if (_isOpen) togglePanel();
|
|
449
|
-
runTour(steps.slice(idx));
|
|
687
|
+
/**
|
|
688
|
+
* Inspects the AI's step list and returns which routes will be visited
|
|
689
|
+
* that the user isn't already on — in visit order, deduplicated.
|
|
690
|
+
*
|
|
691
|
+
* @param {Array} aiSteps
|
|
692
|
+
* @returns {Array<{ route: string, featureName: string }>}
|
|
693
|
+
*/
|
|
694
|
+
function previewRoutesNeeded(aiSteps) {
|
|
695
|
+
const currentRoute = getCurrentRoute();
|
|
696
|
+
const seen = new Set([currentRoute]);
|
|
697
|
+
const ordered = [];
|
|
698
|
+
aiSteps.forEach(step => {
|
|
699
|
+
var _state$config;
|
|
700
|
+
const feature = (_state$config = config) === null || _state$config === void 0 || (_state$config = _state$config.features) === null || _state$config === void 0 ? void 0 : _state$config.find(f => f.id === step.id);
|
|
701
|
+
if (feature !== null && feature !== void 0 && feature.route && !seen.has(feature.route)) {
|
|
702
|
+
seen.add(feature.route);
|
|
703
|
+
ordered.push({
|
|
704
|
+
route: feature.route,
|
|
705
|
+
featureName: feature.name
|
|
450
706
|
});
|
|
451
|
-
if (!_isOpen) togglePanel();
|
|
452
707
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (_tour) {
|
|
457
|
-
_tour.cancel();
|
|
458
|
-
}
|
|
459
|
-
_cleanups.forEach(fn => fn());
|
|
460
|
-
_cleanups = [];
|
|
461
|
-
_tour = null;
|
|
462
|
-
if (!(steps !== null && steps !== void 0 && steps.length)) return;
|
|
463
|
-
const {
|
|
464
|
-
showProgress = true,
|
|
465
|
-
waitTimeout = 8000
|
|
466
|
-
} = options;
|
|
467
|
-
|
|
468
|
-
// Merge AI steps with feature map
|
|
469
|
-
const mergedSteps = steps.map(mergeWithFeature);
|
|
470
|
-
|
|
471
|
-
// Navigate to the correct screen for the first step if needed
|
|
472
|
-
const firstFeature = (_config3 = _config) === null || _config3 === void 0 || (_config3 = _config3.features) === null || _config3 === void 0 ? void 0 : _config3.find(f => {
|
|
473
|
-
var _mergedSteps$;
|
|
474
|
-
return f.id === ((_mergedSteps$ = mergedSteps[0]) === null || _mergedSteps$ === void 0 ? void 0 : _mergedSteps$.id);
|
|
475
|
-
});
|
|
476
|
-
if (firstFeature) await ensureOnCorrectScreen(firstFeature);
|
|
708
|
+
});
|
|
709
|
+
return ordered;
|
|
710
|
+
}
|
|
477
711
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
classes: 'sai-shepherd-step'
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
expandedSteps.forEach((step, i) => {
|
|
499
|
-
var _step$advanceOn, _step$advanceOn2;
|
|
500
|
-
const isLast = i === expandedSteps.length - 1;
|
|
501
|
-
const hasAuto = !!((_step$advanceOn = step.advanceOn) !== null && _step$advanceOn !== void 0 && _step$advanceOn.selector && (_step$advanceOn2 = step.advanceOn) !== null && _step$advanceOn2 !== void 0 && _step$advanceOn2.event);
|
|
502
|
-
const buttons = [];
|
|
503
|
-
if (i > 0) {
|
|
504
|
-
buttons.push({
|
|
505
|
-
text: '← Back',
|
|
506
|
-
action: _tour.back,
|
|
507
|
-
secondary: true
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
buttons.push({
|
|
511
|
-
text: isLast ? 'Done ✓' : 'Next →',
|
|
512
|
-
action: isLast ? _tour.complete : _tour.next
|
|
513
|
-
});
|
|
514
|
-
buttons.push({
|
|
515
|
-
text: '⏸ Pause',
|
|
516
|
-
secondary: true,
|
|
517
|
-
action: function () {
|
|
518
|
-
_tour.cancel();
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
const shepherdStep = _tour.addStep({
|
|
522
|
-
id: step.id || `sai-step-${i}`,
|
|
523
|
-
title: step.title,
|
|
524
|
-
text: step.text,
|
|
525
|
-
attachTo: step.selector ? {
|
|
526
|
-
element: step.selector,
|
|
527
|
-
on: step.position || 'bottom'
|
|
528
|
-
} : undefined,
|
|
529
|
-
beforeShowPromise: step.waitFor ? () => waitForElement(step.waitFor, waitTimeout) : undefined,
|
|
530
|
-
buttons
|
|
531
|
-
});
|
|
532
|
-
shepherdStep._isLast = isLast;
|
|
533
|
-
if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, _tour);
|
|
534
|
-
if (showProgress && expandedSteps.length > 1) {
|
|
535
|
-
addProgressIndicator(shepherdStep, i, expandedSteps.length);
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
_tour.on('complete', () => {
|
|
539
|
-
_cleanups.forEach(fn => fn());
|
|
540
|
-
_cleanups = [];
|
|
541
|
-
_pausedSteps = null;
|
|
542
|
-
_pausedIndex = 0;
|
|
543
|
-
_tour = null;
|
|
544
|
-
});
|
|
712
|
+
/**
|
|
713
|
+
* Adds a human-readable pre-tour navigation announcement to the chat.
|
|
714
|
+
*
|
|
715
|
+
* @param {Array<{ route: string, featureName: string }>} routesNeeded
|
|
716
|
+
*/
|
|
717
|
+
function announceNavigationPlan(routesNeeded) {
|
|
718
|
+
if (!routesNeeded.length) return;
|
|
719
|
+
const names = routesNeeded.map(r => r.featureName || r.route);
|
|
720
|
+
let msg;
|
|
721
|
+
if (names.length === 1) {
|
|
722
|
+
msg = `🗺 I'll navigate you to the ${names[0]} area automatically — no need to go there yourself.`;
|
|
723
|
+
} else {
|
|
724
|
+
const list = names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
|
|
725
|
+
msg = `🗺 This tour visits ${names.length} areas: ${list}. I'll navigate between them automatically.`;
|
|
726
|
+
}
|
|
727
|
+
addMsg('ai', msg);
|
|
728
|
+
}
|
|
545
729
|
|
|
546
|
-
|
|
547
|
-
_tour.on('cancel', () => {
|
|
548
|
-
const currentStepEl = _tour.getCurrentStep();
|
|
549
|
-
const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
|
|
550
|
-
_cleanups.forEach(fn => fn());
|
|
551
|
-
_cleanups = [];
|
|
552
|
-
_pausedSteps = expandedSteps;
|
|
553
|
-
_pausedIndex = Math.max(0, currentIdx);
|
|
554
|
-
_tour = null;
|
|
555
|
-
showResumeButton(_pausedIndex);
|
|
556
|
-
});
|
|
557
|
-
_tour.start();
|
|
558
|
-
}
|
|
559
|
-
function stepComplete() {
|
|
560
|
-
var _tour2;
|
|
561
|
-
if ((_tour2 = _tour) !== null && _tour2 !== void 0 && _tour2.isActive()) _tour.next();
|
|
562
|
-
}
|
|
563
|
-
function stepFail(message) {
|
|
564
|
-
var _tour3, _el$querySelector;
|
|
565
|
-
if (!((_tour3 = _tour) !== null && _tour3 !== void 0 && _tour3.isActive())) return;
|
|
566
|
-
const current = _tour.getCurrentStep();
|
|
567
|
-
if (!current) return;
|
|
568
|
-
const el = current.getElement();
|
|
569
|
-
el === null || el === void 0 || (_el$querySelector = el.querySelector('.sai-step-error')) === null || _el$querySelector === void 0 || _el$querySelector.remove();
|
|
570
|
-
if (message) {
|
|
571
|
-
var _el$querySelector2;
|
|
572
|
-
const err = document.createElement('div');
|
|
573
|
-
err.className = 'sai-step-error';
|
|
574
|
-
err.textContent = '⚠ ' + message;
|
|
575
|
-
el === null || el === void 0 || (_el$querySelector2 = el.querySelector('.shepherd-text')) === null || _el$querySelector2 === void 0 || _el$querySelector2.appendChild(err);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
730
|
+
// ─── Legacy screen navigation ─────────────────────────────────────────────────
|
|
578
731
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
/* ── Trigger ── */
|
|
598
|
-
#sai-trigger {
|
|
599
|
-
position: fixed; ${triggerCSS};
|
|
600
|
-
width: 54px; height: 54px; border-radius: 50%;
|
|
601
|
-
background: var(--sai-surface); border: 2px solid var(--sai-accent);
|
|
602
|
-
color: var(--sai-text); font-size: 20px; cursor: pointer; z-index: 99998;
|
|
603
|
-
display: flex; align-items: center; justify-content: center;
|
|
604
|
-
box-shadow: 0 4px 20px color-mix(in srgb, var(--sai-accent) 40%, transparent);
|
|
605
|
-
transition: transform .2s ease, box-shadow .2s ease;
|
|
606
|
-
font-family: var(--sai-font); padding: 0;
|
|
607
|
-
}
|
|
608
|
-
#sai-trigger:hover {
|
|
609
|
-
transform: scale(1.08);
|
|
610
|
-
box-shadow: 0 6px 28px color-mix(in srgb, var(--sai-accent) 55%, transparent);
|
|
611
|
-
}
|
|
612
|
-
#sai-trigger .sai-pulse {
|
|
613
|
-
position: absolute; width: 100%; height: 100%; border-radius: 50%;
|
|
614
|
-
background: color-mix(in srgb, var(--sai-accent) 30%, transparent);
|
|
615
|
-
animation: sai-pulse 2.2s ease-out infinite; pointer-events: none;
|
|
616
|
-
}
|
|
617
|
-
#sai-trigger.sai-paused .sai-pulse { animation: none; }
|
|
618
|
-
#sai-trigger.sai-paused::after {
|
|
619
|
-
content: '⏸'; position: absolute; bottom: -2px; right: -2px;
|
|
620
|
-
font-size: 12px; background: var(--sai-accent); border-radius: 50%;
|
|
621
|
-
width: 18px; height: 18px; display: flex; align-items: center;
|
|
622
|
-
justify-content: center; color: #fff; line-height: 1;
|
|
623
|
-
}
|
|
624
|
-
@keyframes sai-pulse {
|
|
625
|
-
0% { transform: scale(1); opacity: .8; }
|
|
626
|
-
100% { transform: scale(1.75); opacity: 0; }
|
|
627
|
-
}
|
|
732
|
+
/**
|
|
733
|
+
* Handles the legacy `screen` prop on features (backward-compat).
|
|
734
|
+
* Checks `screen.check()` and calls `screen.navigate()` if needed,
|
|
735
|
+
* then waits for `screen.waitFor` or `feature.selector` to appear.
|
|
736
|
+
*
|
|
737
|
+
* @param {object} feature — a feature config object
|
|
738
|
+
* @returns {Promise<void>}
|
|
739
|
+
*/
|
|
740
|
+
async function ensureOnCorrectScreen(feature) {
|
|
741
|
+
if (!feature.screen) return;
|
|
742
|
+
if (typeof feature.screen.check === 'function' && feature.screen.check()) return;
|
|
743
|
+
addMsg('ai', 'Taking you to the right screen first…');
|
|
744
|
+
if (typeof feature.screen.navigate === 'function') {
|
|
745
|
+
feature.screen.navigate();
|
|
746
|
+
}
|
|
747
|
+
const waitSelector = feature.screen.waitFor || feature.selector;
|
|
748
|
+
if (waitSelector) await waitForElement(waitSelector, 10000);
|
|
749
|
+
}
|
|
628
750
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
width: 340px; background: var(--sai-bg);
|
|
633
|
-
border: 1px solid var(--sai-border); border-radius: var(--sai-radius);
|
|
634
|
-
display: flex; flex-direction: column; z-index: 99999; overflow: hidden;
|
|
635
|
-
box-shadow: 0 16px 48px rgba(0,0,0,.18); font-family: var(--sai-font);
|
|
636
|
-
transform: translateY(12px) scale(.97); opacity: 0; pointer-events: none;
|
|
637
|
-
transition: transform .25s cubic-bezier(.34,1.56,.64,1), opacity .2s ease;
|
|
638
|
-
max-height: 520px;
|
|
639
|
-
}
|
|
640
|
-
#sai-panel.sai-open { transform: translateY(0) scale(1); opacity: 1; pointer-events: all; }
|
|
751
|
+
// Builds and manages the chat panel DOM.
|
|
752
|
+
// Also exports the low-level message helpers used by navigation.js and tour.js
|
|
753
|
+
// so they can surface status messages without importing the full chat module.
|
|
641
754
|
|
|
642
|
-
/* ── Header ── */
|
|
643
|
-
#sai-header {
|
|
644
|
-
display: flex; align-items: center; gap: 10px; padding: 13px 15px;
|
|
645
|
-
background: var(--sai-surface); border-bottom: 1px solid var(--sai-border);
|
|
646
|
-
}
|
|
647
|
-
#sai-avatar {
|
|
648
|
-
width: 30px; height: 30px; border-radius: 50%;
|
|
649
|
-
background: linear-gradient(135deg, var(--sai-accent), var(--sai-accent2));
|
|
650
|
-
display: flex; align-items: center; justify-content: center;
|
|
651
|
-
font-size: 14px; flex-shrink: 0; color: #fff;
|
|
652
|
-
}
|
|
653
|
-
#sai-header-info { flex: 1; min-width: 0; }
|
|
654
|
-
#sai-header-info h4 {
|
|
655
|
-
margin: 0; font-size: 13px; font-weight: 600; color: var(--sai-text);
|
|
656
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
657
|
-
}
|
|
658
|
-
#sai-header-info p {
|
|
659
|
-
margin: 0; font-size: 11px; color: var(--sai-text-dim);
|
|
660
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
661
|
-
}
|
|
662
|
-
#sai-close {
|
|
663
|
-
background: none; border: none; color: var(--sai-text-dim); cursor: pointer;
|
|
664
|
-
font-size: 18px; line-height: 1; padding: 0; flex-shrink: 0; transition: color .15s;
|
|
665
|
-
}
|
|
666
|
-
#sai-close:hover { color: var(--sai-text); }
|
|
667
|
-
|
|
668
|
-
/* ── Messages ── */
|
|
669
|
-
#sai-messages {
|
|
670
|
-
flex: 1; overflow-y: auto; padding: 12px;
|
|
671
|
-
display: flex; flex-direction: column; gap: 9px;
|
|
672
|
-
min-height: 160px; max-height: 280px;
|
|
673
|
-
scrollbar-width: thin; scrollbar-color: var(--sai-border) transparent;
|
|
674
|
-
}
|
|
675
|
-
#sai-messages::-webkit-scrollbar { width: 4px; }
|
|
676
|
-
#sai-messages::-webkit-scrollbar-thumb { background: var(--sai-border); border-radius: 2px; }
|
|
677
|
-
.sai-msg {
|
|
678
|
-
max-width: 86%; padding: 8px 11px; border-radius: 11px;
|
|
679
|
-
font-size: 13px; line-height: 1.55; animation: sai-in .18s ease both;
|
|
680
|
-
}
|
|
681
|
-
@keyframes sai-in {
|
|
682
|
-
from { opacity: 0; transform: translateY(5px); }
|
|
683
|
-
to { opacity: 1; transform: translateY(0); }
|
|
684
|
-
}
|
|
685
|
-
.sai-msg.sai-ai {
|
|
686
|
-
background: var(--sai-surface); color: var(--sai-text);
|
|
687
|
-
border-bottom-left-radius: 3px; align-self: flex-start;
|
|
688
|
-
border: 1px solid var(--sai-border);
|
|
689
|
-
}
|
|
690
|
-
.sai-msg.sai-user {
|
|
691
|
-
background: var(--sai-accent); color: #fff;
|
|
692
|
-
border-bottom-right-radius: 3px; align-self: flex-end;
|
|
693
|
-
}
|
|
694
|
-
.sai-msg.sai-error {
|
|
695
|
-
background: color-mix(in srgb, #ef4444 10%, var(--sai-bg));
|
|
696
|
-
color: #ef4444;
|
|
697
|
-
border: 1px solid color-mix(in srgb, #ef4444 25%, var(--sai-bg));
|
|
698
|
-
align-self: flex-start;
|
|
699
|
-
}
|
|
700
755
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
.sai-typing span {
|
|
708
|
-
width: 5px; height: 5px; border-radius: 50%;
|
|
709
|
-
background: var(--sai-text-dim); animation: sai-bounce 1.2s infinite;
|
|
710
|
-
}
|
|
711
|
-
.sai-typing span:nth-child(2) { animation-delay: .15s; }
|
|
712
|
-
.sai-typing span:nth-child(3) { animation-delay: .3s; }
|
|
713
|
-
@keyframes sai-bounce {
|
|
714
|
-
0%,80%,100% { transform: translateY(0); }
|
|
715
|
-
40% { transform: translateY(-5px); }
|
|
716
|
-
}
|
|
756
|
+
// Imported lazily inside handleSend to avoid a circular dep at module parse time.
|
|
757
|
+
// (chat → tour → chat would otherwise cycle.)
|
|
758
|
+
let _runTour = null;
|
|
759
|
+
function setRunTour(fn) {
|
|
760
|
+
_runTour = fn;
|
|
761
|
+
}
|
|
717
762
|
|
|
718
|
-
|
|
719
|
-
#sai-suggestions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px 10px; }
|
|
720
|
-
.sai-chip {
|
|
721
|
-
font-size: 11px; padding: 5px 10px; border-radius: 20px;
|
|
722
|
-
background: transparent; border: 1px solid var(--sai-border);
|
|
723
|
-
color: var(--sai-text-dim); cursor: pointer; font-family: inherit; transition: all .15s;
|
|
724
|
-
}
|
|
725
|
-
.sai-chip:hover {
|
|
726
|
-
border-color: var(--sai-accent); color: var(--sai-accent);
|
|
727
|
-
background: color-mix(in srgb, var(--sai-accent) 6%, transparent);
|
|
728
|
-
}
|
|
763
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
729
764
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
border-top: 1px solid var(--sai-border); background: var(--sai-bg);
|
|
734
|
-
}
|
|
735
|
-
#sai-input {
|
|
736
|
-
flex: 1; background: var(--sai-surface); border: 1px solid var(--sai-border);
|
|
737
|
-
border-radius: 8px; padding: 7px 11px; color: var(--sai-text);
|
|
738
|
-
font-size: 13px; outline: none; transition: border-color .15s; font-family: inherit;
|
|
739
|
-
}
|
|
740
|
-
#sai-input::placeholder { color: var(--sai-text-dim); }
|
|
741
|
-
#sai-input:focus { border-color: var(--sai-accent); }
|
|
742
|
-
#sai-send {
|
|
743
|
-
width: 32px; height: 32px; flex-shrink: 0; border-radius: 8px;
|
|
744
|
-
background: var(--sai-accent); border: none; color: #fff; font-size: 14px;
|
|
745
|
-
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
746
|
-
transition: filter .15s, transform .1s; line-height: 1;
|
|
747
|
-
}
|
|
748
|
-
#sai-send:hover { filter: brightness(1.1); }
|
|
749
|
-
#sai-send:active { transform: scale(.93); }
|
|
750
|
-
#sai-send:disabled { background: var(--sai-border); cursor: not-allowed; }
|
|
751
|
-
|
|
752
|
-
/* ── Shepherd step overrides ── */
|
|
753
|
-
.sai-shepherd-step {
|
|
754
|
-
background: ${stepBg} !important;
|
|
755
|
-
border: 1px solid ${stepBorder} !important;
|
|
756
|
-
border-radius: 12px !important;
|
|
757
|
-
box-shadow: 0 8px 36px rgba(0,0,0,.18) !important;
|
|
758
|
-
max-width: 290px !important;
|
|
759
|
-
font-family: var(--sai-font, system-ui) !important;
|
|
760
|
-
}
|
|
761
|
-
.sai-shepherd-step.shepherd-has-title .shepherd-content .shepherd-header {
|
|
762
|
-
background: ${stepSurf} !important; padding: 11px 15px 8px !important;
|
|
763
|
-
border-bottom: 1px solid ${stepBorder} !important;
|
|
764
|
-
}
|
|
765
|
-
.sai-shepherd-step .shepherd-title {
|
|
766
|
-
color: var(--sai-accent, #e94560) !important;
|
|
767
|
-
font-size: 13px !important; font-weight: 700 !important;
|
|
768
|
-
}
|
|
769
|
-
.sai-shepherd-step .shepherd-text {
|
|
770
|
-
color: ${stepText} !important; font-size: 13px !important;
|
|
771
|
-
line-height: 1.6 !important; padding: 10px 15px !important;
|
|
772
|
-
}
|
|
773
|
-
.sai-shepherd-step .shepherd-footer {
|
|
774
|
-
border-top: 1px solid ${stepBorder} !important; padding: 8px 11px !important;
|
|
775
|
-
background: ${stepBg} !important; display: flex; gap: 5px; flex-wrap: wrap;
|
|
776
|
-
}
|
|
777
|
-
.sai-shepherd-step .shepherd-button {
|
|
778
|
-
background: var(--sai-accent, #e94560) !important; color: #fff !important;
|
|
779
|
-
border: none !important; border-radius: 6px !important; padding: 6px 13px !important;
|
|
780
|
-
font-size: 12px !important; font-weight: 600 !important; cursor: pointer !important;
|
|
781
|
-
transition: filter .15s !important; font-family: var(--sai-font, system-ui) !important;
|
|
782
|
-
}
|
|
783
|
-
.sai-shepherd-step .shepherd-button:hover { filter: brightness(1.1) !important; }
|
|
784
|
-
.sai-shepherd-step .shepherd-button-secondary {
|
|
785
|
-
background: transparent !important; color: var(--sai-text-dim, #888) !important;
|
|
786
|
-
border: 1px solid ${stepBorder} !important;
|
|
787
|
-
}
|
|
788
|
-
.sai-shepherd-step .shepherd-button-secondary:hover {
|
|
789
|
-
background: ${stepSurf} !important; color: ${stepText} !important;
|
|
790
|
-
}
|
|
791
|
-
.sai-shepherd-step .shepherd-cancel-icon { color: var(--sai-text-dim, #888) !important; }
|
|
792
|
-
.sai-shepherd-step .shepherd-cancel-icon:hover { color: ${stepText} !important; }
|
|
793
|
-
.sai-shepherd-step[data-popper-placement] > .shepherd-arrow::before {
|
|
794
|
-
background: ${stepBorder} !important;
|
|
795
|
-
}
|
|
765
|
+
function escHTML(str) {
|
|
766
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
767
|
+
}
|
|
796
768
|
|
|
797
|
-
|
|
798
|
-
.sai-progress { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; }
|
|
799
|
-
.sai-progress-bar {
|
|
800
|
-
width: 60px; height: 3px; border-radius: 2px; background: ${stepBorder}; overflow: hidden;
|
|
801
|
-
}
|
|
802
|
-
.sai-progress-fill {
|
|
803
|
-
height: 100%; border-radius: 2px;
|
|
804
|
-
background: var(--sai-accent, #e94560); transition: width .3s ease;
|
|
805
|
-
}
|
|
806
|
-
.sai-progress-label {
|
|
807
|
-
font-size: 10px; color: var(--sai-text-dim, #888);
|
|
808
|
-
white-space: nowrap; font-family: var(--sai-font, system-ui);
|
|
809
|
-
}
|
|
769
|
+
// ─── Panel visibility ─────────────────────────────────────────────────────────
|
|
810
770
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
771
|
+
function togglePanel() {
|
|
772
|
+
var _document$getElementB, _document$getElementB2, _document$getElementB3;
|
|
773
|
+
setIsOpen(!isOpen);
|
|
774
|
+
(_document$getElementB = document.getElementById('sai-panel')) === null || _document$getElementB === void 0 || _document$getElementB.classList.toggle('sai-open', isOpen);
|
|
775
|
+
(_document$getElementB2 = document.getElementById('sai-trigger')) === null || _document$getElementB2 === void 0 || _document$getElementB2.classList.toggle('sai-paused', !!pausedSteps);
|
|
776
|
+
if (isOpen) (_document$getElementB3 = document.getElementById('sai-input')) === null || _document$getElementB3 === void 0 || _document$getElementB3.focus();
|
|
777
|
+
}
|
|
819
778
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
779
|
+
// ─── Message helpers (also used by navigation.js / tour.js) ──────────────────
|
|
780
|
+
|
|
781
|
+
function addMsg(type, text) {
|
|
782
|
+
const msgs = document.getElementById('sai-messages');
|
|
783
|
+
if (!msgs) return;
|
|
784
|
+
const div = document.createElement('div');
|
|
785
|
+
div.className = `sai-msg sai-${type}`;
|
|
786
|
+
div.textContent = text;
|
|
787
|
+
msgs.appendChild(div);
|
|
788
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
789
|
+
}
|
|
790
|
+
function showTyping() {
|
|
791
|
+
const msgs = document.getElementById('sai-messages');
|
|
792
|
+
const el = document.createElement('div');
|
|
793
|
+
el.className = 'sai-typing';
|
|
794
|
+
el.id = 'sai-typing';
|
|
795
|
+
el.innerHTML = '<span></span><span></span><span></span>';
|
|
796
|
+
msgs.appendChild(el);
|
|
797
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
798
|
+
}
|
|
799
|
+
function hideTyping() {
|
|
800
|
+
var _document$getElementB4;
|
|
801
|
+
(_document$getElementB4 = document.getElementById('sai-typing')) === null || _document$getElementB4 === void 0 || _document$getElementB4.remove();
|
|
802
|
+
}
|
|
803
|
+
function setDisabled(d) {
|
|
804
|
+
['sai-input', 'sai-send'].forEach(id => {
|
|
805
|
+
const el = document.getElementById(id);
|
|
806
|
+
if (el) el.disabled = d;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ─── Pause/resume UI ──────────────────────────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Renders the "Tour paused — Resume from step N" bubble in the chat.
|
|
814
|
+
* Also opens the panel if it's currently closed.
|
|
815
|
+
*
|
|
816
|
+
* @param {number} fromIndex — zero-based step index to resume from
|
|
817
|
+
*/
|
|
818
|
+
function showResumeButton(fromIndex) {
|
|
819
|
+
var _document$getElementB5, _document$getElementB6;
|
|
820
|
+
const msgs = document.getElementById('sai-messages');
|
|
821
|
+
if (!msgs) return;
|
|
822
|
+
(_document$getElementB5 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB5 === void 0 || _document$getElementB5.remove();
|
|
823
|
+
const div = document.createElement('div');
|
|
824
|
+
div.id = 'sai-resume-prompt';
|
|
825
|
+
div.className = 'sai-msg sai-ai';
|
|
826
|
+
div.innerHTML = `
|
|
827
|
+
Tour paused. Ready when you are.
|
|
828
|
+
<br/>
|
|
829
|
+
<button class="sai-chip" id="sai-resume-btn" style="display:inline-block;margin-top:8px;">
|
|
830
|
+
▶ Resume from step ${fromIndex + 1}
|
|
831
|
+
</button>
|
|
832
|
+
`;
|
|
833
|
+
msgs.appendChild(div);
|
|
834
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
835
|
+
(_document$getElementB6 = document.getElementById('sai-resume-btn')) === null || _document$getElementB6 === void 0 || _document$getElementB6.addEventListener('click', () => {
|
|
836
|
+
var _runTour2;
|
|
837
|
+
div.remove();
|
|
838
|
+
if (!pausedSteps) return;
|
|
839
|
+
const steps = pausedSteps;
|
|
840
|
+
const idx = pausedIndex;
|
|
841
|
+
setPausedSteps(null);
|
|
842
|
+
setPausedIndex(0);
|
|
843
|
+
if (isOpen) togglePanel();
|
|
844
|
+
(_runTour2 = _runTour) === null || _runTour2 === void 0 || _runTour2(steps.slice(idx));
|
|
845
|
+
});
|
|
846
|
+
if (!isOpen) togglePanel();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ─── Send / AI round-trip ─────────────────────────────────────────────────────
|
|
850
|
+
|
|
851
|
+
async function handleSend(text) {
|
|
852
|
+
var _document$getElementB7, _document$getElementB8;
|
|
853
|
+
const input = document.getElementById('sai-input');
|
|
854
|
+
if (input) input.value = '';
|
|
855
|
+
document.getElementById('sai-suggestions').style.display = 'none';
|
|
856
|
+
|
|
857
|
+
// New message clears any existing pause state
|
|
858
|
+
setPausedSteps(null);
|
|
859
|
+
setPausedIndex(0);
|
|
860
|
+
(_document$getElementB7 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB7 === void 0 || _document$getElementB7.remove();
|
|
861
|
+
(_document$getElementB8 = document.getElementById('sai-trigger')) === null || _document$getElementB8 === void 0 || _document$getElementB8.classList.remove('sai-paused');
|
|
862
|
+
addMsg('user', text);
|
|
863
|
+
setDisabled(true);
|
|
864
|
+
showTyping();
|
|
865
|
+
try {
|
|
866
|
+
var _result$steps;
|
|
867
|
+
const result = await callAI(text);
|
|
868
|
+
hideTyping();
|
|
869
|
+
addMsg('ai', result.message);
|
|
870
|
+
if ((_result$steps = result.steps) !== null && _result$steps !== void 0 && _result$steps.length) {
|
|
871
|
+
const routesNeeded = previewRoutesNeeded(result.steps);
|
|
872
|
+
if (routesNeeded.length > 0) {
|
|
873
|
+
announceNavigationPlan(routesNeeded);
|
|
837
874
|
}
|
|
838
|
-
|
|
839
|
-
|
|
875
|
+
const delay = routesNeeded.length > 0 ? 1200 : 600;
|
|
876
|
+
setTimeout(() => {
|
|
877
|
+
var _runTour3;
|
|
878
|
+
togglePanel();
|
|
879
|
+
(_runTour3 = _runTour) === null || _runTour3 === void 0 || _runTour3(result.steps);
|
|
880
|
+
}, delay);
|
|
840
881
|
}
|
|
882
|
+
} catch (err) {
|
|
883
|
+
hideTyping();
|
|
884
|
+
addMsg('error', err.message || 'Something went wrong. Please try again.');
|
|
885
|
+
console.error('[Eventop]', err);
|
|
886
|
+
} finally {
|
|
887
|
+
var _document$getElementB9;
|
|
888
|
+
setDisabled(false);
|
|
889
|
+
(_document$getElementB9 = document.getElementById('sai-input')) === null || _document$getElementB9 === void 0 || _document$getElementB9.focus();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
841
892
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
<
|
|
893
|
+
// ─── DOM builder ─────────────────────────────────────────────────────────────
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Mounts the trigger button and chat panel into document.body.
|
|
897
|
+
* Idempotent — safe to call multiple times (exits early if already mounted).
|
|
898
|
+
*
|
|
899
|
+
* @param {object} theme — resolved theme tokens
|
|
900
|
+
* @param {object} posCSS — resolved position object from resolvePosition()
|
|
901
|
+
*/
|
|
902
|
+
function buildChat(theme, posCSS) {
|
|
903
|
+
var _state$config$theme, _state$config$suggest, _state$config$theme2, _state$config$theme3;
|
|
904
|
+
if (document.getElementById('sai-trigger')) return;
|
|
905
|
+
loadShepherdCSS();
|
|
906
|
+
injectStyles(theme, posCSS);
|
|
907
|
+
|
|
908
|
+
// ── Trigger button ──
|
|
909
|
+
const trigger = document.createElement('button');
|
|
910
|
+
trigger.id = 'sai-trigger';
|
|
911
|
+
trigger.title = 'Need help?';
|
|
912
|
+
trigger.setAttribute('aria-label', 'Open help assistant');
|
|
913
|
+
trigger.innerHTML = '<span class="sai-pulse" aria-hidden="true"></span>✦';
|
|
914
|
+
document.body.appendChild(trigger);
|
|
915
|
+
|
|
916
|
+
// ── Panel ──
|
|
917
|
+
const panel = document.createElement('div');
|
|
918
|
+
panel.id = 'sai-panel';
|
|
919
|
+
panel.setAttribute('role', 'dialog');
|
|
920
|
+
panel.setAttribute('aria-label', `${config.assistantName || 'AI Guide'} chat`);
|
|
921
|
+
panel.innerHTML = `
|
|
922
|
+
<div id="sai-header">
|
|
923
|
+
<div id="sai-avatar" aria-hidden="true">✦</div>
|
|
924
|
+
<div id="sai-header-info">
|
|
925
|
+
<h4>${escHTML(config.assistantName || 'AI Guide')}</h4>
|
|
926
|
+
<p>Ask me anything about ${escHTML(config.appName)}</p>
|
|
875
927
|
</div>
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
928
|
+
<button id="sai-close" aria-label="Close help assistant">×</button>
|
|
929
|
+
</div>
|
|
930
|
+
<div id="sai-messages" role="log" aria-live="polite"></div>
|
|
931
|
+
<div id="sai-suggestions" aria-label="Suggested questions"></div>
|
|
932
|
+
<div id="sai-inputrow">
|
|
933
|
+
<input id="sai-input" type="text" placeholder="What do you need help with?"
|
|
934
|
+
autocomplete="off" aria-label="Ask a question"/>
|
|
935
|
+
<button id="sai-send" aria-label="Send message">➤</button>
|
|
936
|
+
</div>
|
|
937
|
+
`;
|
|
938
|
+
document.body.appendChild(panel);
|
|
939
|
+
if (((_state$config$theme = config.theme) === null || _state$config$theme === void 0 ? void 0 : _state$config$theme.preset) === 'glass') {
|
|
940
|
+
document.body.classList.add('sai-glass-preset');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ── Suggestion chips ──
|
|
944
|
+
if ((_state$config$suggest = config.suggestions) !== null && _state$config$suggest !== void 0 && _state$config$suggest.length) {
|
|
945
|
+
const container = panel.querySelector('#sai-suggestions');
|
|
946
|
+
config.suggestions.forEach(s => {
|
|
947
|
+
const btn = document.createElement('button');
|
|
948
|
+
btn.className = 'sai-chip';
|
|
949
|
+
btn.textContent = s;
|
|
950
|
+
btn.addEventListener('click', () => handleSend(s));
|
|
951
|
+
container.appendChild(btn);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ── Event listeners ──
|
|
956
|
+
trigger.addEventListener('click', togglePanel);
|
|
957
|
+
panel.querySelector('#sai-close').addEventListener('click', togglePanel);
|
|
958
|
+
panel.querySelector('#sai-send').addEventListener('click', () => {
|
|
959
|
+
const v = panel.querySelector('#sai-input').value.trim();
|
|
960
|
+
if (v) handleSend(v);
|
|
961
|
+
});
|
|
962
|
+
panel.querySelector('#sai-input').addEventListener('keydown', e => {
|
|
963
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
964
|
+
e.preventDefault();
|
|
965
|
+
const v = e.target.value.trim();
|
|
966
|
+
if (v) handleSend(v);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
addMsg('ai', `Hey! 👋 I can guide you through ${config.appName}. What would you like to do?`);
|
|
970
|
+
|
|
971
|
+
// ── Auto theme switching ──
|
|
972
|
+
if (((_state$config$theme2 = config.theme) === null || _state$config$theme2 === void 0 ? void 0 : _state$config$theme2.mode) === 'auto' || !((_state$config$theme3 = config.theme) !== null && _state$config$theme3 !== void 0 && _state$config$theme3.mode)) {
|
|
973
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
974
|
+
mq.addEventListener('change', () => {
|
|
975
|
+
var _document$getElementB0;
|
|
976
|
+
const newTheme = resolveTheme(config.theme);
|
|
977
|
+
applyTheme(newTheme);
|
|
978
|
+
(_document$getElementB0 = document.getElementById('sai-styles')) === null || _document$getElementB0 === void 0 || _document$getElementB0.remove();
|
|
979
|
+
injectStyles(newTheme, posCSS);
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Expands a feature's `flow[]` array into individual Shepherd-ready step objects.
|
|
985
|
+
// A feature with no `flow` is returned as-is (wrapped in an array).
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* @param {object} aiStep — the step object returned by the AI
|
|
989
|
+
* @param {object} feature — the matching feature config (may have a .flow array)
|
|
990
|
+
* @returns {Array<object>} one or more expanded step objects
|
|
991
|
+
*/
|
|
992
|
+
function expandFlowSteps(aiStep, feature) {
|
|
993
|
+
var _feature$flow;
|
|
994
|
+
if (!((_feature$flow = feature.flow) !== null && _feature$flow !== void 0 && _feature$flow.length)) return [aiStep];
|
|
995
|
+
return feature.flow.map((entry, i) => ({
|
|
996
|
+
id: `${aiStep.id}-flow-${i}`,
|
|
997
|
+
title: i === 0 ? aiStep.title : `${aiStep.title} (${i + 1}/${feature.flow.length})`,
|
|
998
|
+
text: i === 0 ? aiStep.text : `Step ${i + 1} of ${feature.flow.length}: continue with the action highlighted below.`,
|
|
999
|
+
position: aiStep.position || 'bottom',
|
|
1000
|
+
selector: entry.selector || null,
|
|
1001
|
+
waitFor: entry.waitFor || null,
|
|
1002
|
+
advanceOn: entry.advanceOn || null,
|
|
1003
|
+
_parentId: aiStep.id
|
|
1004
|
+
}));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
|
|
1008
|
+
// progress indicators, pause/resume, and step-level error display.
|
|
1009
|
+
|
|
1010
|
+
const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
|
|
1011
|
+
|
|
1012
|
+
// ─── Shepherd loader ──────────────────────────────────────────────────────────
|
|
1013
|
+
|
|
1014
|
+
function loadScript(src) {
|
|
1015
|
+
return new Promise((res, rej) => {
|
|
1016
|
+
if (document.querySelector(`script[src="${src}"]`)) return res();
|
|
1017
|
+
const s = document.createElement('script');
|
|
1018
|
+
s.src = src;
|
|
1019
|
+
s.onload = res;
|
|
1020
|
+
s.onerror = rej;
|
|
1021
|
+
document.head.appendChild(s);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
async function ensureShepherd() {
|
|
1025
|
+
if (typeof Shepherd !== 'undefined') return;
|
|
1026
|
+
await loadScript(SHEPHERD_JS);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// ─── Feature-map merge ────────────────────────────────────────────────────────
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Merges an AI step with its matching entry from the live feature map.
|
|
1033
|
+
* The feature map always wins for `selector` so mismatches are impossible.
|
|
1034
|
+
*
|
|
1035
|
+
* @param {object} step
|
|
1036
|
+
* @returns {object}
|
|
1037
|
+
*/
|
|
1038
|
+
function mergeWithFeature(step) {
|
|
1039
|
+
var _state$config;
|
|
1040
|
+
if (!((_state$config = config) !== null && _state$config !== void 0 && _state$config.features)) return step;
|
|
1041
|
+
const feature = config.features.find(f => f.id === (step._parentId || step.id));
|
|
1042
|
+
if (!feature) return step;
|
|
1043
|
+
return {
|
|
1044
|
+
waitFor: feature.waitFor || null,
|
|
1045
|
+
advanceOn: feature.advanceOn || null,
|
|
1046
|
+
validate: feature.validate || null,
|
|
1047
|
+
screen: feature.screen || null,
|
|
1048
|
+
flow: feature.flow || null,
|
|
1049
|
+
route: feature.route || null,
|
|
1050
|
+
...step,
|
|
1051
|
+
selector: feature.selector || step.selector // feature map always wins
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ─── Step orchestration ───────────────────────────────────────────────────────
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Builds a `beforeShowPromise` for a Shepherd step that handles:
|
|
1059
|
+
* 1. Cross-page navigation (route prop) with automatic waiting
|
|
1060
|
+
* 2. Legacy screen navigation (screen.navigate)
|
|
1061
|
+
* 3. Element waiting (waitFor prop)
|
|
1062
|
+
*
|
|
1063
|
+
* @param {object} step
|
|
1064
|
+
* @param {number} waitTimeout
|
|
1065
|
+
* @returns {() => Promise<void>}
|
|
1066
|
+
*/
|
|
1067
|
+
function makeBeforeShowPromise(step, waitTimeout) {
|
|
1068
|
+
return () => (async () => {
|
|
1069
|
+
const freshMerged = mergeWithFeature(step);
|
|
1070
|
+
const targetRoute = freshMerged.route;
|
|
1071
|
+
if (targetRoute && getCurrentRoute() !== targetRoute) {
|
|
1072
|
+
await navigateToRoute(targetRoute, freshMerged.name || step.title);
|
|
1073
|
+
const postNavMerge = mergeWithFeature(step);
|
|
1074
|
+
if (postNavMerge.selector) {
|
|
1075
|
+
try {
|
|
1076
|
+
var _step$_shepherdRef, _step$_shepherdRef$up;
|
|
1077
|
+
(_step$_shepherdRef = step._shepherdRef) === null || _step$_shepherdRef === void 0 || (_step$_shepherdRef$up = _step$_shepherdRef.updateStepOptions) === null || _step$_shepherdRef$up === void 0 || _step$_shepherdRef$up.call(_step$_shepherdRef, {
|
|
1078
|
+
attachTo: {
|
|
1079
|
+
element: postNavMerge.selector,
|
|
1080
|
+
on: step.position || 'bottom'
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
} catch (_) {/* non-fatal */}
|
|
1084
|
+
await waitForElement(postNavMerge.selector, waitTimeout);
|
|
1085
|
+
return;
|
|
916
1086
|
}
|
|
917
1087
|
}
|
|
918
|
-
|
|
919
|
-
|
|
1088
|
+
if (freshMerged.screen) {
|
|
1089
|
+
await ensureOnCorrectScreen(freshMerged);
|
|
920
1090
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
_isOpen = !_isOpen;
|
|
924
|
-
(_document$getElementB4 = document.getElementById('sai-panel')) === null || _document$getElementB4 === void 0 || _document$getElementB4.classList.toggle('sai-open', _isOpen);
|
|
925
|
-
(_document$getElementB5 = document.getElementById('sai-trigger')) === null || _document$getElementB5 === void 0 || _document$getElementB5.classList.toggle('sai-paused', !!_pausedSteps);
|
|
926
|
-
if (_isOpen) (_document$getElementB6 = document.getElementById('sai-input')) === null || _document$getElementB6 === void 0 || _document$getElementB6.focus();
|
|
1091
|
+
if (freshMerged.waitFor) {
|
|
1092
|
+
await waitForElement(freshMerged.waitFor, waitTimeout);
|
|
927
1093
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1094
|
+
})();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// ─── Event wiring ─────────────────────────────────────────────────────────────
|
|
1098
|
+
|
|
1099
|
+
function wireAdvanceOn(shepherdStep, advanceOn, tour) {
|
|
1100
|
+
if (!(advanceOn !== null && advanceOn !== void 0 && advanceOn.selector) || !(advanceOn !== null && advanceOn !== void 0 && advanceOn.event)) return;
|
|
1101
|
+
function handler(e) {
|
|
1102
|
+
if (e.target.matches(advanceOn.selector) || e.target.closest(advanceOn.selector)) {
|
|
1103
|
+
setTimeout(() => {
|
|
1104
|
+
if (tour && !tour.isActive()) return;
|
|
1105
|
+
tour.next();
|
|
1106
|
+
}, advanceOn.delay || 300);
|
|
936
1107
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1108
|
+
}
|
|
1109
|
+
document.addEventListener(advanceOn.event, handler, true);
|
|
1110
|
+
pushCleanup(() => document.removeEventListener(advanceOn.event, handler, true));
|
|
1111
|
+
shepherdStep.on('show', () => {
|
|
1112
|
+
var _shepherdStep$getElem;
|
|
1113
|
+
const nextBtn = (_shepherdStep$getElem = shepherdStep.getElement()) === null || _shepherdStep$getElem === void 0 ? void 0 : _shepherdStep$getElem.querySelector('.shepherd-footer .shepherd-button:not(.shepherd-button-secondary)');
|
|
1114
|
+
if (nextBtn && !shepherdStep._isLast) {
|
|
1115
|
+
nextBtn.style.opacity = '0.4';
|
|
1116
|
+
nextBtn.title = 'Complete the action above to continue';
|
|
945
1117
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
function addProgressIndicator(shepherdStep, index, total) {
|
|
1121
|
+
shepherdStep.on('show', () => {
|
|
1122
|
+
var _shepherdStep$getElem2;
|
|
1123
|
+
const header = (_shepherdStep$getElem2 = shepherdStep.getElement()) === null || _shepherdStep$getElem2 === void 0 ? void 0 : _shepherdStep$getElem2.querySelector('.shepherd-header');
|
|
1124
|
+
if (!header || header.querySelector('.sai-progress')) return;
|
|
1125
|
+
const pct = Math.round((index + 1) / total * 100);
|
|
1126
|
+
const wrapper = document.createElement('div');
|
|
1127
|
+
wrapper.className = 'sai-progress';
|
|
1128
|
+
wrapper.innerHTML = `
|
|
1129
|
+
<div class="sai-progress-bar">
|
|
1130
|
+
<div class="sai-progress-fill" style="width:${pct}%"></div>
|
|
1131
|
+
</div>
|
|
1132
|
+
<span class="sai-progress-label">${index + 1} / ${total}</span>
|
|
1133
|
+
`;
|
|
1134
|
+
header.appendChild(wrapper);
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Starts a Shepherd tour from the given steps array.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {Array} steps
|
|
1144
|
+
* @param {object} [options]
|
|
1145
|
+
* @param {boolean} [options.showProgress=true]
|
|
1146
|
+
* @param {number} [options.waitTimeout=8000]
|
|
1147
|
+
*/
|
|
1148
|
+
async function runTour(steps, options = {}) {
|
|
1149
|
+
var _state$config2;
|
|
1150
|
+
await ensureShepherd();
|
|
1151
|
+
if (tour) {
|
|
1152
|
+
tour.cancel();
|
|
1153
|
+
}
|
|
1154
|
+
runAndClearCleanups();
|
|
1155
|
+
setTour(null);
|
|
1156
|
+
if (!(steps !== null && steps !== void 0 && steps.length)) return;
|
|
1157
|
+
const {
|
|
1158
|
+
showProgress = true,
|
|
1159
|
+
waitTimeout = 8000
|
|
1160
|
+
} = options;
|
|
1161
|
+
const mergedSteps = steps.map(mergeWithFeature);
|
|
1162
|
+
|
|
1163
|
+
// Legacy: navigate to the correct screen for the first step
|
|
1164
|
+
const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
|
|
1165
|
+
var _mergedSteps$;
|
|
1166
|
+
return f.id === ((_mergedSteps$ = mergedSteps[0]) === null || _mergedSteps$ === void 0 ? void 0 : _mergedSteps$.id);
|
|
1167
|
+
});
|
|
1168
|
+
if (firstFeature !== null && firstFeature !== void 0 && firstFeature.screen) await ensureOnCorrectScreen(firstFeature);
|
|
1169
|
+
|
|
1170
|
+
// Expand flow[] into individual Shepherd steps
|
|
1171
|
+
const expandedSteps = mergedSteps.flatMap(step => {
|
|
1172
|
+
var _state$config3;
|
|
1173
|
+
const feature = (_state$config3 = config) === null || _state$config3 === void 0 || (_state$config3 = _state$config3.features) === null || _state$config3 === void 0 ? void 0 : _state$config3.find(f => f.id === step.id);
|
|
1174
|
+
return feature ? expandFlowSteps(step, feature) : [step];
|
|
1175
|
+
});
|
|
1176
|
+
const ShepherdClass = typeof Shepherd !== 'undefined' ? Shepherd : window.Shepherd;
|
|
1177
|
+
const tour$1 = new ShepherdClass.Tour({
|
|
1178
|
+
useModalOverlay: true,
|
|
1179
|
+
defaultStepOptions: {
|
|
1180
|
+
scrollTo: {
|
|
1181
|
+
behavior: 'smooth',
|
|
1182
|
+
block: 'center'
|
|
1183
|
+
},
|
|
1184
|
+
cancelIcon: {
|
|
1185
|
+
enabled: true
|
|
1186
|
+
},
|
|
1187
|
+
classes: 'sai-shepherd-step'
|
|
949
1188
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1189
|
+
});
|
|
1190
|
+
setTour(tour$1);
|
|
1191
|
+
expandedSteps.forEach((step, i) => {
|
|
1192
|
+
var _step$advanceOn, _step$advanceOn2;
|
|
1193
|
+
const isLast = i === expandedSteps.length - 1;
|
|
1194
|
+
const hasAuto = !!((_step$advanceOn = step.advanceOn) !== null && _step$advanceOn !== void 0 && _step$advanceOn.selector && (_step$advanceOn2 = step.advanceOn) !== null && _step$advanceOn2 !== void 0 && _step$advanceOn2.event);
|
|
1195
|
+
const buttons = [];
|
|
1196
|
+
if (i > 0) {
|
|
1197
|
+
buttons.push({
|
|
1198
|
+
text: '← Back',
|
|
1199
|
+
action: tour$1.back,
|
|
1200
|
+
secondary: true
|
|
954
1201
|
});
|
|
955
1202
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
(_document$getElementB8 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB8 === void 0 || _document$getElementB8.remove();
|
|
966
|
-
(_document$getElementB9 = document.getElementById('sai-trigger')) === null || _document$getElementB9 === void 0 || _document$getElementB9.classList.remove('sai-paused');
|
|
967
|
-
addMsg('user', text);
|
|
968
|
-
setDisabled(true);
|
|
969
|
-
showTyping();
|
|
970
|
-
try {
|
|
971
|
-
var _result$steps;
|
|
972
|
-
const result = await callAI(text);
|
|
973
|
-
hideTyping();
|
|
974
|
-
addMsg('ai', result.message);
|
|
975
|
-
if ((_result$steps = result.steps) !== null && _result$steps !== void 0 && _result$steps.length) {
|
|
976
|
-
setTimeout(() => {
|
|
977
|
-
togglePanel();
|
|
978
|
-
runTour(result.steps);
|
|
979
|
-
}, 600);
|
|
980
|
-
}
|
|
981
|
-
} catch (err) {
|
|
982
|
-
hideTyping();
|
|
983
|
-
addMsg('error', err.message || 'Something went wrong. Please try again.');
|
|
984
|
-
console.error('[Eventop]', err);
|
|
985
|
-
} finally {
|
|
986
|
-
var _document$getElementB0;
|
|
987
|
-
setDisabled(false);
|
|
988
|
-
(_document$getElementB0 = document.getElementById('sai-input')) === null || _document$getElementB0 === void 0 || _document$getElementB0.focus();
|
|
1203
|
+
buttons.push({
|
|
1204
|
+
text: isLast ? 'Done ✓' : 'Next →',
|
|
1205
|
+
action: isLast ? tour$1.complete : tour$1.next
|
|
1206
|
+
});
|
|
1207
|
+
buttons.push({
|
|
1208
|
+
text: '⏸ Pause',
|
|
1209
|
+
secondary: true,
|
|
1210
|
+
action: function () {
|
|
1211
|
+
tour$1.cancel();
|
|
989
1212
|
}
|
|
1213
|
+
});
|
|
1214
|
+
const shepherdStep = tour$1.addStep({
|
|
1215
|
+
id: step.id || `sai-step-${i}`,
|
|
1216
|
+
title: step.title,
|
|
1217
|
+
text: step.text,
|
|
1218
|
+
attachTo: step.selector ? {
|
|
1219
|
+
element: step.selector,
|
|
1220
|
+
on: step.position || 'bottom'
|
|
1221
|
+
} : undefined,
|
|
1222
|
+
beforeShowPromise: makeBeforeShowPromise(step, waitTimeout),
|
|
1223
|
+
buttons
|
|
1224
|
+
});
|
|
1225
|
+
step._shepherdRef = shepherdStep;
|
|
1226
|
+
shepherdStep._isLast = isLast;
|
|
1227
|
+
if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
|
|
1228
|
+
if (showProgress && expandedSteps.length > 1) {
|
|
1229
|
+
addProgressIndicator(shepherdStep, i, expandedSteps.length);
|
|
990
1230
|
}
|
|
1231
|
+
});
|
|
1232
|
+
tour$1.on('complete', () => {
|
|
1233
|
+
runAndClearCleanups();
|
|
1234
|
+
setPausedSteps(null);
|
|
1235
|
+
setPausedIndex(0);
|
|
1236
|
+
setTour(null);
|
|
1237
|
+
});
|
|
991
1238
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
if (!((_opts$config = opts.config) !== null && _opts$config !== void 0 && _opts$config.appName)) throw new Error('[Eventop] config.appName is required');
|
|
1002
|
-
if (!((_opts$config2 = opts.config) !== null && _opts$config2 !== void 0 && _opts$config2.features)) throw new Error('[Eventop] config.features is required');
|
|
1003
|
-
_provider = opts.provider;
|
|
1004
|
-
_config = opts.config;
|
|
1005
|
-
const theme = resolveTheme(_config.theme);
|
|
1006
|
-
const posCSS = resolvePosition(_config.position);
|
|
1007
|
-
if (document.readyState === 'loading') {
|
|
1008
|
-
document.addEventListener('DOMContentLoaded', () => buildChat(theme, posCSS));
|
|
1009
|
-
} else {
|
|
1010
|
-
buildChat(theme, posCSS);
|
|
1011
|
-
}
|
|
1012
|
-
ensureShepherd();
|
|
1013
|
-
},
|
|
1014
|
-
open() {
|
|
1015
|
-
if (!_isOpen) togglePanel();
|
|
1016
|
-
},
|
|
1017
|
-
close() {
|
|
1018
|
-
if (_isOpen) togglePanel();
|
|
1019
|
-
},
|
|
1020
|
-
runTour,
|
|
1021
|
-
cancelTour() {
|
|
1022
|
-
var _document$getElementB1, _document$getElementB10;
|
|
1023
|
-
_pausedSteps = null;
|
|
1024
|
-
_pausedIndex = 0;
|
|
1025
|
-
if (_tour) {
|
|
1026
|
-
_tour.cancel();
|
|
1027
|
-
}
|
|
1028
|
-
_cleanups.forEach(fn => fn());
|
|
1029
|
-
_cleanups = [];
|
|
1030
|
-
_tour = null;
|
|
1031
|
-
(_document$getElementB1 = document.getElementById('sai-trigger')) === null || _document$getElementB1 === void 0 || _document$getElementB1.classList.remove('sai-paused');
|
|
1032
|
-
(_document$getElementB10 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB10 === void 0 || _document$getElementB10.remove();
|
|
1033
|
-
document.body.classList.remove('sai-glass-preset');
|
|
1034
|
-
},
|
|
1035
|
-
resumeTour() {
|
|
1036
|
-
var _document$getElementB11, _document$getElementB12;
|
|
1037
|
-
if (!_pausedSteps) return;
|
|
1038
|
-
const steps = _pausedSteps;
|
|
1039
|
-
const idx = _pausedIndex;
|
|
1040
|
-
_pausedSteps = null;
|
|
1041
|
-
_pausedIndex = 0;
|
|
1042
|
-
(_document$getElementB11 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB11 === void 0 || _document$getElementB11.remove();
|
|
1043
|
-
(_document$getElementB12 = document.getElementById('sai-trigger')) === null || _document$getElementB12 === void 0 || _document$getElementB12.classList.remove('sai-paused');
|
|
1044
|
-
if (_isOpen) togglePanel();
|
|
1045
|
-
runTour(steps.slice(idx));
|
|
1046
|
-
},
|
|
1047
|
-
isPaused() {
|
|
1048
|
-
return !!_pausedSteps;
|
|
1049
|
-
},
|
|
1050
|
-
isActive() {
|
|
1051
|
-
var _tour4;
|
|
1052
|
-
return !!((_tour4 = _tour) !== null && _tour4 !== void 0 && _tour4.isActive());
|
|
1053
|
-
},
|
|
1054
|
-
stepComplete,
|
|
1055
|
-
stepFail,
|
|
1056
|
-
/** @internal — used by the React package to sync the live feature registry */
|
|
1057
|
-
_updateConfig(partial) {
|
|
1058
|
-
if (!_config) return;
|
|
1059
|
-
_config = {
|
|
1060
|
-
..._config,
|
|
1061
|
-
...partial
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
};
|
|
1065
|
-
return Eventop;
|
|
1239
|
+
// Cancel → pause (not hard destroy)
|
|
1240
|
+
tour$1.on('cancel', () => {
|
|
1241
|
+
const currentStepEl = tour$1.getCurrentStep();
|
|
1242
|
+
const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
|
|
1243
|
+
runAndClearCleanups();
|
|
1244
|
+
setPausedSteps(expandedSteps);
|
|
1245
|
+
setPausedIndex(Math.max(0, currentIdx));
|
|
1246
|
+
setTour(null);
|
|
1247
|
+
showResumeButton(pausedIndex);
|
|
1066
1248
|
});
|
|
1067
|
-
|
|
1068
|
-
|
|
1249
|
+
tour$1.start();
|
|
1250
|
+
}
|
|
1069
1251
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1252
|
+
/**
|
|
1253
|
+
* Advances the active tour to the next step programmatically.
|
|
1254
|
+
* Useful when your own UI signals step completion.
|
|
1255
|
+
*/
|
|
1256
|
+
function stepComplete() {
|
|
1257
|
+
var _state$tour;
|
|
1258
|
+
if ((_state$tour = tour) !== null && _state$tour !== void 0 && _state$tour.isActive()) tour.next();
|
|
1259
|
+
}
|
|
1073
1260
|
|
|
1074
|
-
|
|
1261
|
+
/**
|
|
1262
|
+
* Injects an error message into the current Shepherd step's text area.
|
|
1263
|
+
*
|
|
1264
|
+
* @param {string} [message]
|
|
1265
|
+
*/
|
|
1266
|
+
function stepFail(message) {
|
|
1267
|
+
var _state$tour2, _el$querySelector;
|
|
1268
|
+
if (!((_state$tour2 = tour) !== null && _state$tour2 !== void 0 && _state$tour2.isActive())) return;
|
|
1269
|
+
const current = tour.getCurrentStep();
|
|
1270
|
+
if (!current) return;
|
|
1271
|
+
const el = current.getElement();
|
|
1272
|
+
el === null || el === void 0 || (_el$querySelector = el.querySelector('.sai-step-error')) === null || _el$querySelector === void 0 || _el$querySelector.remove();
|
|
1273
|
+
if (message) {
|
|
1274
|
+
var _el$querySelector2;
|
|
1275
|
+
const err = document.createElement('div');
|
|
1276
|
+
err.className = 'sai-step-error';
|
|
1277
|
+
err.textContent = '⚠ ' + message;
|
|
1278
|
+
el === null || el === void 0 || (_el$querySelector2 = el.querySelector('.shepherd-text')) === null || _el$querySelector2 === void 0 || _el$querySelector2.appendChild(err);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Break the navigation.js ↔ chat.js circular dependency:
|
|
1283
|
+
// chat.js needs runTour but can't import tour.js (tour imports chat).
|
|
1284
|
+
// We inject runTour into chat after both modules are loaded.
|
|
1285
|
+
setRunTour(runTour);
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* ╔══════════════════════════════════════════════════════════════╗
|
|
1289
|
+
* ║ @eventop/sdk v1.3.0 ║
|
|
1290
|
+
* ║ AI-powered guided tours — themeable, provider-agnostic ║
|
|
1291
|
+
* ║ ║
|
|
1292
|
+
* ║ Provider: always proxy through your own server. ║
|
|
1293
|
+
* ║ Never expose API keys in client-side code. ║
|
|
1294
|
+
* ╚══════════════════════════════════════════════════════════════╝
|
|
1295
|
+
*
|
|
1296
|
+
* Module map
|
|
1297
|
+
* ──────────────────────────────────────────────────────────────
|
|
1298
|
+
* state.js Shared mutable state — single source of truth
|
|
1299
|
+
* theme.js Design tokens + CSS variable builder
|
|
1300
|
+
* positioning.js Corner/offset → CSS position values
|
|
1301
|
+
* providers.js AI provider factory helpers
|
|
1302
|
+
* prompt.js System prompt builder
|
|
1303
|
+
* ai.js Provider request/response + conversation history
|
|
1304
|
+
* navigation.js Route navigation, element waiting, announcements
|
|
1305
|
+
* flow.js Expands feature flow[] into step arrays
|
|
1306
|
+
* tour.js Shepherd.js runner, pause/resume, step helpers
|
|
1307
|
+
* styles.js <style> injection (chat panel + Shepherd overrides)
|
|
1308
|
+
* chat.js Chat panel DOM, messaging, send handler
|
|
1309
|
+
* core.js ← YOU ARE HERE — public API + UMD wrapper
|
|
1310
|
+
*/
|
|
1311
|
+
|
|
1312
|
+
(function (global, factory) {
|
|
1313
|
+
const Eventop = factory();
|
|
1314
|
+
if (typeof module === 'object' && module.exports) {
|
|
1315
|
+
module.exports = Eventop;
|
|
1316
|
+
}
|
|
1317
|
+
if (typeof window !== 'undefined') {
|
|
1318
|
+
window.Eventop = Eventop;
|
|
1319
|
+
} else if (typeof global !== 'undefined') {
|
|
1320
|
+
global.Eventop = Eventop;
|
|
1321
|
+
}
|
|
1322
|
+
})(typeof globalThis !== 'undefined' ? globalThis : undefined, function
|
|
1323
|
+
// Dependencies are injected by the bundler (Rollup / esbuild / Vite).
|
|
1324
|
+
// When building for the browser, run: rollup -i core.js -o dist/eventop.umd.js -f umd -n Eventop
|
|
1325
|
+
//
|
|
1326
|
+
// The factory receives nothing — all imports are resolved at bundle time
|
|
1327
|
+
// because static `import` statements are hoisted above the UMD wrapper by
|
|
1328
|
+
// modern bundlers. This file must be processed by a bundler before use in
|
|
1329
|
+
// a plain <script> tag.
|
|
1330
|
+
() {
|
|
1331
|
+
|
|
1332
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1333
|
+
// PUBLIC API
|
|
1334
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1335
|
+
const Eventop = {
|
|
1336
|
+
providers,
|
|
1337
|
+
/**
|
|
1338
|
+
* Initialise the SDK. Must be called once before anything else.
|
|
1339
|
+
*
|
|
1340
|
+
* @param {object} opts
|
|
1341
|
+
* @param {Function} opts.provider — AI provider function
|
|
1342
|
+
* @param {object} opts.config — app config
|
|
1343
|
+
* @param {string} opts.config.appName
|
|
1344
|
+
* @param {Array} opts.config.features
|
|
1345
|
+
* @param {Function} [opts.config.router] — framework navigate fn
|
|
1346
|
+
* @param {object} [opts.config.theme]
|
|
1347
|
+
* @param {object} [opts.config.position]
|
|
1348
|
+
* @param {string[]} [opts.config.suggestions]
|
|
1349
|
+
* @param {string} [opts.config.assistantName]
|
|
1350
|
+
*/
|
|
1351
|
+
init(opts = {}) {
|
|
1352
|
+
var _opts$config, _opts$config2;
|
|
1353
|
+
if (!opts.provider) throw new Error('[Eventop] provider is required');
|
|
1354
|
+
if (!((_opts$config = opts.config) !== null && _opts$config !== void 0 && _opts$config.appName)) throw new Error('[Eventop] config.appName is required');
|
|
1355
|
+
if (!((_opts$config2 = opts.config) !== null && _opts$config2 !== void 0 && _opts$config2.features)) throw new Error('[Eventop] config.features is required');
|
|
1356
|
+
setProvider(opts.provider);
|
|
1357
|
+
setConfig(opts.config);
|
|
1358
|
+
setRouter(opts.config.router || null);
|
|
1359
|
+
const theme = resolveTheme(opts.config.theme);
|
|
1360
|
+
const posCSS = resolvePosition(opts.config.position);
|
|
1361
|
+
if (document.readyState === 'loading') {
|
|
1362
|
+
document.addEventListener('DOMContentLoaded', () => buildChat(theme, posCSS));
|
|
1363
|
+
} else {
|
|
1364
|
+
buildChat(theme, posCSS);
|
|
1365
|
+
}
|
|
1366
|
+
ensureShepherd();
|
|
1367
|
+
},
|
|
1368
|
+
/** Programmatically open the chat panel. */
|
|
1369
|
+
open() {
|
|
1370
|
+
if (!isOpen) togglePanel();
|
|
1371
|
+
},
|
|
1372
|
+
/** Programmatically close the chat panel. */
|
|
1373
|
+
close() {
|
|
1374
|
+
if (isOpen) togglePanel();
|
|
1375
|
+
},
|
|
1376
|
+
/** Start a tour directly with a pre-built step array. */
|
|
1377
|
+
runTour,
|
|
1378
|
+
/** Cancel any active or paused tour and clean up completely. */
|
|
1379
|
+
cancelTour() {
|
|
1380
|
+
var _document$getElementB, _document$getElementB2;
|
|
1381
|
+
setPausedSteps(null);
|
|
1382
|
+
setPausedIndex(0);
|
|
1383
|
+
if (tour) {
|
|
1384
|
+
tour.cancel();
|
|
1385
|
+
}
|
|
1386
|
+
runAndClearCleanups();
|
|
1387
|
+
setTour(null);
|
|
1388
|
+
(_document$getElementB = document.getElementById('sai-trigger')) === null || _document$getElementB === void 0 || _document$getElementB.classList.remove('sai-paused');
|
|
1389
|
+
(_document$getElementB2 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB2 === void 0 || _document$getElementB2.remove();
|
|
1390
|
+
document.body.classList.remove('sai-glass-preset');
|
|
1391
|
+
},
|
|
1392
|
+
/** Resume a paused tour from where it was cancelled. */
|
|
1393
|
+
resumeTour() {
|
|
1394
|
+
var _document$getElementB3, _document$getElementB4;
|
|
1395
|
+
if (!pausedSteps) return;
|
|
1396
|
+
const steps = pausedSteps;
|
|
1397
|
+
const idx = pausedIndex;
|
|
1398
|
+
setPausedSteps(null);
|
|
1399
|
+
setPausedIndex(0);
|
|
1400
|
+
(_document$getElementB3 = document.getElementById('sai-resume-prompt')) === null || _document$getElementB3 === void 0 || _document$getElementB3.remove();
|
|
1401
|
+
(_document$getElementB4 = document.getElementById('sai-trigger')) === null || _document$getElementB4 === void 0 || _document$getElementB4.classList.remove('sai-paused');
|
|
1402
|
+
if (isOpen) togglePanel();
|
|
1403
|
+
runTour(steps.slice(idx));
|
|
1404
|
+
},
|
|
1405
|
+
/** @returns {boolean} true if a tour is currently paused */
|
|
1406
|
+
isPaused() {
|
|
1407
|
+
return !!pausedSteps;
|
|
1408
|
+
},
|
|
1409
|
+
/** @returns {boolean} true if a Shepherd tour is actively running */
|
|
1410
|
+
isActive() {
|
|
1411
|
+
var _state$tour;
|
|
1412
|
+
return !!((_state$tour = tour) !== null && _state$tour !== void 0 && _state$tour.isActive());
|
|
1413
|
+
},
|
|
1414
|
+
/**
|
|
1415
|
+
* Advance the active tour step programmatically.
|
|
1416
|
+
* Call this from your own UI when you want to signal step completion.
|
|
1417
|
+
*/
|
|
1418
|
+
stepComplete,
|
|
1419
|
+
/**
|
|
1420
|
+
* Inject an error message into the current tour step.
|
|
1421
|
+
* @param {string} message
|
|
1422
|
+
*/
|
|
1423
|
+
stepFail,
|
|
1424
|
+
/**
|
|
1425
|
+
* @internal — used by the React package to sync the live feature registry.
|
|
1426
|
+
* @param {object} partial — partial config to merge
|
|
1427
|
+
*/
|
|
1428
|
+
_updateConfig(partial) {
|
|
1429
|
+
if (!config) return;
|
|
1430
|
+
setConfig({
|
|
1431
|
+
...config,
|
|
1432
|
+
...partial
|
|
1433
|
+
});
|
|
1434
|
+
if (partial.router !== undefined) setRouter(partial.router);
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
return Eventop;
|
|
1438
|
+
});
|