ask-junkie 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +864 -0
- package/dist/ask-junkie.css +485 -0
- package/dist/ask-junkie.esm.js +1847 -0
- package/dist/ask-junkie.esm.js.map +1 -0
- package/dist/ask-junkie.min.js +1858 -0
- package/dist/ask-junkie.min.js.map +1 -0
- package/dist/junkie-icon.png +0 -0
- package/package.json +43 -0
- package/src/ai/AIProviderFactory.js +43 -0
- package/src/ai/BaseProvider.js +68 -0
- package/src/ai/GeminiProvider.js +58 -0
- package/src/ai/GroqProvider.js +51 -0
- package/src/ai/OpenAIProvider.js +51 -0
- package/src/ai/OpenRouterProvider.js +53 -0
- package/src/analytics/FirebaseLogger.js +115 -0
- package/src/core/AskJunkie.js +585 -0
- package/src/core/EventEmitter.js +52 -0
- package/src/index.js +19 -0
- package/src/ui/ChatWidget.js +611 -0
- package/src/ui/styles.css +485 -0
- package/src/utils/DOMUtils.js +69 -0
- package/src/utils/StorageManager.js +80 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Widget UI Component
|
|
3
|
+
* Renders and manages the chatbot interface
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { escapeHtml, formatBotResponse, createElement } from '../utils/DOMUtils.js';
|
|
7
|
+
|
|
8
|
+
export class ChatWidget {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.container = null;
|
|
12
|
+
this.messagesContainer = null;
|
|
13
|
+
this.input = null;
|
|
14
|
+
this.isOpen = false;
|
|
15
|
+
this.isDragging = false;
|
|
16
|
+
this.dragOffset = { x: 0, y: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render the chat widget to the DOM
|
|
21
|
+
*/
|
|
22
|
+
render() {
|
|
23
|
+
// Create container
|
|
24
|
+
this.container = createElement('div', {
|
|
25
|
+
id: 'ask-junkie-widget',
|
|
26
|
+
className: 'ask-junkie-container'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Apply position
|
|
30
|
+
this._applyPosition();
|
|
31
|
+
|
|
32
|
+
// Create toggle button
|
|
33
|
+
const toggleBtn = this._createToggleButton();
|
|
34
|
+
this.container.appendChild(toggleBtn);
|
|
35
|
+
|
|
36
|
+
// Create chat window
|
|
37
|
+
const chatWindow = this._createChatWindow();
|
|
38
|
+
this.container.appendChild(chatWindow);
|
|
39
|
+
|
|
40
|
+
// Add to DOM
|
|
41
|
+
document.body.appendChild(this.container);
|
|
42
|
+
|
|
43
|
+
// Apply theme colors
|
|
44
|
+
this._applyTheme();
|
|
45
|
+
|
|
46
|
+
// Load existing chat history
|
|
47
|
+
if (this.config.chatHistory?.length > 0) {
|
|
48
|
+
this._loadHistory(this.config.chatHistory);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Set up event listeners
|
|
52
|
+
this._setupEventListeners();
|
|
53
|
+
|
|
54
|
+
// Enable dragging if configured
|
|
55
|
+
if (this.config.draggable) {
|
|
56
|
+
this._enableDragging(toggleBtn);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Enable resizing if configured
|
|
60
|
+
if (this.config.resizable) {
|
|
61
|
+
this._enableResizing();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Open on load if configured
|
|
65
|
+
if (this.config.openOnLoad) {
|
|
66
|
+
setTimeout(() => this.open(), 500);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create the toggle button
|
|
72
|
+
*/
|
|
73
|
+
_createToggleButton() {
|
|
74
|
+
const btn = createElement('button', {
|
|
75
|
+
id: 'ask-junkie-toggle',
|
|
76
|
+
className: 'ask-junkie-toggle',
|
|
77
|
+
'aria-label': 'Toggle Chat'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Default icon URL (hosted on jsDelivr CDN from npm package)
|
|
81
|
+
const defaultIconUrl = 'https://cdn.jsdelivr.net/npm/ask-junkie-sdk@latest/dist/junkie-icon.png';
|
|
82
|
+
const iconUrl = this.config.toggleIcon || defaultIconUrl;
|
|
83
|
+
|
|
84
|
+
btn.innerHTML = `
|
|
85
|
+
<img class="icon-chat" src="${iconUrl}" alt="Chat" onerror="this.style.display='none';this.nextElementSibling.style.display='block';">
|
|
86
|
+
<svg class="icon-chat-fallback" viewBox="0 0 24 24" width="28" height="28" fill="white" style="display:none">
|
|
87
|
+
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
|
|
88
|
+
</svg>
|
|
89
|
+
<svg class="icon-close" viewBox="0 0 24 24" width="28" height="28" fill="white" style="display:none">
|
|
90
|
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
|
91
|
+
</svg>
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
return btn;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create the chat window
|
|
99
|
+
*/
|
|
100
|
+
_createChatWindow() {
|
|
101
|
+
const window = createElement('div', {
|
|
102
|
+
id: 'ask-junkie-window',
|
|
103
|
+
className: 'ask-junkie-window'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
window.style.display = 'none';
|
|
107
|
+
|
|
108
|
+
// Header
|
|
109
|
+
const header = createElement('div', { className: 'ask-junkie-header' });
|
|
110
|
+
const defaultAvatarUrl = 'https://cdn.jsdelivr.net/npm/ask-junkie-sdk@latest/dist/junkie-icon.png';
|
|
111
|
+
const avatarUrl = this.config.botAvatar || defaultAvatarUrl;
|
|
112
|
+
header.innerHTML = `
|
|
113
|
+
<div class="ask-junkie-avatar">
|
|
114
|
+
<img src="${escapeHtml(avatarUrl)}" alt="Bot">
|
|
115
|
+
</div>
|
|
116
|
+
<div class="ask-junkie-info">
|
|
117
|
+
<span class="ask-junkie-name">${escapeHtml(this.config.botName)}</span>
|
|
118
|
+
<span class="ask-junkie-status">Online • Ready to help</span>
|
|
119
|
+
</div>
|
|
120
|
+
<button class="ask-junkie-clear" title="Clear chat" style="display:none">
|
|
121
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
122
|
+
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
|
123
|
+
</svg>
|
|
124
|
+
</button>
|
|
125
|
+
`;
|
|
126
|
+
window.appendChild(header);
|
|
127
|
+
|
|
128
|
+
// Messages container
|
|
129
|
+
this.messagesContainer = createElement('div', {
|
|
130
|
+
id: 'ask-junkie-messages',
|
|
131
|
+
className: 'ask-junkie-messages'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Add welcome message
|
|
135
|
+
const welcomeMsg = createElement('div', {
|
|
136
|
+
className: 'ask-junkie-message bot welcome'
|
|
137
|
+
});
|
|
138
|
+
welcomeMsg.innerHTML = `<div class="message-content">${escapeHtml(this.config.welcomeMessage)}</div>`;
|
|
139
|
+
this.messagesContainer.appendChild(welcomeMsg);
|
|
140
|
+
|
|
141
|
+
window.appendChild(this.messagesContainer);
|
|
142
|
+
|
|
143
|
+
// Input area
|
|
144
|
+
const inputArea = createElement('div', { className: 'ask-junkie-input-area' });
|
|
145
|
+
|
|
146
|
+
// Suggestions (animated or static based on config)
|
|
147
|
+
if (this.config.suggestions?.length > 0) {
|
|
148
|
+
if (this.config.animatedSuggestions) {
|
|
149
|
+
// Animated marquee suggestions
|
|
150
|
+
const suggestionsContainer = createElement('div', { className: 'ask-junkie-suggestions animated' });
|
|
151
|
+
const marqueeWrapper = createElement('div', { className: 'ask-junkie-marquee-wrapper' });
|
|
152
|
+
const marqueeContent = createElement('div', { className: 'ask-junkie-marquee-content' });
|
|
153
|
+
|
|
154
|
+
// Double the suggestions for seamless loop
|
|
155
|
+
const allSuggestions = [...this.config.suggestions, ...this.config.suggestions];
|
|
156
|
+
|
|
157
|
+
allSuggestions.forEach(text => {
|
|
158
|
+
const chip = createElement('button', {
|
|
159
|
+
className: 'ask-junkie-chip',
|
|
160
|
+
onClick: () => {
|
|
161
|
+
this.input.value = text;
|
|
162
|
+
this.input.focus();
|
|
163
|
+
}
|
|
164
|
+
}, [text]);
|
|
165
|
+
marqueeContent.appendChild(chip);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
marqueeWrapper.appendChild(marqueeContent);
|
|
169
|
+
suggestionsContainer.appendChild(marqueeWrapper);
|
|
170
|
+
|
|
171
|
+
// Pause animation on hover
|
|
172
|
+
marqueeWrapper.addEventListener('mouseenter', () => {
|
|
173
|
+
marqueeContent.style.animationPlayState = 'paused';
|
|
174
|
+
});
|
|
175
|
+
marqueeWrapper.addEventListener('mouseleave', () => {
|
|
176
|
+
marqueeContent.style.animationPlayState = 'running';
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
inputArea.appendChild(suggestionsContainer);
|
|
180
|
+
} else {
|
|
181
|
+
// Static suggestions
|
|
182
|
+
const suggestionsContainer = createElement('div', { className: 'ask-junkie-suggestions' });
|
|
183
|
+
this.config.suggestions.forEach(text => {
|
|
184
|
+
const chip = createElement('button', {
|
|
185
|
+
className: 'ask-junkie-chip',
|
|
186
|
+
onClick: () => {
|
|
187
|
+
this.input.value = text;
|
|
188
|
+
this.input.focus();
|
|
189
|
+
}
|
|
190
|
+
}, [text]);
|
|
191
|
+
suggestionsContainer.appendChild(chip);
|
|
192
|
+
});
|
|
193
|
+
inputArea.appendChild(suggestionsContainer);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Input row
|
|
198
|
+
const inputRow = createElement('div', { className: 'ask-junkie-input-row' });
|
|
199
|
+
|
|
200
|
+
this.input = createElement('input', {
|
|
201
|
+
type: 'text',
|
|
202
|
+
id: 'ask-junkie-input',
|
|
203
|
+
placeholder: 'Type your message...',
|
|
204
|
+
autocomplete: 'off'
|
|
205
|
+
});
|
|
206
|
+
inputRow.appendChild(this.input);
|
|
207
|
+
|
|
208
|
+
// Voice input button (if enabled)
|
|
209
|
+
if (this.config.voiceInput && 'webkitSpeechRecognition' in window) {
|
|
210
|
+
const micBtn = createElement('button', {
|
|
211
|
+
className: 'ask-junkie-mic',
|
|
212
|
+
title: 'Voice input',
|
|
213
|
+
onClick: () => this._startVoiceInput()
|
|
214
|
+
});
|
|
215
|
+
micBtn.innerHTML = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
|
|
216
|
+
inputRow.appendChild(micBtn);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Send button
|
|
220
|
+
const sendBtn = createElement('button', {
|
|
221
|
+
className: 'ask-junkie-send',
|
|
222
|
+
onClick: () => this._sendMessage()
|
|
223
|
+
});
|
|
224
|
+
sendBtn.innerHTML = `<svg viewBox="0 0 24 24" width="20" height="20" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>`;
|
|
225
|
+
inputRow.appendChild(sendBtn);
|
|
226
|
+
|
|
227
|
+
inputArea.appendChild(inputRow);
|
|
228
|
+
window.appendChild(inputArea);
|
|
229
|
+
|
|
230
|
+
return window;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set up event listeners
|
|
235
|
+
*/
|
|
236
|
+
_setupEventListeners() {
|
|
237
|
+
const toggleBtn = this.container.querySelector('#ask-junkie-toggle');
|
|
238
|
+
const clearBtn = this.container.querySelector('.ask-junkie-clear');
|
|
239
|
+
|
|
240
|
+
// Toggle button
|
|
241
|
+
toggleBtn.addEventListener('click', () => {
|
|
242
|
+
if (!this.isDragging) {
|
|
243
|
+
this.toggle();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Input enter key
|
|
248
|
+
this.input.addEventListener('keypress', (e) => {
|
|
249
|
+
if (e.key === 'Enter') {
|
|
250
|
+
this._sendMessage();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Clear button
|
|
255
|
+
if (clearBtn) {
|
|
256
|
+
clearBtn.addEventListener('click', (e) => {
|
|
257
|
+
e.stopPropagation();
|
|
258
|
+
if (confirm('Clear all chat history?')) {
|
|
259
|
+
this._clearMessages();
|
|
260
|
+
if (this.config.onClearHistory) {
|
|
261
|
+
this.config.onClearHistory();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Apply position from config
|
|
270
|
+
*/
|
|
271
|
+
_applyPosition() {
|
|
272
|
+
const pos = this.config.position || 'bottom-right';
|
|
273
|
+
const [vertical, horizontal] = pos.split('-');
|
|
274
|
+
|
|
275
|
+
this.container.style[vertical] = '24px';
|
|
276
|
+
this.container.style[horizontal] = '24px';
|
|
277
|
+
this.container.setAttribute('data-position', pos);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Apply theme colors
|
|
282
|
+
*/
|
|
283
|
+
_applyTheme() {
|
|
284
|
+
const theme = this.config.theme;
|
|
285
|
+
let gradient;
|
|
286
|
+
|
|
287
|
+
if (theme.mode === 'solid') {
|
|
288
|
+
gradient = theme.primary;
|
|
289
|
+
} else {
|
|
290
|
+
gradient = `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Apply to toggle button and header
|
|
294
|
+
const toggleBtn = this.container.querySelector('.ask-junkie-toggle');
|
|
295
|
+
const header = this.container.querySelector('.ask-junkie-header');
|
|
296
|
+
const sendBtn = this.container.querySelector('.ask-junkie-send');
|
|
297
|
+
|
|
298
|
+
if (toggleBtn) toggleBtn.style.background = gradient;
|
|
299
|
+
if (header) header.style.background = gradient;
|
|
300
|
+
if (sendBtn) sendBtn.style.background = gradient;
|
|
301
|
+
|
|
302
|
+
// Set CSS variable for consistency
|
|
303
|
+
this.container.style.setProperty('--ask-junkie-primary', theme.primary);
|
|
304
|
+
this.container.style.setProperty('--ask-junkie-secondary', theme.secondary);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Send a message
|
|
309
|
+
*/
|
|
310
|
+
_sendMessage() {
|
|
311
|
+
const message = this.input.value.trim();
|
|
312
|
+
if (!message) return;
|
|
313
|
+
|
|
314
|
+
// Add user message to UI
|
|
315
|
+
this.addMessage(message, 'user');
|
|
316
|
+
this.input.value = '';
|
|
317
|
+
|
|
318
|
+
// Update clear button visibility
|
|
319
|
+
this._updateClearButton();
|
|
320
|
+
|
|
321
|
+
// Trigger callback
|
|
322
|
+
if (this.config.onSendMessage) {
|
|
323
|
+
this.config.onSendMessage(message);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Add a message to the chat
|
|
329
|
+
*/
|
|
330
|
+
addMessage(text, sender = 'bot') {
|
|
331
|
+
const msgDiv = createElement('div', {
|
|
332
|
+
className: `ask-junkie-message ${sender}`
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const content = createElement('div', { className: 'message-content' });
|
|
336
|
+
|
|
337
|
+
if (sender === 'bot') {
|
|
338
|
+
content.innerHTML = formatBotResponse(text);
|
|
339
|
+
} else {
|
|
340
|
+
content.textContent = text;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
msgDiv.appendChild(content);
|
|
344
|
+
this.messagesContainer.appendChild(msgDiv);
|
|
345
|
+
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
|
|
346
|
+
|
|
347
|
+
this._updateClearButton();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Show typing indicator
|
|
352
|
+
*/
|
|
353
|
+
showTyping() {
|
|
354
|
+
const typing = createElement('div', {
|
|
355
|
+
className: 'ask-junkie-message bot typing',
|
|
356
|
+
id: 'ask-junkie-typing'
|
|
357
|
+
});
|
|
358
|
+
typing.innerHTML = `
|
|
359
|
+
<div class="message-content">
|
|
360
|
+
<div class="typing-dots">
|
|
361
|
+
<span></span><span></span><span></span>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
`;
|
|
365
|
+
this.messagesContainer.appendChild(typing);
|
|
366
|
+
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Hide typing indicator
|
|
371
|
+
*/
|
|
372
|
+
hideTyping() {
|
|
373
|
+
const typing = document.getElementById('ask-junkie-typing');
|
|
374
|
+
if (typing) typing.remove();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Load chat history
|
|
379
|
+
*/
|
|
380
|
+
_loadHistory(history) {
|
|
381
|
+
history.forEach(msg => {
|
|
382
|
+
const sender = msg.role === 'assistant' ? 'bot' : 'user';
|
|
383
|
+
this.addMessage(msg.content, sender);
|
|
384
|
+
});
|
|
385
|
+
this._updateClearButton();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clear messages
|
|
390
|
+
*/
|
|
391
|
+
_clearMessages() {
|
|
392
|
+
const messages = this.messagesContainer.querySelectorAll('.ask-junkie-message:not(.welcome)');
|
|
393
|
+
messages.forEach(m => m.remove());
|
|
394
|
+
this._updateClearButton();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Update clear button visibility
|
|
399
|
+
*/
|
|
400
|
+
_updateClearButton() {
|
|
401
|
+
const clearBtn = this.container.querySelector('.ask-junkie-clear');
|
|
402
|
+
const hasMessages = this.messagesContainer.querySelectorAll('.ask-junkie-message:not(.welcome)').length > 0;
|
|
403
|
+
if (clearBtn) {
|
|
404
|
+
clearBtn.style.display = hasMessages ? 'flex' : 'none';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Start voice input
|
|
410
|
+
*/
|
|
411
|
+
_startVoiceInput() {
|
|
412
|
+
if (!('webkitSpeechRecognition' in window)) return;
|
|
413
|
+
|
|
414
|
+
const recognition = new webkitSpeechRecognition();
|
|
415
|
+
recognition.continuous = false;
|
|
416
|
+
recognition.interimResults = false;
|
|
417
|
+
|
|
418
|
+
const micBtn = this.container.querySelector('.ask-junkie-mic');
|
|
419
|
+
if (micBtn) micBtn.classList.add('listening');
|
|
420
|
+
|
|
421
|
+
recognition.onresult = (event) => {
|
|
422
|
+
const transcript = event.results[0][0].transcript;
|
|
423
|
+
this.input.value = transcript;
|
|
424
|
+
this.input.focus();
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
recognition.onend = () => {
|
|
428
|
+
if (micBtn) micBtn.classList.remove('listening');
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
recognition.start();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Enable dragging
|
|
436
|
+
*/
|
|
437
|
+
_enableDragging(toggleBtn) {
|
|
438
|
+
let startX, startY, startLeft, startTop;
|
|
439
|
+
|
|
440
|
+
const onMouseDown = (e) => {
|
|
441
|
+
if (this.isOpen) return;
|
|
442
|
+
|
|
443
|
+
startX = e.clientX || e.touches?.[0]?.clientX;
|
|
444
|
+
startY = e.clientY || e.touches?.[0]?.clientY;
|
|
445
|
+
|
|
446
|
+
const rect = this.container.getBoundingClientRect();
|
|
447
|
+
startLeft = rect.left;
|
|
448
|
+
startTop = rect.top;
|
|
449
|
+
|
|
450
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
451
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
452
|
+
document.addEventListener('touchmove', onMouseMove);
|
|
453
|
+
document.addEventListener('touchend', onMouseUp);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const onMouseMove = (e) => {
|
|
457
|
+
const clientX = e.clientX || e.touches?.[0]?.clientX;
|
|
458
|
+
const clientY = e.clientY || e.touches?.[0]?.clientY;
|
|
459
|
+
|
|
460
|
+
const deltaX = clientX - startX;
|
|
461
|
+
const deltaY = clientY - startY;
|
|
462
|
+
|
|
463
|
+
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
|
464
|
+
this.isDragging = true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const newLeft = Math.max(0, Math.min(startLeft + deltaX, window.innerWidth - 70));
|
|
468
|
+
const newTop = Math.max(0, Math.min(startTop + deltaY, window.innerHeight - 70));
|
|
469
|
+
|
|
470
|
+
this.container.style.left = newLeft + 'px';
|
|
471
|
+
this.container.style.top = newTop + 'px';
|
|
472
|
+
this.container.style.right = 'auto';
|
|
473
|
+
this.container.style.bottom = 'auto';
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const onMouseUp = () => {
|
|
477
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
478
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
479
|
+
document.removeEventListener('touchmove', onMouseMove);
|
|
480
|
+
document.removeEventListener('touchend', onMouseUp);
|
|
481
|
+
|
|
482
|
+
setTimeout(() => { this.isDragging = false; }, 100);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
toggleBtn.addEventListener('mousedown', onMouseDown);
|
|
486
|
+
toggleBtn.addEventListener('touchstart', onMouseDown);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Enable resizing of chat window
|
|
491
|
+
*/
|
|
492
|
+
_enableResizing() {
|
|
493
|
+
const chatWindow = this.container.querySelector('.ask-junkie-window');
|
|
494
|
+
if (!chatWindow) return;
|
|
495
|
+
|
|
496
|
+
// Create resize handle
|
|
497
|
+
const resizeHandle = createElement('div', { className: 'ask-junkie-resize-handle' });
|
|
498
|
+
resizeHandle.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M22,22H20V20H22V22M22,18H20V16H22V18M18,22H16V20H18V22M18,18H16V16H18V18M14,22H12V20H14V22M22,14H20V12H22V14Z"/></svg>`;
|
|
499
|
+
chatWindow.appendChild(resizeHandle);
|
|
500
|
+
|
|
501
|
+
// Load saved size from localStorage
|
|
502
|
+
const savedSize = localStorage.getItem('ask_junkie_chat_size');
|
|
503
|
+
if (savedSize) {
|
|
504
|
+
try {
|
|
505
|
+
const { width, height } = JSON.parse(savedSize);
|
|
506
|
+
chatWindow.style.width = width + 'px';
|
|
507
|
+
chatWindow.style.height = height + 'px';
|
|
508
|
+
} catch (e) {
|
|
509
|
+
// Ignore parse errors
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let isResizing = false;
|
|
514
|
+
let startX, startY, startWidth, startHeight;
|
|
515
|
+
|
|
516
|
+
const onMouseDown = (e) => {
|
|
517
|
+
e.preventDefault();
|
|
518
|
+
e.stopPropagation();
|
|
519
|
+
isResizing = true;
|
|
520
|
+
|
|
521
|
+
startX = e.clientX || e.touches?.[0]?.clientX;
|
|
522
|
+
startY = e.clientY || e.touches?.[0]?.clientY;
|
|
523
|
+
startWidth = chatWindow.offsetWidth;
|
|
524
|
+
startHeight = chatWindow.offsetHeight;
|
|
525
|
+
|
|
526
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
527
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
528
|
+
document.addEventListener('touchmove', onMouseMove);
|
|
529
|
+
document.addEventListener('touchend', onMouseUp);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const onMouseMove = (e) => {
|
|
533
|
+
if (!isResizing) return;
|
|
534
|
+
|
|
535
|
+
const clientX = e.clientX || e.touches?.[0]?.clientX;
|
|
536
|
+
const clientY = e.clientY || e.touches?.[0]?.clientY;
|
|
537
|
+
|
|
538
|
+
// Calculate new dimensions (resizing from top-left corner)
|
|
539
|
+
const deltaX = startX - clientX;
|
|
540
|
+
const deltaY = startY - clientY;
|
|
541
|
+
|
|
542
|
+
const newWidth = Math.max(280, Math.min(startWidth + deltaX, window.innerWidth - 40));
|
|
543
|
+
const newHeight = Math.max(350, Math.min(startHeight + deltaY, window.innerHeight - 100));
|
|
544
|
+
|
|
545
|
+
chatWindow.style.width = newWidth + 'px';
|
|
546
|
+
chatWindow.style.height = newHeight + 'px';
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const onMouseUp = () => {
|
|
550
|
+
if (isResizing) {
|
|
551
|
+
// Save size to localStorage
|
|
552
|
+
localStorage.setItem('ask_junkie_chat_size', JSON.stringify({
|
|
553
|
+
width: chatWindow.offsetWidth,
|
|
554
|
+
height: chatWindow.offsetHeight
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
isResizing = false;
|
|
558
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
559
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
560
|
+
document.removeEventListener('touchmove', onMouseMove);
|
|
561
|
+
document.removeEventListener('touchend', onMouseUp);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
resizeHandle.addEventListener('mousedown', onMouseDown);
|
|
565
|
+
resizeHandle.addEventListener('touchstart', onMouseDown);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ===== PUBLIC METHODS =====
|
|
569
|
+
|
|
570
|
+
open() {
|
|
571
|
+
this.isOpen = true;
|
|
572
|
+
const window = this.container.querySelector('.ask-junkie-window');
|
|
573
|
+
const chatIcon = this.container.querySelector('.icon-chat');
|
|
574
|
+
const closeIcon = this.container.querySelector('.icon-close');
|
|
575
|
+
|
|
576
|
+
window.style.display = 'flex';
|
|
577
|
+
chatIcon.style.display = 'none';
|
|
578
|
+
closeIcon.style.display = 'block';
|
|
579
|
+
this.input.focus();
|
|
580
|
+
|
|
581
|
+
if (this.config.onOpen) this.config.onOpen();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
close() {
|
|
585
|
+
this.isOpen = false;
|
|
586
|
+
const window = this.container.querySelector('.ask-junkie-window');
|
|
587
|
+
const chatIcon = this.container.querySelector('.icon-chat');
|
|
588
|
+
const closeIcon = this.container.querySelector('.icon-close');
|
|
589
|
+
|
|
590
|
+
window.style.display = 'none';
|
|
591
|
+
chatIcon.style.display = 'block';
|
|
592
|
+
closeIcon.style.display = 'none';
|
|
593
|
+
|
|
594
|
+
if (this.config.onClose) this.config.onClose();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
toggle() {
|
|
598
|
+
if (this.isOpen) {
|
|
599
|
+
this.close();
|
|
600
|
+
} else {
|
|
601
|
+
this.open();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
destroy() {
|
|
606
|
+
if (this.container) {
|
|
607
|
+
this.container.remove();
|
|
608
|
+
this.container = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|