rushchatbot 1.6.9 → 1.7.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # RushChatbot
2
2
 
3
- **Version:** 1.6.9
3
+ **Version:** 1.7.2
4
4
 
5
5
  Embeddable hybrid chatbot plugin that combines a click-based FAQ tree with a free-text query engine backed by n8n. Ships as an npm package with a Node.js/Express server and widgets for React, Vue 3, and Vanilla JS.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # RushChatbot
2
2
 
3
- **Version:** 1.6.9
3
+ **Version:** 1.7.0
4
4
 
5
5
  Embeddable hybrid chatbot plugin that combines a click-based FAQ tree with a free-text query engine backed by n8n. Ships as an npm package with a Node.js/Express server and widgets for React, Vue 3, and Vanilla JS.
6
6
 
package/package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RushChatbot
2
2
 
3
- **Version:** 1.6.9
3
+ **Version:** 1.7.2
4
4
 
5
5
  Embeddable hybrid chatbot plugin that combines a click-based FAQ tree with a free-text query engine backed by n8n. Ships as an npm package with a Node.js/Express server and widgets for React, Vue 3, and Vanilla JS.
6
6
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rushchatbot",
3
- "version": "1.6.9",
3
+ "version": "1.7.2",
4
4
  "description": "Embeddable hybrid chatbot with n8n fallback, license key protection, and configurable FAQ tree",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -44,6 +44,9 @@ function ThinkingDots({ color }: { color: string }) {
44
44
  .rushchatbot-md h1 { font-size: 1.1em; }
45
45
  .rushchatbot-md h2 { font-size: 1em; }
46
46
  .rushchatbot-md h3 { font-size: 0.95em; }
47
+ .rushchatbot-md code { font-family: monospace; font-size: 0.9em; background: rgba(0,0,0,0.08); padding: 1px 5px; border-radius: 4px; }
48
+ .rushchatbot-md pre { background: rgba(0,0,0,0.08); border-radius: 6px; padding: 10px 12px; overflow-x: auto; margin: 6px 0; }
49
+ .rushchatbot-md pre code { background: none; padding: 0; font-size: 0.85em; white-space: pre; }
47
50
  `}</style>
48
51
  </span>
49
52
  )
@@ -139,6 +142,16 @@ export function ChatWidget({ apiBase, width = 380, height = 580 }: ChatWidgetPro
139
142
  </div>
140
143
  )}
141
144
 
145
+ {!state.messages.some(m => m.isEndChat) && (
146
+ <button onClick={() => chat.endChat()} style={{
147
+ textAlign: 'left', padding: '8px 12px', borderRadius: 8,
148
+ border: '1px solid #EF4444', background: 'transparent',
149
+ color: '#EF4444', cursor: 'pointer', fontSize: 14, marginTop: 4, width: '100%',
150
+ }}>
151
+ 👋 I'm done, thanks!
152
+ </button>
153
+ )}
154
+
142
155
  <div ref={bottomRef} />
143
156
  </div>
144
157
 
@@ -1,7 +1,26 @@
1
1
  import { marked } from 'marked'
2
2
  import type { Message, Question, WidgetSettings } from '../shared/types'
3
3
 
4
- marked.setOptions({ breaks: true })
4
+ marked.use({
5
+ breaks: true,
6
+ renderer: {
7
+ code({ text, lang }: { text: string; lang?: string }): string {
8
+ const escaped = text
9
+ .replace(/&/g, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ const langAttr = lang ? ` data-lang="${lang}"` : ''
13
+ return `<pre${langAttr}><code>${escaped}</code></pre>`
14
+ },
15
+ codespan({ text }: { text: string }): string {
16
+ const escaped = text
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ return `<code>${escaped}</code>`
21
+ },
22
+ },
23
+ })
5
24
 
6
25
  export interface RushChatbotConfig {
7
26
  apiBase?: string
@@ -170,6 +189,12 @@ export class RushChatbot {
170
189
 
171
190
  this.setState({ loading: false })
172
191
 
192
+ if (res.source === 'n8n_error' || res.source === 'error') {
193
+ this.addMessage({ role: 'bot', text: res.answer ?? "I'm having trouble right now. Please try again in a moment. ⏳", source: res.source, isError: true })
194
+ this.setState({ showMain: true })
195
+ return
196
+ }
197
+
173
198
  if (res.paragraphs?.length > 0) {
174
199
  for (const p of res.paragraphs) {
175
200
  this.addMessage({ role: 'bot', text: p, source: res.source })
@@ -224,12 +224,25 @@ export function createServer(options: RushChatbotServerOptions) {
224
224
  }
225
225
 
226
226
  saveAnalytics({ sessionId: sid, questionId: 'n8n', timestamp: new Date().toISOString(), type: 'n8n' })
227
- const { data } = await axios.post(
228
- webhookUrl,
229
- { query: query.trim(), sessionId: sid },
230
- { timeout: timeoutMs, headers: { 'Content-Type': 'application/json', 'x-session-id': sid } }
231
- )
232
- return res.json({ ...parseN8nResponse(data), source: 'n8n' })
227
+ try {
228
+ const { data } = await axios.post(
229
+ webhookUrl,
230
+ { query: query.trim(), sessionId: sid },
231
+ { timeout: timeoutMs, headers: { 'Content-Type': 'application/json', 'x-session-id': sid } }
232
+ )
233
+ return res.json({ ...parseN8nResponse(data), source: 'n8n' })
234
+ } catch (n8nErr: unknown) {
235
+ const n8nMsg = n8nErr instanceof Error ? n8nErr.message : String(n8nErr)
236
+ const isTimeout = n8nMsg.includes('timeout') || n8nMsg.includes('ECONNREFUSED') || n8nMsg.includes('ENOTFOUND')
237
+ console.error('[RushChatbot n8n error]', sanitizeLog(n8nMsg))
238
+ saveAnalytics({ sessionId: sid, questionId: 'n8n_error', timestamp: new Date().toISOString(), type: 'error' })
239
+ return res.status(502).json({
240
+ answer: isTimeout
241
+ ? "I'm having trouble reaching my knowledge base right now. Please try again in a moment. ⏳"
242
+ : cfg.chat.fallbackMessage,
243
+ source: 'n8n_error',
244
+ })
245
+ }
233
246
 
234
247
  } catch (err: unknown) {
235
248
  const message = err instanceof Error ? err.message : String(err)
@@ -38,10 +38,11 @@ export interface Message {
38
38
  id: string
39
39
  role: 'user' | 'bot'
40
40
  text: string
41
- source?: 'config' | 'n8n' | 'error'
41
+ source?: 'config' | 'n8n' | 'n8n_error' | 'error'
42
42
  subquestions?: Question[]
43
43
  isFollowUp?: boolean
44
44
  isEndChat?: boolean
45
+ isError?: boolean
45
46
  }
46
47
 
47
48
  export interface WidgetSettings {
@@ -56,6 +56,9 @@ const STYLES = `
56
56
  .rushchatbot-md h1 { font-size: 1.1em; }
57
57
  .rushchatbot-md h2 { font-size: 1em; }
58
58
  .rushchatbot-md h3 { font-size: 0.95em; }
59
+ .rushchatbot-md code { font-family: monospace; font-size: 0.9em; background: rgba(0,0,0,0.08); padding: 1px 5px; border-radius: 4px; }
60
+ .rushchatbot-md pre { background: rgba(0,0,0,0.08); border-radius: 6px; padding: 10px 12px; overflow-x: auto; margin: 6px 0; }
61
+ .rushchatbot-md pre code { background: none; padding: 0; font-size: 0.85em; white-space: pre; }
59
62
  `
60
63
 
61
64
  const OBSERVED = [
@@ -71,11 +74,19 @@ class RushChatbotElement extends HTMLElement {
71
74
  private state: RushChatbotState | null = null
72
75
  private showScrollBtn = false
73
76
  private resizeObserver: ResizeObserver | null = null
77
+ private isOpen = false
78
+ private btnX = -1
79
+ private btnY = -1
80
+ private dragging = false
81
+ private dragOffX = 0
82
+ private dragOffY = 0
83
+ private dragMoved = false
74
84
 
75
85
  static get observedAttributes() { return OBSERVED }
76
86
 
77
87
  connectedCallback() {
78
88
  this.shadow = this.attachShadow({ mode: 'open' })
89
+ this.isOpen = this.readSettings().autoOpen ?? false
79
90
  this.chat = new RushChatbot({ apiBase: this.getAttribute('api-base') ?? undefined, settings: this.readSettings() })
80
91
  this.chat.subscribe(state => { this.state = state; this.render() })
81
92
  this.chat.init()
@@ -125,23 +136,35 @@ class RushChatbotElement extends HTMLElement {
125
136
  }
126
137
  }
127
138
 
128
- private get w() {
129
- const attr = this.getAttribute('width')
130
- return attr ? `${attr}px` : '100%'
131
- }
132
- private get h() {
133
- const attr = this.getAttribute('height')
134
- return attr ? `${attr}px` : '100%'
139
+ private initBtnPos() {
140
+ const pos = this.settings.position ?? 'bottom-right'
141
+ const margin = 24
142
+ const btnSize = 56
143
+ if (this.btnX === -1) {
144
+ this.btnX = pos === 'bottom-left'
145
+ ? margin
146
+ : window.innerWidth - btnSize - margin
147
+ this.btnY = window.innerHeight - btnSize - margin
148
+ }
135
149
  }
136
150
 
137
151
  private render() {
138
152
  if (!this.state || !this.state.config) return
139
153
  const { theme, state, settings } = this
140
154
 
155
+ this.initBtnPos()
156
+
141
157
  const borderRadius = settings.borderRadius ?? 16
142
158
  const shadow = settings.showShadow !== false ? '0 8px 32px rgba(0,0,0,0.15)' : 'none'
143
159
  const placeholder = settings.placeholder ?? 'Type a message...'
144
160
  const sendLabel = settings.sendLabel ?? 'Send'
161
+ const chatW = this.getAttribute('width') ? Number(this.getAttribute('width')) : 380
162
+ const chatH = this.getAttribute('height') ? Number(this.getAttribute('height')) : 580
163
+ const btnSize = 56
164
+
165
+ // Clamp chat box so it stays in viewport
166
+ const boxLeft = Math.min(Math.max(this.btnX + btnSize / 2 - chatW / 2, 8), window.innerWidth - chatW - 8)
167
+ const boxBottom = window.innerHeight - this.btnY + 12
145
168
 
146
169
  const msgsEl = this.shadow.querySelector('.messages') as HTMLElement | null
147
170
  const wasAtBottom = msgsEl ? msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight < 60 : true
@@ -149,32 +172,46 @@ class RushChatbotElement extends HTMLElement {
149
172
  // Build static shell with no user data in innerHTML
150
173
  const shellHtml = `
151
174
  <style>${STYLES}</style>
152
- <div class="container" id="container">
153
- <div class="header" id="header">
154
- <div class="header-title" id="header-title"></div>
155
- <div class="header-subtitle" id="header-subtitle"></div>
156
- </div>
157
- <div class="messages-wrap">
158
- <div class="messages" id="msgs">
159
- <div id="msgs-content"></div>
160
- <div id="bottom"></div>
175
+ <div id="chat-box" style="display:${this.isOpen ? 'flex' : 'none'};position:fixed;left:${boxLeft}px;bottom:${boxBottom}px;z-index:2147483646;flex-direction:column;width:${chatW}px;height:${chatH}px">
176
+ <div class="container" id="container">
177
+ <div class="header" id="header">
178
+ <div style="display:flex;align-items:center;justify-content:space-between">
179
+ <div>
180
+ <div class="header-title" id="header-title"></div>
181
+ <div class="header-subtitle" id="header-subtitle"></div>
182
+ </div>
183
+ <button id="close-btn" style="background:none;border:none;color:#fff;cursor:pointer;font-size:20px;line-height:1;padding:0 4px">&times;</button>
184
+ </div>
185
+ </div>
186
+ <div class="messages-wrap">
187
+ <div class="messages" id="msgs">
188
+ <div id="msgs-content"></div>
189
+ <div id="bottom"></div>
190
+ </div>
161
191
  </div>
192
+ ${settings.hideInput ? '' : `
193
+ <form class="input-row" id="form">
194
+ <input id="input" />
195
+ <button type="submit" id="send-btn"></button>
196
+ </form>`}
162
197
  </div>
163
- ${settings.hideInput ? '' : `
164
- <form class="input-row" id="form">
165
- <input id="input" />
166
- <button type="submit" id="send-btn"></button>
167
- </form>`}
168
198
  </div>
199
+ <button id="fab" style="position:fixed;left:${this.btnX}px;top:${this.btnY}px;z-index:2147483647;width:${btnSize}px;height:${btnSize}px;border-radius:50%;border:none;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px rgba(0,0,0,0.25);touch-action:none">
200
+ <svg id="fab-icon-chat" width="26" height="26" viewBox="0 0 24 24" fill="none" style="display:${this.isOpen ? 'none' : 'block'}"><path d="M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#fff"/></svg>
201
+ <svg id="fab-icon-close" width="22" height="22" viewBox="0 0 24 24" fill="none" style="display:${this.isOpen ? 'block' : 'none'}"><path d="M18 6L6 18M6 6l12 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/></svg>
202
+ </button>
169
203
  `
170
204
  this.shadow.innerHTML = shellHtml
171
205
 
172
206
  // Populate dynamic values via DOM (safe from XSS)
173
207
  const container = this.shadow.getElementById('container') as HTMLElement
174
- container.style.cssText = `background:${escapeHtml(theme.backgroundColor)};color:${escapeHtml(theme.textColor)};width:${this.w};height:${this.h};font-family:${escapeHtml(theme.fontFamily)};border-radius:${borderRadius}px;box-shadow:${shadow}`
208
+ container.style.cssText = `background:${escapeHtml(theme.backgroundColor)};color:${escapeHtml(theme.textColor)};width:100%;height:100%;font-family:${escapeHtml(theme.fontFamily)};border-radius:${borderRadius}px;box-shadow:${shadow};display:flex;flex-direction:column;overflow:hidden`
209
+
210
+ const fab = this.shadow.getElementById('fab') as HTMLElement
211
+ fab.style.background = escapeHtml(theme.primaryColor)
175
212
 
176
213
  const header = this.shadow.getElementById('header') as HTMLElement
177
- header.style.background = escapeHtml(theme.primaryColor)
214
+ header.style.cssText = `background:${escapeHtml(theme.primaryColor)};padding:16px 20px;color:#fff`
178
215
  ;(this.shadow.getElementById('header-title') as HTMLElement).textContent = state.config!.branding.title
179
216
  ;(this.shadow.getElementById('header-subtitle') as HTMLElement).textContent = state.config!.branding.subtitle
180
217
 
@@ -210,16 +247,17 @@ class RushChatbotElement extends HTMLElement {
210
247
  btn.textContent = q.question
211
248
  mainList.appendChild(btn)
212
249
  })
