Last active
June 30, 2024 03:43
-
-
Save paulwababu/b7b4972e2026509b0cb4808a821948cb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Ask Friendly</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link | |
rel="icon" | |
href="https://res.cloudinary.com/prometheusapi/image/upload/v1710534811/image_3_uqxyey.png" | |
type="image/x-icon" | |
/> | |
<link | |
rel="stylesheet" | |
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" | |
/> | |
<link | |
rel="stylesheet" | |
href="https://unpkg.com/[email protected]/dist/leaflet.css" | |
/> | |
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> | |
<style> | |
#map { | |
height: 350px; | |
} | |
.leaflet-container { | |
border-radius: 8px; | |
} | |
.chat-container { | |
max-height: 300px; | |
overflow-y: auto; | |
} | |
.chat-container.expanded { | |
padding-top: 1.5rem; | |
} | |
.user-message, .ai-message { | |
padding: 1rem; | |
margin: 0.5rem 0; | |
border-radius: 10px; | |
border: none; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.user-message { | |
background-color: #e0e0e0; /* Light gray background for user message */ | |
color: #333; /* Dark text for non-dark mode */ | |
text-align: left; | |
} | |
.ai-message { | |
background-color: #f5f5f5; /* Slightly darker gray background for contrast */ | |
color: #333; | |
text-align: left; /* Change this to left */ | |
display: flex; /* Ensure flex display is applied */ | |
flex-direction: column; /* Align content vertically */ | |
align-items: flex-start; /* Align items to the start */ | |
} | |
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap"); | |
body { | |
font-family: "Open Sans", sans-serif; | |
} | |
html, | |
body { | |
height: 100%; | |
margin: 0; | |
} | |
.container { | |
height: 100vh; | |
} | |
.sidebar { | |
background-color: #f3f4f6; | |
padding: 1rem; | |
border-right: 1px solid #e5e7eb; | |
overflow-y: auto; | |
transition: transform 0.3s ease-in-out; | |
} | |
.sidebar.collapsed { | |
transform: translateX(-100%); | |
} | |
.sidebar-header { | |
font-size: 1.125rem; | |
font-weight: 600; | |
margin-bottom: 1rem; | |
} | |
.sidebar-item { | |
display: flex; | |
align-items: center; | |
padding: 0.5rem; | |
border-radius: 0.375rem; | |
margin-bottom: 0.5rem; | |
cursor: pointer; | |
} | |
.sidebar-item:hover { | |
background-color: #e5e7eb; | |
} | |
.sidebar-item-icon { | |
width: 2rem; | |
height: 2rem; | |
border-radius: 50%; | |
margin-right: 0.75rem; | |
} | |
.sidebar-item-text { | |
font-size: 0.875rem; | |
} | |
.sidebar-divider { | |
border-top: 1px solid #e5e7eb; | |
margin: 1rem 0; | |
} | |
.user-icon { | |
width: 2rem; | |
height: 2rem; | |
border-radius: 50%; | |
background-color: #8b5cf6; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
font-weight: 600; | |
margin-right: 0.75rem; | |
} | |
.logged-in-user { | |
display: flex; | |
align-items: center; | |
padding: 0.5rem; | |
border-radius: 0.375rem; | |
cursor: pointer; | |
} | |
.logged-in-user:hover { | |
background-color: #e5e7eb; | |
} | |
.toggle-sidebar-btn { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
background-color: #fff; | |
border: 1px solid #e5e7eb; | |
border-radius: 0.375rem; | |
padding: 0.5rem; | |
cursor: pointer; | |
z-index: 1000; | |
} | |
.sidebar-container { | |
width: 20vw; | |
} | |
.main-container { | |
width: 60vw; | |
} | |
@media (min-width: 1000px) { | |
#toggle-sidebar-btn-2 { | |
display: none; | |
} | |
} | |
@media (max-width: 1000px) { | |
.sidebar-container { | |
position: fixed; | |
height: 100vh; | |
z-index: 10000; | |
width: 20rem; | |
} | |
.main-container { | |
margin: 0 auto; | |
width: 70vw; | |
} | |
} | |
@media (max-width: 600px) { | |
.main-container { | |
width: 80vw; | |
} | |
} | |
@media (max-width: 450px) { | |
.main-container { | |
width: 90vw; | |
} | |
} | |
@media (max-height: 675px) { | |
#map { | |
height: 245px; | |
} | |
} | |
@media (max-height: 585px) { | |
#map { | |
height: 180px; | |
} | |
} | |
@media (max-height: 520px) { | |
#map { | |
height: 120px; | |
} | |
} | |
@media (max-height: 460px) { | |
#map { | |
height: 90px; | |
} | |
} | |
.submit-button { | |
transition: transform 0.3s ease, background-color 0.3s ease; | |
} | |
.submit-button:hover { | |
transform: scale(1.05); | |
background-color: #3b82f6; | |
} | |
.submit-button:active { | |
animation: jump 0.3s ease; | |
} | |
@keyframes jump { | |
0% { | |
transform: scale(1); | |
} | |
50% { | |
transform: scale(0.95) translateY(-5px); | |
} | |
100% { | |
transform: scale(1); | |
} | |
} | |
</style> | |
<script> | |
// Function to fetch conversations from the server | |
async function fetchConversations() { | |
try { | |
const sessionID = sessionStorage.getItem("sessionID"); | |
const response = await fetch(`/chat-history/${sessionID}`); | |
const conversations = await response.json(); | |
updateConversationList(conversations); | |
} catch (error) { | |
console.error("Error fetching conversations:", error); | |
} | |
} | |
// Function to update the conversation list UI with the fetched data | |
function updateConversationList(conversations) { | |
const conversationList = document.getElementById("conversation-list"); | |
conversationList.innerHTML = ""; | |
conversations.forEach((conversation) => { | |
const conversationElement = document.createElement("div"); | |
conversationElement.className = "sidebar-item conversation"; | |
conversationElement.textContent = conversation.latest_message; | |
conversationElement.setAttribute( | |
"data-conversation-id", | |
conversation.conversation_id | |
); | |
conversationElement.addEventListener("click", () => { | |
fetchConversationMessages(conversation.conversation_id); | |
}); | |
conversationList.appendChild(conversationElement); | |
}); | |
} | |
// Function to fetch conversation messages from the server | |
async function fetchConversationMessages(conversationId) { | |
try { | |
const sessionID = sessionStorage.getItem("sessionID"); | |
const response = await fetch( | |
`/chat-conversation/${sessionID}/${conversationId}` | |
); | |
const messages = await response.json(); | |
displayConversationMessages(messages); | |
} catch (error) { | |
console.error("Error fetching conversation messages:", error); | |
} | |
} | |
// Function to display conversation messages in the chat container | |
function displayConversationMessages(messages) { | |
const chatContainer = document.getElementById("chat-container"); | |
chatContainer.innerHTML = ""; | |
messages.forEach((message) => { | |
const messageElement = document.createElement("div"); | |
messageElement.classList.add("mb-4", "p-4", "rounded-lg", "chat-bubble"); | |
if (message.role === "user") { | |
messageElement.classList.add("user-message",); | |
} else { | |
messageElement.classList.add("ai-message", "flex", "flex-col", "items-start"); | |
} | |
messageElement.textContent = message.content; | |
chatContainer.appendChild(messageElement); | |
}); | |
} | |
// Load conversations when the page is loaded | |
window.addEventListener("load", fetchConversations); | |
</script> | |
<style> | |
.chat-bubble { | |
border: none; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.bg-custom-brue { | |
text-align: left; | |
color: #fff; /* Slightly lighter background for contrast */ | |
background-color: #616161; | |
} | |
.bg-custom-blue { | |
text-align: left; | |
background-color: #424242; | |
color: #fff; | |
} | |
</style> | |
</head> | |
<body class="bg-white-100"> | |
<div class="flex" style="height: 100vh;"> | |
<div class="sidebar-container sidebar collapsed flex flex-col"> | |
<div class="flex-grow overflow-y-auto"> | |
<div class="sidebar-header"> | |
<div class="text-lg font-semibold flex items-center"> | |
<img | |
src="https://res.cloudinary.com/prometheusapi/image/upload/v1710534811/image_3_uqxyey.png" | |
alt="ChatBot Icon" | |
class="rounded-full h-12 w-12" | |
/> | |
<span class="ml-2">Ask Friendly</span> | |
</div> | |
</div> | |
<div class="sidebar-divider"></div> | |
<div class="sidebar-header">Conversations</div> | |
<div id="conversation-list"></div> | |
</div> | |
<div class="flex items-center justify-center mt-4"> | |
<button | |
id="dark-mode-toggle" | |
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center" | |
> | |
<i class="fas fa-moon mr-2"></i> Dark Mode | |
</button> | |
</div> | |
<div class="sidebar-item"> | |
<a href="http://127.0.0.1:8000" target="_blank" class="flex items-center"> | |
<i class="fas fa-cog mr-2"></i> | |
<span class="sidebar-item-text">Admin Panel</span> | |
</a> | |
</div> | |
<div class="logged-in-user"> | |
<div class="user-icon" id="user-initials"></div> | |
<span class="sidebar-item-text" id="user-name"></span> | |
</div> | |
</div> | |
<div class="main-container flex flex-col"> | |
<button | |
id="show-map-btn" | |
class="text-gray-400 hover:text-gray-600" | |
style="display: none" | |
> | |
<i class="fas fa-map-marker-alt fa-lg"></i> | |
</button> | |
<script> | |
$("#dark-mode-toggle").on("click", function () { | |
$("body").toggleClass("dark-mode"); | |
// Save the user's preference in local storage | |
if ($("body").hasClass("dark-mode")) { | |
localStorage.setItem("darkMode", "true"); | |
} else { | |
localStorage.setItem("darkMode", "false"); | |
} | |
}); | |
// Load the user's preference from local storage on page load | |
const darkModeEnabled = localStorage.getItem("darkMode") === "true"; | |
if (darkModeEnabled) { | |
$("body").addClass("dark-mode"); | |
} | |
</script> | |
<style> | |
/* Dark mode styles for the form area */ | |
body.dark-mode .bg-white { | |
background-color: #212121; | |
} | |
body.dark-mode input, | |
body.dark-mode textarea, | |
body.dark-mode button { | |
border-color: #4a4a4a; | |
} | |
/* Dark mode styles for input form background color */ | |
body.dark-mode input, | |
body.dark-mode textarea { | |
background-color: #3e3e3e; | |
color: #fff; | |
} | |
body.dark-mode input::placeholder, | |
body.dark-mode textarea::placeholder { | |
color: #888; | |
} | |
body.dark-mode button { | |
background-color: #4a4a4a; | |
} | |
body.dark-mode button:hover { | |
background-color: #5c5c5c; | |
} | |
/* Dark mode styles */ | |
body.dark-mode { | |
background-color: #212121; | |
} | |
body.dark-mode .sidebar { | |
background-color: #171717; | |
} | |
body.dark-mode .sidebar-item { | |
background-color: #1e1e1e; | |
color: #fff; | |
} | |
body.dark-mode .sidebar-item:hover { | |
background-color: #2a2a2a; | |
} | |
body.dark-mode .sidebar-divider { | |
border-top: 1px solid #2a2a2a; | |
} | |
body.dark-mode .user-icon { | |
background-color: #8b5cf6; | |
} | |
body.dark-mode .logged-in-user { | |
background-color: #1e1e1e; | |
color: #fff; | |
} | |
body.dark-mode .sidebar-header { | |
color: #fff; | |
} | |
body.dark-mode .conversation { | |
background-color: #1e1e1e; | |
color: #fff; | |
} | |
body.dark-mode .conversation:hover { | |
background-color: #2a2a2a; | |
} | |
body.dark-mode .chat-container { | |
background-color: #3E3E3E; | |
} | |
body.dark-mode .user-message { | |
background-color: #424242; /* Darker background for user message */ | |
color: #fff; /* Light text for dark mode */ | |
} | |
body.dark-mode .ai-message { | |
background-color: #616161; /* Slightly lighter background for contrast */ | |
color: #fff; | |
} | |
/* You may need to adjust other colors for text, icons, etc. to fit the dark mode theme */ | |
#show-map-btn { | |
background-color: transparent; | |
border: none; | |
cursor: pointer; | |
padding: 0; | |
margin: 0 10px; | |
} | |
</style> | |
<script> | |
$("#show-map-btn").on("click", function () { | |
$("#map-container").show(); | |
}); | |
</script> | |
<div id="map-container" class="relative mb-4"> | |
<div id="toggle-sidebar-btn-container"> | |
<button class="toggle-sidebar-btn" id="toggle-sidebar-btn"> | |
<i class="fas fa-bars"></i> | |
</button> | |
</div> | |
<div | |
class="flex justify-between items-center border-b-2 border-gray-200 pb-4" | |
> | |
<!-- Removed the toggle-sidebar-btn from here --> | |
<div class="flex-grow"></div> | |
<div class="flex justify-end items-center"> | |
<button | |
id="cancel-map-btn" | |
class="text-gray-400 hover:text-gray-600" | |
> | |
<i class="fas fa-times fa-lg"></i> | |
</button> | |
</div> | |
</div> | |
<div id="map" class="h-96 rounded-lg"></div> | |
</div> | |
<div class="flex justify-between items-center mt-4"> | |
<div class="flex items-center"> | |
<i class="fas fa-coins mr-2 coin-icon"></i> | |
<span id="token-count">Loading tokens...</span> | |
</div> | |
</div> | |
<style> | |
body.dark-mode .coin-icon { | |
color: #fff; | |
} | |
body.dark-mode .icon-circle { | |
background-color: #333; | |
color: #ccc; | |
} | |
body.dark-mode #token-count { | |
color: #fff; | |
} | |
body.dark-mode .icon-circle:hover { | |
background-color: #444; | |
color: #fff; | |
} | |
#ask-friendly-logo { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
z-index: -1; | |
width: 200px; /* Adjust the width as needed */ | |
height: auto; /* Maintain the aspect ratio */ | |
pointer-events: none; /* Ignore mouse events to allow interaction with content above the logo */ | |
} | |
#toggle-sidebar-btn-container { | |
position: absolute; | |
top: -10px; /* Keep this the same if the vertical position is good */ | |
left: 45px; /* Increase if you need it to move right, or decrease to move left */ | |
z-index: 1001; /* Keep it above Leaflet's controls */ | |
} | |
.hidden { | |
display: none; | |
} | |
</style> | |
<div id="chat-container" class="flex-grow overflow-y-auto"> | |
<div class="user-message"> | |
Hi, what's happening with the wildlife in Maui? | |
</div> | |
<div class="ai-message"> | |
There's a significant event involving wildlife. We're gathering more | |
information and will update shortly. | |
</div> | |
</div> | |
<div class="text-lg text-gray-500 text-right"> | |
Call 911 for life-threatening emergencies. Assistant is not perfect | |
and can make mistakes or misunderstand information. | |
</div> | |
<div class="bg-white shadow rounded-lg p-4"> | |
<div class="flex items-center justify-between"> | |
<input id="message" type="text" placeholder="What can I do to assist you? 😊" class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none"> | |
<button | |
id="submit-button" | |
class="ml-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
> | |
<i id="submit-icon" class="fas fa-paper-plane"></i> | |
<i id="pause-icon" class="fas fa-pause hidden"></i> | |
</button> | |
</div> | |
<div class="flex items-center justify-between mt-4"> | |
<div class="flex space-x-2"> | |
<button class="icon-circle text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-plus"></i> | |
</button> | |
<button class="icon-circle text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-font"></i> | |
</button> | |
<button class="icon-circle text-gray-500 hover:text-gray-700"> | |
<i class="far fa-smile"></i> | |
</button> | |
<button class="icon-circle text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-at"></i> | |
</button> | |
<button class="icon-circle text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-video"></i> | |
</button> | |
</div> | |
<div class="flex-grow text-center"> | |
<div class="mic-container"> | |
<div class="circle"> | |
<i class="fas fa-microphone"></i> | |
</div> | |
<span id="listening-indicator" style="display: none; margin-left: 10px; color: gray;">Listening...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
window.Speech = (function() { | |
var self; | |
return self = { | |
initialized: false, | |
init: function() { | |
self.initialized = true; | |
self.initSpeechRecognition(); | |
self.responseField = document.getElementById('response'); | |
self.listening = false; | |
self.spoke = false; | |
self.runningAvg = null; | |
self.avgCount = 0; | |
self.spokeCount = 0; | |
self.finished = false; | |
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; | |
navigator.getUserMedia({ | |
audio: true | |
}, self.initStream, self.onError); | |
self.listen(); | |
}, | |
onError: function(err) { | |
return console.log(err); | |
}, | |
initStream: function(stream) { | |
console.info('initStream'); | |
var source; | |
window.AudioContext = window.AudioContext || window.webkitAudioContext; | |
self.audioContext = new AudioContext(); | |
self.analyser = self.audioContext.createAnalyser(); | |
source = self.audioContext.createMediaStreamSource(stream); | |
return source.connect(self.analyser); | |
}, | |
checkVolume: function(level) { | |
console.info('checkVolume'); | |
if (Synth.speaking || self.finished) { | |
return; | |
} | |
if (self.runningAvg == null) { | |
return self.runningAvg = level; | |
} | |
self.avgCount++; | |
self.runningAvg = ((self.runningAvg * self.avgCount) + level) / (self.avgCount + 1); | |
// Update the circle scale based on the volume level | |
const scaleFactor = 1 + (level / 100); | |
document.querySelector('.circle').style.transform = `scale(${scaleFactor})`; | |
if (level > self.runningAvg + 12) { | |
self.startSpokeTimeoutHandler(); | |
} | |
}, | |
startSpokeTimeoutHandler: function() { | |
console.info('startSpokeTimeoutHandler'); | |
clearTimeout(self.spokeTimeout); | |
return self.spokeTimeout = setTimeout(self.spokeTimeoutHandler, 2000); | |
}, | |
spokeTimeoutHandler: function() { | |
console.info('spokeTimeoutHandler'); | |
if (self.finished) { | |
return; | |
} | |
self.spokeCount++; | |
}, | |
clearSpokeCount: function() { | |
console.info('clearSpokeCount'); | |
return self.spokeCount = 0; | |
}, | |
processData: function() { | |
console.info('processData'); | |
var avg, freqByteData, i, total; | |
if (!self.initialized) { | |
return; | |
} | |
if (self.finished) { | |
return; | |
} | |
freqByteData = new Uint8Array(self.analyser.frequencyBinCount); | |
self.analyser.getByteFrequencyData(freqByteData); | |
i = parseInt(freqByteData.length / 3); | |
total = 0; | |
while (i--) { | |
total += freqByteData[i]; | |
} | |
avg = total / freqByteData.length; | |
self.checkVolume(avg); | |
}, | |
initSpeechRecognition: function() { | |
console.info('initSpeechRecognition'); | |
if (typeof webkitSpeechRecognition !== "function") { | |
console.log("Speech recognition not supported"); | |
return; | |
} | |
self.recognition = new webkitSpeechRecognition(); | |
self.recognition.interimResults = true; | |
self.recognition.onend = function(e) { | |
console.log('recognition.onend'); | |
if (self.listening) { | |
return self.listen(); | |
} | |
}; | |
return self.recognition.onresult = function(e) { | |
console.log('on result'); | |
var confidence, i, isFinal, primary, result, transcript; | |
clearTimeout(self.spokeTimeout); | |
self.spoke = false; | |
confidence = 0; | |
transcript = ''; | |
isFinal = false; | |
for (i in e.results) { | |
result = e.results[i]; | |
if (!result.length) { | |
break; | |
} | |
primary = result[0]; | |
confidence = Math.max(primary.confidence, confidence); | |
if (result.isFinal) { | |
isFinal = result.isFinal; | |
} | |
if (i === '0' || primary.transcript.substr(0, 1) === ' ') { | |
transcript += primary.transcript; | |
} | |
} | |
if (isFinal) { | |
return self.test(transcript); | |
} | |
}; | |
}, | |
listen: function() { | |
console.info('listen'); | |
self.listening = true; | |
try { | |
return self.recognition.start(); | |
} catch (_error) { | |
console.info('error listen'); | |
} | |
}, | |
stopListening: function() { | |
console.info('stopListening'); | |
self.listening = false; | |
if (self.recognition) { | |
return self.recognition.abort(); | |
} | |
}, | |
finish: function() { | |
console.info('finish'); | |
self.stopListening(); | |
return self.finished = true; | |
}, | |
test: function(searchString) { | |
console.info('test: ' + searchString); | |
document.getElementById('message').value += ' ' + searchString; | |
} | |
}; | |
})(); | |
const micContainer = document.querySelector('.mic-container'); | |
const circle = document.querySelector('.circle'); | |
const listeningIndicator = document.getElementById('listening-indicator'); | |
micContainer.addEventListener('click', async () => { | |
try { | |
// Check if we are already listening | |
if (!circle.classList.contains('active')) { | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
// If permission is granted and stream is received | |
if (stream) { | |
circle.classList.add('active'); // Activate pulsating effect | |
listeningIndicator.style.display = 'inline'; // Show listening indicator | |
Speech.init(); // Initialize speech recognition or any other functionality | |
} | |
} else { | |
circle.classList.remove('active'); // Deactivate pulsating effect | |
listeningIndicator.style.display = 'none'; // Hide listening indicator | |
Speech.finish(); // Stop speech recognition or any other functionality | |
} | |
} catch (error) { | |
console.error('Microphone access denied or error: ', error); | |
circle.classList.remove('active'); // Ensure circle is not active if access is denied | |
listeningIndicator.style.display = 'none'; // Ensure indicator is not showing if access is denied | |
Speech.finish(); // Clean up or reset speech recognition setup if needed | |
} | |
}); | |
! function() { | |
for (var a = 0, b = ["ms", "moz", "webkit", "o"], d = 0; d < b.length && !window.requestAnimationFrame; ++d) window.requestAnimationFrame = window[b[d] + "RequestAnimationFrame"], window.cancelAnimationFrame = window[b[d] + "CancelAnimationFrame"] || window[b[d] + "CancelRequestAnimationFrame"]; | |
window.requestAnimationFrame || (window.requestAnimationFrame = function(b, d) { | |
var e = (new Date).getTime(), | |
g = Math.max(0, 16 - (e - a)), | |
h = window.setTimeout(function() { | |
b(e + g) | |
}, g); | |
a = e + g; | |
return h | |
}); | |
window.cancelAnimationFrame || (window.cancelAnimationFrame = | |
function(a) { | |
clearTimeout(a) | |
}) | |
}(); | |
</script> | |
<style> | |
#listening-indicator { | |
font-size: 14px; | |
color: #666; | |
transition: opacity 0.3s ease; | |
} | |
.icon-circle { | |
display: inline-flex; /* Ensures the icon is centered inside the circle */ | |
align-items: center; /* Centers the icon vertically */ | |
justify-content: center; /* Centers the icon horizontally */ | |
width: 36px; /* Size of the circle - adjust as necessary */ | |
height: 36px; /* Size of the circle - adjust as necessary */ | |
background-color: #333; /* Background color of the circle */ | |
color: #ccc; /* Icon color */ | |
border-radius: 50%; /* Makes the background a circle */ | |
cursor: pointer; | |
transition: background-color 0.3s, color 0.3s; /* Smooth transition for hover effects */ | |
} | |
.icon-circle:hover { | |
background-color: #444; /* Slightly lighter background on hover */ | |
color: #fff; /* White color on hover for better visibility */ | |
} | |
.circle { | |
width: 50px; | |
height: 50px; | |
border-radius: 50%; | |
background: #ffffff; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
z-index: 1; | |
transition: transform 0.2s ease-in-out; | |
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12), 0 3px 5px -1px rgba(0, 0, 0, .2); | |
cursor: pointer; | |
} | |
.circle i { | |
color: #b2b1b1; | |
font-size: 23px; | |
transition: .9s; | |
} | |
.circle:before { | |
content: ''; | |
width: 80px; | |
height: 80px; | |
border-radius: 50%; | |
opacity: .2; | |
z-index: -1; | |
position: absolute; | |
} | |
.circle.active { | |
background: #1d4ed8; | |
} | |
.circle.active:before { | |
background: gray; | |
animation: bounce .8s ease-in-out infinite .5s; | |
} | |
.circle.active i { | |
color: #ffffff; | |
} | |
.mic-container { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
flex-grow: 0; /* Ensures it does not grow */ | |
margin-left: auto; /* Pushes the microphone container slightly to the left */ | |
margin-right: 80px; /* Adjust this value to move more or less */ | |
} | |
@keyframes bounce { | |
0% { | |
transform: scale(1); | |
} | |
25% { | |
transform: scale(1.4); | |
} | |
75% { | |
transform: scale(1); | |
} | |
100% { | |
transform: scale(1.3); | |
} | |
} | |
</style> | |
<!-- Confirmation Modal --> | |
<div | |
id="confirmation-modal" | |
class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden justify-center items-center" | |
style="z-index: 1000" | |
> | |
<div class="bg-white rounded-lg p-4 max-w-sm mx-auto"> | |
<div class="flex flex-col items-center justify-center"> | |
<img | |
src="https://res.cloudinary.com/prometheusapi/image/upload/v1710534811/image_3_uqxyey.png" | |
alt="ChatBot Icon" | |
class="rounded-full h-12 w-12 mb-4" | |
/> | |
<p class="text-gray-800 font-semibold"> | |
Are you sure you want to clear the chat? | |
</p> | |
<div class="mt-4 flex justify-center space-x-4"> | |
<button | |
id="confirm-clear" | |
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded inline-flex items-center" | |
> | |
<i class="fas fa-trash mr-2"></i> Confirm | |
</button> | |
<button | |
id="cancel-clear" | |
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center" | |
> | |
<i class="fas fa-times mr-2"></i> Cancel | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<style> | |
.custom-popup { | |
width: 300px; | |
font-family: Arial, sans-serif; | |
} | |
.custom-popup .header { | |
padding: 5px; | |
color: white; | |
text-align: center; | |
font-weight: bold; | |
background-color: red; /* Updated */ | |
} | |
.custom-popup .body { | |
padding: 10px; | |
display: flex; | |
flex-direction: column; /* Updated */ | |
align-items: center; /* Updated */ | |
} | |
.custom-popup .body .profile-img { | |
width: 50px; | |
height: 50px; | |
border-radius: 50%; | |
margin-bottom: 10px; /* Updated */ | |
} | |
.custom-popup .body .user-info { /* Added */ | |
text-align: center; | |
} | |
.custom-popup .body p { | |
margin: 0; | |
font-size: 12px; | |
} | |
.custom-popup .body p strong { | |
font-size: 14px; | |
} | |
.custom-popup .footer { | |
padding: 5px; | |
background: #f1f1f1; | |
font-size: 12px; | |
text-align: center; | |
} | |
.custom-popup .footer p { | |
margin: 0; | |
} | |
.custom-popup .interactive-icons { /* Added */ | |
display: flex; | |
justify-content: center; | |
margin-top: 5px; | |
} | |
.custom-popup .interactive-icons i { /* Added */ | |
margin: 0 5px; | |
cursor: pointer; | |
} | |
</style> | |
<script> | |
$(document).ready(function () { | |
// Function to handle successful location retrieval | |
function handleLocationSuccess(position) { | |
const latitude = position.coords.latitude; | |
const longitude = position.coords.longitude; | |
// Store location information for later use | |
window.userLocation = `Latitude: ${latitude}, Longitude: ${longitude}`; | |
getLocationName(latitude, longitude).then((locationName) => { | |
userLocationName = locationName; | |
console.log(userLocationName); | |
}); | |
} | |
function getLocationName(latitude, longitude) { | |
const apiKey = "93e4e817d64d43b1966d7b22059698b5"; | |
const url = `https://api.opencagedata.com/geocode/v1/json?q=${latitude}+${longitude}&key=${apiKey}`; | |
return fetch(url) | |
.then((response) => response.json()) | |
.then((data) => { | |
if (data.status.code === 200 && data.results.length > 0) { | |
return data.results[0].formatted; | |
} else { | |
return "Unknown location"; | |
} | |
}) | |
.catch((error) => { | |
console.error("Error:", error); | |
return "Unknown location"; | |
}); | |
} | |
// Function to handle location retrieval errors | |
function handleLocationError(error) { | |
console.warn(`ERROR(${error.code}): ${error.message}`); | |
window.userLocation = "Location not available"; | |
} | |
// Request user's location | |
if (navigator.geolocation) { | |
navigator.geolocation.getCurrentPosition( | |
handleLocationSuccess, | |
handleLocationError | |
); | |
} else { | |
console.log("Geolocation is not supported by this browser."); | |
window.userLocation = "Location not supported"; | |
} | |
$("#message").on("focus", function () { | |
const text = $(this).val(); | |
if (text.length === 0) { | |
$("#suggestions").show(); // Show suggestions if text area is empty | |
} | |
}); | |
$("#message").on("input", function () { | |
if ($(this).val().length === 0) { | |
$("#suggestions").show(); // Show suggestions if text area is emptied | |
} else { | |
$("#suggestions").hide(); // Hide suggestions if text area is not empty | |
} | |
}); | |
$("#message").on("blur", function () { | |
// Hide suggestions when the text area loses focus | |
// Use a timeout to allow click event to process first | |
setTimeout(function () { | |
$("#suggestions").hide(); | |
}, 100); | |
}); | |
$(".suggestion").on("click", function () { | |
const suggestion = $(this).text(); | |
$("#message").val(suggestion).focus(); // Set the clicked suggestion text to the text area and focus | |
$("#suggestions").hide(); // Hide the suggestions | |
}); | |
// Toggle sidebar on button click | |
$("#toggle-sidebar-btn").on("click", function () { | |
$(".sidebar").toggleClass("collapsed"); | |
}); | |
// Close sidebar when clicking outside of it (mobile) | |
$(document).on("click", function (event) { | |
if (!$(event.target).closest(".sidebar, #toggle-sidebar-btn").length) { | |
$(".sidebar").addClass("collapsed"); | |
} | |
}); | |
// Cancel map on button click | |
$("#cancel-map-btn").on("click", function () { | |
$("#map-container").hide(); | |
$("#show-map-btn").show(); | |
}); | |
}); | |
</script> | |
<script> | |
let userLocationName; | |
window.onload = function () { | |
// Extract the entire query string from the URL | |
const fullUrl = window.location.href; | |
// Find the start of the 'creator' parameter and extract the substring from there | |
const creatorParamStart = fullUrl.indexOf("?creator=") + "?creator=".length; | |
if (creatorParamStart === "?creator=".length - 1) { | |
// 'creator' parameter is missing in the URL | |
alert("Please log in to FriendlyForce first and then access the Ask Friendly platform."); | |
// Redirect the user to the FriendlyForce login page | |
window.location.href = "https://friendlyforce.live/login"; | |
return; | |
} | |
let creatorJson = fullUrl.substring(creatorParamStart); | |
// Assume that 'creator' is the last parameter in the URL | |
// Replace any '&' beyond the first occurrence in the JSON string (caused by incorrect URL parsing) | |
creatorJson = creatorJson.replace(/&(?=\[^&\]\*$)/, "%26"); | |
// Decode the URI component to handle URL encoding | |
creatorJson = decodeURIComponent(creatorJson); | |
try { | |
// Parse the JSON string to an object | |
const creator = JSON.parse(creatorJson); | |
// Extract the 'firstName' and 'lastName' properties | |
const firstName = creator.firstName; | |
const lastName = creator.lastName; | |
const creatorID = creator.id; | |
console.log("Creator ID:", creatorID); | |
// Step 3: Update the user initials and name in the HTML | |
const userInitials = document.getElementById("user-initials"); | |
const userName = document.getElementById("user-name"); | |
userInitials.textContent = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); | |
userName.textContent = `${firstName} ${lastName}`; | |
// Now you can use firstName and lastName as needed | |
console.log("First Name:", firstName); | |
console.log("Last Name:", lastName); | |
console.log("Creator ID:", creatorID); | |
// Get the current user token | |
getCurrentUserToken(creator); | |
} catch (error) { | |
console.error("Error parsing JSON from URL:", error); | |
// Handle the case when the 'creator' parameter is present but has an invalid format | |
alert("Invalid 'creator' parameter. Please log in to FriendlyForce and access the Ask Friendly platform again."); | |
// Redirect the user to the FriendlyForce login page | |
window.location.href = "https://friendlyforce.live/login"; | |
} | |
}; | |
function getCurrentUserToken(creator) { | |
// Make an API call to check if the user exists | |
fetch(`https://admindash.friendlyforce.live/friendlyforce/chat/api/v1/check-user-exists/?creator=${encodeURIComponent(JSON.stringify(creator))}`) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.user_exists) { | |
// User exists, retrieve the token count | |
fetch(`https://admindash.friendlyforce.live/friendlyforce/chat/api/v1/tokens/?creator=${encodeURIComponent(JSON.stringify(creator))}`) | |
.then(response => response.json()) | |
.then(data => { | |
// Extract the remaining tokens from the API response | |
const remainingTokens = data.remaining_tokens; | |
// Update the token count in the HTML | |
const tokenCountElement = document.getElementById("token-count"); | |
tokenCountElement.textContent = `${remainingTokens} tokens available`; | |
}) | |
.catch(error => { | |
console.error("Error retrieving user token count:", error); | |
}); | |
} else { | |
// User doesn't exist, create the user | |
fetch('https://admindash.friendlyforce.live/friendlyforce/chat/api/v1/create-user/', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
user_id: creator.id, | |
tokens: 1000000, | |
avatar: creator.avatar, | |
first_name: creator.firstName, | |
last_name: creator.lastName, | |
}), | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
// User created successfully, update the token count in the HTML | |
const tokenCountElement = document.getElementById("token-count"); | |
tokenCountElement.textContent = `${data.tokens} tokens available`; | |
}) | |
.catch(error => { | |
console.error("Error creating user:", error); | |
}); | |
} | |
}) | |
.catch(error => { | |
console.error("Error checking user existence:", error); | |
}); | |
} | |
</script> | |
<script> | |
var map = L.map("map").setView([20.798363, -156.331925], 10); // Default location | |
$(document).ready(function () { | |
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { | |
maxZoom: 19, | |
attribution: "© OpenStreetMap contributors", | |
}).addTo(map); | |
// Fetch data and add markers | |
function loadMarkers() { | |
fetch("http://localhost:3003/events") // Adjust URL as needed | |
.then((response) => response.json()) | |
.then((data) => { | |
console.log(data); // Log the data to see what's actually being returned | |
// Assuming the data is an object with a "data" key which is an array | |
data.data.forEach((entry) => { | |
const lat = entry.lat || entry.location.latitude; | |
const lng = entry.lng || entry.location.longitude; | |
if (!lat || !lng) { | |
console.error("Missing latitude or longitude:", entry); | |
} else { | |
var marker = L.marker([lat, lng]).addTo(map); | |
const popupContent = ` | |
<div class="custom-popup"> | |
<div class="header" style="background-color: red; color: white;"> | |
<h3>Severe</h3> | |
</div> | |
<div class="body"> | |
<img src="${entry.eventHostProfileImageURL}" alt="${entry.eventHostName}" class="profile-img"> | |
<div class="user-info"> | |
<p><strong>${entry.eventHostName}</strong></p> | |
<p>${new Date(entry.createdDate).toLocaleString()}</p> | |
</div> | |
<p>${entry.name || "No description available"}</p> | |
</div> | |
<div class="footer"> | |
<p>${entry.location.formattedAddr}</p> | |
<p>${entry.lat}, ${entry.lng}</p> | |
<div class="interactive-icons"> | |
<i class="fas fa-comment"></i> | |
<i class="fas fa-thumbs-up"></i> | |
<i class="fas fa-share"></i> | |
</div> | |
</div> | |
</div> | |
`; | |
marker.bindPopup(popupContent); | |
} | |
}); | |
}) | |
.catch((error) => console.error("Error loading markers:", error)); | |
} | |
loadMarkers(); // Call function to load markers | |
// Function to parse the creator data from the URL | |
function parseCreatorData() { | |
const fullUrl = window.location.href; | |
const creatorParamStart = | |
fullUrl.indexOf("?creator=") + "?creator=".length; | |
let creatorJson = fullUrl.substring(creatorParamStart); | |
creatorJson = creatorJson.replace(/&(?=[^&]*$)/, "%26"); | |
creatorJson = decodeURIComponent(creatorJson); | |
try { | |
return JSON.parse(creatorJson); | |
} catch (error) { | |
console.error("Error parsing JSON from URL:", error); | |
return null; // Return null or a default object | |
} | |
} | |
const creatorData = parseCreatorData(); | |
// Function to create an SVG icon with the user's initials | |
function createUserIcon(firstName, lastName) { | |
const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`; | |
const svgIcon = encodeURI( | |
`data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50'><circle cx='25' cy='25' r='20' fill='#5679C0' /><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='16' font-family='Arial, sans-serif'>${initials}</text></svg>` | |
).replace("#", "%23"); | |
return L.icon({ | |
iconUrl: svgIcon, | |
iconSize: [50, 50], // Size of the icon | |
iconAnchor: [25, 50], // Point of the icon which will correspond to marker's location | |
popupAnchor: [0, -50], // Point from which the popup should open relative to the iconAnchor | |
}); | |
} | |
if (creatorData) { | |
var userIcon = createUserIcon( | |
creatorData.firstName, | |
creatorData.lastName | |
); | |
function handleLocationSuccess(position) { | |
const latitude = position.coords.latitude; | |
const longitude = position.coords.longitude; | |
// Fly to the user's location once | |
map.flyTo(new L.LatLng(latitude, longitude), 13, { | |
animate: true, | |
duration: 5, // Duration in seconds for the flying effect | |
}); | |
setTimeout(function () { | |
var marker = L.marker([latitude, longitude], { | |
icon: userIcon, | |
}).addTo(map); | |
marker | |
.bindPopup( | |
`<b>${creatorData.firstName} ${creatorData.lastName} are here!</b>` | |
) | |
.openPopup(); | |
// Add bouncing effect | |
bounceMarker(marker); | |
}, 3500); // Delay the marker appearance slightly after the map flies to the location | |
} | |
function handleLocationError(error) { | |
console.warn(`ERROR(${error.code}): ${error.message}`); | |
} | |
// Function to add a bouncing effect to the marker | |
function bounceMarker(marker) { | |
var originalLatLng = marker.getLatLng(); | |
var bounceHeight = 0.0001; | |
var bounceRate = 400; // Bounce every 400ms | |
setInterval(function () { | |
var newLatLng = [ | |
originalLatLng.lat + (Math.random() - 0.5) * bounceHeight, | |
originalLatLng.lng + (Math.random() - 0.5) * bounceHeight, | |
]; | |
marker.setLatLng(newLatLng); | |
}, bounceRate); | |
} | |
if (navigator.geolocation) { | |
navigator.geolocation.getCurrentPosition( | |
handleLocationSuccess, | |
handleLocationError | |
); | |
} else { | |
console.log("Geolocation is not supported by this browser."); | |
} | |
} else { | |
console.log("Failed to parse creator data from URL."); | |
} | |
}); | |
</script> | |
<!-- Integrate the script into the HTML code --> | |
<script> | |
// Your script code here | |
const messageInput = document.getElementById("message"); | |
const submitButton = document.getElementById("submit-button"); | |
const chatContainer = document.getElementById("chat-container"); | |
const retryButton = document.getElementById("retry-button"); | |
const undoButton = document.getElementById("undo-button"); | |
const clearButton = document.getElementById("clear-button"); | |
const confirmationModal = document.getElementById("confirmation-modal"); | |
const confirmClearButton = document.getElementById("confirm-clear"); | |
const cancelClearButton = document.getElementById("cancel-clear"); | |
let creatorId; | |
let creator; | |
let isVoiceToTextClicked = false; // Flag to track if voice-to-text was clicked | |
window.onload = function () { | |
document.getElementById("chat-container").innerHTML = ""; | |
// Extract the entire query string from the URL | |
const fullUrl = window.location.href; | |
// Find the start of the 'creator' parameter and extract the substring from there | |
const creatorParamStart = fullUrl.indexOf("?creator=") + "?creator=".length; | |
if (creatorParamStart === "?creator=".length - 1) { | |
// 'creator' parameter is missing in the URL | |
alert("Please log in to FriendlyForce first and then access the Ask Friendly platform."); | |
// Redirect the user to the FriendlyForce login page | |
window.location.href = "https://friendlyforce.live/login"; | |
return; | |
} | |
let creatorJson = fullUrl.substring(creatorParamStart); | |
// Assume that 'creator' is the last parameter in the URL | |
// Replace any '&' beyond the first occurrence in the JSON string (caused by incorrect URL parsing) | |
creatorJson = creatorJson.replace(/&(?=\[^&\]\*$)/, "%26"); | |
// Decode the URI component to handle URL encoding | |
creatorJson = decodeURIComponent(creatorJson); | |
try { | |
// Parse the JSON string to an object | |
creator = JSON.parse(creatorJson); | |
// Extract the 'firstName' and 'lastName' properties | |
const firstName = creator.firstName; | |
const lastName = creator.lastName; | |
const creatorId = creator.id; | |
// Step 3: Update the user initials and name in the HTML | |
const userInitials = document.getElementById("user-initials"); | |
const userName = document.getElementById("user-name"); | |
userInitials.textContent = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); | |
userName.textContent = `${firstName} ${lastName}`; | |
// Now you can use firstName and lastName as needed | |
console.log("First Name:", firstName); | |
console.log("Last Name:", lastName); | |
console.log("Creator ID:", creatorId); | |
// Get the current user token | |
getCurrentUserToken(creator); | |
} catch (error) { | |
console.error("Error parsing JSON from URL:", error); | |
// Handle the case when the 'creator' parameter is present but has an invalid format | |
alert("Invalid 'creator' parameter. Please log in to FriendlyForce and access the Ask Friendly platform again."); | |
// Redirect the user to the FriendlyForce login page | |
window.location.href = "https://friendlyforce.live/login"; | |
} | |
}; | |
let lastUserMessage; | |
let lastAIResponseElement; | |
let mapIdCounter = 0; // Counter to assign unique IDs for map elements | |
messageInput.addEventListener("input", () => { | |
messageInput.style.height = "auto"; | |
messageInput.style.height = `${messageInput.scrollHeight}px`; | |
}); | |
messageInput.addEventListener("keydown", (event) => { | |
if (event.key === "Enter" && !event.shiftKey) { | |
event.preventDefault(); | |
submitButton.click(); | |
scrollToBottom(); | |
} | |
}); | |
function updateTokenCount(creator) { | |
// Make an AJAX request to retrieve the current token count | |
$.ajax({ | |
url: 'http://127.0.0.1:8000/friendlyforce/chat/api/v1/tokens/?creator=' + encodeURIComponent(JSON.stringify(creator)), | |
method: 'GET', | |
success: function (data) { | |
// Update the token count in the HTML | |
const tokenCountElement = document.getElementById("token-count"); | |
tokenCountElement.textContent = data.remaining_tokens + " tokens available"; | |
}, | |
error: function (error) { | |
console.error("Error retrieving token count:", error); | |
} | |
}); | |
} | |
async function typeResponse(element, text) { | |
console.log("Hello here is the text:", text); | |
let index = 0; | |
while (index < text.length && !isStopped) { | |
if (text.charAt(index) === "\n") { | |
element.innerHTML += "<br>"; | |
} else { | |
element.innerHTML += text.charAt(index); | |
} | |
index++; | |
await new Promise((resolve) => setTimeout(resolve, 50)); | |
chatContainer.scrollTop = chatContainer.scrollHeight; | |
} | |
// Call the updateTokenCount function after processing the AI response | |
console.log("updating token count"); | |
updateTokenCount(creator); | |
} | |
async function convertTextToSpeech(text) { | |
if (!isVoiceToTextClicked) { | |
return; // Do not convert to speech if the voice-to-text button was not clicked | |
} | |
try { | |
const response = await fetch('/tts', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
text: text, | |
}), | |
}); | |
if (response.ok) { | |
const audioData = await response.arrayBuffer(); | |
const audioContext = new AudioContext(); | |
const audioBuffer = await audioContext.decodeAudioData(audioData); | |
const source = audioContext.createBufferSource(); | |
source.buffer = audioBuffer; | |
source.connect(audioContext.destination); | |
source.start(); | |
} else { | |
console.error('Error converting text to speech:', response.statusText); | |
} | |
} catch (error) { | |
console.error('Error converting text to speech:', error); | |
} | |
} | |
function scrollToBottom() { | |
chatContainer.scrollTop = chatContainer.scrollHeight; | |
} | |
function scrollToTop() { | |
chatContainer.scrollTop = 0; | |
} | |
let isStopped = false; | |
submitButton.addEventListener("click", async () => { | |
let message = messageInput.value.trim(); | |
if (!message) { | |
return; | |
} | |
if (submitButton.classList.contains("bg-red-500")) { | |
isStopped = true; | |
} | |
submitButton.classList.add("bg-red-500", "hover:bg-red-700"); | |
submitButton.classList.remove("bg-blue-500", "hover:bg-blue-700"); | |
document.getElementById("submit-icon").classList.add("hidden"); | |
document.getElementById("pause-icon").classList.remove("hidden"); | |
scrollToTop(); | |
const userMessageDiv = document.createElement("div"); | |
userMessageDiv.className = | |
"mb-4 user-message p-4 rounded-lg chat-bubble"; | |
userMessageDiv.innerHTML = message; | |
chatContainer.appendChild(userMessageDiv); | |
lastUserMessage = message; | |
scrollToBottom(); | |
const aiResponseDiv = document.createElement("div"); | |
aiResponseDiv.className = | |
"mb-4 bg-gray-100 p-4 rounded-lg flex flex-col items-start ai-message"; // Change to flex-col | |
const loaderHTML = ` | |
<div class="loader flex space-x-2"> | |
<div class="animate-pulse w-2 h-2 rounded-full bg-gray-800"></div> | |
<div class="animate-pulse w-2 h-2 rounded-full bg-gray-800"></div> | |
<div class="animate-pulse w-2 h-2 rounded-full bg-gray-800"></div> | |
</div>`; | |
aiResponseDiv.innerHTML = loaderHTML; | |
chatContainer.appendChild(aiResponseDiv); | |
// Function to generate a simple UUID. This is a basic version for demonstration. | |
function generateUUID() { | |
let dt = new Date().getTime(); | |
const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( | |
/[xy]/g, | |
function (c) { | |
const r = (dt + Math.random() * 16) % 16 | 0; | |
dt = Math.floor(dt / 16); | |
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); | |
} | |
); | |
return uuid; | |
} | |
// Check if session ID exists in sessionStorage | |
let sessionID = sessionStorage.getItem("sessionID"); | |
console.log(sessionID); | |
// If session ID does not exist, generate a new one and store it in sessionStorage | |
if (!sessionID) { | |
sessionID = generateUUID(); | |
sessionStorage.setItem("sessionID", sessionID); | |
} | |
// Set an expiration time for the session ID (5 minutes) | |
const expirationTime = 5 * 60 * 1000; // 5 minutes in milliseconds | |
setTimeout(() => { | |
sessionStorage.removeItem("sessionID"); | |
}, expirationTime); | |
console.log(sessionID); | |
// Define the API URL | |
let apiUrl = "/submit-message"; | |
// Check if the message query includes the word 'map' | |
if (message.toLowerCase().includes("map")) { | |
apiUrl = "/dummy-message"; // Use the dummy endpoint | |
} | |
if (message.toLowerCase().includes("report")) { | |
apiUrl = "/reportinformation"; // Use the dummy endpoint | |
} | |
try { | |
let response; | |
if (apiUrl === "/dummy-message") { | |
// Construct query string | |
const queryString = new URLSearchParams({ | |
question: message, | |
}).toString(); | |
const urlWithParams = `${apiUrl}?${queryString}`; | |
const creatorIdd = creator.id; | |
// Use GET request for dummy message | |
response = await fetch(urlWithParams, { | |
method: "GET", | |
headers: { | |
"Content-Type": "application/json", | |
"X-User-ID": creatorIdd, // Include user ID in request headers | |
}, | |
// Do not use 'params' with fetch; query parameters should be in the URL | |
}); | |
} | |
if (apiUrl === "/submit-message") { | |
let requestBody = { | |
message: message, | |
userLocation: null, | |
userDetails: null, | |
}; | |
//const locationKeywords = ['where', 'near', 'around', 'location', 'area']; | |
//if (locationKeywords.some(keyword => message.toLowerCase().includes(keyword))) { | |
//requestBody.userLocation = userLocationName; | |
//} | |
const urlParams = new URLSearchParams(window.location.search); | |
requestBody.userDetails = urlParams.toString(); | |
const creatorId = creator.id; | |
response = await fetch(apiUrl, { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
"X-Session-ID": sessionID, | |
"X-User-ID": creatorId // Include user ID in request headers | |
}, | |
body: JSON.stringify(requestBody), | |
}); | |
} | |
if (apiUrl === "/reportinformation") { | |
// Use POST request for the actual API | |
response = await fetch(apiUrl, { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
"X-Session-ID": sessionID, | |
}, | |
body: JSON.stringify({ message: message }), | |
}); | |
} | |
if (!response.ok) { | |
throw new Error( | |
"Network response was not ok " + response.statusText | |
); | |
} | |
const data = await response.json(); | |
// Clearing the loader and setting up the response | |
aiResponseDiv.innerHTML = ""; | |
const aiResponseText = document.createElement("div"); | |
aiResponseText.className = "response-text mb-2"; | |
aiResponseDiv.appendChild(aiResponseText); | |
// Convert AI response to speech | |
await convertTextToSpeech(data.message); | |
console.log(data); | |
// Typing out the AI response | |
if (!isStopped) { | |
await typeResponse(aiResponseText, data.message); | |
} | |
if (data.map && typeof data.map.latitude !== "undefined" && typeof data.map.longitude !== "undefined") { | |
// Move the main map to the new coordinates | |
map.flyTo([data.map.latitude, data.map.longitude], 13); | |
// Clear existing markers if any | |
map.eachLayer(function (layer) { | |
if (layer instanceof L.Marker) { | |
map.removeLayer(layer); | |
} | |
}); | |
// Define the icon for the new marker | |
const customIcon = L.icon({ | |
iconUrl: data.map.iconUrl, | |
iconSize: [35, 50], | |
iconAnchor: [17, 42], // Adjust if necessary to position the icon correctly | |
}); | |
// Create a marker at the new location with a popup | |
const marker = L.marker([data.map.latitude, data.map.longitude], { | |
icon: customIcon, | |
}).addTo(map); | |
// Log the data object for debugging | |
console.log(data.map); | |
const eventHostProfileImageURL = data.map.eventHostProfileImageURL || 'default-image-url.jpg'; | |
const eventHostName = data.map.eventHostName || 'No Host Name'; | |
const createdDate = data.map.createdDate ? new Date(data.map.createdDate).toLocaleString() : 'No Date Available'; | |
const eventType = data.map.eventType || 'No Event Type'; | |
const popupContent = ` | |
<div class="custom-popup"> | |
<div class="header" style="background-color: red; color: white;"> | |
<h3>Severe</h3> | |
</div> | |
<div class="body"> | |
<img src="${eventHostProfileImageURL}" alt="${eventHostName}" class="profile-img"> | |
<div class="user-info"> | |
<p><strong>${eventHostName}</strong></p> | |
<p>${createdDate}</p> | |
</div> | |
<p>${eventType}</p> | |
</div> | |
<div class="footer"> | |
<p>${data.map.latitude}, ${data.map.longitude}</p> | |
<div class="interactive-icons"> | |
<i class="fas fa-comment"></i> | |
<i class="fas fa-thumbs-up"></i> | |
<i class="fas fa-share"></i> | |
</div> | |
</div> | |
</div> | |
`; | |
marker.bindPopup(popupContent).openPopup(); | |
// Create a small map container and append it below the AI response text | |
const smallMapContainer = document.createElement("div"); | |
smallMapContainer.id = `small-map-${mapIdCounter}`; // Assign a unique ID | |
smallMapContainer.style.width = "100%"; | |
smallMapContainer.style.height = "200px"; | |
smallMapContainer.style.marginTop = "10px"; | |
aiResponseDiv.appendChild(smallMapContainer); | |
// Create a new map instance for the small map | |
const smallMap = L.map(smallMapContainer.id).setView([data.map.latitude, data.map.longitude], 13); | |
// Add the tile layer to the small map | |
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { | |
maxZoom: 19, | |
attribution: "© OpenStreetMap contributors", | |
}).addTo(smallMap); | |
// Add the marker to the small map with the same popup | |
L.marker([data.map.latitude, data.map.longitude], { | |
icon: customIcon, | |
}).addTo(smallMap).bindPopup(popupContent).openPopup(); | |
mapIdCounter++; // Increment the counter for the next map | |
} | |
else { | |
console.log("No map data available for this response."); | |
} | |
} catch (error) { | |
console.error("Error:", error); | |
aiResponseDiv.innerHTML = error; | |
} | |
finally { | |
submitButton.classList.remove("bg-red-500", "hover:bg-red-700"); | |
submitButton.classList.add("bg-blue-500", "hover:bg-blue-700"); | |
document.getElementById("submit-icon").classList.remove("hidden"); | |
document.getElementById("pause-icon").classList.add("hidden"); | |
isStopped = false; | |
} | |
messageInput.value = ""; | |
messageInput.style.height = "auto"; | |
chatContainer.scrollTop = chatContainer.scrollHeight; | |
}); | |
// Handle Retry button click | |
retryButton.addEventListener("click", async () => { | |
if (lastUserMessage) { | |
messageInput.value = lastUserMessage; | |
try { | |
const response = await fetch("/submit-message", { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify({ message: lastUserMessage }), | |
}); | |
const data = await response.json(); | |
const messageText = JSON.stringify(data); | |
console.log("Hello here is the data:", messageText); | |
// Replace the existing AI response with the new response | |
lastAIResponseElement.textContent = ""; | |
await typeResponse(lastAIResponseElement, messageText); | |
} catch (error) { | |
console.error("Error:", error); | |
} | |
} | |
}); | |
// Handle Undo button click | |
undoButton.addEventListener("click", () => { | |
// Remove the last user message and AI response from the chat container | |
const lastUserMessage = chatContainer.lastElementChild; | |
const lastAIResponse = | |
chatContainer.lastElementChild.previousElementSibling; | |
if (lastUserMessage && lastAIResponse) { | |
chatContainer.removeChild(lastUserMessage); | |
chatContainer.removeChild(lastAIResponse); | |
lastUserMessage = null; | |
lastAIResponseElement = null; // Add this line | |
} | |
}); | |
// Modify the Clear button event listener to show the confirmation modal | |
clearButton.addEventListener("click", () => { | |
confirmationModal.classList.remove("hidden"); | |
}); | |
// Clear the chat and hide the modal when Confirm is clicked | |
confirmClearButton.addEventListener("click", () => { | |
chatContainer.innerHTML = ""; | |
lastUserMessage = null; | |
lastAIResponseElement = null; | |
confirmationModal.classList.add("hidden"); | |
}); | |
// Hide the modal without clearing the chat when Cancel is clicked | |
cancelClearButton.addEventListener("click", () => { | |
confirmationModal.classList.add("hidden"); | |
}); | |
// Voice-to-text button event listener | |
//const micContainer = document.querySelector('.mic-container'); | |
//const circle = document.querySelector('.circle'); | |
//const listeningIndicator = document.getElementById('listening-indicator'); | |
micContainer.addEventListener('click', async () => { | |
try { | |
if (!circle.classList.contains('active')) { | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
if (stream) { | |
circle.classList.add('active'); | |
listeningIndicator.style.display = 'inline'; | |
Speech.init(); | |
isVoiceToTextClicked = true; // Set the flag to true when clicked | |
} | |
} else { | |
circle.classList.remove('active'); | |
listeningIndicator.style.display = 'none'; | |
Speech.finish(); | |
isVoiceToTextClicked = false; // Reset the flag when finished | |
} | |
} catch (error) { | |
console.error('Microphone access denied or error: ', error); | |
circle.classList.remove('active'); | |
listeningIndicator.style.display = 'none'; | |
Speech.finish(); | |
isVoiceToTextClicked = false; | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment