@blumessage/react-chat 1.1.4 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BlumessageChat.js +148 -80
- package/dist/types/BlumessageChat.d.ts +3 -9
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/BlumessageChat.js
CHANGED
|
@@ -60,7 +60,7 @@ import ReactMarkdown from 'react-markdown';
|
|
|
60
60
|
import remarkGfm from 'remark-gfm';
|
|
61
61
|
import { MessageCircle, AlertTriangle, Loader2, Send, X, Maximize, Minimize2, Bot, MessageSquare, Phone, Mail, Headphones, Users, User, Heart, Star, Zap } from "lucide-react";
|
|
62
62
|
// Custom CSS animations that don't depend on Tailwind
|
|
63
|
-
var customStyles = "\n @keyframes bounce {\n 0%, 100% {\n transform: translateY(0);\n }\n 50% {\n transform: translateY(-25%);\n }\n }\n \n @keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }\n \n .blumessage-animate-bounce {\n animation: bounce 1s infinite;\n }\n \n .blumessage-animate-spin {\n animation: spin 1s linear infinite;\n }\n";
|
|
63
|
+
var customStyles = "\n @keyframes bounce {\n 0%, 100% {\n transform: translateY(0);\n }\n 50% {\n transform: translateY(-25%);\n }\n }\n \n @keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }\n \n .blumessage-animate-bounce {\n animation: bounce 1s infinite;\n }\n \n .blumessage-animate-spin {\n animation: spin 1s linear infinite;\n }\n \n /* Mobile-specific styles */\n @media (max-width: 768px) {\n .blumessage-mobile-fullscreen {\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n width: 100% !important;\n height: 100% !important;\n max-width: 100% !important;\n max-height: 100% !important;\n border-radius: 0 !important;\n z-index: 9999 !important;\n }\n \n .blumessage-mobile-chat {\n border-radius: 16px !important;\n }\n \n .blumessage-mobile-button {\n position: fixed !important;\n bottom: 20px !important;\n right: 20px !important;\n left: auto !important;\n top: auto !important;\n }\n }\n \n @media (max-width: 480px) {\n .blumessage-mobile-fullscreen {\n border-radius: 0 !important;\n }\n \n .blumessage-mobile-chat {\n border-radius: 12px !important;\n }\n \n .blumessage-mobile-button {\n bottom: 16px !important;\n right: 16px !important;\n }\n }\n";
|
|
64
64
|
// Inject custom styles
|
|
65
65
|
if (typeof document !== 'undefined' && !document.getElementById('blumessage-styles')) {
|
|
66
66
|
var style = document.createElement('style');
|
|
@@ -68,42 +68,59 @@ if (typeof document !== 'undefined' && !document.getElementById('blumessage-styl
|
|
|
68
68
|
style.textContent = customStyles;
|
|
69
69
|
document.head.appendChild(style);
|
|
70
70
|
}
|
|
71
|
-
// Session storage key for conversation
|
|
72
|
-
var
|
|
71
|
+
// Session storage key for conversation token
|
|
72
|
+
var CONVERSATION_TOKEN_KEY = 'blumessage_conversation_token';
|
|
73
73
|
// Utility functions for storage
|
|
74
|
-
var
|
|
74
|
+
var saveConversationToken = function (token, persistent) {
|
|
75
75
|
if (persistent === void 0) { persistent = false; }
|
|
76
76
|
try {
|
|
77
77
|
var storage = persistent ? localStorage : sessionStorage;
|
|
78
|
-
storage.setItem(
|
|
78
|
+
storage.setItem(CONVERSATION_TOKEN_KEY, token);
|
|
79
79
|
}
|
|
80
80
|
catch (error) {
|
|
81
|
-
console.warn("Failed to save conversation
|
|
81
|
+
console.warn("Failed to save conversation token to ".concat(persistent ? 'local' : 'session', " storage:"), error);
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
|
-
var
|
|
84
|
+
var getStoredConversationToken = function (persistent) {
|
|
85
85
|
if (persistent === void 0) { persistent = false; }
|
|
86
86
|
try {
|
|
87
87
|
var storage = persistent ? localStorage : sessionStorage;
|
|
88
|
-
return storage.getItem(
|
|
88
|
+
return storage.getItem(CONVERSATION_TOKEN_KEY);
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
91
|
-
console.warn("Failed to get conversation
|
|
91
|
+
console.warn("Failed to get conversation token from ".concat(persistent ? 'local' : 'session', " storage:"), error);
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
|
-
var
|
|
95
|
+
var clearStoredConversationToken = function (persistent) {
|
|
96
96
|
if (persistent === void 0) { persistent = false; }
|
|
97
97
|
try {
|
|
98
98
|
var storage = persistent ? localStorage : sessionStorage;
|
|
99
|
-
storage.removeItem(
|
|
99
|
+
storage.removeItem(CONVERSATION_TOKEN_KEY);
|
|
100
100
|
}
|
|
101
101
|
catch (error) {
|
|
102
|
-
console.warn("Failed to clear conversation
|
|
102
|
+
console.warn("Failed to clear conversation token from ".concat(persistent ? 'local' : 'session', " storage:"), error);
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
|
+
// Custom hook for mobile detection
|
|
106
|
+
var useIsMobile = function () {
|
|
107
|
+
var _a = useState(false), isMobile = _a[0], setIsMobile = _a[1];
|
|
108
|
+
useEffect(function () {
|
|
109
|
+
var checkIsMobile = function () {
|
|
110
|
+
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
111
|
+
var mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i;
|
|
112
|
+
var isMobileDevice = mobileRegex.test(userAgent.toLowerCase());
|
|
113
|
+
var isSmallScreen = window.innerWidth <= 768;
|
|
114
|
+
setIsMobile(isMobileDevice || isSmallScreen);
|
|
115
|
+
};
|
|
116
|
+
checkIsMobile();
|
|
117
|
+
window.addEventListener('resize', checkIsMobile);
|
|
118
|
+
return function () { return window.removeEventListener('resize', checkIsMobile); };
|
|
119
|
+
}, []);
|
|
120
|
+
return isMobile;
|
|
121
|
+
};
|
|
105
122
|
export var BlumessageChat = function (_a) {
|
|
106
|
-
var apiKey = _a.apiKey, _b = _a.placeholder, placeholder = _b === void 0 ? "Type your message..." : _b, _c = _a.theme, theme = _c === void 0 ? 'light' : _c, width = _a.width, height = _a.height, _d = _a.size, size = _d === void 0 ? 'medium' : _d, _e = _a.name, name = _e === void 0 ? "Blumessage AI" : _e, _f = _a.subtitle, subtitle = _f === void 0 ? "Online • Instant responses" : _f, _g = _a.initialMessages, initialMessages = _g === void 0 ? [] : _g, onUserMessage = _a.onUserMessage, onAssistantMessage = _a.onAssistantMessage,
|
|
123
|
+
var apiKey = _a.apiKey, _b = _a.placeholder, placeholder = _b === void 0 ? "Type your message..." : _b, _c = _a.theme, theme = _c === void 0 ? 'light' : _c, width = _a.width, height = _a.height, _d = _a.size, size = _d === void 0 ? 'medium' : _d, _e = _a.name, name = _e === void 0 ? "Blumessage AI" : _e, _f = _a.subtitle, subtitle = _f === void 0 ? "Online • Instant responses" : _f, _g = _a.initialMessages, initialMessages = _g === void 0 ? [] : _g, onUserMessage = _a.onUserMessage, onAssistantMessage = _a.onAssistantMessage, initialToken = _a.token, onTokenChange = _a.onTokenChange, onChatWidgetOpen = _a.onChatWidgetOpen, onChatWidgetClosed = _a.onChatWidgetClosed, onError = _a.onError, _h = _a.persistent, persistent = _h === void 0 ? false : _h, _j = _a.showTimestamps, showTimestamps = _j === void 0 ? false : _j, _k = _a.typingText, typingText = _k === void 0 ? "Agent is typing..." : _k, _l = _a.emptyStateText, emptyStateText = _l === void 0 ? "Start a conversation!" : _l, _m = _a.markdown, markdown = _m === void 0 ? true : _m,
|
|
107
124
|
// Floating button props
|
|
108
125
|
_o = _a.floating,
|
|
109
126
|
// Floating button props
|
|
@@ -116,13 +133,15 @@ export var BlumessageChat = function (_a) {
|
|
|
116
133
|
var _x = useState(null), error = _x[0], setError = _x[1];
|
|
117
134
|
var _y = useState(initialMessages), messages = _y[0], setMessages = _y[1];
|
|
118
135
|
var _z = useState(''), inputValue = _z[0], setInputValue = _z[1];
|
|
119
|
-
var _0 = useState(
|
|
136
|
+
var _0 = useState(initialToken || getStoredConversationToken(persistent)), token = _0[0], setToken = _0[1];
|
|
120
137
|
var _1 = useState(defaultOpen), isOpen = _1[0], setIsOpen = _1[1];
|
|
121
138
|
var _2 = useState(false), isAnimating = _2[0], setIsAnimating = _2[1];
|
|
122
139
|
var _3 = useState(false), isLoading = _3[0], setIsLoading = _3[1];
|
|
123
140
|
var _4 = useState(false), isMaximized = _4[0], setIsMaximized = _4[1];
|
|
124
141
|
var messagesEndRef = useRef(null);
|
|
125
142
|
var isInitialLoad = useRef(true);
|
|
143
|
+
// Mobile detection
|
|
144
|
+
var isMobile = useIsMobile();
|
|
126
145
|
// Helper function to format timestamp
|
|
127
146
|
var formatTimestamp = function (timestamp) {
|
|
128
147
|
var date = new Date(timestamp);
|
|
@@ -157,7 +176,23 @@ export var BlumessageChat = function (_a) {
|
|
|
157
176
|
if (width && height) {
|
|
158
177
|
return { width: width, height: height };
|
|
159
178
|
}
|
|
160
|
-
//
|
|
179
|
+
// Mobile-first responsive dimensions
|
|
180
|
+
if (isMobile) {
|
|
181
|
+
// On mobile, use fullscreen or near-fullscreen dimensions
|
|
182
|
+
if (floating) {
|
|
183
|
+
return {
|
|
184
|
+
width: 'calc(100vw - 32px)',
|
|
185
|
+
height: 'calc(100vh - 100px)'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
return {
|
|
190
|
+
width: '100%',
|
|
191
|
+
height: '500px'
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Desktop dimensions based on size
|
|
161
196
|
var dimensions;
|
|
162
197
|
switch (size) {
|
|
163
198
|
case 'small':
|
|
@@ -173,28 +208,33 @@ export var BlumessageChat = function (_a) {
|
|
|
173
208
|
return dimensions;
|
|
174
209
|
};
|
|
175
210
|
var _5 = getDimensions(), actualWidth = _5.width, actualHeight = _5.height;
|
|
176
|
-
// Function to
|
|
177
|
-
var
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
// Function to clear conversation and start fresh
|
|
212
|
+
var clearConversation = function () {
|
|
213
|
+
updateConversationToken(null);
|
|
214
|
+
setMessages([]);
|
|
215
|
+
};
|
|
216
|
+
// Function to update conversation token and notify parent
|
|
217
|
+
var updateConversationToken = function (newToken) {
|
|
218
|
+
setToken(newToken);
|
|
219
|
+
if (newToken) {
|
|
220
|
+
saveConversationToken(newToken, persistent);
|
|
181
221
|
}
|
|
182
222
|
else {
|
|
183
|
-
|
|
223
|
+
clearStoredConversationToken(persistent);
|
|
184
224
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
225
|
+
if (onTokenChange) {
|
|
226
|
+
onTokenChange(newToken);
|
|
187
227
|
}
|
|
188
228
|
};
|
|
189
229
|
// Function to fetch conversation history
|
|
190
|
-
var fetchConversationHistory = function (
|
|
230
|
+
var fetchConversationHistory = function (convToken) { return __awaiter(void 0, void 0, void 0, function () {
|
|
191
231
|
var response, sessionData, error_1;
|
|
192
232
|
return __generator(this, function (_a) {
|
|
193
233
|
switch (_a.label) {
|
|
194
234
|
case 0:
|
|
195
235
|
_a.trys.push([0, 5, , 6]);
|
|
196
|
-
console.log("Fetching conversation history for:",
|
|
197
|
-
return [4 /*yield*/, fetch("https://api.blumessage.com/api/v1/conversations/session/".concat(
|
|
236
|
+
console.log("Fetching conversation history for:", convToken);
|
|
237
|
+
return [4 /*yield*/, fetch("https://api.blumessage.com/api/v1/conversations/session/".concat(convToken), {
|
|
198
238
|
method: 'GET',
|
|
199
239
|
headers: {
|
|
200
240
|
'Authorization': "Bearer ".concat(apiKey),
|
|
@@ -211,7 +251,7 @@ export var BlumessageChat = function (_a) {
|
|
|
211
251
|
case 3:
|
|
212
252
|
console.error("Failed to fetch conversation history:", response.status, response.statusText);
|
|
213
253
|
// Clear invalid conversation ID from storage
|
|
214
|
-
|
|
254
|
+
updateConversationToken(null);
|
|
215
255
|
if (onError) {
|
|
216
256
|
onError("Failed to fetch conversation history: ".concat(response.status, " ").concat(response.statusText), "conversation_history");
|
|
217
257
|
}
|
|
@@ -221,7 +261,7 @@ export var BlumessageChat = function (_a) {
|
|
|
221
261
|
error_1 = _a.sent();
|
|
222
262
|
console.error("Error fetching conversation history:", error_1);
|
|
223
263
|
// Clear invalid conversation ID from storage
|
|
224
|
-
|
|
264
|
+
updateConversationToken(null);
|
|
225
265
|
if (onError) {
|
|
226
266
|
onError("Error fetching conversation history: ".concat(error_1), "conversation_history");
|
|
227
267
|
}
|
|
@@ -299,9 +339,9 @@ export var BlumessageChat = function (_a) {
|
|
|
299
339
|
case 0:
|
|
300
340
|
if (!isInitialLoad.current) return [3 /*break*/, 5];
|
|
301
341
|
isInitialLoad.current = false;
|
|
302
|
-
if (!(initialMessages.length > 0 &&
|
|
342
|
+
if (!(initialMessages.length > 0 && token)) return [3 /*break*/, 1];
|
|
303
343
|
// Warn if both are provided - initialMessages takes precedence
|
|
304
|
-
console.warn('Both initialMessages and
|
|
344
|
+
console.warn('Both initialMessages and token provided. Using initialMessages and ignoring stored token.');
|
|
305
345
|
setMessages(initialMessages);
|
|
306
346
|
return [3 /*break*/, 5];
|
|
307
347
|
case 1:
|
|
@@ -310,8 +350,8 @@ export var BlumessageChat = function (_a) {
|
|
|
310
350
|
setMessages(initialMessages);
|
|
311
351
|
return [3 /*break*/, 5];
|
|
312
352
|
case 2:
|
|
313
|
-
if (!
|
|
314
|
-
return [4 /*yield*/, fetchConversationHistory(
|
|
353
|
+
if (!token) return [3 /*break*/, 4];
|
|
354
|
+
return [4 /*yield*/, fetchConversationHistory(token)];
|
|
315
355
|
case 3:
|
|
316
356
|
historyMessages = _a.sent();
|
|
317
357
|
setMessages(historyMessages); // Will be empty array if fetch failed
|
|
@@ -357,9 +397,9 @@ export var BlumessageChat = function (_a) {
|
|
|
357
397
|
requestBody = {
|
|
358
398
|
message: currentInput,
|
|
359
399
|
};
|
|
360
|
-
// Include
|
|
361
|
-
if (
|
|
362
|
-
requestBody.
|
|
400
|
+
// Include token if we have one
|
|
401
|
+
if (token) {
|
|
402
|
+
requestBody.token = token;
|
|
363
403
|
}
|
|
364
404
|
return [4 /*yield*/, fetch('https://api.blumessage.com/api/v1/conversations', {
|
|
365
405
|
method: 'POST',
|
|
@@ -375,10 +415,8 @@ export var BlumessageChat = function (_a) {
|
|
|
375
415
|
return [4 /*yield*/, response.json()];
|
|
376
416
|
case 3:
|
|
377
417
|
apiResponse = _a.sent();
|
|
378
|
-
// Update
|
|
379
|
-
|
|
380
|
-
updateConversationId(apiResponse.conversationId);
|
|
381
|
-
}
|
|
418
|
+
// Update token from response (for both first message and continuing conversations)
|
|
419
|
+
updateConversationToken(apiResponse.token);
|
|
382
420
|
assistantMessages = apiResponse.messages.filter(function (msg) { return msg.role === 'assistant'; });
|
|
383
421
|
latestAssistantMessage = assistantMessages[assistantMessages.length - 1];
|
|
384
422
|
if (latestAssistantMessage) {
|
|
@@ -389,7 +427,6 @@ export var BlumessageChat = function (_a) {
|
|
|
389
427
|
timestamp: latestAssistantMessage.timestamp,
|
|
390
428
|
};
|
|
391
429
|
setMessages(function (prev) { return __spreadArray(__spreadArray([], prev, true), [assistantResponse_1], false); });
|
|
392
|
-
// This was already handled in the component, assistant message callback
|
|
393
430
|
if (onAssistantMessage) {
|
|
394
431
|
onAssistantMessage(assistantResponse_1);
|
|
395
432
|
}
|
|
@@ -397,6 +434,10 @@ export var BlumessageChat = function (_a) {
|
|
|
397
434
|
return [3 /*break*/, 5];
|
|
398
435
|
case 4:
|
|
399
436
|
console.error('Failed to send message to Blumessage API:', response.status, response.statusText);
|
|
437
|
+
// If token is invalid, clear it
|
|
438
|
+
if (response.status === 401 || response.status === 403) {
|
|
439
|
+
updateConversationToken(null);
|
|
440
|
+
}
|
|
400
441
|
errorMessage_1 = {
|
|
401
442
|
id: (Date.now() + 1).toString(),
|
|
402
443
|
content: "Sorry, I'm having trouble connecting right now. Please try again.",
|
|
@@ -431,7 +472,8 @@ export var BlumessageChat = function (_a) {
|
|
|
431
472
|
});
|
|
432
473
|
}); };
|
|
433
474
|
var handleKeyPress = function (e) {
|
|
434
|
-
if (e.key === 'Enter' && !isLoading) {
|
|
475
|
+
if (e.key === 'Enter' && !e.shiftKey && !isLoading) {
|
|
476
|
+
e.preventDefault();
|
|
435
477
|
handleSendMessage();
|
|
436
478
|
}
|
|
437
479
|
};
|
|
@@ -530,6 +572,11 @@ export var BlumessageChat = function (_a) {
|
|
|
530
572
|
pointerEvents: isOpen ? 'none' : 'auto',
|
|
531
573
|
transition: 'opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
532
574
|
};
|
|
575
|
+
// Mobile-specific positioning
|
|
576
|
+
if (isMobile) {
|
|
577
|
+
return __assign(__assign({}, baseStyles), { bottom: '20px', right: '20px', left: 'auto', top: 'auto' });
|
|
578
|
+
}
|
|
579
|
+
// Desktop positioning
|
|
533
580
|
switch (buttonPosition) {
|
|
534
581
|
case 'bottom-right':
|
|
535
582
|
return __assign(__assign({}, baseStyles), { bottom: '24px', right: '24px' });
|
|
@@ -560,6 +607,10 @@ export var BlumessageChat = function (_a) {
|
|
|
560
607
|
opacity: isOpen ? 1 : 0,
|
|
561
608
|
pointerEvents: isOpen ? 'auto' : 'none',
|
|
562
609
|
};
|
|
610
|
+
// Mobile-specific positioning (always fullscreen on mobile)
|
|
611
|
+
if (isMobile) {
|
|
612
|
+
return __assign(__assign({}, baseStyles), { top: '0', left: '0', right: '0', bottom: '0', transform: isOpen ? 'translateY(0)' : 'translateY(100%)', transformOrigin: 'bottom center' });
|
|
613
|
+
}
|
|
563
614
|
// If fullScreen or maximized, override positioning to be centered and full screen
|
|
564
615
|
if (fullScreen || isMaximized) {
|
|
565
616
|
// Set transform origin and direction based on button position for proper animation
|
|
@@ -588,6 +639,7 @@ export var BlumessageChat = function (_a) {
|
|
|
588
639
|
}
|
|
589
640
|
return __assign(__assign({}, baseStyles), { top: '20px', left: '20px', right: '20px', bottom: '20px', transform: isOpen ? 'translateY(0)' : closedTransform, transformOrigin: transformOrigin });
|
|
590
641
|
}
|
|
642
|
+
// Desktop positioning
|
|
591
643
|
switch (buttonPosition) {
|
|
592
644
|
case 'bottom-right':
|
|
593
645
|
return __assign(__assign({}, baseStyles), { bottom: '24px', right: '24px', transformOrigin: 'bottom right' });
|
|
@@ -610,29 +662,32 @@ export var BlumessageChat = function (_a) {
|
|
|
610
662
|
};
|
|
611
663
|
// Render chat window component
|
|
612
664
|
var renderChatWindow = function () {
|
|
613
|
-
var
|
|
665
|
+
var isFullscreenMode = fullScreen || isMaximized || isMobile;
|
|
666
|
+
var chatContent = (_jsxs("div", { className: "shadow-2xl flex flex-col overflow-hidden border ".concat(isMobile ? 'blumessage-mobile-fullscreen' : '', " ").concat(theme === 'dark'
|
|
614
667
|
? 'bg-gray-900 border-gray-700'
|
|
615
668
|
: 'bg-white border-black/10'), style: {
|
|
616
|
-
width:
|
|
617
|
-
height:
|
|
669
|
+
width: isFullscreenMode ? '100%' : actualWidth,
|
|
670
|
+
height: isFullscreenMode ? '100%' : actualHeight,
|
|
618
671
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
619
|
-
borderRadius:
|
|
672
|
+
borderRadius: isMobile ? '0' : (isFullscreenMode ? '12px' : '24px')
|
|
620
673
|
}, children: [_jsxs("div", { className: "border-b ".concat(theme === 'dark'
|
|
621
674
|
? 'bg-gray-900 border-gray-700'
|
|
622
675
|
: 'bg-white border-gray-100'), style: {
|
|
623
676
|
display: 'flex',
|
|
624
677
|
alignItems: 'center',
|
|
625
|
-
padding: '16px 24px'
|
|
678
|
+
padding: isMobile ? '12px 16px' : '16px 24px'
|
|
626
679
|
}, children: [_jsx("div", { className: "rounded-full flex items-center justify-center", style: {
|
|
627
680
|
backgroundImage: primaryColor,
|
|
628
|
-
width: '48px',
|
|
629
|
-
height: '48px',
|
|
630
|
-
marginRight: '24px'
|
|
631
|
-
}, children: React.createElement(getIconComponent(), {
|
|
681
|
+
width: isMobile ? '40px' : '48px',
|
|
682
|
+
height: isMobile ? '40px' : '48px',
|
|
683
|
+
marginRight: isMobile ? '16px' : '24px'
|
|
684
|
+
}, children: React.createElement(getIconComponent(), {
|
|
685
|
+
className: isMobile ? "w-5 h-5 text-white" : "w-6 h-6 text-white"
|
|
686
|
+
}) }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("div", { className: "".concat(isMobile ? 'text-base' : 'text-lg', " font-semibold m-0 leading-6 ").concat(theme === 'dark' ? 'text-gray-100' : 'text-gray-900'), children: name }), _jsx("div", { className: "text-sm m-0 leading-5 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: subtitle })] }), floating && (_jsxs("div", { className: "flex items-center gap-1", children: [maximizeToggleButton && !isMobile && (_jsx("button", { onClick: handleToggleMaximize, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
632
687
|
? 'hover:bg-gray-800 text-gray-400 hover:text-gray-200'
|
|
633
688
|
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: isMaximized ? _jsx(Minimize2, { className: "w-4 h-4" }) : _jsx(Maximize, { className: "w-4 h-4" }) })), _jsx("button", { onClick: handleCloseChat, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
634
689
|
? 'hover:bg-gray-800 text-gray-400 hover:text-gray-200'
|
|
635
|
-
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: _jsx(X, { className: "w-4 h-4" }) })] }))] }), _jsxs("div", { className: "flex-1
|
|
690
|
+
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: _jsx(X, { className: "w-4 h-4" }) })] }))] }), _jsxs("div", { className: "flex-1 overflow-y-auto ".concat(theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'), style: { padding: isMobile ? '12px 16px' : '16px 24px' }, children: [messages.map(function (message) { return (_jsx("div", { className: "flex ".concat(message.role === 'user' ? 'justify-end' : 'justify-start', " mb-4"), children: _jsx("div", { className: "".concat(isMobile ? 'max-w-[85%]' : 'max-w-[80%]', " inline-block ").concat(message.role === 'user' ? '' : 'text-left'), children: _jsxs("div", { className: "inline-block px-4 py-3 rounded-2xl text-sm leading-6 text-left ".concat(message.role === 'user'
|
|
636
691
|
? 'text-white'
|
|
637
692
|
: theme === 'dark'
|
|
638
693
|
? 'bg-gray-700 text-gray-100 border border-gray-600 shadow-sm'
|
|
@@ -642,13 +697,24 @@ export var BlumessageChat = function (_a) {
|
|
|
642
697
|
? 'text-gray-400'
|
|
643
698
|
: 'text-gray-500'), style: {
|
|
644
699
|
textAlign: message.role === 'user' ? 'right' : 'left'
|
|
645
|
-
}, children: formatTimestamp(message.timestamp) }))] }) }) }, message.id)); }), isLoading && (_jsx("div", { className: "flex justify-start", children: _jsx("div", { className: "px-4 py-3 rounded-2xl max-w-[80%] text-sm leading-6 shadow-sm ".concat(theme === 'dark'
|
|
700
|
+
}, children: formatTimestamp(message.timestamp) }))] }) }) }, message.id)); }), isLoading && (_jsx("div", { className: "flex justify-start mb-4", children: _jsx("div", { className: "px-4 py-3 rounded-2xl ".concat(isMobile ? 'max-w-[85%]' : 'max-w-[80%]', " text-sm leading-6 shadow-sm ").concat(theme === 'dark'
|
|
646
701
|
? 'bg-gray-700 text-gray-100 border border-gray-600'
|
|
647
|
-
: 'bg-white text-gray-800 border border-gray-200'), children: _jsx("div", { className: "".concat(theme === 'dark' ? 'text-gray-300' : 'text-gray-600'), style: { fontStyle: 'italic' }, children: typingText }) }) })), messages.length === 0 && !isLoading && (_jsx("div", { className: "text-center text-sm py-8 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: emptyStateText })), _jsx("div", { ref: messagesEndRef })] }), _jsx("div", { className: "
|
|
702
|
+
: 'bg-white text-gray-800 border border-gray-200'), children: _jsx("div", { className: "".concat(theme === 'dark' ? 'text-gray-300' : 'text-gray-600'), style: { fontStyle: 'italic' }, children: typingText }) }) })), messages.length === 0 && !isLoading && (_jsx("div", { className: "text-center text-sm py-8 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: emptyStateText })), _jsx("div", { ref: messagesEndRef })] }), _jsx("div", { className: "border-t ".concat(theme === 'dark'
|
|
648
703
|
? 'bg-gray-900 border-gray-700'
|
|
649
|
-
: 'bg-white border-gray-100'), children: _jsxs("div", { className: "flex items-center rounded-
|
|
704
|
+
: 'bg-white border-gray-100'), style: { padding: isMobile ? '12px 16px' : '16px 24px' }, children: _jsxs("div", { className: "flex items-center rounded-2xl px-4 py-3 border ".concat(theme === 'dark'
|
|
650
705
|
? 'bg-gray-800 border-gray-600'
|
|
651
|
-
: 'bg-gray-50 border-gray-200'), children: [_jsx("
|
|
706
|
+
: 'bg-gray-50 border-gray-200'), children: [_jsx("textarea", { className: "flex-1 border-none bg-transparent outline-none text-sm font-inherit resize-none ".concat(theme === 'dark' ? 'text-gray-100' : 'text-gray-700'), placeholder: placeholder, value: inputValue, onChange: function (e) { return setInputValue(e.target.value); }, onKeyDown: handleKeyPress, rows: 1, style: {
|
|
707
|
+
minHeight: '20px',
|
|
708
|
+
maxHeight: isMobile ? '100px' : '120px',
|
|
709
|
+
overflowY: inputValue.split('\n').length > 4 ? 'auto' : 'hidden',
|
|
710
|
+
lineHeight: '1.5',
|
|
711
|
+
paddingTop: '2px',
|
|
712
|
+
paddingBottom: '2px'
|
|
713
|
+
}, onInput: function (e) {
|
|
714
|
+
var target = e.target;
|
|
715
|
+
target.style.height = 'auto';
|
|
716
|
+
target.style.height = Math.min(target.scrollHeight, isMobile ? 100 : 120) + 'px';
|
|
717
|
+
} }), _jsx("button", { className: "w-8 h-8 rounded-full border-none flex items-center justify-center cursor-pointer ml-2 text-white transition-all disabled:opacity-50 disabled:cursor-not-allowed", style: { backgroundImage: primaryColor }, onClick: handleSendMessage, disabled: !inputValue.trim() || isLoading, children: isLoading ? (_jsx(Loader2, { className: "w-4 h-4 blumessage-animate-spin" })) : (_jsx(Send, { className: "w-4 h-4" })) })] }) })] }));
|
|
652
718
|
if (floating) {
|
|
653
719
|
return (_jsx("div", { style: getChatPositionStyles(), children: chatContent }));
|
|
654
720
|
}
|
|
@@ -659,60 +725,62 @@ export var BlumessageChat = function (_a) {
|
|
|
659
725
|
return null;
|
|
660
726
|
}
|
|
661
727
|
if (error) {
|
|
662
|
-
var
|
|
728
|
+
var isFullscreenMode = fullScreen || isMaximized || isMobile;
|
|
729
|
+
var errorContent = (_jsxs("div", { className: "shadow-2xl flex flex-col overflow-hidden border ".concat(isMobile ? 'blumessage-mobile-fullscreen' : '', " ").concat(theme === 'dark'
|
|
663
730
|
? 'bg-gray-900 border-gray-700'
|
|
664
731
|
: 'bg-white border-black/10'), style: {
|
|
665
|
-
width:
|
|
666
|
-
height:
|
|
732
|
+
width: isFullscreenMode ? '100%' : actualWidth,
|
|
733
|
+
height: isFullscreenMode ? '100%' : actualHeight,
|
|
667
734
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
668
|
-
borderRadius:
|
|
669
|
-
}, children: [_jsxs("div", { className: "flex items-center
|
|
735
|
+
borderRadius: isMobile ? '0' : (isFullscreenMode ? '12px' : '24px')
|
|
736
|
+
}, children: [_jsxs("div", { className: "flex items-center border-b ".concat(theme === 'dark'
|
|
670
737
|
? 'bg-gray-900 border-gray-700'
|
|
671
|
-
: 'bg-white border-gray-100'), children: [_jsx("div", { className: "rounded-full flex items-center justify-center", style: {
|
|
738
|
+
: 'bg-white border-gray-100'), style: { padding: isMobile ? '12px 16px' : '16px 24px' }, children: [_jsx("div", { className: "rounded-full flex items-center justify-center", style: {
|
|
672
739
|
backgroundColor: '#ef4444',
|
|
673
|
-
width: '48px',
|
|
674
|
-
height: '48px',
|
|
675
|
-
marginRight: '24px'
|
|
676
|
-
}, children: _jsx(AlertTriangle, { className: "w-6 h-6 text-white" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-lg font-semibold m-0 leading-6 ".concat(theme === 'dark' ? 'text-gray-100' : 'text-gray-900'), children: "Connection Error" }), _jsx("div", { className: "text-sm m-0 leading-5 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: "Unable to connect" })] }), floating && (_jsxs("div", { className: "flex items-center gap-1", children: [maximizeToggleButton && (_jsx("button", { onClick: handleToggleMaximize, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
740
|
+
width: isMobile ? '40px' : '48px',
|
|
741
|
+
height: isMobile ? '40px' : '48px',
|
|
742
|
+
marginRight: isMobile ? '16px' : '24px'
|
|
743
|
+
}, children: _jsx(AlertTriangle, { className: isMobile ? "w-5 h-5 text-white" : "w-6 h-6 text-white" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "".concat(isMobile ? 'text-base' : 'text-lg', " font-semibold m-0 leading-6 ").concat(theme === 'dark' ? 'text-gray-100' : 'text-gray-900'), children: "Connection Error" }), _jsx("div", { className: "text-sm m-0 leading-5 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: "Unable to connect" })] }), floating && (_jsxs("div", { className: "flex items-center gap-1", children: [maximizeToggleButton && !isMobile && (_jsx("button", { onClick: handleToggleMaximize, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
677
744
|
? 'hover:bg-gray-800 text-gray-400 hover:text-gray-200'
|
|
678
745
|
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: isMaximized ? _jsx(Minimize2, { className: "w-4 h-4" }) : _jsx(Maximize, { className: "w-4 h-4" }) })), _jsx("button", { onClick: handleCloseChat, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
679
746
|
? 'hover:bg-gray-800 text-gray-400 hover:text-gray-200'
|
|
680
|
-
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: _jsx(X, { className: "w-4 h-4" }) })] }))] }), _jsx("div", { className: "flex-1
|
|
747
|
+
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: _jsx(X, { className: "w-4 h-4" }) })] }))] }), _jsx("div", { className: "flex-1 overflow-y-auto ".concat(theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'), style: { padding: isMobile ? '12px 16px' : '16px 24px' }, children: _jsx("div", { className: "flex justify-start", children: _jsx("div", { className: "px-4 py-3 rounded-2xl ".concat(isMobile ? 'max-w-[85%]' : 'max-w-[80%]', " text-sm leading-6 shadow-sm ").concat(theme === 'dark'
|
|
681
748
|
? 'bg-gray-700 text-gray-100 border border-gray-600'
|
|
682
749
|
: 'bg-white text-gray-800 border border-gray-200'), children: error }) }) })] }));
|
|
683
750
|
if (floating) {
|
|
684
|
-
return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleOpenChat, className: "text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center gap-2 ".concat(buttonText ? 'px-4 py-3 h-12' : 'w-14 h-14'), style: __assign(__assign(__assign({}, getButtonPositionStyles()), buttonStyle), { backgroundImage: primaryColor }), children: [React.createElement(getIconComponent(), { className: "w-6 h-6" }), buttonText && _jsx("span", { className: "text-sm font-medium whitespace-nowrap", children: buttonText })] }), _jsx("div", { style: getChatPositionStyles(), children: errorContent })] }));
|
|
751
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleOpenChat, className: "text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center gap-2 ".concat(isMobile ? 'blumessage-mobile-button' : '', " ").concat(buttonText ? 'px-4 py-3 h-12' : 'w-14 h-14'), style: __assign(__assign(__assign({}, getButtonPositionStyles()), buttonStyle), { backgroundImage: primaryColor }), children: [React.createElement(getIconComponent(), { className: "w-6 h-6" }), buttonText && !isMobile && _jsx("span", { className: "text-sm font-medium whitespace-nowrap", children: buttonText })] }), _jsx("div", { style: getChatPositionStyles(), children: errorContent })] }));
|
|
685
752
|
}
|
|
686
753
|
return errorContent;
|
|
687
754
|
}
|
|
688
755
|
if (!isInitialized) {
|
|
689
|
-
var
|
|
756
|
+
var isFullscreenMode = fullScreen || isMaximized || isMobile;
|
|
757
|
+
var loadingContent = (_jsxs("div", { className: "shadow-2xl flex flex-col overflow-hidden border ".concat(isMobile ? 'blumessage-mobile-fullscreen' : '', " ").concat(theme === 'dark'
|
|
690
758
|
? 'bg-gray-900 border-gray-700'
|
|
691
759
|
: 'bg-white border-black/10'), style: {
|
|
692
|
-
width:
|
|
693
|
-
height:
|
|
760
|
+
width: isFullscreenMode ? '100%' : actualWidth,
|
|
761
|
+
height: isFullscreenMode ? '100%' : actualHeight,
|
|
694
762
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
695
|
-
borderRadius:
|
|
696
|
-
}, children: [_jsxs("div", { className: "flex items-center
|
|
763
|
+
borderRadius: isMobile ? '0' : (isFullscreenMode ? '12px' : '24px')
|
|
764
|
+
}, children: [_jsxs("div", { className: "flex items-center border-b ".concat(theme === 'dark'
|
|
697
765
|
? 'bg-gray-900 border-gray-700'
|
|
698
|
-
: 'bg-white border-gray-100'), children: [_jsx("div", { className: "rounded-full flex items-center justify-center", style: {
|
|
766
|
+
: 'bg-white border-gray-100'), style: { padding: isMobile ? '12px 16px' : '16px 24px' }, children: [_jsx("div", { className: "rounded-full flex items-center justify-center", style: {
|
|
699
767
|
backgroundImage: primaryColor,
|
|
700
|
-
width: '48px',
|
|
701
|
-
height: '48px',
|
|
702
|
-
marginRight: '24px'
|
|
703
|
-
}, children: _jsx(Loader2, { className: "w-6 h-6 text-white animate-spin" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-lg font-semibold m-0 leading-6 ".concat(theme === 'dark' ? 'text-gray-100' : 'text-gray-900'), children: name }), _jsx("div", { className: "text-sm m-0 leading-5 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: subtitle })] }), floating && (_jsxs("div", { className: "flex items-center gap-1", children: [maximizeToggleButton && (_jsx("button", { onClick: handleToggleMaximize, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
768
|
+
width: isMobile ? '40px' : '48px',
|
|
769
|
+
height: isMobile ? '40px' : '48px',
|
|
770
|
+
marginRight: isMobile ? '16px' : '24px'
|
|
771
|
+
}, children: _jsx(Loader2, { className: isMobile ? "w-5 h-5 text-white animate-spin" : "w-6 h-6 text-white animate-spin" }) }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "".concat(isMobile ? 'text-base' : 'text-lg', " font-semibold m-0 leading-6 ").concat(theme === 'dark' ? 'text-gray-100' : 'text-gray-900'), children: name }), _jsx("div", { className: "text-sm m-0 leading-5 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: subtitle })] }), floating && (_jsxs("div", { className: "flex items-center gap-1", children: [maximizeToggleButton && !isMobile && (_jsx("button", { onClick: handleToggleMaximize, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
704
772
|
? 'hover:bg-gray-800 text-gray-400 hover:text-gray-200'
|
|
705
773
|
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: isMaximized ? _jsx(Minimize2, { className: "w-4 h-4" }) : _jsx(Maximize, { className: "w-4 h-4" }) })), _jsx("button", { onClick: handleCloseChat, className: "w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors ".concat(theme === 'dark'
|
|
706
774
|
? 'hover:bg-gray-800 text-gray-400 hover:text-gray-200'
|
|
707
|
-
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: _jsx(X, { className: "w-4 h-4" }) })] }))] }), _jsx("div", { className: "flex-1
|
|
775
|
+
: 'hover:bg-gray-100 text-gray-500 hover:text-gray-700'), children: _jsx(X, { className: "w-4 h-4" }) })] }))] }), _jsx("div", { className: "flex-1 overflow-y-auto ".concat(theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'), style: { padding: isMobile ? '12px 16px' : '16px 24px' }, children: _jsx("div", { className: "text-center text-sm py-8 ".concat(theme === 'dark' ? 'text-gray-400' : 'text-gray-500'), children: "Ready to chat..." }) })] }));
|
|
708
776
|
if (floating) {
|
|
709
|
-
return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleOpenChat, className: "text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center gap-2 ".concat(buttonText ? 'px-4 py-3 h-12' : 'w-14 h-14'), style: __assign(__assign(__assign({}, getButtonPositionStyles()), buttonStyle), { backgroundImage: primaryColor }), children: [React.createElement(getIconComponent(), { className: "w-6 h-6" }), buttonText && _jsx("span", { className: "text-sm font-medium whitespace-nowrap", children: buttonText })] }), _jsx("div", { style: getChatPositionStyles(), children: loadingContent })] }));
|
|
777
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleOpenChat, className: "text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center gap-2 ".concat(isMobile ? 'blumessage-mobile-button' : '', " ").concat(buttonText ? 'px-4 py-3 h-12' : 'w-14 h-14'), style: __assign(__assign(__assign({}, getButtonPositionStyles()), buttonStyle), { backgroundImage: primaryColor }), children: [React.createElement(getIconComponent(), { className: "w-6 h-6" }), buttonText && !isMobile && _jsx("span", { className: "text-sm font-medium whitespace-nowrap", children: buttonText })] }), _jsx("div", { style: getChatPositionStyles(), children: loadingContent })] }));
|
|
710
778
|
}
|
|
711
779
|
return loadingContent;
|
|
712
780
|
}
|
|
713
781
|
// Main render for initialized state
|
|
714
782
|
if (floating) {
|
|
715
|
-
return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleOpenChat, className: "text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center gap-2 ".concat(buttonText ? 'px-4 py-3 h-12' : 'w-14 h-14'), style: __assign(__assign(__assign({}, getButtonPositionStyles()), buttonStyle), { backgroundImage: primaryColor }), children: [React.createElement(getIconComponent(), { className: "w-6 h-6" }), buttonText && _jsx("span", { className: "text-sm font-medium whitespace-nowrap", children: buttonText })] }), renderChatWindow()] }));
|
|
783
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: handleOpenChat, className: "text-white rounded-full shadow-lg transition-all duration-200 flex items-center justify-center gap-2 ".concat(isMobile ? 'blumessage-mobile-button' : '', " ").concat(buttonText ? 'px-4 py-3 h-12' : 'w-14 h-14'), style: __assign(__assign(__assign({}, getButtonPositionStyles()), buttonStyle), { backgroundImage: primaryColor }), children: [React.createElement(getIconComponent(), { className: "w-6 h-6" }), buttonText && !isMobile && _jsx("span", { className: "text-sm font-medium whitespace-nowrap", children: buttonText })] }), renderChatWindow()] }));
|
|
716
784
|
}
|
|
717
785
|
return renderChatWindow();
|
|
718
786
|
};
|
|
@@ -6,13 +6,7 @@ export interface Message {
|
|
|
6
6
|
timestamp: number;
|
|
7
7
|
}
|
|
8
8
|
export interface BlumessageApiResponse {
|
|
9
|
-
|
|
10
|
-
messages: Message[];
|
|
11
|
-
created_at: number;
|
|
12
|
-
updated_at: number;
|
|
13
|
-
}
|
|
14
|
-
export interface ConversationSessionResponse {
|
|
15
|
-
id: string;
|
|
9
|
+
token: string;
|
|
16
10
|
messages: Message[];
|
|
17
11
|
created_at: number;
|
|
18
12
|
updated_at: number;
|
|
@@ -29,8 +23,8 @@ export interface BlumessageChatProps {
|
|
|
29
23
|
initialMessages?: Message[];
|
|
30
24
|
onUserMessage?: (message: Message) => void;
|
|
31
25
|
onAssistantMessage?: (message: Message) => void;
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
token?: string;
|
|
27
|
+
onTokenChange?: (token: string | null) => void;
|
|
34
28
|
onChatWidgetOpen?: () => void;
|
|
35
29
|
onChatWidgetClosed?: () => void;
|
|
36
30
|
onError?: (error: string, context?: string) => void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { BlumessageChat, type BlumessageChatProps, type Message, type BlumessageApiResponse
|
|
1
|
+
export { BlumessageChat, type BlumessageChatProps, type Message, type BlumessageApiResponse } from './BlumessageChat';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blumessage/react-chat",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A React TypeScript chat widget component with floating button, theming, and Blumessage API integration",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Blumessage <contact@blumessage.com>",
|