| |
| |
| |
|
|
| |
| function periksaUkuranLayar() { |
| const leftContainer = document.getElementById("leftContainer"); |
| const msgerBox = document.getElementById("msgerBox"); |
| const toggleButton = document.getElementById("toggleButton"); |
| const isSmallScreen = window.innerWidth <= 670; |
|
|
| |
| leftContainer.classList.toggle("hidden", isSmallScreen); |
| |
| msgerBox.classList.toggle("full-width", window.innerWidth > 670 && leftContainer.classList.contains("hidden")); |
| |
| toggleButton.textContent = isSmallScreen ? ">" : "<"; |
| } |
|
|
| |
| window.addEventListener("resize", periksaUkuranLayar); |
| window.addEventListener("load", periksaUkuranLayar); |
|
|
| |
| document.getElementById("toggleButton").addEventListener("click", function() { |
| const leftContainer = document.getElementById("leftContainer"); |
| const msgerBox = document.getElementById("msgerBox"); |
|
|
| leftContainer.classList.toggle("hidden"); |
|
|
| if (window.innerWidth > 670) { |
| msgerBox.classList.toggle("full-width", leftContainer.classList.contains("hidden")); |
| } |
|
|
| this.textContent = leftContainer.classList.contains("hidden") ? ">" : "<"; |
| }); |
|
|
|
|
| |
| |
| |
|
|
| const msgerBox = document.getElementById("msgerBox"); |
| const clearButton = document.getElementById("clearButton"); |
| const confirmationModal = document.getElementById("confirmationModal"); |
| const confirmDeleteButton = document.getElementById("confirmDelete"); |
| const cancelDeleteButton = document.getElementById("cancelDelete"); |
| const msgerForm = document.querySelector(".msger-inputarea"); |
| const msgerInput = document.getElementById("textInput"); |
| const sendButton = document.getElementById("sendButton"); |
| const msgerChat = document.querySelector(".msger-chat"); |
|
|
| const BOT_IMG = "../static/img/headbot.png"; |
| const PERSON_IMG = "../static/img/headuser.png"; |
| const BOT_NAME = "SkinThinc"; |
| const PERSON_NAME = "Anda"; |
|
|
| let lastUserMessage = ""; |
| let isSpeaking = false; |
| let isVoiceInput = false; |
|
|
|
|
| |
| |
| |
|
|
| |
| clearButton.addEventListener("click", () => { |
| confirmationModal.style.display = "flex"; |
| confirmationModal.classList.add("fade-in"); |
| }); |
|
|
| |
| confirmDeleteButton.addEventListener("click", () => { |
| msgerChat.innerHTML = ""; |
| appendWelcomeMessage(); |
|
|
| |
| fetch("/clear_history", { |
| method: "POST" |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| console.log(data.message); |
| }) |
| .catch(error => { |
| console.error("Error saat menghapus riwayat:", error); |
| }); |
|
|
| |
| confirmationModal.style.display = "none"; |
| }); |
|
|
| |
| cancelDeleteButton.addEventListener("click", () => { |
| confirmationModal.style.display = "none"; |
| }); |
|
|
| |
| function loadChatHistory() { |
| fetch("/load_history") |
| .then(response => response.json()) |
| .then(history => { |
| history.forEach(entry => { |
| const { |
| sender, |
| message |
| } = entry; |
| const name = sender === "user" ? PERSON_NAME : BOT_NAME; |
| const img = sender === "user" ? PERSON_IMG : BOT_IMG; |
| const side = sender === "user" ? "right" : "left"; |
|
|
| if (sender === "bot") { |
| const { |
| formattedResponse, |
| plainText |
| } = formatResponse(message); |
| appendMessage(BOT_NAME, BOT_IMG, "left", formattedResponse, plainText); |
| } else { |
| appendMessage(name, img, side, message, message); |
| } |
| }); |
| }) |
| .catch(error => { |
| console.error("Error saat memuat riwayat percakapan:", error); |
| }); |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| msgerForm.addEventListener("submit", event => { |
| event.preventDefault(); |
| sendMessage(); |
| }); |
|
|
| |
| sendButton.addEventListener("click", () => { |
| sendMessage(); |
| }); |
|
|
| |
| textInput.addEventListener("input", () => { |
| textInput.style.height = '50px'; |
| textInput.style.height = Math.min(textInput.scrollHeight, 120) + 'px'; |
| }); |
|
|
| |
| textInput.addEventListener("keypress", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| sendMessage(); |
| } |
| }); |
|
|
| |
| function sendMessage() { |
| const msgText = msgerInput.value.trim(); |
| if (!msgText) return; |
|
|
| lastUserMessage = msgText; |
| appendMessage(PERSON_NAME, PERSON_IMG, "right", msgText, msgText); |
| msgerInput.value = ""; |
| textInput.style.height = "50px"; |
|
|
| botResponse(msgText); |
| } |
|
|
| |
| function botResponse(rawText) { |
| appendTypingIndicator(); |
|
|
| setTimeout(() => { |
| $.get("/get", { |
| msg: rawText |
| }).done(function(data) { |
| const { |
| formattedResponse, |
| plainText |
| } = formatResponse(data); |
| removeTypingIndicator(); |
| appendMessage(BOT_NAME, BOT_IMG, "left", formattedResponse, plainText); |
|
|
| |
| if (isVoiceInput) { |
| speak(plainText); |
| isVoiceInput = false; |
| } |
| }); |
| }, 2500); |
| } |
|
|
|
|
| |
| |
| |
|
|
| document.addEventListener("DOMContentLoaded", function() { |
| const micButton = document.getElementById("micButton"); |
| const textInput = document.getElementById("textInput"); |
| const micAlert = document.getElementById("micAlert"); |
| const micStatus = document.getElementById("micStatus"); |
|
|
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; |
| if (!SpeechRecognition) { |
| console.warn("Browser Anda tidak mendukung fitur Speech Recognition."); |
| return; |
| } |
|
|
| const recognition = new SpeechRecognition(); |
| recognition.lang = "id-ID"; |
| recognition.continuous = true; |
| recognition.interimResults = true; |
|
|
| let isListening = false; |
| let timeout; |
| let isMessageSent = false; |
|
|
| const showMicAlert = (status) => { |
| micStatus.textContent = status; |
| micAlert.style.display = "block"; |
| }; |
|
|
| const hideMicAlert = () => { |
| micAlert.style.display = "none"; |
| }; |
|
|
| micButton.addEventListener("click", () => { |
| if (isListening) { |
| recognition.stop(); |
| micButton.style.color = "#c3cfe2"; |
| hideMicAlert(); |
| isListening = false; |
| isMessageSent = true; |
| clearTimeout(timeout); |
| textInput.value = ""; |
| } else { |
| recognition.start(); |
| micButton.style.color = "#ffff"; |
| showMicAlert("Mikrofon sedang digunakan..."); |
| isListening = true; |
| isMessageSent = false; |
| } |
| }); |
|
|
| recognition.onresult = function(event) { |
| let transcript = ''; |
| for (let i = event.resultIndex; i < event.results.length; i++) { |
| transcript += event.results[i][0].transcript; |
| } |
|
|
| if (isListening) { |
| textInput.value = transcript; |
| } |
| isVoiceInput = true; |
| clearTimeout(timeout); |
|
|
| |
| timeout = setTimeout(() => { |
| if (!isMessageSent) { |
| recognition.stop(); |
| micButton.style.color = "#c3cfe2"; |
| hideMicAlert(); |
| isListening = false; |
| isMessageSent = true; |
| sendMessage(); |
| } |
| }, 3500); |
| }; |
| }); |
|
|
|
|
| |
| |
| |
|
|
| |
| function appendWelcomeMessage() { |
| const welcomeHTML = ` |
| <div class="msg left-msg"> |
| <div class="msg-img" style="background-image: url(${BOT_IMG})"></div> |
| <div class="msg-bubble"> |
| <div class="msg-info"> |
| <div class="msg-info-name">${BOT_NAME}</div> |
| <div class="msg-info-time">${formatDate(new Date())}</div> |
| </div> |
| <div class="msg-text"> |
| Halo, saya SkinThinc..! Asisten skincare yang akan membantu untuk menentukan komposisi skincare terbaik Anda. |
| </div> |
| <button id="volumeButton" class="volume-btn"> |
| <i class="fa-solid fa-volume-high volume-icon"></i> |
| </button> |
| <button id="copyButton" class="copy-btn"> |
| <i class="fas fa-copy copy-icon"></i> |
| </button> |
| </div> |
| </div>`; |
| msgerChat.insertAdjacentHTML("beforeend", welcomeHTML); |
| msgerChat.scrollTop += 500; |
|
|
| |
| const volumeButton = document.getElementById("volumeButton"); |
| volumeButton.addEventListener("click", () => { |
| if (isSpeaking) { |
| speechSynthesis.cancel(); |
| isSpeaking = false; |
| volumeButton.innerHTML = '<i class="fa-solid fa-volume-high volume-icon"></i>'; |
| } else { |
| const messageText = "Halo, saya SkinThinc..! Asisten skincare yang akan membantu untuk menentukan komposisi skincare terbaik Anda."; |
| speak(messageText); |
| volumeButton.innerHTML = '<i class="fa-solid fa-circle-stop stop-icon"></i>'; |
| } |
| }); |
| } |
|
|
| |
| function appendMessage(name, img, side, text, plainText) { |
| const msgHTML = ` |
| <div class="msg ${side}-msg"> |
| <div class="msg-img" style="background-image: url(${img})"></div> |
| <div class="msg-bubble"> |
| <div class="msg-info"> |
| <div class="msg-info-name">${name}</div> |
| <div class="msg-info-time">${formatDate(new Date())}</div> |
| </div> |
| <div class="msg-text">${text}</div> |
| ${side === 'left' ? ` |
| <button class="volume-btn"> |
| <i class="fa-solid ${isVoiceInput ? 'fa-circle-stop stop-icon' : 'fa-volume-high volume-icon'}"></i> |
| </button> |
| <button class="copy-btn"> |
| <i class="fas fa-copy copy-icon"></i> |
| </button>` : '' |
| } |
| </div> |
| </div>`; |
| msgerChat.insertAdjacentHTML("beforeend", msgHTML); |
| msgerChat.scrollTop += 500; |
|
|
| |
| if (side === 'left') { |
| const volumeButton = msgerChat.lastElementChild.querySelector(".volume-btn"); |
| volumeButton.addEventListener("click", () => { |
| if (isSpeaking) { |
| speechSynthesis.cancel(); |
| isSpeaking = false; |
| volumeButton.innerHTML = '<i class="fa-solid fa-volume-high volume-icon"></i>'; |
| } else { |
| speak(plainText); |
| isSpeaking = true; |
| volumeButton.innerHTML = '<i class="fa-solid fa-circle-stop stop-icon"></i>'; |
| } |
| }); |
| } |
| } |
|
|
| |
| function appendTypingIndicator() { |
| const typingHTML = ` |
| <div class="msg left-msg typing" id="typing-indicator"> |
| <div class="msg-img" style="background-image: url(${BOT_IMG})"></div> |
| <div class="msg-bubble"> |
| <div class="msg-info"> |
| <div class="msg-info-name">${BOT_NAME}</div> |
| <div class="msg-info-time">${formatDate(new Date())}</div> |
| </div> |
| <div class="typing-indicator"> |
| <span></span><span></span><span></span> |
| </div> |
| </div> |
| </div>`; |
| msgerChat.insertAdjacentHTML("beforeend", typingHTML); |
| msgerChat.scrollTop += 500; |
| } |
|
|
| |
| function removeTypingIndicator() { |
| const typingIndicator = document.getElementById("typing-indicator"); |
| if (typingIndicator) { |
| typingIndicator.remove(); |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| document.addEventListener("click", function(event) { |
| const copyButton = event.target.closest(".copy-btn"); |
| if (copyButton) { |
| const msgTextElement = copyButton.closest(".msg-bubble").querySelector(".msg-text"); |
| const iconElement = copyButton.querySelector("i"); |
|
|
| if (msgTextElement && iconElement) { |
| copyMessage(msgTextElement.innerText, iconElement); |
| } |
| } |
| }); |
|
|
| |
| function copyMessage(text, iconElement) { |
| const tempTextarea = document.createElement("textarea"); |
| tempTextarea.value = text; |
| document.body.appendChild(tempTextarea); |
| tempTextarea.select(); |
| document.execCommand("copy"); |
| document.body.removeChild(tempTextarea); |
|
|
| iconElement.className = 'fas fa-check check-icon'; |
|
|
| setTimeout(() => { |
| iconElement.className = 'fas fa-copy copy-icon'; |
| }, 2000); |
| } |
|
|
| |
| function formatResponse(response) { |
| const lines = response.split('\n').filter(line => line.trim() !== ''); |
| let formattedResponse = '<div data-testid="stMarkdownContainer" class="st-emotion-cache-1sno8jx e1nzilvr4">'; |
| let plainText = ''; |
| let isOrderedList = false; |
| let isUnorderedList = false; |
|
|
| function applyBoldFormatting(text) { |
| return text.replace(/\*(.*?)\*/g, '<strong>$1</strong>').replace(/\*/g, ''); |
| } |
|
|
| lines.forEach(line => { |
| const formattedLine = applyBoldFormatting(line); |
|
|
| if (/^\d+\./.test(line.trim())) { |
| if (!isOrderedList) { |
| if (isUnorderedList) { |
| formattedResponse += '</ul>'; |
| isUnorderedList = false; |
| } |
| formattedResponse += '<ol>'; |
| isOrderedList = true; |
| } |
| const listItem = formattedLine.replace(/^\d+\.\s*/, ''); |
| formattedResponse += `<li>${listItem}</li>`; |
| plainText += `${line.trim()}\n`; |
| } else if (/^\*/.test(line.trim())) { |
| if (!isUnorderedList) { |
| if (isOrderedList) { |
| formattedResponse += '</ol>'; |
| isOrderedList = false; |
| } |
| formattedResponse += '<ul>'; |
| isUnorderedList = true; |
| } |
| const listItem = formattedLine.replace(/^\*\s*/, ''); |
| formattedResponse += `<li>${listItem}</li>`; |
| plainText += `${line.trim()}\n`; |
| } else { |
| if (isOrderedList) { |
| formattedResponse += '</ol>'; |
| isOrderedList = false; |
| } |
| if (isUnorderedList) { |
| formattedResponse += '</ul>'; |
| isUnorderedList = false; |
| } |
| formattedResponse += `<p>${formattedLine}</p>`; |
| plainText += `${line}\n`; |
| } |
| }); |
|
|
| if (isOrderedList) formattedResponse += '</ol>'; |
| if (isUnorderedList) formattedResponse += '</ul>'; |
|
|
| formattedResponse += '</div>'; |
| return { |
| formattedResponse, |
| plainText |
| }; |
| } |
|
|
| |
| function speak(text) { |
| text = text.replace(/\*/g, ''); |
|
|
| const parts = text.match(/[^.:!?]+[.:!?]*/g)?.map(part => part.trim()) || []; |
| let current = 0; |
|
|
| function speakNext() { |
| if (current >= parts.length) { |
| document.querySelectorAll(".volume-btn").forEach(button => { |
| button.innerHTML = '<i class="fa-solid fa-volume-high volume-icon"></i>'; |
| }); |
| isSpeaking = false; |
| return; |
| } |
|
|
| const utterance = new SpeechSynthesisUtterance(parts[current++]); |
| utterance.lang = 'id-ID'; |
| utterance.onend = speakNext; |
| speechSynthesis.speak(utterance); |
| } |
|
|
| isSpeaking = true; |
| speakNext(); |
| } |
|
|
| |
| function formatDate(date) { |
| const h = "0" + date.getHours(); |
| const m = "0" + date.getMinutes(); |
| return `${h.slice(-2)}:${m.slice(-2)}`; |
| } |
|
|
|
|
| |
| |
| |
|
|
| document.addEventListener("DOMContentLoaded", function() { |
| appendWelcomeMessage(); |
| loadChatHistory(); |
| }); |