@infomatebot/chatbot-widget
Version:
Build intelligent chatbots using pre-designed conversation flows with quick reply options. Upload documents for AI-powered, context-aware responses and reduce support costs by 80% with guided conversations
1,131 lines • 125 kB
JavaScript
const DEFAULT_CONFIG = {
apiBaseUrl: "http://localhost:3000",
apiKey: "",
botName: "InfoMate Assistant",
welcomeMessage: "Hi! How can I help you today?",
inputPlaceholder: "Type your message ...",
primaryColor: "#667eea",
secondaryColor: "#764ba2",
size: "medium",
position: "bottom-right",
autoOpen: false,
enableTyping: true,
theme: "light",
showPoweredBy: true,
welcomeFlow: void 0
};
const ELEMENT_NAME = "infomate-chatbot";
const VERSION = "1.0.5";
const CSS_CLASSES = {
trigger: "chatbot-trigger",
window: "chatbot-window",
windowActive: "chatbot-window-active",
header: "header",
messages: "messages",
message: "message",
messageBot: "message-bot",
messageUser: "message-user",
messageError: "message-error",
typing: "typing-message",
typingIndicator: "typing-indicator",
quickReplies: "quick-replies",
quickReply: "quick-reply",
inputArea: "input-area",
messageInput: "message-input",
sendButton: "send-button",
closeButton: "close-button",
botAvatar: "bot-avatar",
msgAvatar: "msg-avatar",
msgContent: "msg-content",
botInfo: "bot-info",
botName: "bot-name",
botStatus: "bot-status",
maximizeButton: "maximize-button",
windowMaximized: "chatbot-window-maximized",
// Enhanced action-related classes
quickReplyAction: "quick-reply-action",
quickReplyDisabled: "quick-reply-disabled",
inputCollector: "input-collector",
inputCollectorField: "input-collector-field",
inputCollectorButtons: "input-collector-buttons",
inputCollectorSubmit: "input-collector-submit",
inputCollectorCancel: "input-collector-cancel",
inputCollectorError: "input-collector-error",
actionLoader: "action-loader",
messageLoading: "message-loading",
messageProcessing: "message-processing",
inputDisabled: "input-disabled",
actionFeedback: "action-feedback",
poweredBy: "powered-by"
};
const ELEMENT_IDS = {
typingIndicator: "typingIndicator",
messagesContainer: "messagesContainer"
};
const ALLOWED_ATTRIBUTES = {
// API Base URL - multiple formats supported
apiBaseUrl: "api-base-url",
apibaseurl: "apibaseurl",
apiBaseUrlCamel: "apiBaseUrl",
// API Key - multiple formats supported
apiKey: "api-key",
apikey: "apikey",
apiKeyCamel: "apiKey"
};
const API_ENDPOINTS = {
chatInit: "/chat/start",
sendMessage: "/chat/send"
};
const SIZE_CONFIG = {
small: { width: 320, height: 450 },
medium: { width: 380, height: 520 },
large: { width: 450, height: 600 },
xl: { width: 520, height: 680 }
};
const ANIMATIONS = {
windowToggle: 300,
messageSlide: 300,
typingDelay: 1500,
focusDelay: 300,
welcomeDelay: 1e3,
autoOpenDelay: 1e3
};
const Z_INDEX = {
chatbot: 999999
};
const ERROR_MESSAGES = {
INITIALIZATION_FAILED: "Failed to initialize chatbot",
API_REQUEST_FAILED: "Failed to send message",
TENANT_CONFIG_FAILED: "Failed to load tenant configuration",
MESSAGE_SEND_FAILED: "Failed to send message",
INVALID_CONFIGURATION: "Invalid chatbot configuration",
MISSING_TENANT_ID: "Tenant ID is required",
NETWORK_ERROR: "Network error occurred",
UNKNOWN_ERROR: "An unknown error occurred"
};
const MOCK_RESPONSES = {
greeting: [
"Hello! Great to meet you! How can I assist you today?",
"Hi there! I'm here to help. What can I do for you?",
"Hey! Nice to chat with you. What would you like to know?"
],
help: [
"I'm here to help! I can answer questions, provide information, and assist with various tasks. What specific help do you need?",
"Happy to help! Feel free to ask me anything - I'm designed to be as helpful as possible!"
],
services: [
"We offer a comprehensive range of services including customer support, technical assistance, product information, and general inquiries. What specific service interests you?"
],
contact: [
"You can reach our support team at support@infomate.com or call 1-800-INFOMATE. We're available 24/7!"
],
default: [
"That's a great question! I'm working on providing you with the best answer possible. Is there anything specific you'd like to know more about?",
"Thanks for asking! I understand what you're looking for. Let me help you with that right away!",
"Interesting point! I'm here to provide you with accurate and helpful information. How else can I assist you?"
]
};
const MESSAGE_PATTERNS = {
greeting: /\b(hello|hi|hey|greetings|good\s*(morning|afternoon|evening))\b/i,
help: /\b(help|assist|support|guidance)\b/i,
services: /\b(service|offer|provide|what\s*do\s*you\s*do)\b/i,
contact: /\b(contact|phone|email|reach|support)\b/i
};
const ARIA_LABELS = {
trigger: "Open chat",
close: "Close chat",
send: "Send message",
input: "Type your message",
messages: "Chat messages",
quickReply: "Quick reply option",
maximize: "Maximize chat window",
minimize: "Minimize chat window",
// Enhanced action accessibility labels
inputCollector: "Input collector"
};
const TEMPLATE_PATTERNS = {
variable: /\{\{([\w.]+)\}\}/g,
conditionalBlock: /\{\{#([\w.]+)\}\}(.*?)\{\{\/\1\}\}/gs,
arrayLoop: /\{\{#each\s+([\w.]+)\}\}(.*?)\{\{\/each\}\}/gs
};
function generateMessageId() {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `msg_${timestamp}_${random}`;
}
function validateConfig(config) {
const errors = [];
if (!config.apiKey || config.apiKey.trim() === "") {
errors.push("api key is required");
}
if (config.apiBaseUrl && !isValidUrl(config.apiBaseUrl)) {
errors.push("apiBaseUrl must be a valid URL");
}
return {
isValid: errors.length === 0,
errors
};
}
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
function sanitizeHtml(html) {
const div = document.createElement("div");
div.textContent = html;
return div.innerHTML;
}
function scrollToBottom(element, smooth = true) {
const scrollOptions = {
behavior: smooth ? "smooth" : "auto",
block: "end"
};
const bottomElement = document.createElement("div");
element.appendChild(bottomElement);
bottomElement.scrollIntoView(scrollOptions);
element.removeChild(bottomElement);
}
function autoResizeTextarea(textarea, maxHeight = 120) {
if (textarea.scrollHeight <= textarea.clientHeight && textarea.style.height) {
return;
}
textarea.style.height = "auto";
const newHeight = Math.max(46, Math.min(textarea.scrollHeight, maxHeight));
textarea.style.height = newHeight + "px";
}
function classifyMessage(message) {
const lowerMessage = message.toLowerCase();
if (MESSAGE_PATTERNS.greeting.test(lowerMessage)) {
return getRandomResponse(MOCK_RESPONSES.greeting);
}
if (MESSAGE_PATTERNS.help.test(lowerMessage)) {
return getRandomResponse(MOCK_RESPONSES.help);
}
if (MESSAGE_PATTERNS.services.test(lowerMessage)) {
return getRandomResponse(MOCK_RESPONSES.services);
}
if (MESSAGE_PATTERNS.contact.test(lowerMessage)) {
return getRandomResponse(MOCK_RESPONSES.contact);
}
return getRandomResponse(MOCK_RESPONSES.default);
}
function getRandomResponse(responses) {
return responses[Math.floor(Math.random() * responses.length)];
}
function createMessage(content, sender, type = "text") {
return {
id: generateMessageId(),
content: sender === "bot" ? content : sanitizeHtml(content),
// Only sanitize user messages
sender,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
type
};
}
function createCssVar(name) {
return `--chatbot-${name}`;
}
const SECURITY_CONFIG = {
MAX_MESSAGE_LENGTH: 1e3,
REQUEST_TIMEOUT: 3e4,
RETRY_ATTEMPTS: 3,
SESSION_STORAGE_KEY: "infomate_session_tracking",
MAX_CONVERSATION_HISTORY: 50,
MAX_FEEDBACK_COMMENT_LENGTH: 500
};
const ADDITIONAL_API_ENDPOINTS = {
health: "/health",
sessionValidate: "/session/validate",
conversationHistory: "/conversation/history",
feedback: "/feedback"
};
const ADDITIONAL_ERROR_MESSAGES = {
SESSION_EXPIRED: "Session has expired, please refresh",
INVALID_INPUT: "Invalid input provided",
SERVICE_UNAVAILABLE: "Service temporarily unavailable",
NETWORK_ERROR: "Network connection error",
RATE_LIMIT_EXCEEDED: "You have exceeded your limit.. please upgrade the plan"
};
function sanitizeInput(input) {
if (!input || typeof input !== "string") {
return "";
}
return input.trim().substring(0, SECURITY_CONFIG.MAX_MESSAGE_LENGTH).replace(/<[^>]*>/g, "").replace(/&[a-z0-9#]+;/gi, "").replace(/javascript:/gi, "").replace(/vbscript:/gi, "").replace(/data:/gi, "").replace(/on\w+\s*=/gi, "").replace(/\bscript\b/gi, "").replace(/\beval\b/gi, "").replace(/\bfunction\b/gi, "").replace(/['"`;\\]/g, "").replace(/\b(union|select|insert|update|delete|drop|create|alter|exec|execute)\b/gi, "").replace(/[\x00-\x1F\x7F]/g, "");
}
function sanitizeMessageRequest(messageRequest) {
if (!messageRequest || typeof messageRequest !== "object") {
return void 0;
}
const sanitizedMessage = messageRequest.message ? sanitizeInput(messageRequest.message) : "";
const sanitizedReplyChoiceId = messageRequest.replyChoiceId ? sanitizeInput(messageRequest.replyChoiceId) : "";
if (!sanitizedMessage && !sanitizedReplyChoiceId) {
return void 0;
}
return {
message: sanitizedMessage,
replyChoiceId: sanitizedReplyChoiceId
};
}
function logSecureError(error, context) {
const isProduction = typeof window !== "undefined" && (window.location.hostname !== "localhost" && !window.location.hostname.includes("127.0.0.1"));
if (isProduction) {
console.error(`InfoMate Error: ${context || "Unknown"}`);
} else {
console.error(`InfoMate Error [${context}]:`, error);
}
}
class ApiService {
constructor(config) {
this.apiBaseUrl = config.apiBaseUrl.replace(/\/$/, "");
this.apiKey = config.apiKey;
}
async initializeChat() {
if (!this.apiKey || !this.apiBaseUrl) {
return { success: false, error: "Invalid configuration" };
}
const url = this.buildUrl(API_ENDPOINTS.chatInit);
try {
const response = await this.makeRequest(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-chat-token": this.apiKey
},
body: JSON.stringify({})
});
if (!response.ok) {
logSecureError(`Chat init failed: ${response.status}`, "initializeChat");
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SERVICE_UNAVAILABLE };
}
const apiResponse = await response.json();
if (!apiResponse.success || !apiResponse.data) {
const errorMsg = apiResponse.error?.message || "Unknown init error";
logSecureError(errorMsg, "initializeChat");
return { success: false, error: errorMsg || ERROR_MESSAGES.INITIALIZATION_FAILED };
}
if (apiResponse.data.sessionToken) {
try {
localStorage.setItem(SECURITY_CONFIG.SESSION_STORAGE_KEY, apiResponse.data.sessionToken);
} catch {
}
}
if (apiResponse.data.config) {
this.backendConfig = apiResponse.data.config;
}
return {
success: true,
config: this.createInternalConfig()
};
} catch (error) {
logSecureError("Chat initialization exception", "initializeChat");
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SERVICE_UNAVAILABLE };
}
}
async sendMessage(messageRequest) {
const sessionToken = this.getSessionToken();
if (!sessionToken) {
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SESSION_EXPIRED };
}
const sanitizedMessageRequest = sanitizeMessageRequest(messageRequest);
if (!sanitizedMessageRequest) {
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.INVALID_INPUT };
}
const url = this.buildUrl(API_ENDPOINTS.sendMessage);
const payload = {
message: sanitizedMessageRequest.message,
replyChoiceId: sanitizedMessageRequest.replyChoiceId
};
try {
const response = await this.makeRequestWithRetry(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-chat-token": this.apiKey,
"Authorization": `Bearer ${sessionToken}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
if (response.status === 401) {
this.clearSession();
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SESSION_EXPIRED };
}
if (response.status === 429) {
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.RATE_LIMIT_EXCEEDED };
}
logSecureError(`Message send failed: ${response.status}`, "sendMessage");
return { success: false, error: ERROR_MESSAGES.MESSAGE_SEND_FAILED };
}
const data = await response.json();
const responseData = data.data || data;
const sanitizedResponse = this.sanitizeApiResponse(responseData);
return {
success: true,
message: sanitizedResponse.message,
quickReplies: sanitizedResponse.quickReplies,
inputCollector: sanitizedResponse.inputCollector
};
} catch (error) {
logSecureError("Message send exception", "sendMessage");
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SERVICE_UNAVAILABLE };
}
}
async checkHealth() {
try {
const url = this.buildUrl(ADDITIONAL_API_ENDPOINTS.health);
const response = await this.makeRequest(url, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
return response.ok;
} catch {
return false;
}
}
// FIX: Return the correct type
getConfig() {
return this.createInternalConfig();
}
isInitialized() {
return Boolean(this.getSessionToken()) && Boolean(this.backendConfig);
}
clearSession() {
this.backendConfig = void 0;
try {
localStorage.removeItem(SECURITY_CONFIG.SESSION_STORAGE_KEY);
} catch {
}
}
async validateSession() {
const sessionToken = this.getSessionToken();
if (!sessionToken || !this.apiKey) {
return false;
}
try {
const url = this.buildUrl(ADDITIONAL_API_ENDPOINTS.sessionValidate);
const response = await this.makeRequest(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-chat-token": this.apiKey,
"Authorization": `Bearer ${sessionToken}`
},
body: JSON.stringify({})
});
if (!response.ok) {
this.clearSession();
return false;
}
return true;
} catch {
this.clearSession();
return false;
}
}
async getConversationHistory(limit = 20) {
const sessionToken = this.getSessionToken();
if (!sessionToken) {
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SESSION_EXPIRED };
}
try {
const url = this.buildUrl(`${ADDITIONAL_API_ENDPOINTS.conversationHistory}?limit=${limit}`);
const response = await this.makeRequest(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-chat-token": this.apiKey,
"Authorization": `Bearer ${sessionToken}`
}
});
if (!response.ok) {
if (response.status === 401) {
this.clearSession();
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SESSION_EXPIRED };
}
return { success: false, error: "Failed to fetch history" };
}
const data = await response.json();
return {
success: true,
messages: Array.isArray(data.messages) ? data.messages.slice(0, limit) : []
};
} catch (error) {
logSecureError("History fetch exception", "getConversationHistory");
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SERVICE_UNAVAILABLE };
}
}
async sendFeedback(messageId, rating, comment) {
const sessionToken = this.getSessionToken();
if (!sessionToken) {
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SESSION_EXPIRED };
}
if (!messageId || !rating) {
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.INVALID_INPUT };
}
try {
const url = this.buildUrl(ADDITIONAL_API_ENDPOINTS.feedback);
const response = await this.makeRequest(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-chat-token": this.apiKey,
"Authorization": `Bearer ${sessionToken}`
},
body: JSON.stringify({
messageId,
rating,
comment: comment?.trim().substring(0, 500)
})
});
if (!response.ok) {
if (response.status === 401) {
this.clearSession();
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SESSION_EXPIRED };
}
return { success: false, error: "Failed to send feedback" };
}
return { success: true };
} catch (error) {
logSecureError("Feedback send exception", "sendFeedback");
return { success: false, error: ADDITIONAL_ERROR_MESSAGES.SERVICE_UNAVAILABLE };
}
}
getSessionToken() {
try {
return localStorage.getItem(SECURITY_CONFIG.SESSION_STORAGE_KEY);
} catch {
return null;
}
}
createInternalConfig() {
if (!this.backendConfig) {
return {
apiBaseUrl: this.apiBaseUrl,
apiKey: this.apiKey,
botName: "Loading...",
welcomeMessage: "Connecting...",
inputPlaceholder: "Type your message...",
primaryColor: "#667eea",
secondaryColor: "#764ba2",
size: "medium",
position: "bottom-right",
autoOpen: false,
enableTyping: true,
theme: "light",
showPoweredBy: true
};
}
return {
apiBaseUrl: this.apiBaseUrl,
apiKey: this.apiKey,
botName: this.backendConfig.botName || "Assistant",
welcomeMessage: this.backendConfig.welcomeMessage || "Hello!",
inputPlaceholder: this.backendConfig.inputPlaceholder || "Type your message...",
primaryColor: this.backendConfig.primaryColor || "#667eea",
secondaryColor: this.backendConfig.secondaryColor || "#764ba2",
size: this.backendConfig.size || "medium",
position: this.backendConfig.position || "bottom-right",
autoOpen: this.backendConfig.autoOpen ?? false,
enableTyping: this.backendConfig.enableTyping ?? true,
theme: this.backendConfig.theme || "light",
showPoweredBy: this.backendConfig.showPoweredBy ?? true,
welcomeFlow: this.backendConfig.welcomeFlow,
fallbackFlow: this.backendConfig.fallbackFlow
};
}
sanitizeApiResponse(data) {
if (!data || typeof data !== "object") {
throw new Error("Invalid response format");
}
if (!data.message || typeof data.message !== "string") {
throw new Error("Invalid message format");
}
let quickReplies;
if (data.quickReplies && Array.isArray(data.quickReplies)) {
quickReplies = data.quickReplies.filter((reply) => reply && typeof reply === "object" && reply.id && reply.label).map((reply) => ({
id: String(reply.id),
label: String(reply.label)
}));
}
let inputCollector = void 0;
if (data.inputCollector && typeof data.inputCollector === "object") {
inputCollector = data.inputCollector;
}
return {
message: data.message,
quickReplies: quickReplies && quickReplies.length > 0 ? quickReplies : void 0,
inputCollector
};
}
buildUrl(endpoint) {
return `${this.apiBaseUrl}${endpoint}`;
}
async makeRequest(url, options, timeout = SECURITY_CONFIG.REQUEST_TIMEOUT) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}
async makeRequestWithRetry(url, options, retries = SECURITY_CONFIG.RETRY_ATTEMPTS) {
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await this.makeRequest(url, options);
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
return response;
}
if (response.ok || attempt === retries) {
return response;
}
throw new Error(`Server error: ${response.status}`);
} catch (error) {
lastError = error;
if (attempt < retries) {
const delayMs = 1e3 * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
break;
}
}
throw lastError;
}
}
function createApiService(config) {
if (!config.apiBaseUrl || !config.apiKey) {
throw new Error("Invalid configuration: apiBaseUrl and apiKey are required");
}
return new ApiService(config);
}
class ChatbotUI {
constructor(shadow, config) {
this.elements = {};
this.isRendered = false;
this.isMaximized = false;
this.currentQuickReplies = [];
this.shadow = shadow;
this.config = config;
}
create() {
if (this.isRendered) {
return;
}
this.shadow.innerHTML += this.getHTML();
this.cacheElements();
this.setupInitialState();
this.isRendered = true;
}
getHTML() {
return `
<div class="position-${this.config.position}">
${this.getTriggerHTML()}
${this.getWindowHTML()}
</div>
`;
}
getTriggerHTML() {
return `
<button
class="${CSS_CLASSES.trigger}"
aria-label="${ARIA_LABELS.trigger}"
type="button"
role="button"
>
${this.getChatIcon()}
</button>
`;
}
getWindowHTML() {
return `
<div class="${CSS_CLASSES.window} size-${this.config.size}" role="dialog" aria-modal="true" aria-labelledby="bot-name">
${this.getHeaderHTML()}
${this.getMessagesHTML()}
${this.getInputAreaHTML()}
${this.getPoweredByHTML()}
</div>
`;
}
getHeaderHTML() {
return `
<header class="${CSS_CLASSES.header}">
<div class="${CSS_CLASSES.botAvatar}" aria-hidden="true">
${this.getBotIcon()}
</div>
<div class="${CSS_CLASSES.botInfo}">
<h3 class="${CSS_CLASSES.botName}" id="bot-name">${sanitizeHtml(this.config.botName)}</h3>
<div class="${CSS_CLASSES.botStatus}" aria-live="polite">
<span class="status-indicator online"></span>
<span class="status-text">Online</span>
</div>
</div>
<div class="header-controls">
<button
class="${CSS_CLASSES.maximizeButton}"
aria-label="${ARIA_LABELS.maximize}"
type="button"
>
${this.getMaximizeIcon()}
</button>
<button
class="${CSS_CLASSES.closeButton}"
aria-label="${ARIA_LABELS.close}"
type="button"
>
${this.getCloseIcon()}
</button>
</div>
</header>
`;
}
getMessagesHTML() {
return `
<div
class="${CSS_CLASSES.messages}"
id="${ELEMENT_IDS.messagesContainer}"
role="log"
aria-live="polite"
aria-label="${ARIA_LABELS.messages}"
tabindex="0"
></div>
`;
}
getInputAreaHTML() {
return `
<div class="${CSS_CLASSES.inputArea}">
<textarea
class="${CSS_CLASSES.messageInput}"
placeholder="${sanitizeHtml(this.config.inputPlaceholder)}"
aria-label="${ARIA_LABELS.input}"
maxlength="4000"
autocomplete="off"
spellcheck="true"
></textarea>
<button
class="${CSS_CLASSES.sendButton}"
aria-label="${ARIA_LABELS.send}"
type="button"
disabled
>
${this.getSendIcon()}
</button>
</div>
`;
}
getPoweredByHTML() {
if (!this.config.showPoweredBy) {
return "";
}
return `
<div class="${CSS_CLASSES.poweredBy}">
<a href="https://infomatebot.com" target="_blank" rel="noopener noreferrer">
Powered by ${this.getLogoSVG()}
</a>
</div>
`;
}
cacheElements() {
this.elements = {
trigger: this.shadow.querySelector(`.${CSS_CLASSES.trigger}`),
window: this.shadow.querySelector(`.${CSS_CLASSES.window}`),
messages: this.shadow.querySelector(`.${CSS_CLASSES.messages}`),
input: this.shadow.querySelector(`.${CSS_CLASSES.messageInput}`),
sendBtn: this.shadow.querySelector(`.${CSS_CLASSES.sendButton}`),
closeBtn: this.shadow.querySelector(`.${CSS_CLASSES.closeButton}`),
botName: this.shadow.querySelector(`.${CSS_CLASSES.botName}`),
botStatus: this.shadow.querySelector(`.${CSS_CLASSES.botStatus}`),
maximizeBtn: this.shadow.querySelector(`.${CSS_CLASSES.maximizeButton}`)
};
}
setupInitialState() {
if (this.elements.botName) {
this.elements.botName.textContent = this.config.botName;
}
if (this.elements.input) {
this.elements.input.placeholder = this.config.inputPlaceholder;
this.setupInputHandlers();
}
if (this.elements.maximizeBtn) {
this.elements.maximizeBtn.addEventListener("click", () => {
this.toggleMaximize();
});
}
this.updateSendButtonState();
}
setupInputHandlers() {
if (!this.elements.input || !this.elements.sendBtn) return;
const input = this.elements.input;
input.addEventListener("input", () => {
autoResizeTextarea(input, 120);
this.updateSendButtonState();
});
input.addEventListener("paste", (_e) => {
setTimeout(() => {
autoResizeTextarea(input, 120);
this.updateSendButtonState();
}, 0);
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
}
});
}
addMessage(message, quickReplies) {
if (!this.elements.messages) return;
const messageEl = this.createMessageElement(message);
this.elements.messages.appendChild(messageEl);
if (message.sender === "bot" && quickReplies && quickReplies.length > 0) {
this.currentQuickReplies = quickReplies;
const quickRepliesEl = this.createQuickRepliesElement(quickReplies);
this.elements.messages.appendChild(quickRepliesEl);
}
if (message.sender === "bot") {
this.announceToScreenReader(message.content);
}
this.scrollToBottom();
}
getCurrentQuickReplies() {
return this.currentQuickReplies;
}
createQuickRepliesElement(quickReplies) {
const quickRepliesContainer = document.createElement("div");
quickRepliesContainer.className = `inline-quick-replies`;
quickReplies.forEach((reply) => {
const button = document.createElement("button");
button.className = CSS_CLASSES.quickReply;
button.textContent = reply.label;
button.setAttribute("data-label", reply.label);
button.setAttribute("data-reply-choice-id", reply.id);
button.setAttribute("type", "button");
button.setAttribute("aria-label", `${ARIA_LABELS.quickReply}: ${reply.label}`);
quickRepliesContainer.appendChild(button);
});
return quickRepliesContainer;
}
removeAllQuickReplies() {
const allQuickReplies = this.elements.messages?.querySelectorAll(".inline-quick-replies");
allQuickReplies?.forEach((quickReplyContainer) => {
quickReplyContainer.remove();
});
}
toggleMaximize() {
this.isMaximized = !this.isMaximized;
if (this.elements.window && this.elements.maximizeBtn) {
if (this.isMaximized) {
this.elements.window.classList.add(CSS_CLASSES.windowMaximized);
this.elements.maximizeBtn.setAttribute("aria-label", ARIA_LABELS.minimize);
} else {
this.elements.window.classList.remove(CSS_CLASSES.windowMaximized);
this.elements.maximizeBtn.setAttribute("aria-label", ARIA_LABELS.maximize);
}
}
setTimeout(() => this.scrollToBottom(), 300);
}
getMaximizeIcon() {
return `<svg viewBox="0 0 16 16" fill="none">
<path d="M6.49001 8.30999L3.69 11.11V9.40999C3.69 8.93999 3.31 8.55999 2.84 8.55999C2.37 8.55999 1.99001 8.93999 1.99001 9.40999V14.01H6.59C7.06 14.01 7.44 13.63 7.44 13.16C7.44 12.69 7.06 12.31 6.59 12.31H4.89L7.69 9.50999L6.49001 8.30999ZM9.41 1.98999C8.94 1.98999 8.56001 2.36999 8.56001 2.83999C8.56001 3.30999 8.94 3.68999 9.41 3.68999H11.11L8.31001 6.48999L9.51 7.68999L12.31 4.88999V6.58999C12.31 7.05999 12.69 7.43999 13.16 7.43999C13.63 7.43999 14.01 7.05999 14.01 6.58999V1.98999H9.41Z" fill="currentColor"/>
</svg>`;
}
createMessageElement(message) {
const messageEl = document.createElement("div");
messageEl.className = `${CSS_CLASSES.message} ${CSS_CLASSES[`message${message.sender === "bot" ? "Bot" : "User"}`]}`;
messageEl.setAttribute("data-message-id", message.id);
messageEl.setAttribute("data-timestamp", message.timestamp);
if (message.type === "error") {
messageEl.classList.add(CSS_CLASSES.messageError);
}
const avatar = document.createElement("div");
avatar.className = CSS_CLASSES.msgAvatar;
avatar.setAttribute("aria-hidden", "true");
avatar.innerHTML = message.sender === "bot" ? this.getBotIcon() : this.getUserIcon();
const content = document.createElement("div");
content.className = CSS_CLASSES.msgContent;
content.innerHTML = this.formatMessageContent(message.content);
const timestamp = document.createElement("span");
timestamp.className = "sr-only";
timestamp.textContent = ` sent at ${new Date(message.timestamp).toLocaleTimeString()}`;
content.appendChild(timestamp);
messageEl.appendChild(avatar);
messageEl.appendChild(content);
return messageEl;
}
showTypingIndicator() {
if (!this.elements.messages) return;
this.hideTypingIndicator();
const typingEl = document.createElement("div");
typingEl.className = `${CSS_CLASSES.message} ${CSS_CLASSES.messageBot} ${CSS_CLASSES.typing}`;
typingEl.id = ELEMENT_IDS.typingIndicator;
typingEl.setAttribute("aria-label", "Bot is typing");
const avatar = document.createElement("div");
avatar.className = CSS_CLASSES.msgAvatar;
avatar.setAttribute("aria-hidden", "true");
avatar.innerHTML = this.getBotIcon();
const indicator = document.createElement("div");
indicator.className = CSS_CLASSES.typingIndicator;
indicator.innerHTML = `
<div class="typing-dots" aria-hidden="true">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
`;
typingEl.appendChild(avatar);
typingEl.appendChild(indicator);
this.elements.messages.appendChild(typingEl);
this.announceToScreenReader("Bot is typing");
this.scrollToBottom();
}
hideTypingIndicator() {
const typingEl = this.shadow.getElementById(ELEMENT_IDS.typingIndicator);
if (typingEl) {
typingEl.remove();
}
}
clearMessages() {
if (this.elements.messages) {
this.elements.messages.innerHTML = "";
}
}
clearInput() {
if (this.elements.input) {
this.elements.input.value = "";
this.elements.input.style.height = "auto";
this.updateSendButtonState();
}
}
focusInput() {
if (this.elements.input) {
setTimeout(() => {
this.elements.input?.focus();
}, ANIMATIONS.focusDelay);
}
}
setInputDisabled(disabled) {
if (this.elements.input) {
this.elements.input.disabled = disabled;
}
if (this.elements.sendBtn) {
this.elements.sendBtn.disabled = disabled || !this.isInputValid();
}
}
show() {
if (this.elements.window) {
this.elements.window.classList.add(CSS_CLASSES.windowActive);
this.elements.window.setAttribute("aria-hidden", "false");
this.updateTriggerIcon(true);
}
}
hide() {
if (this.elements.window) {
this.elements.window.classList.remove(CSS_CLASSES.windowActive);
this.elements.window.setAttribute("aria-hidden", "true");
this.updateTriggerIcon(false);
setTimeout(() => {
this.elements.trigger?.focus();
});
}
}
updateBotStatus(status) {
if (this.elements.botStatus) {
const indicator = this.elements.botStatus.querySelector(".status-indicator");
const text = this.elements.botStatus.querySelector(".status-text");
if (indicator && text) {
text.textContent = status;
indicator.className = `status-indicator ${status.toLowerCase()}`;
}
}
}
updateConfig(config) {
this.config = config;
if (this.elements.botName) {
this.elements.botName.textContent = config.botName;
}
if (this.elements.input) {
this.elements.input.placeholder = config.inputPlaceholder;
}
if (this.elements.window) {
this.elements.window.className = this.elements.window.className.replace(
/size-(small|medium|large|xl)/,
`size-${config.size}`
);
}
const container = this.shadow.querySelector(".position-bottom-right, .position-bottom-left");
if (container) {
container.className = `position-${config.position}`;
}
}
getInputValue() {
return this.elements.input?.value.trim() || "";
}
onTriggerClick(callback) {
this.elements.trigger?.addEventListener("click", callback);
}
onCloseClick(callback) {
this.elements.closeBtn?.addEventListener("click", callback);
}
onSendMessage(callback) {
const handleSend = () => {
const message = this.getInputValue();
if (message && this.isInputValid()) {
callback(message);
}
};
this.elements.sendBtn?.addEventListener("click", handleSend);
this.elements.input?.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
}
onQuickReplyClick(callback) {
this.elements.messages?.addEventListener("click", async (e) => {
const target = e.target;
if (target.classList.contains(CSS_CLASSES.quickReply)) {
const label = target.dataset.label;
const replyChoiceId = target.dataset.replyChoiceId;
if (label && replyChoiceId) {
this.disableQuickReplies();
try {
await callback(label, replyChoiceId);
} catch (error) {
this.enableQuickReplies();
}
}
}
});
}
disableQuickReplies() {
const quickReplies = this.elements.messages?.querySelectorAll(`.${CSS_CLASSES.quickReply}`);
quickReplies?.forEach((button) => {
button.disabled = true;
button.style.opacity = "0.5";
button.style.pointerEvents = "none";
});
}
enableQuickReplies() {
const quickReplies = this.elements.messages?.querySelectorAll(`.${CSS_CLASSES.quickReply}`);
quickReplies?.forEach((button) => {
button.disabled = false;
button.style.opacity = "1";
button.style.pointerEvents = "auto";
});
}
isInputValid() {
const value = this.getInputValue();
return value.length > 0 && value.length <= 4e3;
}
updateSendButtonState() {
if (this.elements.sendBtn) {
const isValid = this.isInputValid();
this.elements.sendBtn.disabled = !isValid;
this.elements.sendBtn.setAttribute("aria-disabled", (!isValid).toString());
}
}
scrollToBottom() {
if (this.elements.messages) {
setTimeout(() => {
scrollToBottom(this.elements.messages, true);
}, 50);
}
}
updateTriggerIcon(isOpen) {
if (this.elements.trigger) {
this.elements.trigger.innerHTML = isOpen ? this.getDownArrowIcon() : this.getChatIcon();
this.elements.trigger.setAttribute("aria-label", isOpen ? "Close chat" : "Open chat");
}
}
getDownArrowIcon() {
return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.601 8.39897C18.269 8.06702 17.7309 8.06702 17.3989 8.39897L12 13.7979L6.60099 8.39897C6.26904 8.06702 5.73086 8.06702 5.39891 8.39897C5.06696 8.73091 5.06696 9.2691 5.39891 9.60105L11.3989 15.601C11.7309 15.933 12.269 15.933 12.601 15.601L18.601 9.60105C18.9329 9.2691 18.9329 8.73091 18.601 8.39897Z" fill="currentColor"/>
</svg>`;
}
announceToScreenReader(text) {
const announcement = document.createElement("div");
announcement.setAttribute("aria-live", "polite");
announcement.setAttribute("aria-atomic", "true");
announcement.className = "sr-only";
announcement.textContent = text;
this.shadow.appendChild(announcement);
setTimeout(() => {
announcement.remove();
}, 1e3);
}
isUIRendered() {
return this.isRendered;
}
getMessageCount() {
return this.elements.messages?.children.length || 0;
}
destroy() {
this.elements = {};
this.isRendered = false;
}
getChatIcon() {
return `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
`;
}
getBotIcon() {
return `
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
`;
}
getUserIcon() {
return `
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
`;
}
getCloseIcon() {
return `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
`;
}
getSendIcon() {
return `
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
`;
}
getLogoSVG() {
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100px" height="12px" viewBox="0 0 101 12" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(1.568628%,58.823532%,40.784314%);fill-opacity:1;" d="M 0.0195312 2.226562 C 3.195312 2.226562 6.371094 2.226562 9.640625 2.226562 C 9.640625 4.820312 9.640625 7.417969 9.640625 10.09375 C 6.558594 10.019531 6.558594 10.019531 5.601562 9.980469 C 3.929688 9.929688 3.929688 9.929688 2.511719 10.710938 C 2.160156 11.054688 1.851562 11.433594 1.5625 11.835938 C 1.519531 11.890625 1.480469 11.941406 1.4375 12 C 1.414062 12 1.386719 12 1.359375 12 C 1.359375 11.371094 1.359375 10.742188 1.359375 10.09375 C 0.917969 10.09375 0.472656 10.09375 0.0195312 10.09375 C 0.0195312 7.496094 0.0195312 4.898438 0.0195312 2.226562 Z M 1.519531 3.65625 C 1.519531 5.28125 1.519531 6.90625 1.519531 8.582031 C 3.703125 8.582031 5.890625 8.582031 8.144531 8.582031 C 8.144531 6.957031 8.144531 5.332031 8.144531 3.65625 C 5.957031 3.65625 3.769531 3.65625 1.519531 3.65625 Z M 1.519531 3.65625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(1.568628%,58.823532%,40.784314%);fill-opacity:1;" d="M 46.238281 1.273438 C 46.65625 1.273438 47.074219 1.273438 47.503906 1.273438 C 48.429688 3.03125 49.304688 4.816406 50.183594 6.597656 C 50.371094 6.359375 50.507812 6.121094 50.640625 5.847656 C 50.683594 5.761719 50.722656 5.679688 50.765625 5.589844 C 50.808594 5.5 50.855469 5.414062 50.898438 5.320312 C 51.210938 4.679688 51.527344 4.046875 51.859375 3.417969 C 52.234375 2.707031 52.59375 1.992188 52.945312 1.273438 C 53.359375 1.273438 53.777344 1.273438 54.207031 1.273438 C 54.207031 3.738281 54.207031 6.203125 54.207031 8.742188 C 53.84375 8.742188 53.480469 8.742188 53.101562 8.742188 C 53.078125 7.0625 53.050781 5.382812 53.023438 3.65625 C 52.660156 4.390625 52.296875 5.125 51.917969 5.878906 C 51.6875 6.324219 51.460938 6.769531 51.222656 7.214844 C 51.089844 7.46875 50.957031 7.722656 50.824219 7.976562 C 50.691406 8.230469 50.554688 8.488281 50.421875 8.742188 C 50.289062 8.742188 50.160156 8.742188 50.027344 8.742188 C 49.355469 7.46875 48.714844 6.183594 48.082031 4.894531 C 48.050781 4.832031 48.015625 4.769531 47.984375 4.699219 C 47.925781 4.578125 47.863281 4.457031 47.804688 4.335938 C 47.6875 4.097656 47.570312 3.875 47.421875 3.65625 C 47.398438 5.332031 47.371094 7.011719 47.34375 8.742188 C 46.980469 8.742188 46.617188 8.742188 46.238281 8.742188 C 46.238281 6.277344 46.238281 3.8125 46.238281 1.273438 Z M 46.238281 1.273438 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.666668%,73.333335%,29.803923%);fill-opacity:1;" d="M 3.960938 0 C 7.136719 0 10.3125 0 13.585938 0 C 13.585938 2.597656 13.585938 5.191406 13.585938 7.867188 C 13.144531 7.867188 12.699219 7.867188 12.246094 7.867188 C 12.246094 8.496094 12.246094 9.125 12.246094 9.773438 C 12.0625 9.617188 11.878906 9.460938 11.691406 9.296875 C 11.632812 9.246094 11.570312 9.195312 11.507812 9.144531 C 10.34375 8.171875 10.34375 8.171875 10.265625 7.523438 C 10.246094 7.148438 10.304688 6.820312 10.351562 6.4375 C 10.925781 6.4375 11.496094 6.4375 12.085938 6.4375 C 12.085938 4.785156 12.085938 3.132812 12.085938 1.429688 C 9.40625 1.429688 6.722656 1.429688 3.960938 1.429688 C 3.960938 0.957031 3.960938 0.484375 3.960938 0 Z M 3.960938 0 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(1.568628%,58.823532%,40.784314%);fill-opacity:1;" d="M 78.738281 1.273438 C 79.453125 1.265625 80.167969 1.261719 80.90625 1.257812 C 81.132812 1.253906 81.355469 1.253906 81.589844 1.25 C 81.769531 1.25 81.949219 1.25 82.125 1.25 C 82.21875 1.246094 82.3125 1.246094 82.40625 1.246094 C 82.976562 1.246094 83.480469 1.265625 83.945312 1.636719 C 83.980469 1.683594 84.015625 1.726562 84.050781 1.773438 C 84.105469 1.839844 84.105469 1.839844 84.164062 1.90625 C 84.527344 2.507812 84.503906 3.285156 84.375 3.957031 C 84.265625 4.3125 84.117188 4.574219 83.863281 4.847656 C 83.9375 4.902344 84.011719 4.957031 84.085938 5.011719 C 84.476562 5.339844 84.605469 5.78125 84.652344 6.277344 C 84.691406 7.015625 84.6875 7.765625 84.179688 8.34375 C 83.710938 8.652344 83.261719 8.753906 82.703125 8.75 C 82.617188 8.75 82.527344 8.75 82.433594 8.75 C 82.339844 8.75 82.246094 8.75 82.148438 8.75 C 82.0625 8.75 81.976562 8.75 81.886719 8.75 C 81.566406 8.75 81.242188 8.746094 80.921875 8.746094 C 80.203125 8.746094 79.480469 8.742188 78.738281 8.742188 C 78.738281 6.277344 78.738281 3.8125 78.738281 1.273438 Z M 79.84375 2.382812 C 79.84375 3.039062 79.84375 3.695312 79.84375 4.371094 C 80.273438 4.378906 80.703125 4.386719 81.132812 4.394531 C 81.28125 4.394531 81.425781 4.398438 81.574219 4.402344 C 81.785156 4.40625 81.996094 4.40625 82.207031 4.410156 C 82.273438 4.410156 82.335938 4.414062 82.40625 4.417969 C 82.746094 4.417969 82.878906 4.402344 83.15625 4.1875 C 83.355469 3.914062 83.363281 3.753906 83.351562 3.417969 C 83.351562 3.320312 83.351562 3.226562 83.351562 3.128906 C 83.3125 2.847656 83.246094 2.691406 83.074219 2.464844 C 82.8125 2.332031 82.519531 2.375 82.230469 2.375 C 82.164062 2.378906 82.09375 2.378906 82.023438 2.378906 C 81.804688 2.378906 81.585938 2.378906 81.371094 2.378906 C 81.222656 2.378906 81.074219 2.378906 80.925781 2.378906 C 80.566406 2.378906 80.203125 2.382812 79.84375 2.382812 Z M 79.84375 5.484375 C 79.84375 6.191406 79.84375 6.898438 79.84375 7.628906 C 80.285156 7.632812 80.726562 7.640625 81.167969 7.640625 C 81.320312 7.644531 81.46875 7.644531 81.621094 7.648438 C 81.835938 7.652344 82.050781 7.652344 82.269531 7.652344 C 82.335938 7.652344 82.402344 7.65625 82.472656 7.65625 C 82.84375 7.65625 83.089844 7.636719 83.390625 7.390625 C 83.546875 7.113281 83.570312 6.871094 83.570312 6.554688 C 83.570312 6.484375 83.570312 6.410156 83.570312 6.335938 C 83.546875 6.09375 83.507812 5.933594 83.390625 5.722656 C 83.035156 5.429688 82.703125 5.453125 82.269531 5.460938 C 82.164062 5.460938 82.164062 5.460938 82.058594 5.460938 C 81.835938 5.460938 81.617188 5.464844 81.394531 5.46875 C 81.246094 5.46875 81.09375 5.472656 80.945312 5.472656 C 80.578125 5.472656 80.210938 5.480469 79.84375 5.484375 Z M 79.84375 5.484375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(1.568628%,58.823532%,40.784314%);fill-opacity:1;" d="M 38.601562 1.238281 C 38.746094 1.238281 38.746094 1.238281 38.894531 1.234375 C 38.996094 1.234375 39.101562 1.238281 39.207031 1.238281 C 39.316406 1.238281 39.425781 1.234375 39.53125 1.234375 C 39.757812 1.234375 39.984375 1.234375 40.210938 1.234375 C 40.5 1.238281 40.785156 1.234375 41.074219 1.234375 C 41.296875 1.230469 41.519531 1.230469 41.742188 1.230469 C 41.902344 1.230469 42.058594 1.230469 42.21875 1.230469 C 43.125 1.234375 43.125 1.234375 43.480469 1.589844 C 43.71875 1.914062 43.75 2.1875 43.75 2.578125 C 43.75 2.636719 43.75 2.695312 43.75 2.753906 C 43.75 2.945312 43.75 3.136719 43.75 3.332031 C 43.75 3.464844 43.75 3.597656 43.753906 3.730469 C 43.753906 4.011719 43.753906 4.292969 43.75 4.570312 C 43.75 4.929688 43.753906 5.285156 43.753906 5.644531 C 43.757812 5.921875 43.757812 6.199219 43.757812 6.472656 C 43.757812 6.605469 43.757812 6.738281 43.757812 6.871094 C 43.757812 7.054688 43.757812 7.238281 43.757812 7.425781 C 43.757812 7.476562 43.757812 7.53125 43.757812 7.589844 C 43.753906 7.980469 43.65625 8.207031 43.402344 8.503906 C 43.078125 8.710938 42.828125 8.773438 42.449219 8.773438 C 42.308594 8.777344 42.308594 8.777344 42.167969 8.777344 C 42.066406 8.777344 41.96875 8.777344 41.863281 8.777344 C 41.761719 8.777344 41.660156 8.777344 41.554688 8.777344 C 41.335938 8.777344 41.117188 8.777344 40.898438 8.777344 C 40.625 8.777344 40.34375 8.777344 40.066406 8.78125 C 39.800781 8.78125 39.535156 8.78125 39.265625 8.78125 C 39.167969 8.78125 39.070312 8.78125 38.96875 8.785156 C 38.433594 8.777344 38.066406 8.765625 37.644531 8.425781 C 37.402344 8.101562 37.371094 7.824219 37.375 7.433594 C 37.375 7.375 37.375 7.320312 37.371094 7.257812 C 37.371094 7.066406 37.371094 6.875 37.371094 6.683594 C 37.371094 6.550781 37.371094 6.414062 37.371094 6.28125 C 37.371094 6.003906 37.371094 5.722656 37.371094 5.441406 C 37.371094 5.082031 37.371094 4.726562 37.367188 4.367188 C 37.367188 4.089844 37.367188 3.816406 37.367188 3.539062 C 37.367188 3.40625 37.367188 3.273438 37.367188 3.144531 C 37.363281 2.957031 37.367188 2.773438 37.367188 2.589844 C 37.367188 2.535156 37.367188 2.480469 37.363281 2.425781 C 37.371094 2.03125 37.472656 1.808594 37.722656 1.511719 C 38.015625 1.277344 38.234375 1.242188 38.601562 1.238281 Z M 38.511719 2.382812 C 38.511719 4.113281 38.511719 5.847656 38.511719 7.628906 C 39.863281 7.628906 41.21875 7.628906 42.613281 7.628906 C 42.613281 5.898438 42.613281 4.167969 42.613281 2.382812 C 41.257812 2.382812 39.90625 2.382812 38.511719 2.382812 Z M 38.511719 2.382812 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(1.568628%,58.823532%,40.784314%);fill-opacity:1;" d="M 88.671875 1.230469 C 88.78125 1.230469 88.886719 1.230469 88.996094 1.230469 C 89.222656 1.226562 89.445312 1.230469 89.671875 1.230469 C 89.960938 1.230469 90.246094 1.230469 90.535156 1.230469 C 90.757812 1.226562 90.980469 1.230469 91.203125 1.230469 C 91.359375 1.230469 91.519531 1.230469 91.675781 1.226562 C 92.582031 1.238281 92.582031 1.238281 92.9375 1.589844 C 93.175781 1.914062 93.207031 2.1875 93.203125 2.578125 C 93.203125 2.636719 93.203125 2.695312 93.207031 2.753906 C 93.207031 2.945312 93.207031 3.136719 93.207031 3.332031 C 93.207031 3.464844 93.207031 3.597656 93.207031 3.730469 C 93.207031 4.011719 93.207031 4.292969 93.207031 4.570312 C 93.207031 4.929688 93.207031 5.285156 93.210938 5.644531 C 93.210938 5.921875 93.210938 6.199219 93.210938 6.472656 C 93.210938 6.605469 93.210938 6.738281 93.214844 6.871094 C 93.214844 7.054688 93.214844 7.238281 93.210938 7.425781 C 93.210938 7.476562 93.214844 7.53125 93.214844 7.589844 C 93.207031 7.980469 93.101562 8.203125 92.855469 8.503906 C 92.359375 8.820312 91.824219 8.777344 91.253906 8.777344 C 91.101562 8.777344 91.101562 8.777344 90.945312 8.777344 C 90.730469 8.777344 90.519531 8.777344 90.304688 8.777344 C 90.03125 8.777344 89.761719 8.777344 89.488281 8.78125 C 89.226562 8.78125 88.964844 8.78125 88.703125 8.78125 C 88.558594 8.78125 88.558594 8.78125 88.410156 8.785156 C 87.878906 8.777344 87.519531 8.761719 87.097656 8.425781 C 86.855469 8.101562 86.828125 7.824219 86.832031 7.433594 C 86.832031 7.375 86.832031 7.316406 86.828125 7.257812 C 86.828125 7.066406 86.828125 6.871094 86.832031 6.679688 C 86.832031 6.546875 86.828125 6.414062 86.828125 6.277344 C 86.828125 6 86.828125 5.71875 86.832031 5.4375 C 86.832031 5.078125 86.832031 4.71875 86.832031 4.363281 C 86.828125 4.085938 86.828125 3.808594 86.832031 3.53125 C 86.832031 3.398438 86.832031 3.269531 86.828125 3.136719 C 86.828125 2.949219 86.828125 2.765625 86.832031 2.582031 C 86.832031 2.523438 86.832031 2.472656 86.828125 2.414062 C 86.835938 1.964844 86.96875 1.71875 87.28125 1.410156 C 87.691406 1.160156 88.210938 1.226562 88.671875 1.230469 Z M 87.964844 2.382812 C 87.964844 4.113281 87.964844 5.847656 87.964844 7.628906 C 89.320312 7.628906 90.671875 7.628906 92.070312 7.628906 C 92.070312 5.898438 92.070312 4.167969 92.070312 2.382812 C 90.714844 2.382812 89.363281 2.382812 87.964844 2.382812 Z M 87.9