Newer
Older
vmk-demo-bot / frontend / src / js / index.js
function doQuestionEventHandler(e) {
	const self = e.currentTarget;
	const question = self.dataset.doQuestion;

	const chat = document.querySelector(".chat-container");
	chat.appendChild(createRenderBubble("user", true, question).message);

	const msgid = makeAssistantMsg();
	sendMessages(
		msgid,
		prepareUserMessage(question), 
		function(resp, msgid){
			if(resp["status"] != "done") {
				return;
			}
			
			let content = marked.parse(resp["content"]).trim();
			updateMsgContent(msgid, content);
			updateMsgHistory();
		}
	);
}

const specialViewMap = {

	product_card: data => {
		console.log("product_card", data);
		let html = `<div class="kb-card kb-product-card">
			<h4 class="heading">${data.title}</h4>
			<div class="actions-group">
				<div class="price">
					<span class="now">${data.price}</span>
				</div>
				<div>
					<a href="${data.link}" class="kb-btn tricky go-to-product">
						Купити
						<svg class="kb-icon" width="24" height="24">
							<use href="#kb-arrow-right"></use>
						</svg>
					</a>
				</div>
			</div>		
		</div>`;
		let container = document.createElement("div");
		container.innerHTML = html;
		return container.querySelector(".kb-card");
	},

	identification_card: data => {
		console.log("identification_card", data);
		let html = `
			<div class="kb-card kb-auth-card">
				<div class="description">Введіть номер телефону для перевірки статусу замовлення</div>

				<div class="kb-input-form">
					<label for="kb-phone" class="kb-input-label">Номер телефону</label>
					<div class="kb-input-container">
						<div class="prefix">
							<svg class="kb-icon" width="16" height="16">
								<use href="#kb-ua-flag"></use>
							</svg>
							<span>+380</span>
							<div class="sep"></div>
						</div>
						<input type="phone" id="kb-phone" maxlength="9" placeholder="000000000" class="kb-input">
					</div>
				</div>

				<button class="kb-btn large disabled kb-auth-btn">Відправити</button>
			</div>
		`;

		let container = document.createElement("div");
		container.innerHTML = html;
		const authCard = container.querySelector(".kb-auth-card");

		const kbAuthBtn = authCard.querySelector(".kb-auth-btn");
		authCard.querySelector("#kb-phone").addEventListener("input", e => {
			e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '');
			let val = e.currentTarget.value;

			if(val.length < 9) {
				kbAuthBtn.classList.add("disabled");
			} else {
				kbAuthBtn.classList.remove("disabled");
			}
		});

		return authCard;
	},

	frequentlyQuestions: data => {
		let questions = ``;
		for(let q of data.questions) {
			questions += `
				<button class="kb-btn tricky question" data-do-question="${q}">${q}</button>
			`;
		}

		const html = `
			<div class="frequently-questions">
				<h4 class="heading">Часті запитання:</h4>
				<div class="questions-container">
					${questions}
				</div>
			</div>
		`;

		let container = document.createElement("div");
		container.innerHTML = html;
		const fquestions = container.querySelector(".frequently-questions");

		fquestions.querySelectorAll("[data-do-question]").forEach(i => {
			i.addEventListener("click", e => {
				doQuestionEventHandler(e);
			});
		});

		return fquestions;
	},

	frequentlyAsked: data => {

		let questions = ``;
		for(let q of data.questions) {
			questions += `
				<button class="kb-btn tricky question" data-do-question="Мене цікавить ${q}">
					<svg class="kb-icon" width="16" height="16">
						<use href="#kb-star"></use>
					</svg>
					${q}
				</button>
			`;
		}

		const html = `
			<div class="frequently-asked">
				<h4 class="heading">Поширені запити:</h4>
				<div class="questions-container">
					${questions}

					<button class="kb-btn tricky question" onclick="showFreqQuests()">
						<svg class="kb-icon" width="16" height="16">
							<use href="#kb-star"></use>
						</svg>
						Часті запитання
					</button>

				</div>
			</div>
		`;

		let container = document.createElement("div");
		container.innerHTML = html;
		const fquestions = container.querySelector(".frequently-asked");

		fquestions.querySelectorAll("[data-do-question]").forEach(i => {
			i.addEventListener("click", e => {
				doQuestionEventHandler(e);
			});
		});
		return fquestions;
	}

};

function showFreqQuests() {
	const freqQuests = specialViewMap.frequentlyQuestions({
		questions: [
			"Як мені працевлаштуватися у вас",
			"Які умови співпраці?",
			"Де можна отримати консультацію?",
		]
	});

	document.querySelector(".chat-container").appendChild(freqQuests);
	scrollMsgsContainerDown();
	updateMsgHistory();
}

async function sendMessages(msgid, message, onChunk) {
	url = AISERV;

	hostMap = {
		"kitanbot.vmk-group.tech": "kitan-group",
		"localhost:3000": "kitan-group",
		"kitan-group.com": "kitan-group",
		"hbloom.com.ua": "hbloom",
		"nubea.com.ua": "nubea",
		"mediceuticals.com.ua": "mediceuticals",
		"bosleymd.com.ua": "bosleymd"
	};

	message["site"] = hostMap[document.location.host] ?? document.location.host;

	try {
		const response = await fetch(url, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({ action: "kitan", message })
		});

		if (!response.ok) {
			throw new Error(`Ошибка при отправке: ${response.status} ${response.statusText}`);
		}

		const reader = response.body.getReader();
		const decoder = new TextDecoder("utf-8");
		let buffer = "";

		while (true) {
			const { done, value } = await reader.read();
			if (done) break;

			buffer += decoder.decode(value, { stream: true });

			let lines = buffer.split("\n");
			buffer = lines.pop(); // последний может быть неполной строкой

			for (const line of lines) {
				if (!line.trim()) continue;

				try {
					const data = JSON.parse(line);
					if (onChunk) onChunk(data, msgid); // вызвать обработчик
				} catch (e) {
					console.warn("Ошибка разбора JSON:", e, line);
				}
			}
		}

		if (buffer) {
			try {
				const final = JSON.parse(buffer);
				if (onChunk) onChunk(final);
			} catch (e) {
				console.warn("Ошибка разбора последнего чанка:", e, buffer);
			}
		}

	} catch (error) {
		console.error("Ошибка при отправке сообщений:", error);
	}
}


function prepareUserMessage(text) {
	return {
		message: {
			id: 1,
			date: getCurrentDateTime(),
			text: text,
			reply_to_message: null,
			entities: null,
			photo: null,
			document: null,
			voice: null,
			audio: null,
			video: null,
			sticker: null,
			caption: null
		},
		author: {
			id: 1,
			username: '',
			first_name: '',
			last_name: '',
			title: null,
			type: 'user'
		},
		is_forwarding: false,
		origin: {
			from: 'kitan_bot',
			chat_id: getChatId()
		}
	}
}

function getTimeView() {
	const now = new Date();
	const hours = now.getHours().toString().padStart(2, '0');
	const minutes = now.getMinutes().toString().padStart(2, '0');
	return time = `${hours}:${minutes}`;
}

function getCurrentDateTime() {
	const now = new Date();

	const pad = n => n.toString().padStart(2, '0');

	const year = now.getFullYear();
	const month = pad(now.getMonth() + 1); // Месяцы от 0 до 11
	const day = pad(now.getDate());

	const hours = pad(now.getHours());
	const minutes = pad(now.getMinutes());
	const seconds = pad(now.getSeconds());

	return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

function updateMsgHistory() {
	const chat = document.querySelector(".chat-container");
	localStorage.setItem("chat", chat.innerHTML);
}

function restoreMsgStorage() {
	const chat = document.querySelector(".chat-container");
	let content = localStorage.getItem("chat");
	if(content) {
		chat.innerHTML = content;
	}

	chat.querySelectorAll("[data-do-question]").forEach(i => {
		i.addEventListener("click", e => {
			doQuestionEventHandler(e);
		});
	});
}

function getChatId() {
	chatId = localStorage.getItem("chatId");
	if(!chatId) {
		chatId = Math.floor(Math.random() * (99999999 - 1000000 + 1)) + 1000000;
		localStorage.setItem("chatId", chatId);
	}

	return chatId;
}

function resetChat() {
	localStorage.removeItem("chatId");
	localStorage.removeItem("chat");
	localStorage.removeItem("user_rating");
}

function genMsgId() {
	return "msgid" + (new Date()).getTime();
}

function createRenderBubble(type, isFirst, msgtext, mid, time) {
	time = time || getTimeView();
	mid = mid || genMsgId();

	waitingLoader = `
		<span class="waiting">
			<span></span>
			<span></span>
			<span></span>
		</span>
	`;

	msgtext = msgtext || waitingLoader;

	const additionalCls = [];
	let icon = "";
	if(type == "user") {
		additionalCls.push("user");
		icon = `
			<svg class="kb-icon" width="16" height="16">
				<use href="#kb-user"></use>
			</svg>
		`;
	} else {
		additionalCls.push("agent");
		additionalCls.push("assistant");
		additionalCls.push("state-waiting");
		icon = `
			<svg class="kb-icon" width="32" height="32">
				<use href="#kb-redes-sm-avatar"></use>
			</svg>
		`;
	}

	const classes = additionalCls.join(" ");
	const msg = `
		<div class="message ${classes}" id="${mid}">
			<div class="leftside">
				<div class="avatar">
					${icon}
				</div>
			</div>

			<div class="rightside">
				<div class="kb-card bubble first">
					${msgtext}
				</div>
				<div class="foot">
					<span class="timestamp">${time}</span>
				</div>
			</div>
		</div>
	`;

	const cont = document.createElement("DIV");
	cont.innerHTML = msg;

	return {
		"id": mid,
		"message": cont.querySelector(".message")
	}
}

function updateMsgContent(mid, msgtext) {
	const msg = document.querySelector(`.assistant#${mid}`);
	msg.classList.remove("state-waiting");

	msg.querySelector(".bubble").innerHTML = msgtext;
	updateMsgHistory();
	scrollMsgsContainerDown();
}


function doAction(action, params, callback) {
	url = BACKSERV;
	fetch(url, {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json'
		},
		body: JSON.stringify({ action: action, params: params })
	})
	.then(response => response.json())
	.then(data => {
    callback(data);
  })
  .catch(error => {
    console.error("Err of request:", error);
  });
}

function getSpecialViewFromMsg(msg) {
	const sep = "front_special_view";
	if(msg.indexOf(sep) == -1) {
		return false;
	}

	let specialView = msg.split(sep)[1].trim();

	let textarea = document.createElement('textarea');
	textarea.innerHTML = specialView;

	specialView = textarea.value;
	if(!specialView) {
		return false;
	}

	specialView = JSON.parse(specialView);

	if(typeof specialViewMap[specialView.special_view] == "undefined") {
		return false;
	}

	return specialViewMap[specialView.special_view](specialView.data);
}

function initAuthCard() {

}

document.addEventListener("DOMContentLoaded", e => {
	console.log("App start");
});