213
- if (!settings.hideEndChat) {
214
- const endBtn = document.createElement('button')
215
- endBtn.className = 'end-btn'
216
- endBtn.dataset.action = 'endchat'
217
- endBtn.textContent = "\uD83D\uDC4B I'm done, thanks!"
218
- mainList.appendChild(endBtn)
219
- }
220
250
  msgsContent.appendChild(mainList)
221
251
  }
222
252
 
253
+ if (!settings.hideEndChat && !state.messages.some(m => m.isEndChat)) {
254
+ const endBtn = document.createElement('button')
255
+ endBtn.className = 'end-btn'
256
+ endBtn.dataset.action = 'endchat'
257
+ endBtn.textContent = "\uD83D\uDC4B I'm done, thanks!"
258
+ msgsContent.appendChild(endBtn)
259
+ }
260
+
223
261
  if (this.showScrollBtn) {
224
262
  const scrollBtn = document.createElement('button')
225
263
  scrollBtn.className = 'scroll-btn'
@@ -242,6 +280,7 @@ class RushChatbotElement extends HTMLElement {
242
280
  }
243
281
 
244
282
  const newMsgsEl = this.shadow.getElementById('msgs') as HTMLElement
283
+ if (!this.isOpen) return
245
284
  if (wasAtBottom) {
246
285
  newMsgsEl.scrollTop = newMsgsEl.scrollHeight
247
286
  this.showScrollBtn = false
@@ -262,6 +301,55 @@ class RushChatbotElement extends HTMLElement {
262
301
  }
263
302
 
264
303
  this.bindEvents()
304
+ this.bindDrag()
305
+ }
306
+
307
+ private bindDrag() {
308
+ const fab = this.shadow.getElementById('fab') as HTMLElement
309
+ if (!fab) return
310
+
311
+ fab.addEventListener('pointerdown', (e: PointerEvent) => {
312
+ this.dragging = true
313
+ this.dragMoved = false
314
+ this.dragOffX = e.clientX - this.btnX
315
+ this.dragOffY = e.clientY - this.btnY
316
+ fab.setPointerCapture(e.pointerId)
317
+ fab.style.cursor = 'grabbing'
318
+ })
319
+
320
+ fab.addEventListener('pointermove', (e: PointerEvent) => {
321
+ if (!this.dragging) return
322
+ const newX = e.clientX - this.dragOffX
323
+ const newY = e.clientY - this.dragOffY
324
+ if (Math.abs(newX - this.btnX) > 4 || Math.abs(newY - this.btnY) > 4) this.dragMoved = true
325
+ this.btnX = Math.min(Math.max(newX, 0), window.innerWidth - 56)
326
+ this.btnY = Math.min(Math.max(newY, 0), window.innerHeight - 56)
327
+ fab.style.left = `${this.btnX}px`
328
+ fab.style.top = `${this.btnY}px`
329
+ const chatBox = this.shadow.getElementById('chat-box') as HTMLElement
330
+ if (chatBox && this.isOpen) {
331
+ const chatW = chatBox.offsetWidth || 380
332
+ const chatH = chatBox.offsetHeight || 580
333
+ const boxLeft = Math.min(Math.max(this.btnX + 28 - chatW / 2, 8), window.innerWidth - chatW - 8)
334
+ const boxBottom = window.innerHeight - this.btnY + 12
335
+ chatBox.style.left = `${boxLeft}px`
336
+ chatBox.style.bottom = `${boxBottom}px`
337
+ }
338
+ })
339
+
340
+ fab.addEventListener('pointerup', () => {
341
+ if (!this.dragMoved) {
342
+ this.isOpen = !this.isOpen
343
+ this.render()
344
+ }
345
+ this.dragging = false
346
+ fab.style.cursor = 'grab'
347
+ })
348
+
349
+ this.shadow.getElementById('close-btn')?.addEventListener('click', () => {
350
+ this.isOpen = false
351
+ this.render()
352
+ })
265
353
  }
266
354
 
267
355
  private bindEvents() {
@@ -372,6 +460,10 @@ class RushChatbotElement extends HTMLElement {
372
460
  mdDiv.className = 'rushchatbot-md'
373
461
  mdDiv.innerHTML = parseMarkdown(msg.text)
374
462
  bubble.appendChild(mdDiv)
463
+ if (msg.isError) {
464
+ bubble.style.border = '1px solid #EF4444'
465
+ bubble.style.color = '#B91C1C'
466
+ }
375
467
  if (msg.isEndChat) {
376
468
  const newChatBtn = document.createElement('button')
377
469
  newChatBtn.className = 'new-chat-btn'
@@ -42,9 +42,10 @@
42
42
  :style="mainBtnStyle">
43
43
  {{ q.question }}
44
44
  </button>
45
- <button v-if="!hideEndChat" @click="chat.endChat()" :style="endBtnStyle">👋 I'm done, thanks!</button>
46
45
  </div>
47
46
 
47
+ <button v-if="!hideEndChat && !state.messages.some(m => m.isEndChat)" @click="chat.endChat()" :style="endBtnStyle">👋 I'm done, thanks!</button>
48
+
48
49
  <div ref="bottom" />
49
50
  </div>
50
51
 
@@ -195,4 +196,7 @@ const sendBtnStyle = computed(() => ({
195
196
  .rushchatbot-md h1 { font-size: 1.1em; }
196
197
  .rushchatbot-md h2 { font-size: 1em; }
197
198
  .rushchatbot-md h3 { font-size: 0.95em; }
199
+ .rushchatbot-md code { font-family: monospace; font-size: 0.9em; background: rgba(0,0,0,0.08); padding: 1px 5px; border-radius: 4px; }
200
+ .rushchatbot-md pre { background: rgba(0,0,0,0.08); border-radius: 6px; padding: 10px 12px; overflow-x: auto; margin: 6px 0; }
201
+ .rushchatbot-md pre code { background: none; padding: 0; font-size: 0.85em; white-space: pre; }
198
202
  </style>
@@ -44,9 +44,10 @@
44
44
  :style="mainBtnStyle">
45
45
  {{ q.question }}
46
46
  </button>
47
- <button v-if="!hideEndChat" @click="chat.endChat()" :style="endBtnStyle">👋 I'm done, thanks!</button>
48
47
  </div>
49
48
 
49
+ <button v-if="!hideEndChat && !state.messages.some(function(m) { return m.isEndChat })" @click="chat.endChat()" :style="endBtnStyle">👋 I'm done, thanks!</button>
50
+
50
51
  <div ref="bottom" />
51
52
  </div>
52
53
 
@@ -201,4 +202,7 @@ export default {
201
202
  0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
202
203
  40% { transform: translateY(-6px); opacity: 1; }
203
204
  }
205
+ .rushchatbot-md code { font-family: monospace; font-size: 0.9em; background: rgba(0,0,0,0.08); padding: 1px 5px; border-radius: 4px; }
206
+ .rushchatbot-md pre { background: rgba(0,0,0,0.08); border-radius: 6px; padding: 10px 12px; overflow-x: auto; margin: 6px 0; }
207
+ .rushchatbot-md pre code { background: none; padding: 0; font-size: 0.85em; white-space: pre; }
204
208
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rushchatbot",
3
- "version": "1.6.9",
3
+ "version": "1.7.2",
4
4
  "scripts": {
5
5
  "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
6
6
  "dev:client": "vite",
@@ -797,5 +797,71 @@
797
797
  "questionId": "q1",
798
798
  "timestamp": "2026-09-25T06:14:13.722Z",
799
799
  "type": "click"
800
+ },
801
+ {
802
+ "sessionId": "sess_mugn4uve_9yrh57",
803
+ "questionId": "q3",
804
+ "timestamp": "2026-09-25T07:29:56.881Z",
805
+ "type": "click"
806
+ },
807
+ {
808
+ "sessionId": "sess_mugn4uve_9yrh57",
809
+ "questionId": "q3h",
810
+ "timestamp": "2026-09-25T07:29:59.276Z",
811
+ "type": "click"
812
+ },
813
+ {
814
+ "sessionId": "sess_mugn4uve_9yrh57",
815
+ "questionId": "q6",
816
+ "timestamp": "2026-09-25T07:30:05.858Z",
817
+ "type": "click"
818
+ },
819
+ {
820
+ "sessionId": "sess_mugn4uve_9yrh57",
821
+ "questionId": "q8",
822
+ "timestamp": "2026-09-25T07:31:58.988Z",
823
+ "type": "click"
824
+ },
825
+ {
826
+ "sessionId": "sess_mugn4uve_9yrh57",
827
+ "questionId": "q8c",
828
+ "timestamp": "2026-09-25T07:32:01.577Z",
829
+ "type": "click"
830
+ },
831
+ {
832
+ "sessionId": "sess_mugn4uve_9yrh57",
833
+ "questionId": "q8",
834
+ "timestamp": "2026-09-25T08:03:16.219Z",
835
+ "type": "click"
836
+ },
837
+ {
838
+ "sessionId": "sess_mugn4uve_9yrh57",
839
+ "questionId": "q8c",
840
+ "timestamp": "2026-09-25T08:03:18.364Z",
841
+ "type": "click"
842
+ },
843
+ {
844
+ "sessionId": "sess_mugn4uve_9yrh57",
845
+ "questionId": "q8b",
846
+ "timestamp": "2026-09-25T08:03:21.319Z",
847
+ "type": "click"
848
+ },
849
+ {
850
+ "sessionId": "sess_mugn4uve_9yrh57",
851
+ "questionId": "q8a",
852
+ "timestamp": "2026-09-25T08:03:24.033Z",
853
+ "type": "click"
854
+ },
855
+ {
856
+ "sessionId": "sess_mugn4uve_9yrh57",
857
+ "questionId": "q1",
858
+ "timestamp": "2026-09-25T08:03:29.705Z",
859
+ "type": "click"
860
+ },
861
+ {
862
+ "sessionId": "sess_mugn4uve_9yrh57",
863
+ "questionId": "q1b",
864
+ "timestamp": "2026-09-25T08:03:33.517Z",
865
+ "type": "click"
800
866
  }
801
867
  ]